diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/.changelog/488.added b/instrumentation/opentelemetry-instrumentation-genai-bedrock/.changelog/488.added new file mode 100644 index 000000000..fa110eb29 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/.changelog/488.added @@ -0,0 +1 @@ +Add instrumentation for ``Converse`` and ``ConverseStream``. diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/README.rst b/instrumentation/opentelemetry-instrumentation-genai-bedrock/README.rst index e851fc59a..7502c6e96 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/README.rst +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/README.rst @@ -1,9 +1,14 @@ OpenTelemetry Amazon Bedrock Instrumentation ============================================ -This package provides the setup for instrumenting Amazon Bedrock with OpenTelemetry -Generative AI semantic conventions. Amazon Bedrock operation instrumentation will be -added in follow-up changes. +This package provides OpenTelemetry instrumentation for Amazon Bedrock (via the AWS SDK for Python, ``boto3``), +implementing the OpenTelemetry Generative AI semantic conventions. + +Supported Operations +-------------------- + +* Synchronous chat via the Converse API (``client.converse``) +* Streaming chat via the ConverseStream API (``client.converse_stream``) Installation ------------ @@ -15,12 +20,21 @@ Installation Usage ----- -:: +.. code-block:: python + import boto3 from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor + # Enable instrumentation BedrockInstrumentor().instrument() + # Use Bedrock runtime client normally + client = boto3.client("bedrock-runtime", region_name="us-east-1") + response = client.converse( + modelId="amazon.nova-micro-v1:0", + messages=[{"role": "user", "content": [{"text": "Hello, Bedrock!"}]}], + ) + Configuration ------------- diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/custom_hook.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/custom_hook.py new file mode 100644 index 000000000..480c120e7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/custom_hook.py @@ -0,0 +1,96 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +# pylint: skip-file +"""Same as main.py, but instruments Amazon Bedrock with a custom CompletionHook +that prints prompts and completions to stdout. + +Run with: dotenv run -- python custom_hook.py +""" + +import boto3 + +from opentelemetry import _logs, metrics, trace +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( + OTLPLogExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( + OTLPMetricExporter, +) +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( + OTLPSpanExporter, +) +from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.util.genai.completion_hook import CompletionHook +from opentelemetry.util.genai.types import ( + InputMessage, + MessagePart, + OutputMessage, + ToolDefinition, +) + + +class PrintCompletionHook(CompletionHook): + """Minimal CompletionHook that prints inputs/outputs to stdout. + + Real hooks typically forward content to external storage (object store, + database, etc.) and record reference URIs on the span/log_record. + """ + + def on_completion( + self, + *, + inputs: list[InputMessage], + outputs: list[OutputMessage], + system_instruction: list[MessagePart], + tool_definitions: list[ToolDefinition] | None = None, + span=None, + log_record=None, + ) -> None: + print(f"[hook] inputs: {inputs}") + print(f"[hook] outputs: {outputs}") + + +trace.set_tracer_provider(TracerProvider()) +trace.get_tracer_provider().add_span_processor( + BatchSpanProcessor(OTLPSpanExporter()) +) + +_logs.set_logger_provider(LoggerProvider()) +_logs.get_logger_provider().add_log_record_processor( + BatchLogRecordProcessor(OTLPLogExporter()) +) + +metrics.set_meter_provider( + MeterProvider( + metric_readers=[ + PeriodicExportingMetricReader(OTLPMetricExporter()), + ] + ) +) + +BedrockInstrumentor().instrument(completion_hook=PrintCompletionHook()) + + +def main(): + client = boto3.client("bedrock-runtime", region_name="us-east-1") + response = client.converse( + modelId="amazon.nova-micro-v1:0", + messages=[ + { + "role": "user", + "content": [{"text": "Write a short poem on OpenTelemetry."}], + } + ], + ) + print(response) + + +if __name__ == "__main__": + main() diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/main.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/main.py index 2d262545a..f8b8b647f 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/main.py +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/manual/main.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file -import json import boto3 @@ -37,16 +36,16 @@ def main(): client = boto3.client("bedrock-runtime", region_name="us-east-1") - response = client.invoke_model( - modelId="amazon.titan-text-lite-v1", - body=json.dumps( + response = client.converse( + modelId="amazon.nova-micro-v1:0", + messages=[ { - "inputText": "Write a short poem on OpenTelemetry.", + "role": "user", + "content": [{"text": "Write a short poem on OpenTelemetry."}], } - ), + ], ) - result = json.loads(response["body"].read()) - print(result) + print(response) if __name__ == "__main__": diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/zero-code/main.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/zero-code/main.py index 0ff36ffd1..51e93872b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/zero-code/main.py +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/examples/zero-code/main.py @@ -2,23 +2,22 @@ # SPDX-License-Identifier: Apache-2.0 # pylint: skip-file -import json import boto3 def main(): client = boto3.client("bedrock-runtime", region_name="us-east-1") - response = client.invoke_model( - modelId="amazon.titan-text-lite-v1", - body=json.dumps( + response = client.converse( + modelId="amazon.nova-micro-v1:0", + messages=[ { - "inputText": "Write a short poem on OpenTelemetry.", + "role": "user", + "content": [{"text": "Write a short poem on OpenTelemetry."}], } - ), + ], ) - result = json.loads(response["body"].read()) - print(result) + print(response) if __name__ == "__main__": diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/pyproject.toml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/pyproject.toml index be8fd60a6..886bfb17b 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/pyproject.toml +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "opentelemetry-api ~= 1.43", "opentelemetry-instrumentation >= 0.64b0, <1", "opentelemetry-semantic-conventions >= 0.64b0, <1", - "opentelemetry-util-genai >= 1.0b0, <2", + "opentelemetry-util-genai >= 1.2b0.dev, <2", ] [project.optional-dependencies] diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/__init__.py index 14c46306a..bf26d5073 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/__init__.py +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/__init__.py @@ -8,11 +8,13 @@ from collections.abc import Collection from typing import Any -from opentelemetry.instrumentation.genai.bedrock.package import _instruments from opentelemetry.instrumentation.instrumentor import BaseInstrumentor from opentelemetry.util.genai.completion_hook import load_completion_hook from opentelemetry.util.genai.handler import TelemetryHandler +from .package import _instruments +from .patch import patch_bedrock, unpatch_bedrock + __all__ = ["BedrockInstrumentor"] @@ -35,9 +37,9 @@ def _instrument(self, **kwargs: Any) -> None: logger_provider=kwargs.get("logger_provider"), completion_hook=completion_hook, ) - # Amazon Bedrock patching will be added in a follow-up change. + patch_bedrock(self._handler) def _uninstrument(self, **kwargs: Any) -> None: """Disable Amazon Bedrock instrumentation.""" + unpatch_bedrock() self._handler = None - # Amazon Bedrock unpatching will be added in a follow-up change. diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/extractors.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/extractors.py new file mode 100644 index 000000000..27c90aa5c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/extractors.py @@ -0,0 +1,283 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +from typing import Any, TypeGuard +from urllib.parse import urlparse + +from opentelemetry.util.genai.invocation import InferenceInvocation +from opentelemetry.util.genai.types import ( + BlobPart, + FunctionToolDefinition, + GenericPart, + GenericToolDefinition, + InputMessage, + MessagePart, + OutputMessage, + ReasoningPart, + TextPart, + ToolCallRequestPart, + ToolCallResponsePart, + ToolDefinition, +) + + +def _is_dict(val: object) -> TypeGuard[dict[str, Any]]: + return isinstance(val, dict) + + +def _is_list(val: object) -> TypeGuard[list[Any]]: + return isinstance(val, list) + + +_FINISH_REASON_MAP: dict[str, str] = { + "end_turn": "stop", + "stop_sequence": "stop", + "tool_use": "tool_call", + "max_tokens": "length", + "content_filtered": "content_filter", + "guardrail_intervened": "content_filter", +} + +_DOC_MIME_TYPES: dict[str, str] = { + "pdf": "application/pdf", + "csv": "text/csv", + "doc": "application/msword", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xls": "application/vnd.ms-excel", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "html": "text/html", + "txt": "text/plain", + "md": "text/markdown", +} + + +def map_finish_reason(stop_reason: str | None) -> str | None: + """Map Bedrock stopReason to GenAI semantic convention finish_reason.""" + if stop_reason is None: + return None + return _FINISH_REASON_MAP.get(stop_reason, stop_reason.lower()) + + +def extract_server_address_and_port( + endpoint_url: str | None, +) -> tuple[str | None, int | None]: + """Parse server address and port from a client endpoint URL.""" + if not endpoint_url: + return None, None + parsed = urlparse(endpoint_url) + server_address = parsed.hostname + server_port = parsed.port + if server_port is None and parsed.scheme in ("http", "https"): + server_port = 443 if parsed.scheme == "https" else 80 + return server_address, server_port + + +def extract_content_block(block: dict[str, Any]) -> MessagePart | None: + """Map a single Bedrock content block to an OpenTelemetry MessagePart.""" + if "text" in block: + return TextPart(content=block["text"]) + + reasoning = block.get("reasoningContent") + if _is_dict(reasoning): + reasoning_text = reasoning.get("reasoningText") + if _is_dict(reasoning_text) and "text" in reasoning_text: + return ReasoningPart(content=reasoning_text["text"]) + if "redactedContent" in reasoning: + return ReasoningPart(content="") + + image = block.get("image") + if _is_dict(image): + fmt = image.get("format", "jpeg") + source = image.get("source") + content_bytes = source.get("bytes", b"") if _is_dict(source) else b"" + return BlobPart( + content=content_bytes, + mime_type=f"image/{fmt}", + modality="image", + ) + + document = block.get("document") + if _is_dict(document): + fmt = document.get("format", "pdf") + source = document.get("source") + content_bytes = source.get("bytes", b"") if _is_dict(source) else b"" + mime_type = _DOC_MIME_TYPES.get(fmt, f"application/{fmt}") + return BlobPart( + content=content_bytes, + mime_type=mime_type, + modality="document", + ) + + tool_use = block.get("toolUse") + if _is_dict(tool_use): + return ToolCallRequestPart( + id=tool_use.get("toolUseId"), + name=tool_use.get("name", ""), + arguments=tool_use.get("input"), + ) + + tool_result = block.get("toolResult") + if _is_dict(tool_result): + return ToolCallResponsePart( + id=tool_result.get("toolUseId"), + response=tool_result.get("content"), + ) + + for key in ( + "video", + "audio", + "guardContent", + "cachePoint", + "citationsContent", + "searchResult", + "toolAddition", + "toolRemoval", + ): + if key in block: + return GenericPart(type=key, value=None) + + return None + + +def extract_converse_request( + kwargs: dict[str, Any], + invocation: InferenceInvocation, + *, + capture_content: bool = True, +) -> None: + """Populate request attributes from converse kwargs onto the invocation.""" + inf_config = kwargs.get("inferenceConfig") + if _is_dict(inf_config): + invocation.temperature = inf_config.get("temperature") + invocation.top_p = inf_config.get("topP") + invocation.max_tokens = inf_config.get("maxTokens") + invocation.stop_sequences = inf_config.get("stopSequences") + invocation.top_k = inf_config.get("topK") or inf_config.get("top_k") + invocation.seed = inf_config.get("seed") + + add_fields = kwargs.get("additionalModelRequestFields") + if _is_dict(add_fields): + add_inf = add_fields.get("inferenceConfig") + top_k = ( + add_fields.get("topK") + or add_fields.get("top_k") + or ( + (add_inf.get("topK") or add_inf.get("top_k")) + if _is_dict(add_inf) + else None + ) + ) + if top_k is not None: + invocation.top_k = top_k + if "seed" in add_fields: + invocation.seed = add_fields["seed"] + + # system instruction + raw_system = kwargs.get("system") + if capture_content and _is_list(raw_system): + system_parts: list[MessagePart] = [] + for item in raw_system: + if _is_dict(item): + part = extract_content_block(item) + if part is not None: + system_parts.append(part) + if system_parts: + invocation.system_instruction = system_parts + + # input messages + raw_messages = kwargs.get("messages") + if capture_content and _is_list(raw_messages): + input_messages: list[InputMessage] = [] + for msg in raw_messages: + if not _is_dict(msg): + continue + role = msg.get("role", "user") + parts: list[MessagePart] = [] + content = msg.get("content") + if _is_list(content): + for block in content: + if _is_dict(block): + part = extract_content_block(block) + if part is not None: + parts.append(part) + input_messages.append(InputMessage(role=role, parts=parts)) + invocation.input_messages = input_messages + + # tool definitions + tool_config = kwargs.get("toolConfig") + if _is_dict(tool_config): + tools = tool_config.get("tools") + if _is_list(tools): + tool_defs: list[ToolDefinition] = [] + for tool in tools: + if not _is_dict(tool): + continue + tool_spec = tool.get("toolSpec") + if _is_dict(tool_spec): + name = tool_spec.get("name", "") + description = tool_spec.get("description") + raw_schema = tool_spec.get("inputSchema", {}) + params = ( + raw_schema.get("json", raw_schema) + if _is_dict(raw_schema) + else raw_schema + ) + tool_defs.append( + FunctionToolDefinition( + name=name, + description=description, + parameters=params, + ) + ) + elif "name" in tool and "type" in tool: + tool_defs.append( + GenericToolDefinition( + name=tool["name"], + type=tool["type"], + ) + ) + invocation.tool_definitions = tool_defs + + +def extract_converse_response( + response: dict[str, Any], + invocation: InferenceInvocation, + *, + capture_content: bool = True, +) -> None: + stop_reason = response.get("stopReason") + finish_reason = map_finish_reason(stop_reason) + if finish_reason: + invocation.finish_reasons = [finish_reason] + + output = response.get("output") + if capture_content and _is_dict(output): + msg = output.get("message") + if _is_dict(msg): + role = msg.get("role", "assistant") + parts: list[MessagePart] = [] + content = msg.get("content") + if _is_list(content): + for block in content: + if _is_dict(block): + part = extract_content_block(block) + if part is not None: + parts.append(part) + invocation.output_messages = [ + OutputMessage( + role=role, + parts=parts, + finish_reason=finish_reason or "stop", + ) + ] + + usage = response.get("usage") + if _is_dict(usage): + invocation.input_tokens = usage.get("inputTokens") + invocation.output_tokens = usage.get("outputTokens") + invocation.cache_read_input_tokens = usage.get("cacheReadInputTokens") + invocation.cache_creation_input_tokens = usage.get( + "cacheWriteInputTokens" + ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/patch.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/patch.py new file mode 100644 index 000000000..7683e5ec5 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/patch.py @@ -0,0 +1,140 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging +from collections.abc import Callable +from typing import Any, cast + +from botocore.client import BaseClient +from wrapt import wrap_function_wrapper + +from opentelemetry.instrumentation.utils import unwrap +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import ( + GenAiOperationNameValues, + GenAiProviderNameValues, +) +from opentelemetry.util.genai.handler import TelemetryHandler + +from .extractors import ( + extract_converse_request, + extract_converse_response, + extract_server_address_and_port, +) +from .stream import BedrockConverseStreamWrapper + +_logger = logging.getLogger(__name__) + +BEDROCK_RUNTIME = "bedrock-runtime" + + +def _handle_converse( + wrapped: Callable[..., Any], + instance: BaseClient, + args: tuple[Any, ...], + kwargs: dict[str, Any], + api_params: dict[str, Any], + handler: TelemetryHandler, + *, + is_stream: bool = False, +) -> Any: + endpoint_url = getattr( + getattr(instance, "meta", None), "endpoint_url", None + ) + server_address, server_port = extract_server_address_and_port(endpoint_url) + raw_model_id = api_params.get("modelId") + model_id = str(raw_model_id) if raw_model_id else None + invocation = handler.inference( + provider=GenAiProviderNameValues.AWS_BEDROCK.value, + request_model=model_id, + server_address=server_address, + server_port=server_port, + operation_name=GenAiOperationNameValues.CHAT.value, + ) + capture_content = handler.should_capture_content() + extract_converse_request( + api_params, invocation, capture_content=capture_content + ) + try: + response: Any = wrapped(*args, **kwargs) + except Exception as exc: + invocation.fail(exc) + raise + + if is_stream: + # Any 200 response is guaranteed to have this.. + if "stream" in response: + response["stream"] = BedrockConverseStreamWrapper( + response["stream"], + invocation=invocation, + capture_content=capture_content, + ) + return response + else: + extract_converse_response( + response, + invocation, + capture_content=capture_content, + ) + + invocation.stop() + return response + + +def _make_api_call_wrapper(handler: TelemetryHandler) -> Callable[..., Any]: + def _wrapper( + wrapped: Callable[..., Any], + instance: BaseClient, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> Any: + service_name = getattr( + getattr(instance, "_service_model", None), "service_name", None + ) + if service_name != BEDROCK_RUNTIME: + return wrapped(*args, **kwargs) + + operation_name = args[0] if args else kwargs.get("operation_name") + raw_params = args[1] if len(args) > 1 else kwargs.get("api_params") + api_params: dict[str, Any] = ( + cast(dict[str, Any], raw_params) + if isinstance(raw_params, dict) + else {} + ) + + if operation_name in ("Converse", "ConverseStream"): + return _handle_converse( + wrapped, + instance, + args, + kwargs, + api_params, + handler, + is_stream=(operation_name == "ConverseStream"), + ) + + return wrapped(*args, **kwargs) + + return _wrapper + + +def patch_bedrock(handler: TelemetryHandler) -> None: + """Patch botocore BaseClient to instrument Bedrock runtime operations.""" + wrap_function_wrapper( + "botocore.client", + "BaseClient._make_api_call", + _make_api_call_wrapper(handler), + ) + + +def unpatch_bedrock() -> None: + """Unpatch botocore BaseClient._make_api_call.""" + import botocore.client + + try: + unwrap(botocore.client.BaseClient, "_make_api_call") + except Exception: + _logger.debug( + "Failed to unwrap BaseClient._make_api_call", exc_info=True + ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/stream.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/stream.py new file mode 100644 index 000000000..6c1cea514 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/src/opentelemetry/instrumentation/genai/bedrock/stream.py @@ -0,0 +1,169 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +from typing import Any + +from botocore.eventstream import EventStream + +from opentelemetry.util.genai.invocation import InferenceInvocation +from opentelemetry.util.genai.stream import SyncStreamWrapper +from opentelemetry.util.genai.types import ( + MessagePart, + OutputMessage, + ReasoningPart, + TextPart, + ToolCallRequestPart, +) + +from .extractors import _is_dict, map_finish_reason + + +class BedrockConverseStreamWrapper(SyncStreamWrapper[dict[str, Any]]): + """Wrapper for Bedrock converse_stream EventStream.""" + + def __init__( + self, + stream: EventStream, + invocation: InferenceInvocation, + *, + capture_content: bool = True, + ) -> None: + super().__init__(stream, invocation=invocation) + self._self_invocation = invocation + self._self_capture_content = capture_content + self._self_role = "assistant" + self._self_stop_reason: str | None = None + self._self_input_tokens: int | None = None + self._self_output_tokens: int | None = None + self._self_cache_read_input_tokens: int | None = None + self._self_cache_creation_input_tokens: int | None = None + self._self_text_blocks: dict[int, str] = {} + self._self_reasoning_blocks: dict[int, str] = {} + self._self_tool_blocks: dict[int, dict[str, Any]] = {} + self._self_all_block_indices: list[int] = [] + + def _process_chunk(self, chunk: dict[str, Any]) -> None: + if "messageStart" in chunk and "role" in chunk["messageStart"]: + self._self_role = chunk["messageStart"]["role"] + + if self._self_capture_content and "contentBlockStart" in chunk: + cb_start = chunk["contentBlockStart"] + idx = cb_start.get("contentBlockIndex", 0) + if idx not in self._self_all_block_indices: + self._self_all_block_indices.append(idx) + start = cb_start.get("start", {}) + if "toolUse" in start: + tool_use = start["toolUse"] + self._self_tool_blocks[idx] = { + "toolUseId": tool_use.get("toolUseId"), + "name": tool_use.get("name", ""), + "input_chunks": [], + } + + if self._self_capture_content and "contentBlockDelta" in chunk: + cb_delta = chunk["contentBlockDelta"] + idx = cb_delta.get("contentBlockIndex", 0) + if idx not in self._self_all_block_indices: + self._self_all_block_indices.append(idx) + delta = cb_delta.get("delta", {}) + if "text" in delta: + self._self_text_blocks[idx] = ( + self._self_text_blocks.get(idx, "") + delta["text"] + ) + elif "reasoningContent" in delta: + rc_delta = delta["reasoningContent"] + if _is_dict(rc_delta) and "text" in rc_delta: + self._self_reasoning_blocks[idx] = ( + self._self_reasoning_blocks.get(idx, "") + + rc_delta["text"] + ) + elif "toolUse" in delta: + tool_delta = delta["toolUse"] + input_str = tool_delta.get("input", "") + if idx not in self._self_tool_blocks: + self._self_tool_blocks[idx] = { + "toolUseId": None, + "name": "", + "input_chunks": [], + } + self._self_tool_blocks[idx]["input_chunks"].append(input_str) + + if "messageStop" in chunk: + msg_stop = chunk["messageStop"] + if "stopReason" in msg_stop: + self._self_stop_reason = msg_stop["stopReason"] + + if "metadata" in chunk: + metadata = chunk["metadata"] + usage = metadata.get("usage", {}) + if "inputTokens" in usage: + self._self_input_tokens = usage["inputTokens"] + if "outputTokens" in usage: + self._self_output_tokens = usage["outputTokens"] + if "cacheReadInputTokens" in usage: + self._self_cache_read_input_tokens = usage[ + "cacheReadInputTokens" + ] + if "cacheWriteInputTokens" in usage: + self._self_cache_creation_input_tokens = usage[ + "cacheWriteInputTokens" + ] + + def _on_stream_end(self) -> None: + finish_reason = map_finish_reason(self._self_stop_reason) + if finish_reason: + self._self_invocation.finish_reasons = [finish_reason] + + if self._self_capture_content: + parts: list[MessagePart] = [] + for idx in sorted(self._self_all_block_indices): + if idx in self._self_text_blocks: + parts.append(TextPart(content=self._self_text_blocks[idx])) + elif idx in self._self_reasoning_blocks: + parts.append( + ReasoningPart(content=self._self_reasoning_blocks[idx]) + ) + elif idx in self._self_tool_blocks: + tool_info = self._self_tool_blocks[idx] + input_chunks = tool_info["input_chunks"] + raw_input = "".join(input_chunks) + args: object + if raw_input: + try: + args = json.loads(raw_input) + except Exception: + args = raw_input + else: + args = {} + parts.append( + ToolCallRequestPart( + id=tool_info["toolUseId"], + name=tool_info["name"], + arguments=args, + ) + ) + + if parts or finish_reason: + self._self_invocation.output_messages = [ + OutputMessage( + role=self._self_role, + parts=parts, + finish_reason=finish_reason or "stop", + ) + ] + self._self_invocation.input_tokens = self._self_input_tokens + self._self_invocation.output_tokens = self._self_output_tokens + self._self_invocation.cache_read_input_tokens = ( + self._self_cache_read_input_tokens + ) + self._self_invocation.cache_creation_input_tokens = ( + self._self_cache_creation_input_tokens + ) + + self._self_invocation.stop() + + def _on_stream_error(self, error: BaseException) -> None: + self._self_invocation.fail(error) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_no_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_no_content.yaml new file mode 100644 index 000000000..2b80c9fce --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_no_content.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzA3Wg== + amz-sdk-invocation-id: + - !!binary | + NjZhZTQyYjQtNGE0Ni00ZTNmLTliMDUtNWNlNWM4ZjdhNzI4 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse + response: + body: + string: |- + { + "metrics": { + "latencyMs": 231 + }, + "output": { + "message": { + "content": [ + { + "text": "It looks like you're indicating that this is" + } + ], + "role": "assistant" + } + }, + "stopReason": "max_tokens", + "usage": { + "inputTokens": 5, + "outputTokens": 10, + "serverToolUsage": {}, + "totalTokens": 15 + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '247' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:08 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_caller_side_error.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_caller_side_error.yaml new file mode 100644 index 000000000..8aca3b3a9 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_caller_side_error.yaml @@ -0,0 +1,92 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEzWg== + amz-sdk-invocation-id: + - !!binary | + MTU2ZGZiODYtZjk0YS00NzdkLThiMGMtYTI1NjMzZWQ3MTMx + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAAnwAAAFKzEV9wCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0giLCJyb2xlIjoiYXNzaXN0YW50In0i9wVBAAAA1wAAAFer+Lh3 + CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 + aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVs + dGEiOnsidGV4dCI6Ikl0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI + SUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQ1NjcifWy9jXYAAADKAAAAVzOI60QLOmV2ZW50LXR5cGUH + ABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVz + c2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0Ijoi + IHNvdW5kcyBsaWtlIHlvdSdyZSBjaGVja2luZyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 + In3bJCkUAAAAqwAAAFeX2rW5CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpZiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5v + In2LgU56AAAArgAAAFdfOjrJCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB5b3VyIn0sInAiOiJhYmNkZWZnaGlqa2xt + bm9wIn0vEecIAAAAsAAAAFeA6hMqCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNv + bnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250 + ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBtZXNzYWdlIn0sInAiOiJhYmNkZWZn + aGlqa2xtbm8ifX0woYIAAACaAAAAVnycFBkLOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9w + DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi + Y29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2In2Jz2GTAAAA + nAAAAFFtuHQaCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxp + Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy + c3R1dnd4eSIsInN0b3BSZWFzb24iOiJtYXhfdG9rZW5zIn0mksGdAAAA6wAAAE6rQkUwCzpldmVu + dC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh + Z2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MjQxfSwicCI6ImFiY2RlZmdo + aWprbG1ub3BxcnN0dXZ3IiwidXNhZ2UiOnsiaW5wdXRUb2tlbnMiOjUsIm91dHB1dFRva2VucyI6 + MTAsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5zIjoxNX19qfk4Fw== + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:13 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_close_before_consumption.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_close_before_consumption.yaml new file mode 100644 index 000000000..51543ac77 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_close_before_consumption.yaml @@ -0,0 +1,90 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEyWg== + amz-sdk-invocation-id: + - !!binary | + MzdmZDM4ZmMtNTAyMS00MThjLTllMDktZTk4YTUwZmMxMWRl + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAAnAAAAFL0sSWgCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREUiLCJyb2xlIjoiYXNzaXN0YW50In1Q/B+YAAAA0wAAAFdeeB63Czpl + dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u + L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi + OnsidGV4dCI6Ikl0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL + TE1OT1BRUlNUVVZXWFlaMDEyMyJ9s+vl6QAAANYAAABXlpiRxws6ZXZlbnQtdHlwZQcAEWNvbnRl + bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 + cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgc291bmRz + IGxpa2UgeW91J3JlIGxldHRpbmcifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD + REVGR0giffI0gSIAAACrAAAAV5fatbkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 + Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv + bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIG1lIn0sInAiOiJhYmNkZWZnaGlq + a2xtbm8ifRiXyMcAAAC9AAAAV3h615sLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 + Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv + bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGtub3cgdGhhdCJ9LCJwIjoiYWJj + ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoiffTfr8EAAACjAAAAVtCtzu4LOmV2ZW50LXR5cGUHABBj + b250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl + LXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9w + cXJzdHV2d3h5ekFCQ0RFIn1D4qBqAAAAogAAAFFzqXL9CzpldmVudC10eXBlBwALbWVzc2FnZVN0 + b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 + eyJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERSIsInN0b3BSZWFzb24iOiJtYXhf + dG9rZW5zIn01Ob+zAAAA/AAAAE55gg6iCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQt + dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7 + ImxhdGVuY3lNcyI6MjE3fSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJ + SktMTU4iLCJ1c2FnZSI6eyJpbnB1dFRva2VucyI6NSwib3V0cHV0VG9rZW5zIjoxMCwic2VydmVy + VG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjE1fX11BBPd + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:12 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_handles_event_stream_error.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_handles_event_stream_error.yaml new file mode 100644 index 000000000..53a089af6 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_handles_event_stream_error.yaml @@ -0,0 +1,87 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEyWg== + amz-sdk-invocation-id: + - !!binary | + MzQ1ZDRlZDQtMDYwYi00N2EwLTlhMzEtNjY5YzJhNTE2ZmY1 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAArAAAAFJVkJ0mCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIiwicm9sZSI6ImFzc2lzdGFudCJ9mBqg + YwAAAL4AAABXP9qtSws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 + cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr + SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJJdCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 + dnd4eXpBQkNERUZHSEkifdA1NkAAAAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50Qmxv + Y2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF + ZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHNvdW5kcyBsaWtl + IHlvdSdyZSBpbml0aWF0aW5nIn0sInAiOiJhYmMifZ0+iGAAAADRAAAAVyS4TdcLOmV2ZW50LXR5 + cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 + bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 + IjoiIGEgdGVzdCBvciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElK + S0xNTk9QUVJTVCJ9l34XfAAAAJ4AAABWiRyy2Qs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0 + b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 + eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6In3W + EFHyAAAAwAAAAFGQW1bQCzpldmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcA + EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjZGVmZ2hpamts + bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3OCIsInN0b3BS + ZWFzb24iOiJtYXhfdG9rZW5zIn36j1jiAAAA8gAAAE7GsrDDCzpldmVudC10eXBlBwAIbWV0YWRh + dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 + eyJtZXRyaWNzIjp7ImxhdGVuY3lNcyI6MzIyfSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 + eHl6QUJDRCIsInVzYWdlIjp7ImlucHV0VG9rZW5zIjo1LCJvdXRwdXRUb2tlbnMiOjEwLCJzZXJ2 + ZXJUb29sVXNhZ2UiOnt9LCJ0b3RhbFRva2VucyI6MTV9fdLfUrc= + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:13 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content.yaml new file mode 100644 index 000000000..797fabe3b --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content.yaml @@ -0,0 +1,89 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEwWg== + amz-sdk-invocation-id: + - !!binary | + N2JlMWQ3NWQtNzZhOS00OTk3LTlmN2MtMmNiYWIyNjAyNDA2 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAAnwAAAFKzEV9wCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0giLCJyb2xlIjoiYXNzaXN0YW50In0i9wVBAAAAsAAAAFeA6hMq + CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 + aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVs + dGEiOnsidGV4dCI6IlN1cmUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifVONv7YAAAC0AAAA + V3VqteoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw + bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow + LCJkZWx0YSI6eyJ0ZXh0IjoiLCBoZXJlIGl0In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXIifbpw + vkUAAADEAAAAV4y4VSULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGlzOlxuXG4ifSwicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJIn2K3PDMAAAApgAAAFdvSnEICzpldmVudC10eXBlBwARY29u + dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut + dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IlwiVGhp + cyBpcyBhIn0sInAiOiJhYiJ9IE2L3QAAAJMAAABWcYx2aAs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRC + bG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA + BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ubyJ9uOXHGAAA + ALUAAABRoWk5bws6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBhcHBs + aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1ub3Bx + cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYIiwic3RvcFJlYXNvbiI6Im1heF90b2tl + bnMifdiFkF4AAADmAAAATlPSgYELOmV2ZW50LXR5cGUHAAhtZXRhZGF0YQ06Y29udGVudC10eXBl + BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7Im1ldHJpY3MiOnsibGF0 + ZW5jeU1zIjoyNzR9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyIiwidXNhZ2UiOnsiaW5wdXRUb2tl + bnMiOjUsIm91dHB1dFRva2VucyI6MTAsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9rZW5z + IjoxNX19ZSLgYg== + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:10 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content_tool_call.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content_tool_call.yaml new file mode 100644 index 000000000..73dec464a --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_no_content_tool_call.yaml @@ -0,0 +1,300 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "What is the weather in Seattle and San Francisco today?" + } + ] + } + ], + "toolConfig": { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": "Get the current weather in a given location.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city" + } + }, + "required": [ + "location" + ] + } + } + } + } + ] + } + } + headers: + Content-Length: + - '409' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzExWg== + amz-sdk-invocation-id: + - !!binary | + YmEyMDRjOTktMmNlYi00ODIwLTlkYzYtNjNlNGRjZmU2OTEy + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAArwAAAFISMOf2CzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYIiwicm9sZSI6ImFzc2lzdGFudCJ9 + BOAdPgAAANgAAABXKagvpgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50 + LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJs + b2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiI8dGhpbmtpbmcifSwicCI6ImFiY2RlZmdoaWpr + bG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMSJ96b9FDgAAALIAAABX + +ipASgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs + aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs + ImRlbHRhIjp7InRleHQiOiI+In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3gifceRr9oA + AADEAAAAV4y4VSULOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl + BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu + ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIFRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 + dnd4eXpBQkNERUZHSElKS0xNIn1WulsiAAAArAAAAFcl+mmpCzpldmVudC10eXBlBwARY29udGVu + dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw + ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBVc2VyIn0s + InAiOiJhYmNkZWZnaGlqa2xtbiJ9UMwprgAAALMAAABXx0pp+gs6ZXZlbnQtdHlwZQcAEWNvbnRl + bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 + cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaGFzIn0s + InAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2In3HCPwmAAAAogAAAFeaytfICzpldmVudC10eXBl + BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l + c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 + IiBhc2tlZCJ9LCJwIjoiYWJjIn033nVGAAAAnwAAAFfDe6v/CzpldmVudC10eXBlBwARY29udGVu + dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw + ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBmb3IifSwi + cCI6ImFiIn20eTBuAAAAvQAAAFd4etebCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN + OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj + b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdo + aWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGIn29OgrsAAAAuQAAAFeN+nFbCzpldmVudC10eXBlBwAR + Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh + Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBj + dXJyZW50In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3gifUtNDJoAAADFAAAAV7HYfJUL + OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp + b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 + YSI6eyJ0ZXh0IjoiIHdlYXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD + REVGR0hJSiJ9+qOWLgAAAMEAAABXRFjaVQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh + DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi + Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgaW4ifSwicCI6ImFiY2RlZmdo + aWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSksiffDI0UYAAADNAAAAV4GoN1QLOmV2ZW50LXR5 + cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 + bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 + IjoiIHR3byJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q + UVJTVFVWIn3BU/aQAAAAuAAAAFewmljrCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGEN + OmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJj + b250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBsb2NhdGlvbnMifSwicCI6ImFi + Y2RlZmdoaWprbG1ub3BxcnN0dSJ9Ie8+9wAAAMgAAABXSUi4JAs6ZXZlbnQtdHlwZQcAEWNvbnRl + bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 + cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiI6In0sInAi + OiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUIn2wwReiAAAA + xwAAAFfLGC/1CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA + EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl + eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBTZWF0dGxlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJz + dHV2d3h5ekFCQ0RFRkdISUpLTCJ9wSM5LgAAAKwAAABXJfppqQs6ZXZlbnQtdHlwZQcAEWNvbnRl + bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 + cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgYW5kIn0s + InAiOiJhYmNkZWZnaGlqa2xtbm8ifQgieAsAAACyAAAAV/oqQEoLOmV2ZW50LXR5cGUHABFjb250 + ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 + eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIFNhbiJ9 + LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1In01vb9mAAAArAAAAFcl+mmpCzpldmVudC10eXBl + BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l + c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 + IiBGcmFuY2lzY28ifSwicCI6ImFiY2RlZmdoaSJ9L0MsIwAAAL8AAABXArqE+ws6ZXZlbnQtdHlw + ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt + ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi + OiIuIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLIn14p2QLAAAA + 1AAAAFfsWMKnCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA + EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl + eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBJIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5 + ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifQuvS1gAAACzAAAAV8dKafoLOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiIGNhbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ93ogybgAAAMkAAABXdCiR + lAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNh + dGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRl + bHRhIjp7InRleHQiOiIgdXNlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RF + RkdISUpLTE1OT1BRUiJ9SJGLnQAAANMAAABXXngetws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j + a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl + dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdGhlIn0sInAiOiJh + YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEifcV9 + oj8AAACtAAAAVxiaQBkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGAifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxciJ9 + ErfbZAAAAM8AAABX+2hkNAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50 + LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJs + b2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJnZXQifSwicCI6ImFiY2RlZmdoaWprbG1ub3Bx + cnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9bc3tswAAAMMAAABXPpiJNQs6ZXZl + bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q + c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 + InRleHQiOiJfIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1O + TyJ95SruhgAAANcAAABXq/i4dws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 + ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu + dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJjdXJyZW50In0sInAiOiJhYmNkZWZnaGlq + a2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn0TXpm5AAAAxgAA + AFf2eAZFCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 + MCwiZGVsdGEiOnsidGV4dCI6Il8ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJD + REVGR0hJSktMTU5PUFFSIn2tW33SAAAA0QAAAFckuE3XCzpldmVudC10eXBlBwARY29udGVudEJs + b2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcA + BWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IndlYXRoZXIifSwi + cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlcifd4N + cpsAAACwAAAAV4DqEyoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiYCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 + diJ9CSaBfgAAAKwAAABXJfppqQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 + ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu + dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdG9vbCJ9LCJwIjoiYWJjZGVmZ2hpamts + bW4ifcXV3yUAAADVAAAAV9E46xcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u + dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl + bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRvIn0sInAiOiJhYmNkZWZnaGlqa2xt + bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyMzQifTP93awAAACtAAAA + VxiaQBkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw + bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow + LCJkZWx0YSI6eyJ0ZXh0IjoiIGdldCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9r/10xgAAAK8A + AABXYloTeQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi + OjAsImRlbHRhIjp7InRleHQiOiIgdGhlIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXIifZyWgM8A + AACmAAAAV29KcQgLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl + BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu + ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHdlYXRoZXIifSwicCI6ImFiY2RlIn0dzksTAAAAzAAA + AFe8yB7kCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 + MCwiZGVsdGEiOnsidGV4dCI6IiBmb3IifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 + QUJDREVGR0hJSktMTU5PUFFSU1RVIn07CxBLAAAAowAAAFenqv54CzpldmVudC10eXBlBwARY29u + dGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2Ut + dHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBlYWNo + In0sInAiOiJhYmNkZSJ9BrfFggAAAKsAAABXl9q1uQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j + a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl + dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgbG9jYXRpb24ifSwi + cCI6ImFiY2RlZmdoaSJ9TvsPdwAAAMUAAABXsdh8lQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j + a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl + dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgc2VwYXJhdGVseSJ9 + LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHIn37crwwAAAArwAAAFdiWhN5 + CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 + aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVs + dGEiOnsidGV4dCI6Ii4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dSJ9NRMzWQAAAMkAAABX + dCiRlAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs + aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs + ImRlbHRhIjp7InRleHQiOiIgSSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNE + RUZHSElKS0xNTk9QUVJTVCJ9Zi2H2wAAAMYAAABX9ngGRQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRC + bG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH + AAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgd2lsbCJ9LCJw + IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTiJ9s+SQCQAAAMcAAABX + yxgv9Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBs + aWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAs + ImRlbHRhIjp7InRleHQiOiIgZmlyc3QifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6 + QUJDREVGR0hJSktMTU4ifReOdhcAAADDAAAAVz6YiTULOmV2ZW50LXR5cGUHABFjb250ZW50Qmxv + Y2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF + ZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGdldCJ9LCJwIjoi + YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0wifbMYMbsAAADaAAAAV1NofMYL + OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp + b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 + YSI6eyJ0ZXh0IjoiIHRoZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZH + SElKS0xNTk9QUVJTVFVWV1hZWjAxMjM0NTY3OCJ9HxJKyAAAAMgAAABXSUi4JAs6ZXZlbnQtdHlw + ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt + ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQi + OiIgd2VhdGhlciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN + In3VmNumAAAA2AAAAFcpqC+mCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBmb3IifSwicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2In0VBq6MAAAAqwAA + AFeX2rW5CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 + MCwiZGVsdGEiOnsidGV4dCI6IiBTZWF0dGxlIn0sInAiOiJhYmNkZWZnaGlqIn05gYpuAAAAqgAA + AFequpwJCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 + MCwiZGVsdGEiOnsidGV4dCI6IiBhbmQifSwicCI6ImFiY2RlZmdoaWprbG0iffkeHQ0AAACxAAAA + V72KOpoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw + bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow + LCJkZWx0YSI6eyJ0ZXh0IjoiIHRoZW4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnMifef9WiMA + AADVAAAAV9E46xcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBl + BwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0lu + ZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGZvciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1 + dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAxMjMifaQQDNMAAACtAAAAVxiaQBkLOmV2 + ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v + anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 + eyJ0ZXh0IjoiIFNhbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ974I4cAAAAMsAAABXDujC9As6 + ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv + bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh + Ijp7InRleHQiOiIgRnJhbmNpc2NvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFC + Q0RFRkdISUpLTE1OIn19GPOFAAAAuAAAAFewmljrCzpldmVudC10eXBlBwARY29udGVudEJsb2Nr + RGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2 + ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii48LyJ9LCJwIjoiYWJj + ZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQiJ97mOShQAAAMcAAABXyxgv9Qs6ZXZlbnQtdHlwZQcA + EWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz + YWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiJ0 + aGlua2luZyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0wifYoK + y/UAAACrAAAAV5fatbkLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiPlxuIn0sInAiOiJhYmNkZWZnaGlqa2xtbm8ifSC0 + 2UsAAACvAAAAVhVdI+8LOmV2ZW50LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5 + cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr + SW5kZXgiOjAsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BR + In1K+3IPAAABGgAAAFdxIUe4CzpldmVudC10eXBlBwARY29udGVudEJsb2NrU3RhcnQNOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM + TU5PUFFSU1RVVldYWVowIiwic3RhcnQiOnsidG9vbFVzZSI6eyJuYW1lIjoiZ2V0X2N1cnJlbnRf + d2VhdGhlciIsInRvb2xVc2VJZCI6InRvb2x1c2VfbEk4TkpHY1pHelFXWkxJTk1zeXVWeSJ9fX2N + 0ys3AAAA1gAAAFeWmJHHCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt + dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv + Y2tJbmRleCI6MSwiZGVsdGEiOnsidG9vbFVzZSI6eyJpbnB1dCI6IntcImxvY2F0aW9uXCI6XCJT + ZWF0dGxlXCJ9In19LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1diJ9uPQyDQAAAKgAAABWp33/ + /ws6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 + aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MSwicCI6 + ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSiJ9v6Aw2wAAARQAAABXzhH52Qs6 + ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja1N0YXJ0DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv + bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsInAiOiJh + YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVSIsInN0YXJ0Ijp7 + InRvb2xVc2UiOnsibmFtZSI6ImdldF9jdXJyZW50X3dlYXRoZXIiLCJ0b29sVXNlSWQiOiJ0b29s + dXNlXzBYSU5FcE5OeWgycUJ2a3UwT2xQNnMifX19mfG9PAAAANQAAABX7FjCpws6ZXZlbnQtdHlw + ZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTpt + ZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsImRlbHRhIjp7InRvb2xV + c2UiOnsiaW5wdXQiOiJ7XCJsb2NhdGlvblwiOlwiU2FuIEZyYW5jaXNjb1wifSJ9fSwicCI6ImFi + Y2RlZmdoaWprbG1uIn2U5YyxAAAAigAAAFYcfIObCzpldmVudC10eXBlBwAQY29udGVudEJsb2Nr + U3RvcA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl + bnR7ImNvbnRlbnRCbG9ja0luZGV4IjoyLCJwIjoiYWJjZGVmIn3ebcidAAAAhAAAAFE9KKhZCzpl + dmVudC10eXBlBwALbWVzc2FnZVN0b3ANOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N + Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJwIjoiYWJjIiwic3RvcFJlYXNvbiI6InRvb2xfdXNlIn1I + D52tAAAA3AAAAE64QyGmCzpldmVudC10eXBlBwAIbWV0YWRhdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJtZXRyaWNzIjp7ImxhdGVuY3lN + cyI6NTIxfSwicCI6ImFiY2RlIiwidXNhZ2UiOnsiaW5wdXRUb2tlbnMiOjQxNiwib3V0cHV0VG9r + ZW5zIjo4Niwic2VydmVyVG9vbFVzYWdlIjp7fSwidG90YWxUb2tlbnMiOjUwMn19d1Ru+g== + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:11 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content.yaml new file mode 100644 index 000000000..18b37e241 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content.yaml @@ -0,0 +1,90 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MSBtZC9Cb3RvY29yZSMxLjQzLjgxIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjE0LjMgbWQvcHlpbXBs + I0NQeXRob24gbS9ELGIsWiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS40My44MQ== + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzMzWg== + amz-sdk-invocation-id: + - !!binary | + N2UyMDNlZjEtZTg3OS00M2Y3LWJlMzEtOGUzYjFjNDUwMzc4 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAApQAAAFJYgP9XCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU4iLCJyb2xlIjoiYXNzaXN0YW50In27QtplAAAAqgAA + AFequpwJCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFw + cGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6 + MCwiZGVsdGEiOnsidGV4dCI6IkNlcnRhaW5seSJ9LCJwIjoiYWJjZGVmZ2gife1VwnMAAAC/AAAA + VwK6hPsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw + bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow + LCJkZWx0YSI6eyJ0ZXh0IjoiISBJdCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpB + QkNERUZHSCJ9taQHtAAAANsAAABXbghVdgs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRh + DTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsi + Y29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgc291bmRzIGxpa2UgeW91J3Jl + IHNheWluZyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTiJ9 + rJcdWwAAAKoAAABXqrqcCQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50 + LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJs + b2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIsIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wIn0H + tflRAAAAqQAAAFaaHdZPCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKSyJ9YZcg + owAAAJkAAABRpVj7ags6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXYiLCJzdG9wUmVhc29uIjoibWF4X3Rva2VucyJ9SO16pQAAAQIAAABORdozOws6ZXZl + bnQtdHlwZQcACG1ldGFkYXRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNz + YWdlLXR5cGUHAAVldmVudHsibWV0cmljcyI6eyJsYXRlbmN5TXMiOjI1Nn0sInAiOiJhYmNkZWZn + aGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUIiwidXNhZ2UiOnsiaW5wdXRU + b2tlbnMiOjUsIm91dHB1dFRva2VucyI6MTAsInNlcnZlclRvb2xVc2FnZSI6e30sInRvdGFsVG9r + ZW5zIjoxNX19QKqxXw== + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:33 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content_tool_call.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content_tool_call.yaml new file mode 100644 index 000000000..093a1dc9f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_content_tool_call.yaml @@ -0,0 +1,296 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "What is the weather in Seattle and San Francisco today?" + } + ] + } + ], + "toolConfig": { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": "Get the current weather in a given location.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city" + } + }, + "required": [ + "location" + ] + } + } + } + } + ] + } + } + headers: + Content-Length: + - '409' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEwWg== + amz-sdk-invocation-id: + - !!binary | + ODAzMzMzODYtM2FjMi00ZGNkLWE3NDAtNGQxNDYyYjM1NTA2 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse-stream + response: + body: + string: !!binary | + AAAAnAAAAFL0sSWgCzpldmVudC10eXBlBwAMbWVzc2FnZVN0YXJ0DTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREUiLCJyb2xlIjoiYXNzaXN0YW50In1Q/B+YAAAApwAAAFdSKli4Czpl + dmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9u + L2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEi + OnsidGV4dCI6Ijx0aGlua2luZyJ9LCJwIjoiYWJjZGUiffcQczwAAACxAAAAV72KOpoLOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiPiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dncifZtUTzUAAACjAAAAV6eq/ngL + OmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRp + b24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0 + YSI6eyJ0ZXh0IjoiIFRoZSJ9LCJwIjoiYWJjZGVmIn19FWIaAAAAtwAAAFcyys86CzpldmVudC10 + eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N + Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 + dCI6IiBVc2VyIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5In1m7bwCAAAAuQAAAFeN + +nFbCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxp + Y2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwi + ZGVsdGEiOnsidGV4dCI6IiB3YW50cyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoi + fa2hwJYAAAC5AAAAV436cVsLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVu + dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRC + bG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9w + cXJzdHV2d3h5ekFCQyJ9E+Qk3gAAAL8AAABXArqE+ws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j + a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl + dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIga25vdyJ9LCJwIjoi + YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHIn3vSt5hAAAA1gAAAFeWmJHHCzpldmVu + dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz + b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi + dGV4dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM + TU5PUFFSU1RVVldYWVowMTIzNCJ9Zd0E8AAAAK8AAABXYloTeQs6ZXZlbnQtdHlwZQcAEWNvbnRl + bnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5 + cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgY3VycmVu + dCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW4ifbsHH1YAAACyAAAAV/oqQEoLOmV2ZW50LXR5cGUHABFj + b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn + ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHdl + YXRoZXIifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxIn04tX7kAAAArAAAAFcl+mmpCzpldmVudC10 + eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N + Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 + dCI6IiBpbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcCJ9WceT3gAAAL8AAABXArqE+ws6ZXZlbnQt + dHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u + DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRl + eHQiOiIgdHdvIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdIIn2yLyis + AAAA0QAAAFckuE3XCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlw + ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ + bmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBsb2NhdGlvbnMifSwicCI6ImFiY2RlZmdoaWprbG1u + b3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1QifTotyCIAAACtAAAAVxiaQBkLOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiOiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFycyJ9ygI8HQAAAKkAAABX7Rrm2Qs6ZXZl + bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q + c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 + InRleHQiOiIgU2VhdHRsZSJ9LCJwIjoiYWJjZGVmZ2gifVtaK0QAAADSAAAAV2MYNwcLOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiIGFuZCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN + Tk9QUVJTVFVWV1hZWjAifRt6XaQAAAC6AAAAV8paC4sLOmV2ZW50LXR5cGUHABFjb250ZW50Qmxv + Y2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAF + ZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIFNhbiJ9LCJwIjoi + YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkMifRaz1PAAAADBAAAAV0RY2lULOmV2ZW50LXR5 + cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06 + bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0 + IjoiIEZyYW5jaXNjbyJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNEIn1ai/to + AAAAzAAAAFe8yB7kCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlw + ZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJ + bmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ii4ifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 + eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYIn1+Y+VsAAAAsAAAAFeA6hMqCzpldmVudC10eXBl + BwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1l + c3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6 + IiBJIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHUifdGRuJYAAADFAAAAV7HYfJULOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiIGNhbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xN + TiJ9JQ0YAwAAAMIAAABXA/ighQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 + ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu + dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgdXNlIn0sInAiOiJhYmNkZWZnaGlqa2xt + bm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLIn3ONhKPAAAAswAAAFfHSmn6CzpldmVudC10eXBlBwAR + Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh + Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0 + aGUifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXYifX4o9ZUAAADEAAAAV4y4VSULOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoiIHByb3ZpZGVkIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdI + In1AiKAkAAAAogAAAFeaytfICzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0b29sIn0sInAiOiJhYmNkIn3WETVbAAAA + uQAAAFeN+nFbCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA + EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl + eCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0byJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 + eXpBQkMifRPkJN4AAACvAAAAV2JaE3kLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 + Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv + bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGdldCJ9LCJwIjoiYWJjZGVmZ2hp + amtsbW5vcHFyIn1xmhGkAAAAzgAAAFfGCE2ECzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVs + dGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50 + eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0aGUifSwicCI6ImFiY2Rl + ZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlcifUJafn4AAADOAAAA + V8YITYQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBw + bGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4Ijow + LCJkZWx0YSI6eyJ0ZXh0IjoiIGN1cnJlbnQifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3 + eHl6QUJDREVGR0hJSktMTU5PUFFSUyJ9ywuJEwAAANAAAABXGdhkZws6ZXZlbnQtdHlwZQcAEWNv + bnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdl + LXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgd2Vh + dGhlciJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJT + VFUifZQT8qcAAADLAAAAVw7owvQLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29u + dGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRl + bnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGZvciJ9LCJwIjoiYWJjZGVmZ2hpamts + bW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVCJ9EcB+AAAAAMEAAABXRFjaVQs6ZXZl + bnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9q + c29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7 + InRleHQiOiIgZWFjaCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSEki + ffAhEF0AAACxAAAAV72KOpoLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVu + dC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRC + bG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIGxvY2F0aW9uIn0sInAiOiJhYmNkZWZnaGlq + a2xtbm8ifQ8VbM0AAADgAAAAV7j53OELOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06 + Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNv + bnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHNlcGFyYXRlbHkifSwicCI6ImFi + Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIzNDU2 + NyJ9YjTE8gAAAKEAAABX3WqtGAs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250 + ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVu + dEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIuIn0sInAiOiJhYmNkZWZnIn07OCn6AAAA + sAAAAFeA6hMqCzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcA + EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRl + eCI6MCwiZGVsdGEiOnsidGV4dCI6IiBJIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHUifdGR + uJYAAADQAAAAVxnYZGcLOmV2ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10 + eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9j + a0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHNob3VsZCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5v + cHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWIn0kdcePAAAAqAAAAFfQes9pCzpldmVu + dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz + b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi + dGV4dCI6IiBjYWxsIn0sInAiOiJhYmNkZWZnaGlqIn0A4nB4AAAApQAAAFco6gvYCzpldmVudC10 + eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24N + Om1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4 + dCI6IiB0aGUifSwicCI6ImFiY2RlZmdoIn3BzeIHAAAApwAAAFdSKli4CzpldmVudC10eXBlBwAR + Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh + Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiB0 + b29sIn0sInAiOiJhYmNkZWZnaGkifVt2MuQAAAC8AAAAV0Ua/isLOmV2ZW50LXR5cGUHABFjb250 + ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 + eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHR3aWNl + In0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQyJ9XvApuAAAAMMAAABXPpiJNQs6 + ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlv + bi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRh + Ijp7InRleHQiOiIsIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpL + TE1OTyJ9iBv0egAAALQAAABXdWq16gs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpj + b250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29u + dGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgb25jZSJ9LCJwIjoiYWJjZGVmZ2hp + amtsbW5vcHFyc3R1diJ9+GPG0QAAAMwAAABXvMge5As6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9j + a0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVl + dmVudHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgZm9yIn0sInAiOiJh + YmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVSJ9OwsQSwAAAMAA + AABXeTjz5Qs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBh + cHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgi + OjAsImRlbHRhIjp7InRleHQiOiIgZWFjaCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4 + eXpBQkNERUZHSCJ9aO/4gAAAANUAAABX0TjrFws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0Rl + bHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVu + dHsiY29udGVudEJsb2NrSW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIgbG9jYXRpb24ifSwicCI6 + ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWSJ9M39i + /gAAANEAAABXJLhN1ws6ZXZlbnQtdHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5 + cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2Nr + SW5kZXgiOjAsImRlbHRhIjp7InRleHQiOiIsIn0sInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2 + d3h5ekFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaMDEyIn1jQXUgAAAArQAAAFcYmkAZCzpldmVu + dC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pz + b24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsi + dGV4dCI6IiB0byJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHEifYV7Rw0AAACfAAAAV8N7q/8LOmV2 + ZW50LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24v + anNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6 + eyJ0ZXh0IjoiIGdldCJ9LCJwIjoiYWIifX9Q7sAAAACsAAAAVyX6aakLOmV2ZW50LXR5cGUHABFj + b250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2Fn + ZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0ZXh0IjoiIHRo + ZSJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vIn1PW1viAAAA3gAAAFem6NoGCzpldmVudC10eXBlBwAR + Y29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3Nh + Z2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBy + ZXF1aXJlZCJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9Q + UVJTVFVWV1hZWjAxMjM0NTY3In3xWMTYAAAAwAAAAFd5OPPlCzpldmVudC10eXBlBwARY29udGVu + dEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlw + ZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6IiBpbmZvcm1h + dGlvbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBIn1+EFzlAAAApAAAAFcViiJo + CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQtdHlwZQcAEGFwcGxpY2F0 + aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50QmxvY2tJbmRleCI6MCwiZGVs + dGEiOnsidGV4dCI6Ii48LyJ9LCJwIjoiYWJjZGVmZ2gifaPyMmkAAADBAAAAV0RY2lULOmV2ZW50 + LXR5cGUHABFjb250ZW50QmxvY2tEZWx0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNv + bg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjowLCJkZWx0YSI6eyJ0 + ZXh0IjoidGhpbmtpbmcifSwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGIn2P + OUaIAAAArwAAAFdiWhN5CzpldmVudC10eXBlBwARY29udGVudEJsb2NrRGVsdGENOmNvbnRlbnQt + dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50Qmxv + Y2tJbmRleCI6MCwiZGVsdGEiOnsidGV4dCI6Ij5cbiJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy + cyJ9DuJlCwAAAL4AAABWSN2d3Qs6ZXZlbnQtdHlwZQcAEGNvbnRlbnRCbG9ja1N0b3ANOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MCwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktM + TU5PUFFSU1RVVldYWVowMTIzNDUifeyUkM0AAAEZAAAAVzaBPWgLOmV2ZW50LXR5cGUHABFjb250 + ZW50QmxvY2tTdGFydA06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10 + eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFy + c3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWiIsInN0YXJ0Ijp7InRvb2xVc2UiOnsi + bmFtZSI6ImdldF9jdXJyZW50X3dlYXRoZXIiLCJ0b29sVXNlSWQiOiJ0b29sdXNlX2N5WVJpMmtZ + dmZzY3owTUJDYjIyU3cifX19zhXz/AAAAMUAAABXsdh8lQs6ZXZlbnQtdHlwZQcAEWNvbnRlbnRC + bG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH + AAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjEsImRlbHRhIjp7InRvb2xVc2UiOnsiaW5wdXQi + OiJ7XCJsb2NhdGlvblwiOlwiU2VhdHRsZVwifSJ9fSwicCI6ImFiY2RlIn1GQjMZAAAAugAAAFa9 + XTsdCzpldmVudC10eXBlBwAQY29udGVudEJsb2NrU3RvcA06Y29udGVudC10eXBlBwAQYXBwbGlj + YXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImNvbnRlbnRCbG9ja0luZGV4IjoxLCJw + IjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWjAx + In3BqDNNAAABAwAAAFcc0bJLCzpldmVudC10eXBlBwARY29udGVudEJsb2NrU3RhcnQNOmNvbnRl + bnQtdHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJjb250ZW50 + QmxvY2tJbmRleCI6MiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDRCIsInN0YXJ0 + Ijp7InRvb2xVc2UiOnsibmFtZSI6ImdldF9jdXJyZW50X3dlYXRoZXIiLCJ0b29sVXNlSWQiOiJ0 + b29sdXNlX3g1b2ZTM216anZabTlrZGZNcXN2ZnQifX19Shyd6AAAAOUAAABXcBlTkQs6ZXZlbnQt + dHlwZQcAEWNvbnRlbnRCbG9ja0RlbHRhDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u + DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsImRlbHRhIjp7InRv + b2xVc2UiOnsiaW5wdXQiOiJ7XCJsb2NhdGlvblwiOlwiU2FuIEZyYW5jaXNjb1wifSJ9fSwicCI6 + ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREUifXoII4AAAACSAAAAVkzsX9gLOmV2ZW50 + LXR5cGUHABBjb250ZW50QmxvY2tTdG9wDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29u + DTptZXNzYWdlLXR5cGUHAAVldmVudHsiY29udGVudEJsb2NrSW5kZXgiOjIsInAiOiJhYmNkZWZn + aGlqa2xtbiJ90z+z0gAAALgAAABRWfn93gs6ZXZlbnQtdHlwZQcAC21lc3NhZ2VTdG9wDTpjb250 + ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsicCI6ImFi + Y2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVowMTIiLCJz + dG9wUmVhc29uIjoidG9vbF91c2UifWRpgAMAAADpAAAATtGCFlALOmV2ZW50LXR5cGUHAAhtZXRh + ZGF0YQ06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg06bWVzc2FnZS10eXBlBwAFZXZl + bnR7Im1ldHJpY3MiOnsibGF0ZW5jeU1zIjo0OTJ9LCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyIiwi + dXNhZ2UiOnsiaW5wdXRUb2tlbnMiOjQxNiwib3V0cHV0VG9rZW5zIjo4NSwic2VydmVyVG9vbFVz + YWdlIjp7fSwidG90YWxUb2tlbnMiOjUwMX19Kem3XA== + headers: + Connection: + - keep-alive + Content-Type: + - application/vnd.amazon.eventstream + Date: + - Mon, 31 Aug 2026 18:23:11 GMT + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_invalid_model.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_invalid_model.yaml new file mode 100644 index 000000000..24d4b0956 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_stream_with_invalid_model.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ] + } + headers: + Content-Length: + - '77' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzEyWg== + amz-sdk-invocation-id: + - !!binary | + MDA4NjY0ZjYtNDY1Yy00MGQ5LTk3NTktMzQ5ZmQ2OTA4Yjlj + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/converse-stream + response: + body: + string: |- + { + "message": "The provided model identifier is invalid." + } + headers: + Connection: + - keep-alive + Content-Length: + - '55' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:12 GMT + x-amzn-ErrorType: + - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ + status: + code: 400 + message: Bad Request +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_no_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_no_content.yaml new file mode 100644 index 000000000..e4390add7 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_no_content.yaml @@ -0,0 +1,119 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "What is the weather in Seattle and San Francisco today?" + } + ] + } + ], + "toolConfig": { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": "Get the current weather in a given location.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city" + } + }, + "required": [ + "location" + ] + } + } + } + } + ] + } + } + headers: + Content-Length: + - '409' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzA4Wg== + amz-sdk-invocation-id: + - !!binary | + NGY3MDY2N2YtYWViYS00NGQ5LWExM2MtMWZkOThhZTdhMDVi + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse + response: + body: + string: |- + { + "metrics": { + "latencyMs": 500 + }, + "output": { + "message": { + "content": [ + { + "text": " The User has asked for the current weather in two locations: Seattle and San Francisco. To provide this information, I will use the \"get_current_weather\" tool for each location. I will start by calling the tool for Seattle and then for San Francisco.\n" + }, + { + "toolUse": { + "input": { + "location": "Seattle" + }, + "name": "get_current_weather", + "toolUseId": "tooluse_3KMEc44E5xeIcjbbCzRM1I" + } + }, + { + "toolUse": { + "input": { + "location": "San Francisco" + }, + "name": "get_current_weather", + "toolUseId": "tooluse_4X5BlBaeBdcWHWpTAxPkBs" + } + } + ], + "role": "assistant" + } + }, + "stopReason": "tool_use", + "usage": { + "inputTokens": 416, + "outputTokens": 87, + "serverToolUsage": {}, + "totalTokens": 503 + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '724' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:09 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_with_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_with_content.yaml new file mode 100644 index 000000000..f95e82204 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_tool_call_with_content.yaml @@ -0,0 +1,119 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "What is the weather in Seattle and San Francisco today?" + } + ] + } + ], + "toolConfig": { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": "Get the current weather in a given location.", + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city" + } + }, + "required": [ + "location" + ] + } + } + } + } + ] + } + } + headers: + Content-Length: + - '409' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MSBtZC9Cb3RvY29yZSMxLjQzLjgxIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjE0LjMgbWQvcHlpbXBs + I0NQeXRob24gbS9ELGIsWiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS40My44MQ== + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzQwWg== + amz-sdk-invocation-id: + - !!binary | + NTc1MDk0NTQtYTM3Ni00MjRkLThkNzUtYTFiY2MxZmNjNDQz + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse + response: + body: + string: |- + { + "metrics": { + "latencyMs": 522 + }, + "output": { + "message": { + "content": [ + { + "text": " To provide the current weather in both Seattle and San Francisco, I will need to use the `get_current_weather` tool twice, once for each city. I will call the tool with the location argument set to \"Seattle\" and then again with the location argument set to \"San Francisco\". \n" + }, + { + "toolUse": { + "input": { + "location": "Seattle" + }, + "name": "get_current_weather", + "toolUseId": "tooluse_Zz8T4pYt9ha27V5Qz6Qige" + } + }, + { + "toolUse": { + "input": { + "location": "San Francisco" + }, + "name": "get_current_weather", + "toolUseId": "tooluse_SxvvcYkS9z332WLbGpKY4F" + } + } + ], + "role": "assistant" + } + }, + "stopReason": "tool_use", + "usage": { + "inputTokens": 416, + "outputTokens": 95, + "serverToolUsage": {}, + "totalTokens": 511 + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '750' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:41 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_content.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_content.yaml new file mode 100644 index 000000000..aa71dd76f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_content.yaml @@ -0,0 +1,85 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ], + "inferenceConfig": { + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": [ + "|" + ] + } + } + headers: + Content-Length: + - '170' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MSBtZC9Cb3RvY29yZSMxLjQzLjgxIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjE0LjMgbWQvcHlpbXBs + I0NQeXRob24gbS9ELGIsZyxaIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgx + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzI0Wg== + amz-sdk-invocation-id: + - !!binary | + ZTk4NDc0ZDYtMDMwOC00MTY3LThiM2MtYmRkYzZmYjAyMDkz + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/amazon.nova-micro-v1%3A0/converse + response: + body: + string: |- + { + "metrics": { + "latencyMs": 251 + }, + "output": { + "message": { + "content": [ + { + "text": "It sounds like you're introducing a scenario or" + } + ], + "role": "assistant" + } + }, + "stopReason": "max_tokens", + "usage": { + "inputTokens": 5, + "outputTokens": 10, + "serverToolUsage": {}, + "totalTokens": 15 + } + } + headers: + Connection: + - keep-alive + Content-Length: + - '250' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:24 GMT + status: + code: 200 + message: OK +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_invalid_model.yaml b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_invalid_model.yaml new file mode 100644 index 000000000..cf11c5049 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/cassettes/test_converse_with_invalid_model.yaml @@ -0,0 +1,60 @@ +interactions: +- request: + body: |- + { + "messages": [ + { + "role": "user", + "content": [ + { + "text": "Say this is a test" + } + ] + } + ] + } + headers: + Content-Length: + - '77' + Content-Type: + - !!binary | + YXBwbGljYXRpb24vanNvbg== + User-Agent: + - !!binary | + Qm90bzMvMS40My44MCBtZC9Cb3RvY29yZSMxLjQzLjgwIHVhLzIuMSBvcy9saW51eCM3LjEuNi0x + cm9kZXRlMS1hbWQ2NCBtZC9hcmNoI3g4Nl82NCBsYW5nL3B5dGhvbiMzLjEyLjkgbWQvcHlpbXBs + I0NQeXRob24gbS9iLFosZyxEIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjQzLjgw + X-Amz-Date: + - !!binary | + MjAyNjA4MzFUMTgyMzA5Wg== + amz-sdk-invocation-id: + - !!binary | + OWYyYzdiMGYtNWU3Yy00MzEwLWIwYTktYmY3ZDA5YzE0ZTU2 + amz-sdk-request: + - !!binary | + YXR0ZW1wdD0x + authorization: + - Bearer test_aws_authorization + method: POST + uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/does-not-exist/converse + response: + body: + string: |- + { + "message": "The provided model identifier is invalid." + } + headers: + Connection: + - keep-alive + Content-Length: + - '55' + Content-Type: + - application/json + Date: + - Mon, 31 Aug 2026 18:23:09 GMT + x-amzn-ErrorType: + - ValidationException:http://internal.amazon.com/coral/com.amazon.bedrock/ + status: + code: 400 + message: Bad Request +version: 1 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/__init__.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/__init__.py new file mode 100644 index 000000000..e57cf4aba --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/__init__.py @@ -0,0 +1,2 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference.py new file mode 100644 index 000000000..d7820b9f0 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference.py @@ -0,0 +1,73 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Conformance scenario: bedrock chat (inference).""" + +from __future__ import annotations + +from typing import Any + +import boto3 + +from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test_util_genai.conformance import ( + ExpectedViolation, + Scenario, +) +from opentelemetry.test_util_genai.instrumentor import instrument + + +class InferenceScenario(Scenario): + expected_spans = {"chat": 1} + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + ) + expected_violations = ( + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.id", + ), + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.model", + ), + ) + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + BedrockInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette("test_converse_with_content.yaml"): + client = boto3.client( + "bedrock-runtime", region_name="us-east-1" + ) + client.converse( + modelId="amazon.nova-micro-v1:0", + messages=[ + { + "role": "user", + "content": [{"text": "Say this is a test"}], + } + ], + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference_streaming.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference_streaming.py new file mode 100644 index 000000000..0d8924824 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/inference_streaming.py @@ -0,0 +1,94 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Conformance scenario: bedrock streaming chat (inference).""" + +from __future__ import annotations + +from typing import Any + +import boto3 + +from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test.weaver_live_check import LiveCheckReport +from opentelemetry.test_util_genai.conformance import ( + ExpectedViolation, + Scenario, +) +from opentelemetry.test_util_genai.instrumentor import instrument + + +class InferenceStreamingScenario(Scenario): + expected_spans = {"chat": 1} + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + "gen_ai.client.operation.time_to_first_chunk", + "gen_ai.client.operation.time_per_output_chunk", + ) + expected_violations = ( + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.id", + ), + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.model", + ), + ) + + def validate(self, report: LiveCheckReport) -> None: + super().validate(report) + stream_values = [ + attr["value"] + for entry in report["samples"] + if "span" in entry + for attr in entry["span"]["attributes"] + if attr["name"] == "gen_ai.request.stream" + ] + assert stream_values == [True], ( + "streaming messages should set gen_ai.request.stream=true on the " + f"chat span; saw {stream_values}" + ) + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + BedrockInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette("test_converse_stream_with_content.yaml"): + client = boto3.client( + "bedrock-runtime", region_name="us-east-1" + ) + response = client.converse_stream( + modelId="amazon.nova-micro-v1:0", + messages=[ + { + "role": "user", + "content": [{"text": "Say this is a test"}], + } + ], + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + stream = response.get("stream") + if stream: + for _ in stream: + pass diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/tool_calling.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/tool_calling.py new file mode 100644 index 000000000..645acad43 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conformance/tool_calling.py @@ -0,0 +1,102 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Conformance scenario: bedrock chat with tool calls.""" + +from __future__ import annotations + +from typing import Any + +import boto3 + +from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.test_util_genai.conformance import ( + ExpectedViolation, + Scenario, +) +from opentelemetry.test_util_genai.instrumentor import instrument + + +class ToolCallingScenario(Scenario): + expected_spans = {"chat": 1} + expected_metrics = ( + "gen_ai.client.operation.duration", + "gen_ai.client.token.usage", + ) + expected_violations = ( + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.id", + ), + ExpectedViolation( + advice_id="genai_expected_attribute_missing", + message_substring="gen_ai.response.model", + ), + ) + + def run( + self, + *, + tracer_provider: TracerProvider, + meter_provider: MeterProvider, + logger_provider: LoggerProvider, + vcr: Any, + ) -> None: + with instrument( + BedrockInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ): + with vcr.use_cassette("test_converse_tool_call_with_content.yaml"): + messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "What is the weather in Seattle and San" + " Francisco today?" + ) + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": ( + "Get the current weather in a given location." + ), + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": ( + "The name of the city" + ), + } + }, + "required": ["location"], + } + }, + } + } + ] + } + client = boto3.client( + "bedrock-runtime", region_name="us-east-1" + ) + client.converse( + messages=messages, + modelId="amazon.nova-micro-v1:0", + toolConfig=tool_config, + ) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conftest.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conftest.py index 3f916eeac..2ed59eff4 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conftest.py +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/conftest.py @@ -3,8 +3,10 @@ """Test configuration for Amazon Bedrock instrumentation.""" +import os from collections.abc import Iterator +import boto3 import pytest from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor @@ -12,8 +14,63 @@ from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.trace import TracerProvider from opentelemetry.test_util_genai.instrumentor import instrument +from opentelemetry.test_util_genai.vcr import scrub_response_headers -pytest_plugins = ["opentelemetry.test_util_genai.fixtures"] +pytest_plugins = [ + "opentelemetry.test_util_genai.fixtures", + "opentelemetry.test_util_genai.vcr", +] + + +@pytest.fixture(autouse=True) +def environment() -> None: + """Set up environment variables for testing.""" + if not ( + os.getenv("AWS_ACCESS_KEY_ID") + or os.getenv("AWS_PROFILE") + or os.getenv("AWS_WEB_IDENTITY_TOKEN_FILE") + ): + os.environ.setdefault("AWS_ACCESS_KEY_ID", "test_aws_access_key_id") + os.environ.setdefault( + "AWS_SECRET_ACCESS_KEY", "test_aws_secret_access_key" + ) + os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1") + + +@pytest.fixture +def bedrock_client(): + """Create and return a Bedrock runtime client.""" + session = boto3.session.Session() + kwargs = { + "region_name": os.getenv("AWS_DEFAULT_REGION", "us-east-1"), + } + endpoint_url = os.getenv("AWS_ENDPOINT_URL_BEDROCK_RUNTIME") or os.getenv( + "AWS_ENDPOINT_URL" + ) + if endpoint_url: + kwargs["endpoint_url"] = endpoint_url + return session.client("bedrock-runtime", **kwargs) + + +@pytest.fixture(scope="module") +def vcr_config(): + """Configure VCR for recording/replaying HTTP interactions.""" + return { + "filter_headers": [ + ("authorization", "Bearer test_aws_authorization"), + ("x-amz-security-token", "test_aws_token"), + ("x-api-key", "test_anthropic_api_key"), + ], + "decode_compressed_response": True, + "before_record_response": scrub_response_headers( + [ + "set-cookie", + "x-amzn-requestid", + "anthropic-organization-id", + "anthropic-workspace-id", + ] + ), + } @pytest.fixture @@ -30,3 +87,37 @@ def instrument_bedrock( meter_provider=meter_provider, ) as instrumentor: yield instrumentor + + +@pytest.fixture +def instrument_no_content( + tracer_provider: TracerProvider, + logger_provider: LoggerProvider, + meter_provider: MeterProvider, +) -> Iterator[BedrockInstrumentor]: + """Instrument Bedrock without content capture.""" + with instrument( + BedrockInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="NO_CONTENT", + ) as instrumentor: + yield instrumentor + + +@pytest.fixture +def instrument_with_content( + tracer_provider: TracerProvider, + logger_provider: LoggerProvider, + meter_provider: MeterProvider, +) -> Iterator[BedrockInstrumentor]: + """Instrument Bedrock with SPAN_ONLY content capture.""" + with instrument( + BedrockInstrumentor(), + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + content_capture="SPAN_ONLY", + ) as instrumentor: + yield instrumentor diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.oldest.txt b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.oldest.txt index c97b1761d..7d79d8080 100644 --- a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.oldest.txt +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.oldest.txt @@ -3,3 +3,6 @@ # The oldest tox factor resolves declared dependencies from pyproject.toml # with UV_RESOLUTION=lowest-direct. There are no additional test-only pins. + +# Drop this once opentelemetry-util-genai 1.2b0 is published. +-e util/opentelemetry-util-genai diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_completion_hook.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_completion_hook.py new file mode 100644 index 000000000..b6c7d2893 --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_completion_hook.py @@ -0,0 +1,114 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import os +from unittest.mock import MagicMock, patch + +from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability, +) +from opentelemetry.instrumentation.genai.bedrock import BedrockInstrumentor +from opentelemetry.util.genai.environment_variables import ( + OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, +) + + +def test_custom_hook_is_called( + span_exporter, + log_exporter, + tracer_provider, + logger_provider, + meter_provider, + bedrock_client, + vcr, +): + """A hook passed to instrument() is called after converse.""" + hook = MagicMock() + instrumentor = BedrockInstrumentor() + _OpenTelemetrySemanticConventionStability._initialized = False + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + completion_hook=hook, + ) + + try: + with vcr.use_cassette("test_converse_with_content.yaml"): + bedrock_client.converse( + messages=[ + { + "role": "user", + "content": [{"text": "Say this is a test"}], + } + ], + modelId="amazon.nova-micro-v1:0", + ) + finally: + instrumentor.uninstrument() + + hook.on_completion.assert_called_once() + kwargs = hook.on_completion.call_args.kwargs + assert kwargs["inputs"] + assert kwargs["outputs"] + assert kwargs["span"] is not None + + # Content goes to the hook only — not to span attributes or log records + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span_attrs = spans[0].attributes or {} + assert "gen_ai.input.messages" not in span_attrs + assert "gen_ai.output.messages" not in span_attrs + assert log_exporter.get_finished_logs() == () + + +@patch.dict( + os.environ, + {OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT: "span_only"}, +) +def test_default_hook_loaded_from_env( + span_exporter, + tracer_provider, + logger_provider, + meter_provider, + bedrock_client, + vcr, +): + """When no hook kwarg is given, load_completion_hook() provides the default.""" + default_hook = MagicMock() + instrumentor = BedrockInstrumentor() + with patch( + "opentelemetry.instrumentation.genai.bedrock.load_completion_hook", + return_value=default_hook, + ): + instrumentor.instrument( + tracer_provider=tracer_provider, + logger_provider=logger_provider, + meter_provider=meter_provider, + ) + + try: + with vcr.use_cassette("test_converse_with_content.yaml"): + bedrock_client.converse( + messages=[ + { + "role": "user", + "content": [{"text": "Say this is a test"}], + } + ], + modelId="amazon.nova-micro-v1:0", + ) + finally: + instrumentor.uninstrument() + + default_hook.on_completion.assert_called_once() + kwargs = default_hook.on_completion.call_args.kwargs + assert kwargs["inputs"] + assert kwargs["outputs"] + assert kwargs["span"] is not None + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span_attrs = spans[0].attributes or {} + assert "gen_ai.input.messages" in span_attrs + assert "gen_ai.output.messages" in span_attrs diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_conformance.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_conformance.py new file mode 100644 index 000000000..28877b15c --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_conformance.py @@ -0,0 +1,38 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Per-scenario conformance tests for bedrock.""" + +from __future__ import annotations + +from typing import Any + +import pytest + +pytest.importorskip("opentelemetry.test.weaver_live_check") +pytest.importorskip("opentelemetry.exporter.otlp.proto.grpc") + +from opentelemetry.test.weaver_live_check import WeaverLiveCheck +from opentelemetry.test_util_genai.conformance import ( + Scenario, + run_conformance, +) + +from .conformance.inference import InferenceScenario +from .conformance.inference_streaming import InferenceStreamingScenario +from .conformance.tool_calling import ToolCallingScenario + + +@pytest.mark.parametrize( + "scenario", + [ + InferenceScenario(), + InferenceStreamingScenario(), + ToolCallingScenario(), + ], + ids=lambda s: type(s).__name__, +) +def test_conformance( + scenario: Scenario, vcr: Any, weaver_live_check: WeaverLiveCheck +) -> None: + run_conformance(scenario, vcr=vcr, weaver=weaver_live_check) diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse.py new file mode 100644 index 000000000..8fc396d6f --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse.py @@ -0,0 +1,468 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Amazon Bedrock Converse API instrumentation.""" + +import json + +import pytest +from botocore.exceptions import ClientError + +from opentelemetry.instrumentation.genai.bedrock.extractors import ( + extract_content_block, + extract_converse_request, + extract_converse_response, +) +from opentelemetry.semconv._incubating.attributes import ( + error_attributes as ErrorAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.trace import StatusCode +from opentelemetry.util.genai.handler import TelemetryHandler + + +@pytest.mark.vcr +def test_converse_with_content( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + assert response + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert ( + span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] + == GenAIAttributes.GenAiOperationNameValues.CHAT.value + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] + == GenAIAttributes.GenAiProviderNameValues.AWS_BEDROCK.value + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "amazon.nova-micro-v1:0" + ) + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL not in span.attributes + assert GenAIAttributes.GEN_AI_RESPONSE_ID not in span.attributes + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] == 10 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] == 0.8 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == 1.0 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] == ( + "|", + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + is not None + ) + assert ( + span.attributes[ServerAttributes.SERVER_ADDRESS] + == "bedrock-runtime.us-east-1.amazonaws.com" + ) + assert span.attributes[ServerAttributes.SERVER_PORT] == 443 + assert span.status.status_code == StatusCode.UNSET + + # Check captured message content + input_msgs = json.loads( + span.attributes[GenAIAttributes.GEN_AI_INPUT_MESSAGES] + ) + assert len(input_msgs) == 1 + assert input_msgs[0]["role"] == "user" + assert input_msgs[0]["parts"][0]["content"] == "Say this is a test" + + output_msgs = json.loads( + span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES] + ) + assert len(output_msgs) == 1 + assert output_msgs[0]["role"] == "assistant" + assert len(output_msgs[0]["parts"]) > 0 + + +@pytest.mark.vcr +def test_converse_no_content( + bedrock_client, + instrument_no_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + assert response + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "amazon.nova-micro-v1:0" + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + is not None + ) + + +@pytest.mark.vcr +def test_converse_tool_call_with_content( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "What is the weather in Seattle and San Francisco" + " today?" + ) + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": ( + "Get the current weather in a given location." + ), + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city", + } + }, + "required": ["location"], + } + }, + } + } + ] + } + + response = bedrock_client.converse( + messages=messages, + modelId="amazon.nova-micro-v1:0", + toolConfig=tool_config, + ) + + assert response + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "tool_call", + ) + + output_msgs = json.loads( + span.attributes[GenAIAttributes.GEN_AI_OUTPUT_MESSAGES] + ) + assert len(output_msgs) == 1 + parts = output_msgs[0]["parts"] + tool_calls = [p for p in parts if p.get("type") == "tool_call"] + assert len(tool_calls) >= 1 + assert tool_calls[0]["name"] == "get_current_weather" + + +@pytest.mark.vcr +def test_converse_tool_call_no_content( + bedrock_client, + instrument_no_content, + span_exporter, +) -> None: + messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "What is the weather in Seattle and San Francisco" + " today?" + ) + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": ( + "Get the current weather in a given location." + ), + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city", + } + }, + "required": ["location"], + } + }, + } + } + ] + } + + response = bedrock_client.converse( + messages=messages, + modelId="amazon.nova-micro-v1:0", + toolConfig=tool_config, + ) + + assert response + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "tool_call", + ) + assert GenAIAttributes.GEN_AI_TOOL_DEFINITIONS in (span.attributes or {}) + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in (span.attributes or {}) + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in ( + span.attributes or {} + ) + + +@pytest.mark.vcr +def test_converse_with_invalid_model( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + with pytest.raises(ClientError) as exc_info: + bedrock_client.converse( + messages=messages, + modelId="does-not-exist", + ) + + assert exc_info.value.response["Error"]["Code"] == "ValidationException" + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat does-not-exist" + assert span.status.status_code == StatusCode.ERROR + assert span.attributes[ErrorAttributes.ERROR_TYPE] in ( + "ValidationException", + "botocore.errorfactory.ValidationException", + ) + + +def test_extract_converse_request_no_content(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + extract_converse_request( + { + "messages": [{"role": "user", "content": [{"text": "hello"}]}], + "system": [{"text": "system instruction"}], + "inferenceConfig": {"temperature": 0.5}, + "toolConfig": { + "tools": [{"toolSpec": {"name": "get_weather"}}], + }, + }, + invocation, + capture_content=False, + ) + assert not invocation.input_messages + assert not invocation.system_instruction + assert invocation.tool_definitions + assert invocation.temperature == 0.5 + + +def test_extract_converse_response_no_content(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + extract_converse_response( + { + "output": { + "message": { + "role": "assistant", + "content": [{"text": "hi"}], + } + }, + "stopReason": "end_turn", + "usage": { + "inputTokens": 5, + "outputTokens": 2, + "cacheReadInputTokens": 3, + "cacheWriteInputTokens": 7, + }, + }, + invocation, + capture_content=False, + ) + assert not invocation.output_messages + assert invocation.finish_reasons == ["stop"] + assert invocation.input_tokens == 5 + assert invocation.output_tokens == 2 + assert invocation.cache_read_input_tokens == 3 + assert invocation.cache_creation_input_tokens == 7 + + +def test_extract_converse_request_top_k_and_seed(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + extract_converse_request( + { + "inferenceConfig": {"topK": 40, "seed": 123}, + }, + invocation, + ) + assert invocation.top_k == 40.0 + assert invocation.seed == 123 + + invocation2 = handler.inference(provider="aws.bedrock") + extract_converse_request( + { + "additionalModelRequestFields": {"top_k": 250, "seed": 456}, + }, + invocation2, + ) + assert invocation2.top_k == 250.0 + assert invocation2.seed == 456 + + invocation3 = handler.inference(provider="aws.bedrock") + extract_converse_request( + { + "additionalModelRequestFields": {"inferenceConfig": {"topK": 20}}, + }, + invocation3, + ) + assert invocation3.top_k == 20.0 + + +def test_extract_content_block_reasoning() -> None: + part = extract_content_block( + { + "reasoningContent": { + "reasoningText": {"text": "Thinking step by step..."} + } + } + ) + assert part is not None + assert part.type == "reasoning" + assert getattr(part, "content") == "Thinking step by step..." + + # Redacted reasoningContent + part2 = extract_content_block( + {"reasoningContent": {"redactedContent": b"redacted_bytes"}} + ) + assert part2 is not None + assert part2.type == "reasoning" + assert getattr(part2, "content") == "" + + +def test_extract_content_block_document() -> None: + doc_part = extract_content_block( + { + "document": { + "format": "pdf", + "name": "sample.pdf", + "source": {"bytes": b"pdf_data"}, + } + } + ) + assert doc_part is not None + assert doc_part.type == "blob" + assert getattr(doc_part, "modality") == "document" + assert getattr(doc_part, "mime_type") == "application/pdf" + assert getattr(doc_part, "content") == b"pdf_data" + + +def test_extract_converse_response_reasoning(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + extract_converse_response( + { + "output": { + "message": { + "role": "assistant", + "content": [ + { + "reasoningContent": { + "reasoningText": {"text": "Let me think"} + } + }, + {"text": "The answer is 42."}, + ], + } + }, + "stopReason": "end_turn", + }, + invocation, + capture_content=True, + ) + assert len(invocation.output_messages) == 1 + out_msg = invocation.output_messages[0] + assert len(out_msg.parts) == 2 + assert out_msg.parts[0].type == "reasoning" + assert getattr(out_msg.parts[0], "content") == "Let me think" + assert out_msg.parts[1].type == "text" + assert getattr(out_msg.parts[1], "content") == "The answer is 42." + + +def test_extract_content_block_generic_parts() -> None: + for key in ( + "video", + "audio", + "guardContent", + "cachePoint", + "citationsContent", + "searchResult", + "toolAddition", + "toolRemoval", + ): + part = extract_content_block({key: {"foo": "bar"}}) + assert part is not None + assert part.type == key diff --git a/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse_stream.py b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse_stream.py new file mode 100644 index 000000000..53fe2deec --- /dev/null +++ b/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_converse_stream.py @@ -0,0 +1,467 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for Amazon Bedrock converse_stream API instrumentation.""" + +from unittest import mock + +import pytest +from botocore.eventstream import EventStream +from botocore.exceptions import ClientError, EventStreamError + +from opentelemetry.instrumentation.genai.bedrock.stream import ( + BedrockConverseStreamWrapper, +) +from opentelemetry.semconv._incubating.attributes import ( + error_attributes as ErrorAttributes, +) +from opentelemetry.semconv._incubating.attributes import ( + gen_ai_attributes as GenAIAttributes, +) +from opentelemetry.semconv.attributes import ( + server_attributes as ServerAttributes, +) +from opentelemetry.trace import StatusCode +from opentelemetry.util.genai.handler import TelemetryHandler + + +@pytest.mark.vcr +def test_converse_stream_with_content( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + events = list(response["stream"]) + assert len(events) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert ( + span.attributes[GenAIAttributes.GEN_AI_OPERATION_NAME] + == GenAIAttributes.GenAiOperationNameValues.CHAT.value + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_PROVIDER_NAME] + == GenAIAttributes.GenAiProviderNameValues.AWS_BEDROCK.value + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] + == "amazon.nova-micro-v1:0" + ) + assert GenAIAttributes.GEN_AI_RESPONSE_MODEL not in span.attributes + assert GenAIAttributes.GEN_AI_RESPONSE_ID not in span.attributes + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS] == 10 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE] == 0.8 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_TOP_P] == 1.0 + assert span.attributes[GenAIAttributes.GEN_AI_REQUEST_STOP_SEQUENCES] == ( + "|", + ) + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + is not None + ) + assert ( + span.attributes[ServerAttributes.SERVER_ADDRESS] + == "bedrock-runtime.us-east-1.amazonaws.com" + ) + assert span.attributes[ServerAttributes.SERVER_PORT] == 443 + assert span.status.status_code == StatusCode.UNSET + + +@pytest.mark.vcr +def test_converse_stream_no_content( + bedrock_client, + instrument_no_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + events = list(response["stream"]) + assert len(events) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in span.attributes + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in span.attributes + assert ( + span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] + is not None + ) + + +@pytest.mark.vcr +def test_converse_stream_with_content_tool_call( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "What is the weather in Seattle and San Francisco" + " today?" + ) + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": ( + "Get the current weather in a given location." + ), + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city", + } + }, + "required": ["location"], + } + }, + } + } + ] + } + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + toolConfig=tool_config, + ) + + events = list(response["stream"]) + assert len(events) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "tool_call", + ) + + +@pytest.mark.vcr +def test_converse_stream_no_content_tool_call( + bedrock_client, + instrument_no_content, + span_exporter, +) -> None: + messages = [ + { + "role": "user", + "content": [ + { + "text": ( + "What is the weather in Seattle and San Francisco" + " today?" + ) + } + ], + } + ] + tool_config = { + "tools": [ + { + "toolSpec": { + "name": "get_current_weather", + "description": ( + "Get the current weather in a given location." + ), + "inputSchema": { + "json": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The name of the city", + } + }, + "required": ["location"], + } + }, + } + } + ] + } + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + toolConfig=tool_config, + ) + + events = list(response["stream"]) + assert len(events) > 0 + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat amazon.nova-micro-v1:0" + assert span.attributes[GenAIAttributes.GEN_AI_RESPONSE_FINISH_REASONS] == ( + "tool_call", + ) + assert GenAIAttributes.GEN_AI_TOOL_DEFINITIONS in (span.attributes or {}) + assert GenAIAttributes.GEN_AI_INPUT_MESSAGES not in (span.attributes or {}) + assert GenAIAttributes.GEN_AI_OUTPUT_MESSAGES not in ( + span.attributes or {} + ) + + +@pytest.mark.vcr +def test_converse_stream_close_before_consumption( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + response["stream"].close() + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + assert span.name == "chat amazon.nova-micro-v1:0" + assert span.status.status_code == StatusCode.UNSET + + +@pytest.mark.vcr +def test_converse_stream_with_invalid_model( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + with pytest.raises(ClientError) as exc_info: + bedrock_client.converse_stream( + messages=messages, + modelId="does-not-exist", + ) + + assert exc_info.value.response["Error"]["Code"] == "ValidationException" + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.name == "chat does-not-exist" + assert span.status.status_code == StatusCode.ERROR + assert span.attributes[ErrorAttributes.ERROR_TYPE] in ( + "ValidationException", + "botocore.errorfactory.ValidationException", + ) + + +@pytest.mark.vcr +def test_converse_stream_handles_event_stream_error( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + with mock.patch.object( + EventStream, + "_parse_event", + side_effect=EventStreamError( + {"modelStreamErrorException": {}}, "ConverseStream" + ), + ): + with pytest.raises(EventStreamError): + for _event in response["stream"]: + pass + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.status.status_code == StatusCode.ERROR + assert span.attributes[ErrorAttributes.ERROR_TYPE] in ( + "EventStreamError", + "botocore.exceptions.EventStreamError", + ) + + +@pytest.mark.vcr +def test_converse_stream_caller_side_error( + bedrock_client, + instrument_with_content, + span_exporter, +) -> None: + messages = [{"role": "user", "content": [{"text": "Say this is a test"}]}] + + response = bedrock_client.converse_stream( + messages=messages, + modelId="amazon.nova-micro-v1:0", + inferenceConfig={ + "maxTokens": 10, + "temperature": 0.8, + "topP": 1, + "stopSequences": ["|"], + }, + ) + + with pytest.raises(RuntimeError, match="caller aborted stream"): + with response["stream"] as stream: + for _event in stream: + raise RuntimeError("caller aborted stream") + + spans = span_exporter.get_finished_spans() + assert len(spans) == 1 + span = spans[0] + + assert span.status.status_code == StatusCode.ERROR + assert span.attributes[ErrorAttributes.ERROR_TYPE] == "RuntimeError" + + +def test_stream_wrapper_no_content(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + wrapper = BedrockConverseStreamWrapper( + stream=mock.MagicMock(), + invocation=invocation, + capture_content=False, + ) + wrapper._process_chunk( + { + "contentBlockStart": {"contentBlockIndex": 0, "start": {}}, + "contentBlockDelta": { + "contentBlockIndex": 0, + "delta": {"text": "hello"}, + }, + "messageStop": {"stopReason": "end_turn"}, + "metadata": { + "usage": { + "inputTokens": 10, + "outputTokens": 5, + "cacheReadInputTokens": 4, + "cacheWriteInputTokens": 8, + } + }, + } + ) + wrapper._on_stream_end() + assert not invocation.output_messages + assert invocation.finish_reasons == ["stop"] + assert invocation.response_model_name is None + assert invocation.input_tokens == 10 + assert invocation.output_tokens == 5 + assert invocation.cache_read_input_tokens == 4 + assert invocation.cache_creation_input_tokens == 8 + + +def test_stream_wrapper_with_reasoning(tracer_provider) -> None: + handler = TelemetryHandler(tracer_provider=tracer_provider) + invocation = handler.inference(provider="aws.bedrock") + wrapper = BedrockConverseStreamWrapper( + stream=mock.MagicMock(), + invocation=invocation, + capture_content=True, + ) + wrapper._process_chunk( + { + "contentBlockStart": { + "contentBlockIndex": 0, + "start": {}, + }, + "contentBlockDelta": { + "contentBlockIndex": 0, + "delta": {"reasoningContent": {"text": "Step 1: thinking..."}}, + }, + } + ) + wrapper._process_chunk( + { + "contentBlockDelta": { + "contentBlockIndex": 0, + "delta": {"reasoningContent": {"text": " Step 2: concluded."}}, + } + } + ) + wrapper._process_chunk( + { + "contentBlockStart": {"contentBlockIndex": 1, "start": {}}, + "contentBlockDelta": { + "contentBlockIndex": 1, + "delta": {"text": "Here is the result."}, + }, + "messageStop": {"stopReason": "end_turn"}, + } + ) + wrapper._on_stream_end() + assert len(invocation.output_messages) == 1 + out_msg = invocation.output_messages[0] + assert len(out_msg.parts) == 2 + assert out_msg.parts[0].type == "reasoning" + assert ( + getattr(out_msg.parts[0], "content") + == "Step 1: thinking... Step 2: concluded." + ) + assert out_msg.parts[1].type == "text" + assert getattr(out_msg.parts[1], "content") == "Here is the result." + assert invocation.finish_reasons == ["stop"] diff --git a/tox.ini b/tox.ini index 6b0e36909..f89a8bb8c 100644 --- a/tox.ini +++ b/tox.ini @@ -41,6 +41,7 @@ envlist = ; instrumentation-genai-bedrock py3{10,14}-test-instrumentation-genai-bedrock-latest py310-test-instrumentation-genai-bedrock-oldest + py314-test-instrumentation-genai-bedrock-conformance ; instrumentation-genai-claude-agent-sdk py3{10,14}-test-instrumentation-genai-claude-agent-sdk-latest @@ -92,6 +93,12 @@ envlist = readme [testenv] +passenv = + AWS_* + ANTHROPIC_* + OPENAI_* + GOOGLE_* + OTEL_* test_deps = opentelemetry-api opentelemetry-semantic-conventions @@ -171,6 +178,8 @@ deps = bedrock-latest: {[testenv]test_deps} bedrock-latest: {[testenv]pytest_deps} bedrock-latest: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.latest.txt + bedrock-conformance: {[testenv]pytest_deps} + bedrock-conformance: -r {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/requirements.latest.txt claude-agent-sdk-oldest: {[testenv]pytest_deps} claude-agent-sdk-oldest: -e {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk[instruments] @@ -279,7 +288,8 @@ commands = test-instrumentation-genai-anthropic-{oldest,latest}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests --vcr-record=none {posargs} test-instrumentation-genai-anthropic-conformance: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-anthropic/tests/test_conformance.py --vcr-record=none {posargs} - test-instrumentation-genai-bedrock-{oldest,latest}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests --vcr-record=none {posargs} + test-instrumentation-genai-bedrock-{oldest,latest}: pytest --ignore={toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_conformance.py {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests --vcr-record=none {posargs} + test-instrumentation-genai-bedrock-conformance: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-bedrock/tests/test_conformance.py --vcr-record=none {posargs} test-instrumentation-genai-claude-agent-sdk: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-claude-agent-sdk/tests --vcr-record=none {posargs} test-instrumentation-genai-crewai-{oldest,latest}: pytest {toxinidir}/instrumentation/opentelemetry-instrumentation-genai-crewai/tests --vcr-record=none {posargs}