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}