diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/CHANGELOG.md b/instrumentation-genai/opentelemetry-instrumentation-anthropic/CHANGELOG.md index ba164c0ebc..5cf210fbd1 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/CHANGELOG.md +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add sync streaming support for `Messages.create(stream=True)` and `Messages.stream()` + ([#4155](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4155)) + - `StreamWrapper` for handling `Messages.create(stream=True)` telemetry + - `MessageStreamManagerWrapper` for handling `Messages.stream()` telemetry + - `MessageWrapper` for non-streaming response telemetry extraction - Initial implementation of Anthropic instrumentation ([#3978](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3978)) - Implement sync `Messages.create` instrumentation with GenAI semantic convention attributes diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/pyproject.toml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/pyproject.toml index 32642eaf2b..74c411a1a5 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/pyproject.toml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ [project.optional-dependencies] instruments = [ - "anthropic >= 0.16.0", + "anthropic >= 0.51.0", ] [project.entry-points.opentelemetry_instrumentor] diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/__init__.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/__init__.py index bf76798462..f5286438a4 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/__init__.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/__init__.py @@ -54,7 +54,9 @@ ) from opentelemetry.instrumentation.anthropic.package import _instruments -from opentelemetry.instrumentation.anthropic.patch import messages_create +from opentelemetry.instrumentation.anthropic.patch import ( + messages_create, +) from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.instrumentation.utils import unwrap from opentelemetry.util.genai.handler import TelemetryHandler @@ -89,11 +91,12 @@ def _instrument(self, **kwargs: Any) -> None: # Get providers from kwargs tracer_provider = kwargs.get("tracer_provider") meter_provider = kwargs.get("meter_provider") + logger_provider = kwargs.get("logger_provider") - # TODO: Add logger_provider to TelemetryHandler to capture content events. handler = TelemetryHandler( tracer_provider=tracer_provider, meter_provider=meter_provider, + logger_provider=logger_provider, ) # Patch Messages.create diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/messages_extractors.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/messages_extractors.py new file mode 100644 index 0000000000..907aa08d39 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/messages_extractors.py @@ -0,0 +1,219 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Get/extract helpers for Anthropic Messages instrumentation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Sequence + +from anthropic.types import MessageDeltaUsage + +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.util.genai.types import ( + InputMessage, + MessagePart, + OutputMessage, +) +from opentelemetry.util.types import AttributeValue + +from .utils import ( + convert_content_to_parts, + normalize_finish_reason, +) + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + + import httpx + from anthropic.resources.messages import Messages + from anthropic.types import ( + Message, + MessageParam, + MetadataParam, + TextBlockParam, + ThinkingConfigParam, + ToolChoiceParam, + ToolUnionParam, + Usage, + ) + + +@dataclass +class MessageRequestParams: + model: str | None = None + max_tokens: int | None = None + temperature: float | None = None + top_k: int | None = None + top_p: float | None = None + stop_sequences: Sequence[str] | None = None + stream: bool | None = None + messages: Iterable[MessageParam] | None = None + system: str | Iterable[TextBlockParam] | None = None + + +GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS = ( + "gen_ai.usage.cache_creation.input_tokens" +) +GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS = "gen_ai.usage.cache_read.input_tokens" + + +@dataclass +class UsageTokens: + input_tokens: int | None = None + output_tokens: int | None = None + cache_creation_input_tokens: int | None = None + cache_read_input_tokens: int | None = None + + +def extract_usage_tokens( + usage: Usage | MessageDeltaUsage | None, +) -> UsageTokens: + if usage is None: + return UsageTokens() + + input_tokens = usage.input_tokens + output_tokens = usage.output_tokens + cache_creation_input_tokens = usage.cache_creation_input_tokens + cache_read_input_tokens = usage.cache_read_input_tokens + + if ( + input_tokens is None + and cache_creation_input_tokens is None + and cache_read_input_tokens is None + ): + total_input_tokens = None + else: + total_input_tokens = ( + (input_tokens or 0) + + (cache_creation_input_tokens or 0) + + (cache_read_input_tokens or 0) + ) + + return UsageTokens( + input_tokens=total_input_tokens, + output_tokens=output_tokens, + cache_creation_input_tokens=cache_creation_input_tokens, + cache_read_input_tokens=cache_read_input_tokens, + ) + + +def get_input_messages( + messages: Iterable[MessageParam] | None, +) -> list[InputMessage]: + if messages is None: + return [] + result: list[InputMessage] = [] + for message in messages: + role = message["role"] + parts = convert_content_to_parts(message["content"]) + result.append(InputMessage(role=role, parts=parts)) + return result + + +def get_system_instruction( + system: str | Iterable[TextBlockParam] | None, +) -> list[MessagePart]: + if system is None: + return [] + return convert_content_to_parts(system) + + +def get_output_messages_from_message( + message: Message | None, +) -> list[OutputMessage]: + if message is None: + return [] + + parts = convert_content_to_parts(message.content) + finish_reason = normalize_finish_reason(message.stop_reason) + return [ + OutputMessage( + role=message.role, + parts=parts, + finish_reason=finish_reason or "", + ) + ] + + +def extract_params( # pylint: disable=too-many-locals + *, + max_tokens: int | None = None, + messages: Iterable[MessageParam] | None = None, + model: str | None = None, + metadata: MetadataParam | None = None, + service_tier: str | None = None, + stop_sequences: Sequence[str] | None = None, + stream: bool | None = None, + system: str | Iterable[TextBlockParam] | None = None, + temperature: float | None = None, + thinking: ThinkingConfigParam | None = None, + tool_choice: ToolChoiceParam | None = None, + tools: Iterable[ToolUnionParam] | None = None, + top_k: int | None = None, + top_p: float | None = None, + extra_headers: Mapping[str, str] | None = None, + extra_query: Mapping[str, object] | None = None, + extra_body: object | None = None, + timeout: float | httpx.Timeout | None = None, + **_kwargs: object, +) -> MessageRequestParams: + return MessageRequestParams( + model=model, + max_tokens=max_tokens, + temperature=temperature, + top_p=top_p, + top_k=top_k, + stop_sequences=stop_sequences, + stream=stream, + messages=messages, + system=system, + ) + + +def _set_server_address_and_port( + client_instance: "Messages", + attributes: dict[str, AttributeValue | None], +) -> None: + base_url = client_instance._client.base_url + host = base_url.host + if host: + attributes[ServerAttributes.SERVER_ADDRESS] = host + + port = base_url.port + if port and port != 443 and port > 0: + attributes[ServerAttributes.SERVER_PORT] = port + + +def get_llm_request_attributes( + params: MessageRequestParams, client_instance: "Messages" +) -> dict[str, AttributeValue]: + attributes: dict[str, AttributeValue | None] = { + GenAIAttributes.GEN_AI_OPERATION_NAME: GenAIAttributes.GenAiOperationNameValues.CHAT.value, + GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.ANTHROPIC.value, # pyright: ignore[reportDeprecated] + GenAIAttributes.GEN_AI_REQUEST_MODEL: params.model, + GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: params.max_tokens, + GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: params.temperature, + GenAIAttributes.GEN_AI_REQUEST_TOP_P: params.top_p, + GenAIAttributes.GEN_AI_REQUEST_TOP_K: params.top_k, + GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: params.stop_sequences, + } + _set_server_address_and_port(client_instance, attributes) + return {k: v for k, v in attributes.items() if v is not None} diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/patch.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/patch.py index 0562d638e2..1fadb78a38 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/patch.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/patch.py @@ -14,65 +14,116 @@ """Patching functions for Anthropic instrumentation.""" -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any, Callable, Union, cast + +from anthropic._streaming import Stream as AnthropicStream +from anthropic.types import Message as AnthropicMessage from opentelemetry.semconv._incubating.attributes import ( gen_ai_attributes as GenAIAttributes, ) from opentelemetry.util.genai.handler import TelemetryHandler -from opentelemetry.util.genai.types import LLMInvocation +from opentelemetry.util.genai.types import Error, LLMInvocation +from opentelemetry.util.genai.utils import should_capture_content -from .utils import ( +from .messages_extractors import ( extract_params, + get_input_messages, get_llm_request_attributes, + get_system_instruction, +) +from .wrappers import ( + MessagesStreamWrapper, + MessageWrapper, ) if TYPE_CHECKING: from anthropic.resources.messages import Messages - from anthropic.types import Message + from anthropic.types import RawMessageStreamEvent + + +ANTHROPIC = "anthropic" def messages_create( handler: TelemetryHandler, -) -> Callable[..., "Message"]: +) -> Callable[ + ..., Union["AnthropicMessage", "AnthropicStream[RawMessageStreamEvent]"] +]: """Wrap the `create` method of the `Messages` class to trace it.""" + capture_content = should_capture_content() def traced_method( - wrapped: Callable[..., "Message"], + wrapped: Callable[ + ..., + Union[ + "AnthropicMessage", + "AnthropicStream[RawMessageStreamEvent]", + ], + ], instance: "Messages", args: tuple[Any, ...], kwargs: dict[str, Any], - ) -> "Message": + ) -> Union["AnthropicMessage", MessagesStreamWrapper]: params = extract_params(*args, **kwargs) attributes = get_llm_request_attributes(params, instance) - request_model = str( - attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL) - or params.model - or "unknown" + request_model_attribute = attributes.get( + GenAIAttributes.GEN_AI_REQUEST_MODEL + ) + request_model = ( + request_model_attribute + if isinstance(request_model_attribute, str) + else params.model ) invocation = LLMInvocation( request_model=request_model, - provider="anthropic", + provider=ANTHROPIC, + input_messages=get_input_messages(params.messages) + if capture_content + else [], + system_instruction=get_system_instruction(params.system) + if capture_content + else [], attributes=attributes, ) - with handler.llm(invocation) as invocation: - result = wrapped(*args, **kwargs) - - if result.model: - invocation.response_model_name = result.model - - if result.id: - invocation.response_id = result.id - - if result.stop_reason: - invocation.finish_reasons = [result.stop_reason] + is_streaming = params.stream is True - if result.usage: - invocation.input_tokens = result.usage.input_tokens - invocation.output_tokens = result.usage.output_tokens - - return result - - return traced_method + # Use manual lifecycle management for both streaming and non-streaming + handler.start_llm(invocation) + try: + result = wrapped(*args, **kwargs) + if is_streaming: + if not isinstance(result, AnthropicStream): + raise TypeError( + "Expected anthropic Stream when stream=True" + ) + return MessagesStreamWrapper( + result, handler, invocation, capture_content + ) + if not isinstance(result, AnthropicMessage): + raise TypeError( + "Expected anthropic Message when stream is disabled" + ) + + wrapper = MessageWrapper(result, capture_content) + wrapper.extract_into(invocation) + handler.stop_llm(invocation) + return wrapper.message + except Exception as exc: + handler.fail_llm( + invocation, Error(message=str(exc), type=type(exc)) + ) + raise + + return cast( + Callable[ + ..., + Union[ + "AnthropicMessage", + "AnthropicStream[RawMessageStreamEvent]", + ], + ], + traced_method, + ) diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py index 4c2003f441..2b9769d096 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/utils.py @@ -12,134 +12,230 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Utility functions for Anthropic instrumentation.""" +"""Shared helper utilities for Anthropic instrumentation.""" from __future__ import annotations +import base64 +import json from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Optional, Sequence -from urllib.parse import urlparse - -from opentelemetry.semconv._incubating.attributes import ( - gen_ai_attributes as GenAIAttributes, +from typing import TYPE_CHECKING + +from anthropic.types import ( + InputJSONDelta, + RedactedThinkingBlock, + ServerToolUseBlock, + TextBlock, + TextDelta, + ThinkingBlock, + ThinkingDelta, + ToolUseBlock, + WebSearchToolResultBlock, ) -from opentelemetry.semconv._incubating.attributes import ( - server_attributes as ServerAttributes, + +from opentelemetry.util.genai.types import ( + Blob, + MessagePart, + Reasoning, + Text, + ToolCall, + ToolCallResponse, ) -from opentelemetry.util.types import AttributeValue if TYPE_CHECKING: - from anthropic.resources.messages import Messages + from collections.abc import Iterable, Mapping + + from anthropic.types import ( + ContentBlock, + ContentBlockParam, + RawContentBlockDelta, + ) @dataclass -class MessageCreateParams: - """Parameters extracted from Messages.create() call.""" - - model: str | None = None - max_tokens: int | None = None - temperature: float | None = None - top_p: float | None = None - top_k: int | None = None - stop_sequences: Sequence[str] | None = None - - -# Use parameter signature from -# https://github.com/anthropics/anthropic-sdk-python/blob/9b5ab24ba17bcd5e762e5a5fd69bb3c17b100aaa/src/anthropic/resources/messages/messages.py#L92 -# to handle named vs positional args robustly -def extract_params( # pylint: disable=too-many-locals - *, - max_tokens: int | None = None, - messages: Any | None = None, - model: str | None = None, - metadata: Any | None = None, - service_tier: Any | None = None, - stop_sequences: Sequence[str] | None = None, - stream: Any | None = None, - system: Any | None = None, - temperature: float | None = None, - thinking: Any | None = None, - tool_choice: Any | None = None, - tools: Any | None = None, - top_p: float | None = None, - top_k: int | None = None, - extra_headers: Any | None = None, - extra_query: Any | None = None, - extra_body: Any | None = None, - timeout: Any | None = None, - **_kwargs: Any, -) -> MessageCreateParams: - """Extract relevant parameters from Messages.create() arguments.""" - return MessageCreateParams( - model=model, - max_tokens=max_tokens, - temperature=temperature, - top_p=top_p, - top_k=top_k, - stop_sequences=stop_sequences, +class StreamBlockState: + type: str + text: str = "" + tool_id: str | None = None + tool_name: str = "" + tool_input: dict[str, object] | None = None + input_json: str = "" + thinking: str = "" + + +def normalize_finish_reason(stop_reason: str | None) -> str | None: + if stop_reason is None: + return None + normalized = { + "end_turn": "stop", + "stop_sequence": "stop", + "max_tokens": "length", + "tool_use": "tool_calls", + }.get(stop_reason) + return normalized or stop_reason + + +def _decode_base64(data: str) -> bytes | None: + try: + return base64.b64decode(data) + except Exception: # pylint: disable=broad-exception-caught + return None + + +def _extract_base64_blob(source: object, modality: str) -> Blob | None: + """Extract a Blob from a base64-encoded source dict.""" + if not isinstance(source, dict): + return None + # source is a TypedDict (e.g. Base64ImageSourceParam) narrowed to dict; + # pyright cannot infer value types from isinstance-narrowed dicts. + data: object = source.get("data") # type: ignore[reportUnknownMemberType] + if not isinstance(data, str): + return None + decoded = _decode_base64(data) + if decoded is None: + return None + media_type: object = source.get("media_type") # type: ignore[reportUnknownMemberType] + return Blob( + mime_type=media_type if isinstance(media_type, str) else None, + modality=modality, + content=decoded, ) -def set_server_address_and_port( - client_instance: "Messages", attributes: dict[str, Any] +def _convert_dict_block_to_part( + block: Mapping[str, object], +) -> MessagePart | None: + """Convert a request-param content block (TypedDict/dict) to a MessagePart.""" + block_type = block.get("type") + + if block_type == "text": + text = block.get("text") + return Text(content=str(text) if text is not None else "") + + if block_type == "tool_use": + inp = block.get("input") + return ToolCall( + arguments=inp if isinstance(inp, dict) else None, + name=str(block.get("name", "")), + id=str(block.get("id", "")), + ) + + if block_type == "tool_result": + return ToolCallResponse( + response=block.get("content"), + id=str(block.get("tool_use_id", "")), + ) + + if block_type in ("thinking", "redacted_thinking"): + thinking = block.get("thinking") or block.get("data") + return Reasoning(content=str(thinking) if thinking is not None else "") + + if block_type in ("image", "audio", "video", "document", "file"): + return _extract_base64_blob(block.get("source"), str(block_type)) + + return None + + +def _convert_content_block_to_part( + block: ContentBlock | ContentBlockParam, +) -> MessagePart | None: + """Convert an Anthropic content block to a MessagePart.""" + if isinstance(block, TextBlock): + return Text(content=block.text) + + if isinstance(block, (ToolUseBlock, ServerToolUseBlock)): + return ToolCall(arguments=block.input, name=block.name, id=block.id) + + if isinstance(block, (ThinkingBlock, RedactedThinkingBlock)): + content = ( + block.thinking if isinstance(block, ThinkingBlock) else block.data + ) + return Reasoning(content=content) + + if isinstance(block, WebSearchToolResultBlock): + return ToolCallResponse( + response=block.model_dump().get("content"), + id=block.tool_use_id, + ) + + # ContentBlockParam variants are TypedDicts (dicts at runtime); + # newer SDK versions may add Pydantic block types not handled above. + if isinstance(block, dict): + return _convert_dict_block_to_part(block) + + return None + + +def convert_content_to_parts( + content: str | Iterable[ContentBlock | ContentBlockParam] | None, +) -> list[MessagePart]: + if content is None: + return [] + if isinstance(content, str): + return [Text(content=content)] + parts: list[MessagePart] = [] + for item in content: + part = _convert_content_block_to_part(item) + if part is not None: + parts.append(part) + return parts + + +def create_stream_block_state(content_block: ContentBlock) -> StreamBlockState: + if isinstance(content_block, TextBlock): + return StreamBlockState(type="text", text=content_block.text) + + if isinstance(content_block, (ToolUseBlock, ServerToolUseBlock)): + return StreamBlockState( + type="tool_use", + tool_id=content_block.id, + tool_name=content_block.name, + tool_input=content_block.input, + ) + + if isinstance(content_block, ThinkingBlock): + return StreamBlockState( + type="thinking", thinking=content_block.thinking + ) + + if isinstance(content_block, RedactedThinkingBlock): + return StreamBlockState(type="redacted_thinking") + + return StreamBlockState(type=content_block.type) + + +def update_stream_block_state( + state: StreamBlockState, delta: RawContentBlockDelta ) -> None: - """Extract server address and port from the Anthropic client instance.""" - base_client = getattr(client_instance, "_client", None) - base_url = getattr(base_client, "base_url", None) - if not base_url: - return - - port: Optional[int] = None - if hasattr(base_url, "host"): - # httpx.URL object - attributes[ServerAttributes.SERVER_ADDRESS] = base_url.host - port = getattr(base_url, "port", None) - elif isinstance(base_url, str): - url = urlparse(base_url) - attributes[ServerAttributes.SERVER_ADDRESS] = url.hostname - port = url.port - - if port and port != 443 and port > 0: - attributes[ServerAttributes.SERVER_PORT] = port - - -def get_llm_request_attributes( - params: MessageCreateParams, client_instance: "Messages" -) -> dict[str, AttributeValue]: - """Extract LLM request attributes from MessageCreateParams. - - Returns a dictionary of OpenTelemetry semantic convention attributes for LLM requests. - The attributes follow the GenAI semantic conventions (gen_ai.*) and server semantic - conventions (server.*) as defined in the OpenTelemetry specification. - - GenAI attributes included: - - gen_ai.operation.name: The operation name (e.g., "chat") - - gen_ai.system: The GenAI system identifier (e.g., "anthropic") - - gen_ai.request.model: The model identifier - - gen_ai.request.max_tokens: Maximum tokens in the request - - gen_ai.request.temperature: Sampling temperature - - gen_ai.request.top_p: Top-p sampling parameter - - gen_ai.request.top_k: Top-k sampling parameter - - gen_ai.request.stop_sequences: Stop sequences for the request - - Server attributes included (if available): - - server.address: The server hostname - - server.port: The server port (if not default 443) - - Only non-None values are included in the returned dictionary. - """ - attributes = { - GenAIAttributes.GEN_AI_OPERATION_NAME: GenAIAttributes.GenAiOperationNameValues.CHAT.value, - GenAIAttributes.GEN_AI_SYSTEM: GenAIAttributes.GenAiSystemValues.ANTHROPIC.value, # pyright: ignore[reportDeprecated] - GenAIAttributes.GEN_AI_REQUEST_MODEL: params.model, - GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS: params.max_tokens, - GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE: params.temperature, - GenAIAttributes.GEN_AI_REQUEST_TOP_P: params.top_p, - GenAIAttributes.GEN_AI_REQUEST_TOP_K: params.top_k, - GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES: params.stop_sequences, - } - - set_server_address_and_port(client_instance, attributes) - - # Filter out None values - return {k: v for k, v in attributes.items() if v is not None} + if isinstance(delta, TextDelta): + state.type = "text" + state.text += delta.text + elif isinstance(delta, InputJSONDelta): + state.type = "tool_use" + state.input_json += delta.partial_json + elif isinstance(delta, ThinkingDelta): + state.type = "thinking" + state.thinking += delta.thinking + + +def stream_block_state_to_part(state: StreamBlockState) -> MessagePart | None: + if state.type == "text": + return Text(content=state.text) + + if state.type == "tool_use": + arguments: str | dict[str, object] | None = state.tool_input + if state.input_json: + try: + arguments = json.loads(state.input_json) + except ValueError: + arguments = state.input_json + return ToolCall( + arguments=arguments, + name=state.tool_name, + id=state.tool_id, + ) + + if state.type in ("thinking", "redacted_thinking"): + return Reasoning(content=state.thinking) + + return None diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/wrappers.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/wrappers.py new file mode 100644 index 0000000000..585d70a9f9 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/src/opentelemetry/instrumentation/anthropic/wrappers.py @@ -0,0 +1,275 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import logging +from types import TracebackType +from typing import TYPE_CHECKING, Callable, Iterator, Optional + +from opentelemetry.util.genai.handler import TelemetryHandler +from opentelemetry.util.genai.types import ( + Error, + LLMInvocation, + MessagePart, + OutputMessage, +) + +from .messages_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, + extract_usage_tokens, + get_output_messages_from_message, +) +from .utils import ( + StreamBlockState, + create_stream_block_state, + normalize_finish_reason, + stream_block_state_to_part, + update_stream_block_state, +) + +if TYPE_CHECKING: + from anthropic._streaming import Stream + from anthropic.types import ( + Message, + MessageDeltaUsage, + RawMessageStreamEvent, + Usage, + ) + + +_logger = logging.getLogger(__name__) + + +class MessageWrapper: + """Wrapper for non-streaming Message response that handles telemetry.""" + + def __init__(self, message: Message, capture_content: bool): + self._message = message + self._capture_content = capture_content + + def extract_into(self, invocation: LLMInvocation) -> None: + """Extract response data into the invocation.""" + if self._message.model: + invocation.response_model_name = self._message.model + + if self._message.id: + invocation.response_id = self._message.id + + finish_reason = normalize_finish_reason(self._message.stop_reason) + if finish_reason: + invocation.finish_reasons = [finish_reason] + + if self._message.usage: + tokens = extract_usage_tokens(self._message.usage) + invocation.input_tokens = tokens.input_tokens + invocation.output_tokens = tokens.output_tokens + if tokens.cache_creation_input_tokens is not None: + invocation.attributes[ + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS + ] = tokens.cache_creation_input_tokens + if tokens.cache_read_input_tokens is not None: + invocation.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] = ( + tokens.cache_read_input_tokens + ) + + if self._capture_content: + invocation.output_messages = get_output_messages_from_message( + self._message + ) + + @property + def message(self) -> Message: + """Return the wrapped Message object.""" + return self._message + + +class MessagesStreamWrapper(Iterator["RawMessageStreamEvent"]): + """Wrapper for Anthropic Stream that handles telemetry.""" + + def __init__( + self, + stream: Stream[RawMessageStreamEvent], + handler: TelemetryHandler, + invocation: LLMInvocation, + capture_content: bool, + ): + self._stream = stream + self._handler = handler + self._invocation = invocation + self._response_id: Optional[str] = None + self._response_model: Optional[str] = None + self._stop_reason: Optional[str] = None + self._input_tokens: Optional[int] = None + self._output_tokens: Optional[int] = None + self._cache_creation_input_tokens: Optional[int] = None + self._cache_read_input_tokens: Optional[int] = None + self._capture_content = capture_content + self._content_blocks: dict[int, StreamBlockState] = {} + self._finalized = False + + def _update_usage(self, usage: Usage | MessageDeltaUsage | None) -> None: + tokens = extract_usage_tokens(usage) + if tokens.input_tokens is not None: + self._input_tokens = tokens.input_tokens + if tokens.output_tokens is not None: + self._output_tokens = tokens.output_tokens + if tokens.cache_creation_input_tokens is not None: + self._cache_creation_input_tokens = ( + tokens.cache_creation_input_tokens + ) + if tokens.cache_read_input_tokens is not None: + self._cache_read_input_tokens = tokens.cache_read_input_tokens + + def _process_chunk(self, chunk: RawMessageStreamEvent) -> None: + """Extract telemetry data from a streaming chunk.""" + if chunk.type == "message_start": + message = chunk.message + if message.id: + self._response_id = message.id + if message.model: + self._response_model = message.model + self._update_usage(message.usage) + elif chunk.type == "message_delta": + if chunk.delta.stop_reason: + self._stop_reason = normalize_finish_reason( + chunk.delta.stop_reason + ) + self._update_usage(chunk.usage) + elif self._capture_content and chunk.type == "content_block_start": + self._content_blocks[chunk.index] = create_stream_block_state( + chunk.content_block + ) + elif self._capture_content and chunk.type == "content_block_delta": + block = self._content_blocks.get(chunk.index) + if block is not None: + update_stream_block_state(block, chunk.delta) + + @staticmethod + def _safe_instrumentation( + callback: Callable[[], object], context: str + ) -> None: + try: + callback() + except Exception: # pylint: disable=broad-exception-caught + _logger.debug( + "Anthropic MessagesStreamWrapper instrumentation error in %s", + context, + exc_info=True, + ) + + def _set_invocation_response_attributes(self) -> None: + """Extract accumulated stream state into the invocation.""" + if self._response_model: + self._invocation.response_model_name = self._response_model + if self._response_id: + self._invocation.response_id = self._response_id + if self._stop_reason: + self._invocation.finish_reasons = [self._stop_reason] + if self._input_tokens is not None: + self._invocation.input_tokens = self._input_tokens + if self._output_tokens is not None: + self._invocation.output_tokens = self._output_tokens + if self._cache_creation_input_tokens is not None: + self._invocation.attributes[ + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS + ] = self._cache_creation_input_tokens + if self._cache_read_input_tokens is not None: + self._invocation.attributes[ + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS + ] = self._cache_read_input_tokens + + if self._capture_content and self._content_blocks: + parts: list[MessagePart] = [] + for index in sorted(self._content_blocks): + part = stream_block_state_to_part(self._content_blocks[index]) + if part is not None: + parts.append(part) + self._invocation.output_messages = [ + OutputMessage( + role="assistant", + parts=parts, + finish_reason=self._stop_reason or "", + ) + ] + + def _stop(self) -> None: + if self._finalized: + return + self._safe_instrumentation( + self._set_invocation_response_attributes, + "response attribute extraction", + ) + self._safe_instrumentation( + lambda: self._handler.stop_llm(self._invocation), + "stop_llm", + ) + self._finalized = True + + def _fail(self, message: str, error_type: type[BaseException]) -> None: + if self._finalized: + return + self._safe_instrumentation( + lambda: self._handler.fail_llm( + self._invocation, Error(message=message, type=error_type) + ), + "fail_llm", + ) + self._finalized = True + + def __iter__(self) -> MessagesStreamWrapper: + return self + + def __getattr__(self, name: str) -> object: + return getattr(self._stream, name) + + def __next__(self) -> RawMessageStreamEvent: + try: + chunk = next(self._stream) + except StopIteration: + self._stop() + raise + except Exception as exc: + self._fail(str(exc), type(exc)) + raise + self._safe_instrumentation( + lambda: self._process_chunk(chunk), + "stream chunk processing", + ) + return chunk + + def __enter__(self) -> MessagesStreamWrapper: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + if exc_type is not None: + self._fail( + str(exc_val), type(exc_val) if exc_val else Exception + ) + finally: + self.close() + return False + + def close(self) -> None: + try: + self._stream.close() + finally: + self._stop() diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_stream_wrapper_finalize_idempotent.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_stream_wrapper_finalize_idempotent.yaml new file mode 100644 index 0000000000..e6ac40e646 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_stream_wrapper_finalize_idempotent.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01PJ2JUkNkA7g8oZhVFZr4FZ","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d43406fcfec1a5c-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:12 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:11Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:11Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:11Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:11Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1134' + request-id: + - req_011CYXZCAm6y8MuXWCF1YQhY + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '1627' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_aggregates_cache_tokens.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_aggregates_cache_tokens.yaml new file mode 100644 index 0000000000..00d584de8c --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_aggregates_cache_tokens.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514" + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '117' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - '600' + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |- + { + "model": "claude-sonnet-4-20250514", + "id": "msg_016rZmFAqBwocMJPynTw59VW", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 13, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 5, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + headers: + CF-RAY: + - 9d43407bb986c34e-EWR + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Thu, 26 Feb 2026 23:34:14 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:14Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:14Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:13Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:14Z' + cf-cache-status: + - DYNAMIC + content-length: + - '441' + request-id: + - req_011CYXZCJxUVNdTSnXuC8xvk + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '1182' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_api_error.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_api_error.yaml index 3940582198..d74a78518a 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_api_error.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_api_error.yaml @@ -1,343 +1,4 @@ interactions: -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Hello" - } - ], - "model": "invalid-model-name" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '110' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python - x-api-key: - - test_anthropic_api_key - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "not_found_error", - "message": "model: invalid-model-name" - } - } - headers: - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Mon, 15 Dec 2024 10:00:04 GMT - Server: - - cloudflare - content-length: - - '105' - status: - code: 404 - message: Not Found -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Hello" - } - ], - "model": "invalid-model-name" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '94' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "authentication_error", - "message": "invalid x-api-key" - }, - "request_id": "req_011CX88XGWSm82bN96ZkDWcr" - } - headers: - CF-RAY: - - 9be0e03eff016e28-EWR - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:22:32 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88XGWSm82bN96ZkDWcr - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '13' - x-should-retry: - - 'false' - status: - code: 401 - message: Unauthorized -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Hello" - } - ], - "model": "invalid-model-name" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '94' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88ZmBumGkvj7aK6Gqzx" - } - headers: - CF-RAY: - - 9be0e1127fd1b1bc-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:23:06 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88ZmBumGkvj7aK6Gqzx - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '59' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Hello" - } - ], - "model": "invalid-model-name" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '94' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "not_found_error", - "message": "model: invalid-model-name" - }, - "request_id": "req_011CX89f9XqPgyRJC1ASfAab" - } - headers: - CF-RAY: - - 9be0f610add5b89f-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:37:25 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - content-length: - - '133' - request-id: - - req_011CX89f9XqPgyRJC1ASfAab - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '27' - x-should-retry: - - 'false' - status: - code: 404 - message: Not Found - request: body: |- { @@ -400,35 +61,35 @@ interactions: "type": "not_found_error", "message": "model: invalid-model-name" }, - "request_id": "req_011CX89kiWcGNsjEPrPGA42x" + "request_id": "req_011CYXZB4QLX5jSBkzfgUcn7" } headers: CF-RAY: - - 9be0f7e8dbf70ab9-EWR + - 9d434011aefdcd7c-EWR Connection: - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' Content-Type: - application/json Date: - - Wed, 14 Jan 2026 23:38:41 GMT + - Thu, 26 Feb 2026 23:33:56 GMT Server: - cloudflare Transfer-Encoding: - chunked X-Robots-Tag: - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 cf-cache-status: - DYNAMIC content-length: - '133' request-id: - - req_011CX89kiWcGNsjEPrPGA42x + - req_011CYXZB4QLX5jSBkzfgUcn7 strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '78' + - '66' x-should-retry: - 'false' status: diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_basic.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_basic.yaml index 205b1c2e49..84d567f200 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_basic.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_basic.yaml @@ -1,720 +1,4 @@ interactions: -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '128' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python - x-api-key: - - test_anthropic_api_key - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "id": "msg_01XFDUDYJgAACzvnptvVoYEL", - "type": "message", - "role": "assistant", - "model": "claude-3-5-sonnet-20241022", - "content": [ - { - "type": "text", - "text": "Hello!" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 14, - "output_tokens": 4 - } - } - headers: - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Mon, 15 Dec 2024 10:00:00 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - content-length: - - '350' - status: - code: 200 - message: OK -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '119' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "authentication_error", - "message": "invalid x-api-key" - }, - "request_id": "req_011CX88X7DM3SrsuuERgZeYJ" - } - headers: - CF-RAY: - - 9be0e0315fa2a02c-EWR - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:22:30 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88X7DM3SrsuuERgZeYJ - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '13' - x-should-retry: - - 'false' - status: - code: 401 - message: Unauthorized -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '119' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88Zakc7NS5rDAkMMK45" - } - headers: - CF-RAY: - - 9be0e1032e73ace5-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:23:03 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88Zakc7NS5rDAkMMK45 - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '30' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '119' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88qYh2YvHnk7Hp8vCR7" - } - headers: - CF-RAY: - - 9be0e64cc92eb4c6-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:26:40 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88qYh2YvHnk7Hp8vCR7 - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '20' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '119' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "not_found_error", - "message": "model: claude-3-5-sonnet-20241022" - }, - "request_id": "req_011CX89Wba1H8DoNZMAq5M9M" - } - headers: - CF-RAY: - - 9be0f33baa01cdf0-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:35:29 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - content-length: - - '141' - request-id: - - req_011CX89Wba1H8DoNZMAq5M9M - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '28' - x-should-retry: - - 'false' - status: - code: 404 - message: Not Found -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-sonnet-4-20250514" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '117' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "model": "claude-sonnet-4-20250514", - "id": "msg_01ChhLEFb4TSQWHpQzFqEQsj", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Hello!" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 13, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "cache_creation": { - "ephemeral_5m_input_tokens": 0, - "ephemeral_1h_input_tokens": 0 - }, - "output_tokens": 5, - "service_tier": "standard" - } - } - headers: - CF-RAY: - - 9be0f55b2b560866-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:36:58 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - anthropic-ratelimit-input-tokens-limit: - - '30000' - anthropic-ratelimit-input-tokens-remaining: - - '30000' - anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:36:58Z' - anthropic-ratelimit-output-tokens-limit: - - '8000' - anthropic-ratelimit-output-tokens-remaining: - - '8000' - anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:36:58Z' - anthropic-ratelimit-requests-limit: - - '50' - anthropic-ratelimit-requests-remaining: - - '49' - anthropic-ratelimit-requests-reset: - - '2026-01-14T23:36:58Z' - anthropic-ratelimit-tokens-limit: - - '38000' - anthropic-ratelimit-tokens-remaining: - - '38000' - anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:36:58Z' - cf-cache-status: - - DYNAMIC - content-length: - - '409' - request-id: - - req_011CX89d1Mu8qapBc5y9KdXf - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '1596' - status: - code: 200 - message: OK -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hello in one word." - } - ], - "model": "claude-sonnet-4-20250514" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '117' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "model": "claude-sonnet-4-20250514", - "id": "msg_01W7B22k9o9QrCqiEWmU1v9G", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Hello!" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 13, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "cache_creation": { - "ephemeral_5m_input_tokens": 0, - "ephemeral_1h_input_tokens": 0 - }, - "output_tokens": 5, - "service_tier": "standard" - } - } - headers: - CF-RAY: - - 9be0f5d0cffcdcde-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:37:17 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - anthropic-ratelimit-input-tokens-limit: - - '30000' - anthropic-ratelimit-input-tokens-remaining: - - '30000' - anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:37:16Z' - anthropic-ratelimit-output-tokens-limit: - - '8000' - anthropic-ratelimit-output-tokens-remaining: - - '8000' - anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:37:17Z' - anthropic-ratelimit-requests-limit: - - '50' - anthropic-ratelimit-requests-remaining: - - '49' - anthropic-ratelimit-requests-reset: - - '2026-01-14T23:37:16Z' - anthropic-ratelimit-tokens-limit: - - '38000' - anthropic-ratelimit-tokens-remaining: - - '38000' - anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:37:16Z' - cf-cache-status: - - DYNAMIC - content-length: - - '409' - request-id: - - req_011CX89ePqXjam4ByzovDWC6 - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '1516' - status: - code: 200 - message: OK - request: body: |- { @@ -773,7 +57,7 @@ interactions: string: |- { "model": "claude-sonnet-4-20250514", - "id": "msg_01MpMMdwiz43MhCffJBjWRZP", + "id": "msg_01AaCx2TjLiN5JUc1d8y6yj5", "type": "message", "role": "assistant", "content": [ @@ -793,60 +77,61 @@ interactions: "ephemeral_1h_input_tokens": 0 }, "output_tokens": 5, - "service_tier": "standard" + "service_tier": "standard", + "inference_geo": "not_available" } } headers: CF-RAY: - - 9be0f7a98b01b734-EWR + - 9d433fe00fb0012e-EWR Connection: - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' Content-Type: - application/json Date: - - Wed, 14 Jan 2026 23:38:33 GMT + - Thu, 26 Feb 2026 23:33:49 GMT Server: - cloudflare Transfer-Encoding: - chunked X-Robots-Tag: - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 anthropic-ratelimit-input-tokens-limit: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-remaining: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:38:33Z' + - '2026-02-26T23:33:49Z' anthropic-ratelimit-output-tokens-limit: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-remaining: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:38:33Z' + - '2026-02-26T23:33:49Z' anthropic-ratelimit-requests-limit: - - '50' + - '1000' anthropic-ratelimit-requests-remaining: - - '49' + - '999' anthropic-ratelimit-requests-reset: - - '2026-01-14T23:38:32Z' + - '2026-02-26T23:33:48Z' anthropic-ratelimit-tokens-limit: - - '38000' + - '540000' anthropic-ratelimit-tokens-remaining: - - '38000' + - '540000' anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:38:33Z' + - '2026-02-26T23:33:49Z' cf-cache-status: - DYNAMIC content-length: - - '409' + - '441' request-id: - - req_011CX89jyLYwphKuArFEcRij + - req_011CYXZAUSRcWb3CfyAW5tBH strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '1970' + - '1087' status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_content.yaml new file mode 100644 index 0000000000..01f97a8689 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_content.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514" + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '117' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - '600' + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |- + { + "model": "claude-sonnet-4-20250514", + "id": "msg_01MX8SyzXjwPncft5XtVKf8Y", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 13, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 5, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + headers: + CF-RAY: + - 9d433fe86db4c540-EWR + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Thu, 26 Feb 2026 23:33:50 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:33:50Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:33:50Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:33:49Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:33:50Z' + cf-cache-status: + - DYNAMIC + content-length: + - '441' + request-id: + - req_011CYXZAaCSwAhvfCgF262WH + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '1031' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_thinking_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_thinking_content.yaml new file mode 100644 index 0000000000..73902b8104 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_thinking_content.yaml @@ -0,0 +1,147 @@ +interactions: +- request: + body: |- + { + "max_tokens": 16000, + "messages": [ + { + "role": "user", + "content": "What is 17*19? Think first." + } + ], + "model": "claude-sonnet-4-20250514", + "thinking": { + "type": "enabled", + "budget_tokens": 10000 + } + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '176' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - '600' + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |- + { + "model": "claude-sonnet-4-20250514", + "id": "msg_01Wr63EVQ99qSimMzVWidt6S", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "thinking", + "thinking": "I need to calculate 17 * 19.\n\nLet me think about this step by step.\n\nI can use the standard multiplication method:\n17 \u00d7 19\n\nI can break this down:\n17 \u00d7 19 = 17 \u00d7 (20 - 1) = 17 \u00d7 20 - 17 \u00d7 1 = 340 - 17 = 323\n\nOr I can do it the traditional way:\n 17\n\u00d7 19\n----\n 153 (17 \u00d7 9)\n170 (17 \u00d7 10)\n----\n323\n\nLet me double-check the first line: 17 \u00d7 9\n17 \u00d7 9 = 17 \u00d7 (10 - 1) = 170 - 17 = 153 \u2713\n\nAnd 17 \u00d7 10 = 170 \u2713\n\nSo 153 + 170 = 323\n\nLet me verify this another way:\n17 \u00d7 19 = (20 - 3) \u00d7 (20 - 1) = 20\u00b2 - 20 - 3\u00d720 + 3 = 400 - 20 - 60 + 3 = 400 - 80 + 3 = 320 + 3 = 323 \u2713\n\nYes, 17 \u00d7 19 = 323.", + "signature": "EogGCkYICxgCKkAxs8t07yMFiwAaO61FwoyqVpxj3ia/JWKx1cE7b9xqI/c2lYLIGy5eof9e1k5fEUix5OWHCLkCaon0dm5LUBtkEgz/oDM2mTBB27Xx6GYaDDYsQqXAIlkTkWyC0yIwHCKfezWvrT/BpU0Z09wqT19UNXjC0twg31t9lQYBV1/9glQtQgBkU8ml19Qd8JvJKu8EYW3lxaCiu7m0crt48lNhluutl9L6J/OGE9P5AbsQw6oNUY0VmRXSJZTAibR3Hy7IfxDLXFDak2f/K0oK31ouuN19lCoXkL/s64IqrWA5AvXALoKSoCaCv4Rkv1decZgyamVlYnIa4dbDaHlVzMYeO+qm9dG/haPObJ09F5nDhB1dfjggGSZhumLoli5jwN6PKwOH9enwpAymb1pSvbQpcYh3GoCdo0qzrzX3ScIeXZ40Vv6vfn49+glSDFRGUnG4xwv3ikefdSXiUBiuIIsub4oDADMfsZV/1dci03F59p2nEJ52TGGuIkRVNTFOoEzbRpnrdZiCF7zeXdAN8Jfefqrw6hUWrdkaBwVmhiqkpwMEnTfF204ZYSuHMbSwlYcM4GVqZn63htgtPBD132ym0oaPe3w28kJFbDfG0jea9maXn0YtfOc060/tt/ftvGVBXORZENhrt+kbKcczK2IBvRjciqgxUJeqlyyiagY+KWa+qHymO21d+HYG+WEyTTRgHpoz8dAVW2aK3S3AcnU79narB3rqJXQGBfDc/5jo+mzjPV2zBb7NYp6doGNpwEAlkDnPqTzPVNecSwL9/GrrvWYfR0ABKrDPAD2XVCbOpm8pURZSMThSpailrIBz5+WiY/jOVUSjZlR4SMpBsOnXm6/VyxW/XvlJ/YrBkpQnHE1gyZfVN1d1FPFjGUzd2RPH127ThANP+ZDQxCfPZ6L1pIbEDEddehMCLTjkCmqNkQsIxBYsVR3pAGCmCOJtVFc311SpHiryVdmeDnV9PVP096M930a5+O4EdZSX+2PMgRPhpQzc+REpCeoErBPm5kEYAQ==" + }, + { + "type": "text", + "text": "Looking at 17 \u00d7 19, I'll calculate this step by step.\n\nI can use the method of breaking down one number:\n17 \u00d7 19 = 17 \u00d7 (20 - 1) = (17 \u00d7 20) - (17 \u00d7 1) = 340 - 17 = 323\n\nLet me verify with traditional multiplication:\n```\n 17\n\u00d7 19\n-----\n 153 (17 \u00d7 9)\n 170 (17 \u00d7 10, shifted one place)\n-----\n 323\n```\n\nTherefore, 17 \u00d7 19 = 323." + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 46, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 469, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + headers: + CF-RAY: + - 9d434044d9205d8f-EWR + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Thu, 26 Feb 2026 23:34:11 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:05Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:11Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:04Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:05Z' + cf-cache-status: + - DYNAMIC + content-length: + - '2523' + request-id: + - req_011CYXZBfP2AZrnRcZpL7v33 + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '6707' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_tool_use_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_tool_use_content.yaml new file mode 100644 index 0000000000..6994c0a4d2 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_captures_tool_use_content.yaml @@ -0,0 +1,166 @@ +interactions: +- request: + body: |- + { + "max_tokens": 256, + "messages": [ + { + "role": "user", + "content": "What is the weather in SF?" + } + ], + "model": "claude-sonnet-4-20250514", + "tool_choice": { + "type": "tool", + "name": "get_weather" + }, + "tools": [ + { + "name": "get_weather", + "description": "Get weather by city", + "input_schema": { + "type": "object", + "properties": { + "city": { + "type": "string" + } + }, + "required": [ + "city" + ] + } + } + ] + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '334' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - '600' + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |- + { + "model": "claude-sonnet-4-20250514", + "id": "msg_01CAX3NfQbrV1V1L38j8HFoN", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "toolu_01KxvRJ8rVKcC69hTfFXvwnU", + "name": "get_weather", + "input": { + "city": "San Francisco" + }, + "caller": { + "type": "direct" + } + } + ], + "stop_reason": "tool_use", + "stop_sequence": null, + "usage": { + "input_tokens": 386, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 34, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + headers: + CF-RAY: + - 9d43403ccf2ebe82-EWR + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Thu, 26 Feb 2026 23:34:04 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:04Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:04Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:03Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:04Z' + cf-cache-status: + - DYNAMIC + content-length: + - '550' + request-id: + - req_011CYXZBZsscGf21yQ2EnhRJ + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '1131' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_event_only_no_content_in_span.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_event_only_no_content_in_span.yaml new file mode 100644 index 0000000000..5eb661dc10 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_event_only_no_content_in_span.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514" + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '117' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - '600' + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |- + { + "model": "claude-sonnet-4-20250514", + "id": "msg_01CjYy52FHcPjyvxXbu3CBtw", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Hello!" + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "usage": { + "input_tokens": 13, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 5, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + headers: + CF-RAY: + - 9d4340a3695606a1-EWR + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - application/json + Date: + - Thu, 26 Feb 2026 23:34:20 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:20Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:20Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:19Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:20Z' + cf-cache-status: + - DYNAMIC + content-length: + - '441' + request-id: + - req_011CYXZCn77hQNGjLFsEhmk7 + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '1134' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_instrumentation_error_swallowed.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_instrumentation_error_swallowed.yaml new file mode 100644 index 0000000000..2780ab5541 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_instrumentation_error_swallowed.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_0135WGDEevMwHieJDYDUWtFi","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d43409be9491dcc-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:19 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:18Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:18Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:18Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:18Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1112' + request-id: + - req_011CYXZCgxJTcRprqcP37XSX + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '952' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stop_reason.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stop_reason.yaml index 17e0fb8715..d4a259c928 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stop_reason.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stop_reason.yaml @@ -1,395 +1,4 @@ interactions: -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hi." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '114' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python - x-api-key: - - test_anthropic_api_key - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "id": "msg_04AGDUDYJgAACzvnptvVoYEL", - "type": "message", - "role": "assistant", - "model": "claude-3-5-sonnet-20241022", - "content": [ - { - "type": "text", - "text": "Hi!" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 10, - "output_tokens": 3 - } - } - headers: - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Mon, 15 Dec 2024 10:00:03 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - content-length: - - '340' - status: - code: 200 - message: OK -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hi." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '104' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "authentication_error", - "message": "invalid x-api-key" - }, - "request_id": "req_011CX88XA9TbQ5AUJfKb95H1" - } - headers: - CF-RAY: - - 9be0e035a86cc60f-EWR - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:22:30 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88XA9TbQ5AUJfKb95H1 - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '14' - x-should-retry: - - 'false' - status: - code: 401 - message: Unauthorized -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hi." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '104' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88Ze7X2Hzt93cQSJVwx" - } - headers: - CF-RAY: - - 9be0e1081e29cb6b-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:23:04 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88Ze7X2Hzt93cQSJVwx - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '32' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Say hi." - } - ], - "model": "claude-sonnet-4-20250514" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '102' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "model": "claude-sonnet-4-20250514", - "id": "msg_0179PsyreizP7wUuiZek9cY1", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Hi! How are you doing today?" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 10, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "cache_creation": { - "ephemeral_5m_input_tokens": 0, - "ephemeral_1h_input_tokens": 0 - }, - "output_tokens": 11, - "service_tier": "standard" - } - } - headers: - CF-RAY: - - 9be0f5f8dbb9c484-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:37:24 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - anthropic-ratelimit-input-tokens-limit: - - '30000' - anthropic-ratelimit-input-tokens-remaining: - - '30000' - anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:37:23Z' - anthropic-ratelimit-output-tokens-limit: - - '8000' - anthropic-ratelimit-output-tokens-remaining: - - '8000' - anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:37:24Z' - anthropic-ratelimit-requests-limit: - - '50' - anthropic-ratelimit-requests-remaining: - - '49' - anthropic-ratelimit-requests-reset: - - '2026-01-14T23:37:23Z' - anthropic-ratelimit-tokens-limit: - - '38000' - anthropic-ratelimit-tokens-remaining: - - '38000' - anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:37:23Z' - cf-cache-status: - - DYNAMIC - content-length: - - '432' - request-id: - - req_011CX89esEoi7zVpEaWsLZc8 - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '2140' - status: - code: 200 - message: OK - request: body: |- { @@ -448,7 +57,7 @@ interactions: string: |- { "model": "claude-sonnet-4-20250514", - "id": "msg_015tg8KmiFK3cef86zqT6mbU", + "id": "msg_011rhQfbitTJY8ks1WedEbRj", "type": "message", "role": "assistant", "content": [ @@ -468,60 +77,61 @@ interactions: "ephemeral_1h_input_tokens": 0 }, "output_tokens": 11, - "service_tier": "standard" + "service_tier": "standard", + "inference_geo": "not_available" } } headers: CF-RAY: - - 9be0f7d4490ae55d-EWR + - 9d4340007d7109a2-EWR Connection: - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' Content-Type: - application/json Date: - - Wed, 14 Jan 2026 23:38:39 GMT + - Thu, 26 Feb 2026 23:33:54 GMT Server: - cloudflare Transfer-Encoding: - chunked X-Robots-Tag: - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 anthropic-ratelimit-input-tokens-limit: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-remaining: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:38:39Z' + - '2026-02-26T23:33:54Z' anthropic-ratelimit-output-tokens-limit: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-remaining: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:38:39Z' + - '2026-02-26T23:33:54Z' anthropic-ratelimit-requests-limit: - - '50' + - '1000' anthropic-ratelimit-requests-remaining: - - '49' + - '999' anthropic-ratelimit-requests-reset: - - '2026-01-14T23:38:39Z' + - '2026-02-26T23:33:53Z' anthropic-ratelimit-tokens-limit: - - '38000' + - '540000' anthropic-ratelimit-tokens-remaining: - - '38000' + - '540000' anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:38:39Z' + - '2026-02-26T23:33:54Z' cf-cache-status: - DYNAMIC content-length: - - '432' + - '464' request-id: - - req_011CX89kUUFsmSE8auY18PrD + - req_011CYXZArcvEvEw5LVGezFBd strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '1745' + - '1160' status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stream_propagation_error.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stream_propagation_error.yaml new file mode 100644 index 0000000000..55ed1dc693 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_stream_propagation_error.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01MhLKxY2xkfCF72SSTxUgQf","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d43408bdcfdb295-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:16 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:15Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:15Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:15Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:15Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1145' + request-id: + - req_011CYXZCVzk8sXGKiZDjKsRG + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '996' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming.yaml new file mode 100644 index 0000000000..5a9d8420c9 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_015fGzcRwVixKfgX3TK7WfTX","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d4340131ee7f2f9-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:33:57 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:33:56Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:33:56Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:33:56Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:33:56Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1132' + request-id: + - req_011CYXZB5RMcxBgcANAjoGWQ + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '936' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_aggregates_cache_tokens.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_aggregates_cache_tokens.yaml new file mode 100644 index 0000000000..a2ca41f879 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_aggregates_cache_tokens.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01BUsYc9vsjw9jfoJ527C7Xq","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d4340845b9b6d50-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:15 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:14Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:14Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:14Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:14Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1127' + request-id: + - req_011CYXZCQuQNimM1GZwNVqEj + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '950' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_captures_content.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_captures_content.yaml new file mode 100644 index 0000000000..1e5b7a8ea5 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_captures_content.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01BKdCiHiwMjwTnmgTRdLJbq","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d43401a9c7f43d0-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:33:58 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:33:57Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:33:57Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:33:57Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:33:57Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1144' + request-id: + - req_011CYXZBAVhhGEfySc8q42Tv + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '1011' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_delegates_response_attribute.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_delegates_response_attribute.yaml new file mode 100644 index 0000000000..e4ee85afcf --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_delegates_response_attribute.yaml @@ -0,0 +1,150 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hi." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '116' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_014sguaNvcSCgDympoX7TmPT","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"!"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" How"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you doing today?"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d43402b8933f793-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:01 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:00Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:00Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:00Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:00Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1619' + request-id: + - req_011CYXZBN6x8ePkJKHHEM3YC + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '1035' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_iteration.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_iteration.yaml new file mode 100644 index 0000000000..571d1dfc68 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_iteration.yaml @@ -0,0 +1,147 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hi." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '116' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01YQkMnqaSZTZ1tCXoVrzqoP","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"! How"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" are"} } + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" you doing today?"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d434022da8bb734-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:33:59 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:33:58Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:33:58Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:33:58Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:33:58Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1540' + request-id: + - req_011CYXZBG836JU3tAPte1soW + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '998' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_user_exception.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_user_exception.yaml new file mode 100644 index 0000000000..36f4b54107 --- /dev/null +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_streaming_user_exception.yaml @@ -0,0 +1,138 @@ +interactions: +- request: + body: |- + { + "max_tokens": 100, + "messages": [ + { + "role": "user", + "content": "Say hello in one word." + } + ], + "model": "claude-sonnet-4-20250514", + "stream": true + } + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + anthropic-version: + - '2023-06-01' + connection: + - keep-alive + content-length: + - '131' + content-type: + - application/json + host: + - api.anthropic.com + user-agent: + - Anthropic/Python 0.75.0 + x-api-key: + - test_anthropic_api_key + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 0.75.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.9.6 + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: |+ + event: message_start + data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01V4xbJHGawdjruyUCZ6imGT","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":2,"service_tier":"standard","inference_geo":"not_available"}} } + + event: content_block_start + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + event: ping + data: {"type": "ping"} + + event: content_block_delta + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello!"} } + + event: content_block_stop + data: {"type":"content_block_stop","index":0 } + + event: message_delta + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":13,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":5} } + + event: message_stop + data: {"type":"message_stop" } + + headers: + CF-RAY: + - 9d4340940cef9cdd-EWR + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Thu, 26 Feb 2026 23:34:17 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '450000' + anthropic-ratelimit-input-tokens-remaining: + - '450000' + anthropic-ratelimit-input-tokens-reset: + - '2026-02-26T23:34:17Z' + anthropic-ratelimit-output-tokens-limit: + - '90000' + anthropic-ratelimit-output-tokens-remaining: + - '90000' + anthropic-ratelimit-output-tokens-reset: + - '2026-02-26T23:34:17Z' + anthropic-ratelimit-requests-limit: + - '1000' + anthropic-ratelimit-requests-remaining: + - '999' + anthropic-ratelimit-requests-reset: + - '2026-02-26T23:34:17Z' + anthropic-ratelimit-tokens-limit: + - '540000' + anthropic-ratelimit-tokens-remaining: + - '540000' + anthropic-ratelimit-tokens-reset: + - '2026-02-26T23:34:17Z' + cf-cache-status: + - DYNAMIC + content-length: + - '1124' + request-id: + - req_011CYXZCbabapLuDEi6anyVX + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + vary: + - Accept-Encoding + x-envoy-upstream-service-time: + - '979' + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_token_usage.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_token_usage.yaml index e6f90a0951..b08d18409d 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_token_usage.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_token_usage.yaml @@ -1,395 +1,4 @@ interactions: -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Count to 5." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '118' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python - x-api-key: - - test_anthropic_api_key - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "id": "msg_03ZGDUDYJgAACzvnptvVoYEL", - "type": "message", - "role": "assistant", - "model": "claude-3-5-sonnet-20241022", - "content": [ - { - "type": "text", - "text": "1, 2, 3, 4, 5" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 12, - "output_tokens": 14 - } - } - headers: - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Mon, 15 Dec 2024 10:00:02 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - content-length: - - '355' - status: - code: 200 - message: OK -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Count to 5." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '108' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "authentication_error", - "message": "invalid x-api-key" - }, - "request_id": "req_011CX88X9HN93DBTEXsjo9DZ" - } - headers: - CF-RAY: - - 9be0e0346f01a0f4-EWR - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:22:30 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88X9HN93DBTEXsjo9DZ - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '11' - x-should-retry: - - 'false' - status: - code: 401 - message: Unauthorized -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Count to 5." - } - ], - "model": "claude-3-5-sonnet-20241022" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '108' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88Zd4WpWvH6NbSKLd4T" - } - headers: - CF-RAY: - - 9be0e1068d28f9a9-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:23:04 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88Zd4WpWvH6NbSKLd4T - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '23' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 100, - "messages": [ - { - "role": "user", - "content": "Count to 5." - } - ], - "model": "claude-sonnet-4-20250514" - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '106' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "model": "claude-sonnet-4-20250514", - "id": "msg_01ASpi9ptEqzyzyCGk3aB1tr", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "1\n2\n3\n4\n5" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 12, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "cache_creation": { - "ephemeral_5m_input_tokens": 0, - "ephemeral_1h_input_tokens": 0 - }, - "output_tokens": 13, - "service_tier": "standard" - } - } - headers: - CF-RAY: - - 9be0f5e96d19cc98-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:37:21 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - anthropic-ratelimit-input-tokens-limit: - - '30000' - anthropic-ratelimit-input-tokens-remaining: - - '30000' - anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:37:21Z' - anthropic-ratelimit-output-tokens-limit: - - '8000' - anthropic-ratelimit-output-tokens-remaining: - - '8000' - anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:37:21Z' - anthropic-ratelimit-requests-limit: - - '50' - anthropic-ratelimit-requests-remaining: - - '49' - anthropic-ratelimit-requests-reset: - - '2026-01-14T23:37:20Z' - anthropic-ratelimit-tokens-limit: - - '38000' - anthropic-ratelimit-tokens-remaining: - - '38000' - anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:37:21Z' - cf-cache-status: - - DYNAMIC - content-length: - - '417' - request-id: - - req_011CX89egfZS3S8npjMnb4jb - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '2246' - status: - code: 200 - message: OK - request: body: |- { @@ -448,7 +57,7 @@ interactions: string: |- { "model": "claude-sonnet-4-20250514", - "id": "msg_01JFQS8RhcxvidJNQvmANwSp", + "id": "msg_01BenGkMmyoqjV3wcoTksLFe", "type": "message", "role": "assistant", "content": [ @@ -468,60 +77,61 @@ interactions: "ephemeral_1h_input_tokens": 0 }, "output_tokens": 17, - "service_tier": "standard" + "service_tier": "standard", + "inference_geo": "not_available" } } headers: CF-RAY: - - 9be0f7c51d21c62c-EWR + - 9d433ff7c9bd939a-EWR Connection: - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' Content-Type: - application/json Date: - - Wed, 14 Jan 2026 23:38:37 GMT + - Thu, 26 Feb 2026 23:33:53 GMT Server: - cloudflare Transfer-Encoding: - chunked X-Robots-Tag: - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 anthropic-ratelimit-input-tokens-limit: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-remaining: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:38:36Z' + - '2026-02-26T23:33:53Z' anthropic-ratelimit-output-tokens-limit: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-remaining: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:38:38Z' + - '2026-02-26T23:33:53Z' anthropic-ratelimit-requests-limit: - - '50' + - '1000' anthropic-ratelimit-requests-remaining: - - '49' + - '999' anthropic-ratelimit-requests-reset: - - '2026-01-14T23:38:36Z' + - '2026-02-26T23:33:52Z' anthropic-ratelimit-tokens-limit: - - '38000' + - '540000' anthropic-ratelimit-tokens-remaining: - - '38000' + - '540000' anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:38:36Z' + - '2026-02-26T23:33:53Z' cf-cache-status: - DYNAMIC content-length: - - '417' + - '449' request-id: - - req_011CX89kJ5gA2k39mva9DeiB + - req_011CYXZAkgV5nXq6SWeYx1xy strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '2244' + - '1233' status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_with_all_params.yaml b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_with_all_params.yaml index b63b5b56da..0100ed7640 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_with_all_params.yaml +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/cassettes/test_sync_messages_create_with_all_params.yaml @@ -1,419 +1,4 @@ interactions: -- request: - body: |- - { - "max_tokens": 50, - "messages": [ - { - "role": "user", - "content": "Say hello." - } - ], - "model": "claude-3-5-sonnet-20241022", - "stop_sequences": [ - "STOP" - ], - "temperature": 0.7, - "top_k": 40, - "top_p": 0.9 - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '200' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python - x-api-key: - - test_anthropic_api_key - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "id": "msg_02YGDUDYJgAACzvnptvVoYEL", - "type": "message", - "role": "assistant", - "model": "claude-3-5-sonnet-20241022", - "content": [ - { - "type": "text", - "text": "Hello! How can I help you today?" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 10, - "output_tokens": 10 - } - } - headers: - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Mon, 15 Dec 2024 10:00:01 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - content-length: - - '380' - status: - code: 200 - message: OK -- request: - body: |- - { - "max_tokens": 50, - "messages": [ - { - "role": "user", - "content": "Say hello." - } - ], - "model": "claude-3-5-sonnet-20241022", - "stop_sequences": [ - "STOP" - ], - "temperature": 0.7, - "top_k": 40, - "top_p": 0.9 - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '173' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "authentication_error", - "message": "invalid x-api-key" - }, - "request_id": "req_011CX88X8R2SNXCvHpf8jdxa" - } - headers: - CF-RAY: - - 9be0e0331fe3effa-EWR - Connection: - - keep-alive - Content-Length: - - '130' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:22:30 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88X8R2SNXCvHpf8jdxa - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '14' - x-should-retry: - - 'false' - status: - code: 401 - message: Unauthorized -- request: - body: |- - { - "max_tokens": 50, - "messages": [ - { - "role": "user", - "content": "Say hello." - } - ], - "model": "claude-3-5-sonnet-20241022", - "stop_sequences": [ - "STOP" - ], - "temperature": 0.7, - "top_k": 40, - "top_p": 0.9 - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '173' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "type": "error", - "error": { - "type": "invalid_request_error", - "message": "Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits." - }, - "request_id": "req_011CX88Zc1kwbPkJkhghetKz" - } - headers: - CF-RAY: - - 9be0e10508204ba5-EWR - Connection: - - keep-alive - Content-Length: - - '234' - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:23:03 GMT - Server: - - cloudflare - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - cf-cache-status: - - DYNAMIC - request-id: - - req_011CX88Zc1kwbPkJkhghetKz - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '31' - x-should-retry: - - 'false' - status: - code: 400 - message: Bad Request -- request: - body: |- - { - "max_tokens": 50, - "messages": [ - { - "role": "user", - "content": "Say hello." - } - ], - "model": "claude-sonnet-4-20250514", - "stop_sequences": [ - "STOP" - ], - "temperature": 0.7, - "top_k": 40, - "top_p": 0.9 - } - headers: - accept: - - application/json - accept-encoding: - - gzip, deflate - anthropic-version: - - '2023-06-01' - connection: - - keep-alive - content-length: - - '171' - content-type: - - application/json - host: - - api.anthropic.com - user-agent: - - Anthropic/Python 0.75.0 - x-api-key: - - test_anthropic_api_key - x-stainless-arch: - - arm64 - x-stainless-async: - - 'false' - x-stainless-lang: - - python - x-stainless-os: - - MacOS - x-stainless-package-version: - - 0.75.0 - x-stainless-read-timeout: - - '600' - x-stainless-retry-count: - - '0' - x-stainless-runtime: - - CPython - x-stainless-runtime-version: - - 3.9.6 - x-stainless-timeout: - - '600' - method: POST - uri: https://api.anthropic.com/v1/messages - response: - body: - string: |- - { - "model": "claude-sonnet-4-20250514", - "id": "msg_01Nf6xgm48TeELiSicTA83cX", - "type": "message", - "role": "assistant", - "content": [ - { - "type": "text", - "text": "Hello! How are you doing today?" - } - ], - "stop_reason": "end_turn", - "stop_sequence": null, - "usage": { - "input_tokens": 10, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0, - "cache_creation": { - "ephemeral_5m_input_tokens": 0, - "ephemeral_1h_input_tokens": 0 - }, - "output_tokens": 11, - "service_tier": "standard" - } - } - headers: - CF-RAY: - - 9be0f5db9b8110f3-EWR - Connection: - - keep-alive - Content-Type: - - application/json - Date: - - Wed, 14 Jan 2026 23:37:19 GMT - Server: - - cloudflare - Transfer-Encoding: - - chunked - X-Robots-Tag: - - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 - anthropic-ratelimit-input-tokens-limit: - - '30000' - anthropic-ratelimit-input-tokens-remaining: - - '30000' - anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:37:18Z' - anthropic-ratelimit-output-tokens-limit: - - '8000' - anthropic-ratelimit-output-tokens-remaining: - - '8000' - anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:37:19Z' - anthropic-ratelimit-requests-limit: - - '50' - anthropic-ratelimit-requests-remaining: - - '49' - anthropic-ratelimit-requests-reset: - - '2026-01-14T23:37:18Z' - anthropic-ratelimit-tokens-limit: - - '38000' - anthropic-ratelimit-tokens-remaining: - - '38000' - anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:37:18Z' - cf-cache-status: - - DYNAMIC - content-length: - - '435' - request-id: - - req_011CX89eXCYZvbfUZDzWQK3e - strict-transport-security: - - max-age=31536000; includeSubDomains; preload - x-envoy-upstream-service-time: - - '1953' - status: - code: 200 - message: OK - request: body: |- { @@ -478,13 +63,13 @@ interactions: string: |- { "model": "claude-sonnet-4-20250514", - "id": "msg_015XXNApQAi4ZgazLmEDHne6", + "id": "msg_01TmHCb7QRNTPGCbdZCsFH8q", "type": "message", "role": "assistant", "content": [ { "type": "text", - "text": "Hello! How are you doing today?" + "text": "Hello! It's nice to meet you. How are you doing today?" } ], "stop_reason": "end_turn", @@ -497,61 +82,62 @@ interactions: "ephemeral_5m_input_tokens": 0, "ephemeral_1h_input_tokens": 0 }, - "output_tokens": 11, - "service_tier": "standard" + "output_tokens": 18, + "service_tier": "standard", + "inference_geo": "not_available" } } headers: CF-RAY: - - 9be0f7b73af7c269-EWR + - 9d433fefb83acd8b-EWR Connection: - keep-alive + Content-Security-Policy: + - default-src 'none'; frame-ancestors 'none' Content-Type: - application/json Date: - - Wed, 14 Jan 2026 23:38:35 GMT + - Thu, 26 Feb 2026 23:33:51 GMT Server: - cloudflare Transfer-Encoding: - chunked X-Robots-Tag: - none - anthropic-organization-id: - - 455ea6be-bd92-4199-83ec-0c6b39c5c169 anthropic-ratelimit-input-tokens-limit: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-remaining: - - '30000' + - '450000' anthropic-ratelimit-input-tokens-reset: - - '2026-01-14T23:38:35Z' + - '2026-02-26T23:33:51Z' anthropic-ratelimit-output-tokens-limit: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-remaining: - - '8000' + - '90000' anthropic-ratelimit-output-tokens-reset: - - '2026-01-14T23:38:35Z' + - '2026-02-26T23:33:51Z' anthropic-ratelimit-requests-limit: - - '50' + - '1000' anthropic-ratelimit-requests-remaining: - - '49' + - '999' anthropic-ratelimit-requests-reset: - - '2026-01-14T23:38:34Z' + - '2026-02-26T23:33:50Z' anthropic-ratelimit-tokens-limit: - - '38000' + - '540000' anthropic-ratelimit-tokens-remaining: - - '38000' + - '540000' anthropic-ratelimit-tokens-reset: - - '2026-01-14T23:38:35Z' + - '2026-02-26T23:33:51Z' cf-cache-status: - DYNAMIC content-length: - - '435' + - '490' request-id: - - req_011CX89k8aBRkvFFLPZSBSrX + - req_011CYXZAfAqyTW82t5hSJb5z strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '1955' + - '1150' status: code: 200 message: OK diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/conftest.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/conftest.py index 9f660d1fd4..c9573d8251 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/conftest.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/conftest.py @@ -22,6 +22,10 @@ import yaml from anthropic import Anthropic +from opentelemetry.instrumentation._semconv import ( + OTEL_SEMCONV_STABILITY_OPT_IN, + _OpenTelemetrySemanticConventionStability, +) from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( @@ -37,6 +41,7 @@ ) from opentelemetry.util.genai.environment_variables import ( OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, + OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) @@ -115,8 +120,12 @@ def vcr_config(): @pytest.fixture(scope="function") def instrument_no_content(tracer_provider, logger_provider, meter_provider): """Instrument Anthropic without content capture.""" + _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update( - {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "False"} + { + OTEL_SEMCONV_STABILITY_OPT_IN: "stable", + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "NO_CONTENT", + } ) instrumentor = AnthropicInstrumentor() @@ -126,16 +135,26 @@ def instrument_no_content(tracer_provider, logger_provider, meter_provider): meter_provider=meter_provider, ) - yield instrumentor - os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) - instrumentor.uninstrument() + try: + yield instrumentor + finally: + os.environ.pop( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None + ) + os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) + instrumentor.uninstrument() + _OpenTelemetrySemanticConventionStability._initialized = False @pytest.fixture(scope="function") def instrument_with_content(tracer_provider, logger_provider, meter_provider): """Instrument Anthropic with content capture enabled.""" + _OpenTelemetrySemanticConventionStability._initialized = False os.environ.update( - {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "True"} + { + OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "SPAN_ONLY", + } ) instrumentor = AnthropicInstrumentor() instrumentor.instrument( @@ -144,9 +163,45 @@ def instrument_with_content(tracer_provider, logger_provider, meter_provider): meter_provider=meter_provider, ) - yield instrumentor - os.environ.pop(OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None) - instrumentor.uninstrument() + try: + yield instrumentor + finally: + os.environ.pop( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None + ) + os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) + instrumentor.uninstrument() + _OpenTelemetrySemanticConventionStability._initialized = False + + +@pytest.fixture(scope="function") +def instrument_event_only(tracer_provider, logger_provider, meter_provider): + """Instrument Anthropic with EVENT_ONLY content capture.""" + _OpenTelemetrySemanticConventionStability._initialized = False + os.environ.update( + { + OTEL_SEMCONV_STABILITY_OPT_IN: "gen_ai_latest_experimental", + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "EVENT_ONLY", + OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT: "true", + } + ) + instrumentor = AnthropicInstrumentor() + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + try: + yield instrumentor + finally: + os.environ.pop( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, None + ) + os.environ.pop(OTEL_SEMCONV_STABILITY_OPT_IN, None) + os.environ.pop(OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, None) + instrumentor.uninstrument() + _OpenTelemetrySemanticConventionStability._initialized = False @pytest.fixture @@ -230,6 +285,16 @@ def fixture_vcr(vcr): return vcr +_SCRUBBED_RESPONSE_HEADERS = frozenset( + { + "anthropic-organization-id", + } +) + + def scrub_response_headers(response): - """Scrub sensitive response headers.""" + """Scrub sensitive headers from recorded responses.""" + headers = response.get("headers", {}) + for header in _SCRUBBED_RESPONSE_HEADERS: + headers.pop(header, None) return response diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/requirements.oldest.txt b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/requirements.oldest.txt index b962a02555..0b77b1a7cb 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/requirements.oldest.txt +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/requirements.oldest.txt @@ -16,8 +16,7 @@ # the oldest supported version of external dependencies. -e util/opentelemetry-util-genai -anthropic==0.16.0 -httpx>=0.25.2,<0.28.0 # Pin to version compatible with anthropic 0.16.0 (proxies arg removed in 0.28) +anthropic==0.51.0 pytest==7.4.4 pytest-vcr==1.0.2 pytest-asyncio==0.21.0 diff --git a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py index f656f61eef..6bce6f2e16 100644 --- a/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py +++ b/instrumentation-genai/opentelemetry-instrumentation-anthropic/tests/test_sync_messages.py @@ -14,10 +14,23 @@ """Tests for sync Messages.create instrumentation.""" +import inspect +import json +import os +from pathlib import Path + import pytest from anthropic import Anthropic, APIConnectionError, NotFoundError +from anthropic.resources.messages import Messages as _Messages from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor +from opentelemetry.instrumentation.anthropic.messages_extractors import ( + GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS, + GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS, +) +from opentelemetry.instrumentation.anthropic.wrappers import ( + MessagesStreamWrapper, +) from opentelemetry.semconv._incubating.attributes import ( error_attributes as ErrorAttributes, ) @@ -28,8 +41,32 @@ server_attributes as ServerAttributes, ) +# Detect whether the installed anthropic SDK supports tools / thinking params. +# Older SDK versions (e.g. 0.16.0) do not accept these keyword arguments. +_create_params = set(inspect.signature(_Messages.create).parameters) +_has_tools_param = "tools" in _create_params +_has_thinking_param = "thinking" in _create_params + + +def normalize_stop_reason(stop_reason): + """Map Anthropic stop reasons to GenAI semconv values.""" + return { + "end_turn": "stop", + "stop_sequence": "stop", + "max_tokens": "length", + "tool_use": "tool_calls", + }.get(stop_reason, stop_reason) -def assert_span_attributes( + +def expected_input_tokens(usage): + """Compute semconv input tokens from Anthropic usage.""" + base = getattr(usage, "input_tokens", 0) or 0 + cache_creation = getattr(usage, "cache_creation_input_tokens", 0) or 0 + cache_read = getattr(usage, "cache_read_input_tokens", 0) or 0 + return base + cache_creation + cache_read + + +def assert_span_attributes( # pylint: disable=too-many-arguments span, request_model, response_id=None, @@ -86,6 +123,27 @@ def assert_span_attributes( ) +def _load_span_messages(span, attribute): + value = span.attributes.get(attribute) + assert value is not None + assert isinstance(value, str) + parsed = json.loads(value) + assert isinstance(parsed, list) + return parsed + + +def _skip_if_cassette_missing_and_no_real_key(request): + cassette_path = ( + Path(__file__).parent / "cassettes" / f"{request.node.name}.yaml" + ) + api_key = os.getenv("ANTHROPIC_API_KEY") + if not cassette_path.exists() and api_key == "test_anthropic_api_key": + pytest.skip( + f"Cassette {cassette_path.name} is missing. " + "Set a real ANTHROPIC_API_KEY to record it." + ) + + @pytest.mark.vcr() def test_sync_messages_create_basic( span_exporter, anthropic_client, instrument_no_content @@ -108,11 +166,42 @@ def test_sync_messages_create_basic( request_model=model, response_id=response.id, response_model=response.model, - input_tokens=response.usage.input_tokens, + input_tokens=expected_input_tokens(response.usage), output_tokens=response.usage.output_tokens, - finish_reasons=[response.stop_reason], + finish_reasons=[normalize_stop_reason(response.stop_reason)], + ) + + +@pytest.mark.vcr() +def test_sync_messages_create_captures_content( + span_exporter, anthropic_client, instrument_with_content +): + """Test content capture on non-streaming create.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + input_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_INPUT_MESSAGES + ) + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES ) + assert input_messages[0]["role"] == "user" + assert input_messages[0]["parts"][0]["type"] == "text" + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"][0]["type"] == "text" + @pytest.mark.vcr() def test_sync_messages_create_with_all_params( @@ -165,10 +254,9 @@ def test_sync_messages_create_token_usage( span = spans[0] assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS in span.attributes assert GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS in span.attributes - assert ( - span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] - == response.usage.input_tokens - ) + assert span.attributes[ + GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS + ] == expected_input_tokens(response.usage) assert ( span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] == response.usage.output_tokens @@ -195,7 +283,7 @@ def test_sync_messages_create_stop_reason( span = spans[0] # Anthropic's stop_reason should be wrapped in a tuple (OTel converts lists) assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( - response.stop_reason, + normalize_stop_reason(response.stop_reason), ) @@ -300,3 +388,554 @@ def test_multiple_instrument_uninstrument_cycles( meter_provider=meter_provider, ) instrumentor.uninstrument() + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming( # pylint: disable=too-many-locals + span_exporter, anthropic_client, instrument_no_content +): + """Test streaming message creation produces correct span.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + # Collect response data from stream + response_text = "" + response_id = None + response_model = None + stop_reason = None + input_tokens = None + output_tokens = None + + with anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) as stream: + for chunk in stream: + # Extract data from chunks for assertion + if chunk.type == "message_start": + message = getattr(chunk, "message", None) + if message: + response_id = getattr(message, "id", None) + response_model = getattr(message, "model", None) + usage = getattr(message, "usage", None) + if usage: + input_tokens = getattr(usage, "input_tokens", None) + elif chunk.type == "content_block_delta": + delta = getattr(chunk, "delta", None) + if delta and hasattr(delta, "text"): + response_text += delta.text + elif chunk.type == "message_delta": + delta = getattr(chunk, "delta", None) + if delta: + stop_reason = getattr(delta, "stop_reason", None) + usage = getattr(chunk, "usage", None) + if usage: + output_tokens = getattr(usage, "output_tokens", None) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + + assert_span_attributes( + spans[0], + request_model=model, + response_id=response_id, + response_model=response_model, + input_tokens=input_tokens, + output_tokens=output_tokens, + finish_reasons=[normalize_stop_reason(stop_reason)] + if stop_reason + else None, + ) + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming_captures_content( + span_exporter, anthropic_client, instrument_with_content +): + """Test content capture on create(stream=True).""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + with anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) as stream: + for _ in stream: + pass + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + input_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_INPUT_MESSAGES + ) + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + assert input_messages[0]["role"] == "user" + assert output_messages[0]["role"] == "assistant" + assert output_messages[0]["parts"] + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming_iteration( + span_exporter, anthropic_client, instrument_no_content +): + """Test streaming with direct iteration (without context manager).""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hi."}] + + stream = anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) + + # Consume the stream by iterating + chunks = list(stream) + assert len(chunks) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + # Verify span has response attributes from streaming + assert GenAIAttributes.GEN_AI_RESPONSE_ID in span.attributes + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming_delegates_response_attribute( + request, anthropic_client, instrument_no_content +): + """Stream wrapper should expose attributes from the wrapped stream.""" + _skip_if_cassette_missing_and_no_real_key(request) + + stream = anthropic_client.messages.create( + model="claude-sonnet-4-20250514", + max_tokens=100, + messages=[{"role": "user", "content": "Say hi."}], + stream=True, + ) + + # `response` exists on Anthropic Stream and should be reachable on wrapper. + assert stream.response is not None + assert stream.response.status_code == 200 + assert stream.response.headers.get("request-id") is not None + stream.close() + + +def test_sync_messages_create_streaming_connection_error( + span_exporter, instrument_no_content +): + """Test that connection errors during streaming are handled correctly.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Hello"}] + + # Create client with invalid endpoint + client = Anthropic(base_url="http://localhost:9999") + + with pytest.raises(APIConnectionError): + client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + timeout=0.1, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + + span = spans[0] + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + assert ErrorAttributes.ERROR_TYPE in span.attributes + assert "APIConnectionError" in span.attributes[ErrorAttributes.ERROR_TYPE] + + +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_tools_param, + reason="anthropic SDK too old to support 'tools' parameter", +) +def test_sync_messages_create_captures_tool_use_content( + request, span_exporter, anthropic_client, instrument_with_content +): + """Test that tool_use blocks are captured as tool_call parts.""" + _skip_if_cassette_missing_and_no_real_key(request) + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "What is the weather in SF?"}] + + anthropic_client.messages.create( + model=model, + max_tokens=256, + messages=messages, + tools=[ + { + "name": "get_weather", + "description": "Get weather by city", + "input_schema": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + } + ], + tool_choice={"type": "tool", "name": "get_weather"}, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + + assert any( + part.get("type") == "tool_call" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.vcr() +@pytest.mark.skipif( + not _has_thinking_param, + reason="anthropic SDK too old to support 'thinking' parameter", +) +def test_sync_messages_create_captures_thinking_content( + request, span_exporter, anthropic_client, instrument_with_content +): + """Test that thinking blocks are captured as reasoning parts.""" + _skip_if_cassette_missing_and_no_real_key(request) + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "What is 17*19? Think first."}] + + anthropic_client.messages.create( + model=model, + max_tokens=16000, + messages=messages, + thinking={"type": "enabled", "budget_tokens": 10000}, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + output_messages = _load_span_messages( + span, GenAIAttributes.GEN_AI_OUTPUT_MESSAGES + ) + + assert any( + part.get("type") == "reasoning" + for message in output_messages + for part in message.get("parts", []) + ) + + +@pytest.mark.vcr() +def test_stream_wrapper_finalize_idempotent( # pylint: disable=too-many-locals + span_exporter, + anthropic_client, + instrument_no_content, +): + """Fully consumed stream plus explicit close should still yield one span.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + stream = anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) + + response_id = None + response_model = None + stop_reason = None + input_tokens = None + output_tokens = None + + # Consume the stream fully, then call close() to verify idempotent finalization. + for chunk in stream: + if chunk.type == "message_start": + message = getattr(chunk, "message", None) + if message: + response_id = getattr(message, "id", None) + response_model = getattr(message, "model", None) + usage = getattr(message, "usage", None) + if usage: + input_tokens = expected_input_tokens(usage) + elif chunk.type == "message_delta": + delta = getattr(chunk, "delta", None) + if delta: + stop_reason = getattr(delta, "stop_reason", None) + usage = getattr(chunk, "usage", None) + if usage: + output_tokens = getattr(usage, "output_tokens", None) + input_tokens = expected_input_tokens(usage) + + stream.close() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + assert_span_attributes( + spans[0], + request_model=model, + response_id=response_id, + response_model=response_model, + input_tokens=input_tokens, + output_tokens=output_tokens, + finish_reasons=[normalize_stop_reason(stop_reason)] + if stop_reason + else None, + ) + + +@pytest.mark.vcr() +def test_sync_messages_create_aggregates_cache_tokens( + span_exporter, anthropic_client, instrument_no_content +): + """Non-streaming response with non-zero cache tokens aggregates correctly.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + response = anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS in span.attributes + assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS in span.attributes + assert span.attributes[ + GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS + ] == expected_input_tokens(response.usage) + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] + == response.usage.output_tokens + ) + cache_creation = getattr(response.usage, "cache_creation_input_tokens", 0) + cache_read = getattr(response.usage, "cache_read_input_tokens", 0) + assert ( + span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] + == cache_creation + ) + assert span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == cache_read + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming_aggregates_cache_tokens( + span_exporter, anthropic_client, instrument_no_content +): + """Streaming response with non-zero cache tokens aggregates correctly.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + input_tokens = None + output_tokens = None + cache_creation = None + cache_read = None + + with anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) as stream: + for chunk in stream: + if chunk.type == "message_delta": + usage = getattr(chunk, "usage", None) + if usage: + input_tokens = expected_input_tokens(usage) + output_tokens = getattr(usage, "output_tokens", None) + cache_creation = getattr( + usage, "cache_creation_input_tokens", None + ) + cache_read = getattr( + usage, "cache_read_input_tokens", None + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS in span.attributes + assert GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS in span.attributes + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS] + == input_tokens + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS] + == output_tokens + ) + assert ( + span.attributes[GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS] + == cache_creation + ) + assert span.attributes[GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS] == cache_read + + +@pytest.mark.vcr() +def test_sync_messages_create_stream_propagation_error( + span_exporter, anthropic_client, instrument_no_content, monkeypatch +): + """Mid-stream errors from the underlying iterator must propagate and record error on span.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + stream = anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) + + # Wrap the underlying stream so we inject an iteration error but still + # delegate close()/other behavior to the real stream. + class ErrorInjectingStreamDelegate: + def __init__(self, inner): + self._inner = inner + self._count = 0 + + def __iter__(self): + return self + + def __next__(self): + # Fail after yielding one chunk so this exercises a mid-stream error. + if self._count == 1: + raise ConnectionError("connection reset during stream") + self._count += 1 + return next(self._inner) + + def close(self): + return self._inner.close() + + def __getattr__(self, name): + return getattr(self._inner, name) + + monkeypatch.setattr( + stream, "_stream", ErrorInjectingStreamDelegate(stream._stream) + ) + + with pytest.raises( + ConnectionError, match="connection reset during stream" + ): + with stream: + for _ in stream: + pass + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ConnectionError" + + +@pytest.mark.vcr() +def test_sync_messages_create_streaming_user_exception( + span_exporter, anthropic_client, instrument_no_content +): + """Test that user raised exceptions are propagated.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + with pytest.raises(ValueError, match="User raised exception"): + with anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) as stream: + for _ in stream: + raise ValueError("User raised exception") + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "ValueError" + + +@pytest.mark.vcr() +def test_sync_messages_create_instrumentation_error_swallowed( + span_exporter, anthropic_client, instrument_no_content, monkeypatch +): + """Instrumentation errors in _process_chunk must not propagate to user code.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + def exploding_process_chunk(self, chunk): + raise RuntimeError("instrumentation bug") + + monkeypatch.setattr( + MessagesStreamWrapper, "_process_chunk", exploding_process_chunk + ) + + with anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + stream=True, + ) as stream: + chunks = list(stream) + + assert len(chunks) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + assert ErrorAttributes.ERROR_TYPE not in span.attributes + + +# ============================================================================= +# Tests for EVENT_ONLY content capture mode +# ============================================================================= + + +@pytest.mark.vcr() +def test_sync_messages_create_event_only_no_content_in_span( + span_exporter, log_exporter, anthropic_client, instrument_event_only +): + """Test that EVENT_ONLY mode does not capture content in span attributes + but does emit a log event with the content.""" + model = "claude-sonnet-4-20250514" + messages = [{"role": "user", "content": "Say hello in one word."}] + + anthropic_client.messages.create( + model=model, + max_tokens=100, + messages=messages, + ) + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + # Content should NOT be in span attributes under EVENT_ONLY + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + + # Basic span attributes should still be present + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == model + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL in span.attributes + assert GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS in span.attributes + assert GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS in span.attributes + + # A log event should have been emitted with the content + logs = log_exporter.get_finished_logs() + assert len(logs) == 1 + log_record = logs[0].log_record + assert log_record.event_name == "gen_ai.client.inference.operation.details" diff --git a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py index 616d9ab471..3fb6417cee 100644 --- a/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py +++ b/util/opentelemetry-util-genai/src/opentelemetry/util/genai/utils.py @@ -111,6 +111,18 @@ def should_emit_event() -> bool: return False +def should_capture_content() -> bool: + """Return True when content conversion should be performed.""" + if not is_experimental_mode(): + return False + mode = get_content_capturing_mode() + if mode == ContentCapturingMode.NO_CONTENT: + return False + if mode == ContentCapturingMode.EVENT_ONLY and not should_emit_event(): + return False + return True + + class _GenAiJsonEncoder(json.JSONEncoder): def default(self, o: Any) -> Any: if isinstance(o, bytes): diff --git a/util/opentelemetry-util-genai/tests/test_events_options.py b/util/opentelemetry-util-genai/tests/test_events_options.py index 78476de880..f83fc9562e 100644 --- a/util/opentelemetry-util-genai/tests/test_events_options.py +++ b/util/opentelemetry-util-genai/tests/test_events_options.py @@ -25,6 +25,7 @@ OTEL_INSTRUMENTATION_GENAI_EMIT_EVENT, ) from opentelemetry.util.genai.utils import ( + should_capture_content, should_emit_event, ) @@ -206,3 +207,45 @@ def test_should_emit_event_user_setting_overrides_default_for_no_content( ): # pylint: disable=no-self-use # User explicitly setting emit_event="true" should override the default (False for NO_CONTENT) assert should_emit_event() is True + + +class TestShouldCaptureContent(unittest.TestCase): + @patch_env_vars( + stability_mode="default", + content_capturing="SPAN_AND_EVENT", + emit_event="true", + ) + def test_should_capture_content_false_when_not_experimental( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="NO_CONTENT", + emit_event="true", + ) + def test_should_capture_content_false_when_no_content( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="EVENT_ONLY", + emit_event="false", + ) + def test_should_capture_content_false_when_event_only_but_event_disabled( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is False + + @patch_env_vars( + stability_mode="gen_ai_latest_experimental", + content_capturing="SPAN_ONLY", + emit_event="", + ) + def test_should_capture_content_true_for_span_only( + self, + ): # pylint: disable=no-self-use + assert should_capture_content() is True diff --git a/uv.lock b/uv.lock index d75bd7e73c..f7cb5dc20b 100644 --- a/uv.lock +++ b/uv.lock @@ -2858,7 +2858,7 @@ instruments = [ [package.metadata] requires-dist = [ - { name = "anthropic", marker = "extra == 'instruments'", specifier = ">=0.16.0" }, + { name = "anthropic", marker = "extra == 'instruments'", specifier = ">=0.51.0" }, { name = "opentelemetry-api", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-api&branch=main" }, { name = "opentelemetry-instrumentation", editable = "opentelemetry-instrumentation" }, { name = "opentelemetry-semantic-conventions", git = "https://github.com/open-telemetry/opentelemetry-python?subdirectory=opentelemetry-semantic-conventions&branch=main" },