diff --git a/pyproject.toml b/pyproject.toml index 548e24d7..464b2c26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,3 +144,6 @@ build-backend = "uv_build" [tool.uv.workspace] members = ["examples/fastapi-app"] + +[tool.ruff] +target-version = "py310" \ No newline at end of file diff --git a/src/lmnr/opentelemetry_lib/decorators/__init__.py b/src/lmnr/opentelemetry_lib/decorators/__init__.py index 8ab0d4bf..b3b2212f 100644 --- a/src/lmnr/opentelemetry_lib/decorators/__init__.py +++ b/src/lmnr/opentelemetry_lib/decorators/__init__.py @@ -218,7 +218,7 @@ def wrap(*args, **kwargs): except Exception as e: _process_exception(span, e) _cleanup_span(span, wrapper) - raise e + raise finally: # Always restore global context context_api.detach(ctx_token) diff --git a/src/lmnr/opentelemetry_lib/litellm/__init__.py b/src/lmnr/opentelemetry_lib/litellm/__init__.py index 71913d65..8e1f7d91 100644 --- a/src/lmnr/opentelemetry_lib/litellm/__init__.py +++ b/src/lmnr/opentelemetry_lib/litellm/__init__.py @@ -3,10 +3,12 @@ import json from datetime import datetime +from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_PROMPT from opentelemetry.trace import SpanKind, Status, StatusCode, Tracer from lmnr.opentelemetry_lib.decorators import json_dumps from lmnr.opentelemetry_lib.litellm.utils import ( get_tool_definition, + is_validator_iterator, model_as_dict, set_span_attribute, ) @@ -245,35 +247,107 @@ def _process_input_messages(self, span, messages): if not isinstance(messages, list): return - for i, message in enumerate(messages): - message_dict = model_as_dict(message) - role = message_dict.get("role", "unknown") - set_span_attribute(span, f"gen_ai.prompt.{i}.role", role) - - tool_calls = message_dict.get("tool_calls", []) - self._process_tool_calls(span, tool_calls, i, is_response=False) + prompt_index = 0 + for item in messages: + block_dict = model_as_dict(item) + if block_dict.get("type", "message") == "message": + tool_calls = block_dict.get("tool_calls", []) + self._process_tool_calls( + span, tool_calls, prompt_index, is_response=False + ) + content = block_dict.get("content") + if is_validator_iterator(content): + # Have not been able to catch this in the wild, but keeping + # just in case, as raw OpenAI responses do that + content = [self._process_content_part(part) for part in content] + try: + stringified_content = ( + content if isinstance(content, str) else json_dumps(content) + ) + except Exception: + stringified_content = ( + str(content) if content is not None else "" + ) + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.content", + stringified_content, + ) + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.role", + block_dict.get("role"), + ) + prompt_index += 1 - content = message_dict.get("content", "") - if content is None: - continue - if isinstance(content, str): - set_span_attribute(span, f"gen_ai.prompt.{i}.content", content) - elif isinstance(content, list): + elif block_dict.get("type") == "computer_call_output": set_span_attribute( - span, f"gen_ai.prompt.{i}.content", json.dumps(content) + span, + f"{GEN_AI_PROMPT}.{prompt_index}.role", + "computer_call_output", ) - else: + output_image_url = block_dict.get("output", {}).get("image_url") + if output_image_url: + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.content", + json.dumps( + [ + { + "type": "image_url", + "image_url": {"url": output_image_url}, + } + ] + ), + ) + prompt_index += 1 + elif block_dict.get("type") == "computer_call": + set_span_attribute( + span, f"{GEN_AI_PROMPT}.{prompt_index}.role", "assistant" + ) + call_content = {} + if block_dict.get("id"): + call_content["id"] = block_dict.get("id") + if block_dict.get("action"): + call_content["action"] = block_dict.get("action") set_span_attribute( span, - f"gen_ai.prompt.{i}.content", - json.dumps(model_as_dict(content)), + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.arguments", + json.dumps(call_content), ) - if role == "tool": set_span_attribute( span, - f"gen_ai.prompt.{i}.tool_call_id", - message_dict.get("tool_call_id"), + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.id", + block_dict.get("call_id"), ) + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.name", + "computer_call", + ) + prompt_index += 1 + elif block_dict.get("type") == "reasoning": + reasoning_summary = block_dict.get("summary") + if reasoning_summary and isinstance(reasoning_summary, list): + processed_chunks = [ + {"type": "text", "text": chunk.get("text")} + for chunk in reasoning_summary + if isinstance(chunk, dict) + and chunk.get("type") == "summary_text" + ] + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.reasoning", + json_dumps(processed_chunks), + ) + set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.role", + "assistant", + ) + # reasoning is followed by other content parts in the same messge, + # so we don't increment the prompt index + # TODO: handle other block types def _process_request_tool_definitions(self, span, tools): """Process and set tool definitions attributes on the span""" @@ -493,11 +567,19 @@ def _process_response_output(self, span, output): ) tool_call_index += 1 elif block_dict.get("type") == "reasoning": - set_span_attribute( - span, - "gen_ai.completion.0.reasoning", - block_dict.get("summary"), - ) + reasoning_summary = block_dict.get("summary") + if reasoning_summary and isinstance(reasoning_summary, list): + processed_chunks = [ + {"type": "text", "text": chunk.get("text")} + for chunk in reasoning_summary + if isinstance(chunk, dict) + and chunk.get("type") == "summary_text" + ] + set_span_attribute( + span, + "gen_ai.completion.0.reasoning", + json_dumps(processed_chunks), + ) # TODO: handle other block types, in particular other calls def _process_success_response(self, span, response_obj): diff --git a/src/lmnr/opentelemetry_lib/litellm/utils.py b/src/lmnr/opentelemetry_lib/litellm/utils.py index f56c3982..a81a0b78 100644 --- a/src/lmnr/opentelemetry_lib/litellm/utils.py +++ b/src/lmnr/opentelemetry_lib/litellm/utils.py @@ -1,3 +1,4 @@ +import re from pydantic import BaseModel from opentelemetry.sdk.trace import Span from opentelemetry.util.types import AttributeValue @@ -80,3 +81,14 @@ def get_tool_definition(tool: dict) -> ToolDefinition: description=description, parameters=parameters, ) + + +def is_validator_iterator(content): + """ + Some OpenAI objects contain fields typed as Iterable, which pydantic + internally converts to a ValidatorIterator, and they cannot be trivially + serialized without consuming the iterator to, for example, a list. + + See: https://github.com/pydantic/pydantic/issues/9541#issuecomment-2189045051 + """ + return re.search(r"pydantic.*ValidatorIterator'>$", str(type(content))) diff --git a/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py new file mode 100644 index 00000000..0a6c7925 --- /dev/null +++ b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py @@ -0,0 +1,100 @@ +"""OpenTelemetry CUA instrumentation""" + +import logging +from typing import Any, AsyncGenerator, Collection + +from lmnr.opentelemetry_lib.decorators import json_dumps +from lmnr import Laminar +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap + +from opentelemetry.trace import Span +from opentelemetry.trace.status import Status, StatusCode +from wrapt import wrap_function_wrapper + +logger = logging.getLogger(__name__) + +_instruments = ("cua-agent >= 0.4.0",) + + +def _wrap_run( + wrapped, + instance, + args, + kwargs, +): + parent_span = Laminar.start_span("ComputerAgent.run") + instance._lmnr_parent_span = parent_span + + try: + result: AsyncGenerator[dict[str, Any], None] = wrapped(*args, **kwargs) + return _abuild_from_streaming_response(parent_span, result) + except Exception as e: + if parent_span.is_recording(): + parent_span.set_status(Status(StatusCode.ERROR)) + parent_span.record_exception(e) + parent_span.end() + raise + + +async def _abuild_from_streaming_response( + parent_span: Span, response: AsyncGenerator[dict[str, Any], None] +) -> AsyncGenerator[dict[str, Any], None]: + with Laminar.use_span(parent_span, end_on_exit=True): + response_iter = aiter(response) + while True: + step = None + step_span = Laminar.start_span("ComputerAgent.step") + with Laminar.use_span(step_span): + try: + step = await anext(response_iter) + step_span.set_attribute("lmnr.span.output", json_dumps(step)) + try: + # When processing tool calls, each output item is processed separately, + # if the output is message, agent.step returns an empty array + # https://github.com/trycua/cua/blob/17d670962970a1d1774daaec029ebf92f1f9235e/libs/python/agent/agent/agent.py#L459 + if len(step.get("output", [])) == 0: + continue + except Exception: + pass + if step_span.is_recording(): + step_span.end() + except StopAsyncIteration: + # don't end on purpose, there is no iteration step here. + break + + if step is not None: + yield step + + +class CuaAgentInstrumentor(BaseInstrumentor): + def __init__(self): + super().__init__() + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + wrap_package = "agent.agent" + wrap_object = "ComputerAgent" + wrap_method = "run" + try: + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _wrap_run, + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + def _uninstrument(self, **kwargs): + wrap_package = "agent.agent" + wrap_object = "ComputerAgent" + wrap_method = "run" + try: + unwrap( + f"{wrap_package}.{wrap_object}", + wrap_method, + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist diff --git a/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py new file mode 100644 index 00000000..21279bb2 --- /dev/null +++ b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py @@ -0,0 +1,477 @@ +"""OpenTelemetry CUA instrumentation""" + +import logging +from typing import Collection + +from lmnr.opentelemetry_lib.decorators import json_dumps +from lmnr.sdk.utils import get_input_from_func_args +from lmnr import Laminar +from lmnr.opentelemetry_lib.tracing.context import get_current_context +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.instrumentation.utils import unwrap + +from opentelemetry import trace +from opentelemetry.trace import Span +from opentelemetry.trace.status import Status, StatusCode +from wrapt import wrap_function_wrapper + +from .utils import payload_to_placeholder + +logger = logging.getLogger(__name__) + +_instruments = ("cua-computer >= 0.4.0",) + + +WRAPPED_METHODS = [ + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "close", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "force_close", + }, +] +WRAPPED_AMETHODS = [ + { + "package": "computer.computer", + "object": "Computer", + "method": "__aenter__", + "action": "start_parent_span", + }, + { + "package": "computer.computer", + "object": "Computer", + "method": "__aexit__", + "action": "end_parent_span", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "mouse_down", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "mouse_up", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "left_click", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "right_click", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "double_click", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "move_cursor", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "drag_to", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "drag", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "key_down", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "key_up", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "type_text", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "press", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "hotkey", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "scroll", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "scroll_down", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "scroll_up", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "screenshot", + "output_formatter": payload_to_placeholder, + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "get_screen_size", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "get_cursor_position", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "copy_to_clipboard", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "set_clipboard", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "file_exists", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "directory_exists", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "list_dir", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "read_text", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "write_text", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "read_bytes", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "write_bytes", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "delete_file", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "create_dir", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "delete_dir", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "get_file_size", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "run_command", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "get_accessibility_tree", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "to_screen_coordinates", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "get_active_window_bounds", + }, + { + "package": "computer.interface.generic", + "object": "GenericComputerInterface", + "method": "to_screenshot_coordinates", + }, +] + + +def _with_wrapper(func): + """Helper for providing tracer for wrapper functions. Includes metric collectors.""" + + def wrapper( + to_wrap, + ): + def wrapper(wrapped, instance, args, kwargs): + return func( + to_wrap, + wrapped, + instance, + args, + kwargs, + ) + + return wrapper + + return wrapper + + +def add_input_to_parent_span(span, instance): + # api_key is skipped on purpose + params = {} + if hasattr(instance, "display"): + params["display"] = instance.display + if hasattr(instance, "memory"): + params["memory"] = instance.memory + if hasattr(instance, "cpu"): + params["cpu"] = instance.cpu + if hasattr(instance, "os_type"): + params["os_type"] = instance.os_type + if hasattr(instance, "name"): + params["name"] = instance.name + if hasattr(instance, "image"): + params["image"] = instance.image + if hasattr(instance, "shared_directories"): + params["shared_directories"] = instance.shared_directories + if hasattr(instance, "use_host_computer_server"): + params["use_host_computer_server"] = instance.use_host_computer_server + if hasattr(instance, "verbosity"): + if ( + isinstance(instance.verbosity, int) + and instance.verbosity in logging._levelToName + ): + params["verbosity"] = logging._levelToName[instance.verbosity] + else: + params["verbosity"] = instance.verbosity + if hasattr(instance, "telemetry_enabled"): + params["telemetry_enabled"] = instance.telemetry_enabled + if hasattr(instance, "provider_type"): + params["provider_type"] = instance.provider_type + if hasattr(instance, "port"): + params["port"] = instance.port + if hasattr(instance, "noVNC_port"): + params["noVNC_port"] = instance.noVNC_port + if hasattr(instance, "host"): + params["host"] = instance.host + if hasattr(instance, "storage"): + params["storage"] = instance.storage + if hasattr(instance, "ephemeral"): + params["ephemeral"] = instance.ephemeral + if hasattr(instance, "experiments"): + params["experiments"] = instance.experiments + span.set_attribute("lmnr.span.input", json_dumps(params)) + + +@_with_wrapper +def _wrap( + to_wrap, + wrapped, + instance, + args, + kwargs, +): + if to_wrap.get("action") == "start_parent_span": + parent_span = Laminar.start_span("computer.run") + add_input_to_parent_span(parent_span, instance) + result = wrapped(*args, **kwargs) + try: + instance._interface._lmnr_parent_span = parent_span + except Exception: + pass + return result + elif to_wrap.get("action") == "end_parent_span": + result = wrapped(*args, **kwargs) + try: + parent_span: Span = instance._interface._lmnr_parent_span + if parent_span and parent_span.is_recording(): + parent_span.end() + except Exception: + pass + return result + + # if there's no parent span, use + parent_span = trace.get_current_span(context=get_current_context()) + try: + if instance._lmnr_parent_span: + parent_span: Span = instance._lmnr_parent_span + except Exception: + pass + + with Laminar.use_span(parent_span): + instance_name = "interface" + with Laminar.start_as_current_span( + f"{instance_name}.{to_wrap.get('method')}", span_type="TOOL" + ) as span: + span.set_attribute( + "lmnr.span.input", + json_dumps(get_input_from_func_args(wrapped, True, args, kwargs)), + ) + try: + result = wrapped(*args, **kwargs) + except Exception as e: # pylint: disable=broad-except + span.set_status(Status(StatusCode.ERROR)) + span.record_exception(e) + span.end() + raise + output_formatter = to_wrap.get("output_formatter") or ( + lambda x: json_dumps(x) + ) + span.set_attribute("lmnr.span.output", output_formatter(result)) + return result + + +@_with_wrapper +async def _wrap_async( + to_wrap, + wrapped, + instance, + args, + kwargs, +): + if to_wrap.get("action") == "start_parent_span": + parent_span = Laminar.start_span("computer.run") + add_input_to_parent_span(parent_span, instance) + result = await wrapped(*args, **kwargs) + try: + instance._interface._lmnr_parent_span = parent_span + except Exception: + pass + return result + elif to_wrap.get("action") == "end_parent_span": + result = await wrapped(*args, **kwargs) + try: + parent_span: Span = instance._interface._lmnr_parent_span + if parent_span and parent_span.is_recording(): + parent_span.end() + except Exception: + pass + return result + + # if there's no parent span, use + parent_span = trace.get_current_span(context=get_current_context()) + try: + parent_span: Span = instance._lmnr_parent_span + except Exception: + pass + + with Laminar.use_span(parent_span): + instance_name = "interface" + with Laminar.start_as_current_span( + f"{instance_name}.{to_wrap.get('method')}", + span_type="TOOL", + ) as span: + span.set_attribute( + "lmnr.span.input", + json_dumps(get_input_from_func_args(wrapped, True, args, kwargs)), + ) + try: + result = await wrapped(*args, **kwargs) + except Exception as e: # pylint: disable=broad-except + span.set_status(Status(StatusCode.ERROR)) + span.record_exception(e) + span.end() + raise + output_formatter = to_wrap.get("output_formatter") or ( + lambda x: json_dumps(x) + ) + span.set_attribute("lmnr.span.output", output_formatter(result)) + return result + + +class CuaComputerInstrumentor(BaseInstrumentor): + def __init__(self): + super().__init__() + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + for wrapped_method in WRAPPED_METHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + wrap_method = wrapped_method.get("method") + + try: + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _wrap(wrapped_method), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + for wrapped_method in WRAPPED_AMETHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + wrap_method = wrapped_method.get("method") + try: + wrap_function_wrapper( + wrap_package, + f"{wrap_object}.{wrap_method}", + _wrap_async(wrapped_method), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + def _uninstrument(self, **kwargs): + for wrapped_method in WRAPPED_METHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + try: + unwrap( + f"{wrap_package}.{wrap_object}", + wrapped_method.get("method"), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist + + for wrapped_method in WRAPPED_AMETHODS: + wrap_package = wrapped_method.get("package") + wrap_object = wrapped_method.get("object") + try: + unwrap( + f"{wrap_package}.{wrap_object}", + wrapped_method.get("method"), + ) + except ModuleNotFoundError: + pass # that's ok, we don't want to fail if some methods do not exist diff --git a/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py new file mode 100644 index 00000000..cabc2822 --- /dev/null +++ b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py @@ -0,0 +1,12 @@ +import base64 +import orjson + + +def payload_to_base64url(payload_bytes: bytes) -> bytes: + data = base64.b64encode(payload_bytes).decode("utf-8") + url = f"data:image/png;base64,{data}" + return orjson.dumps({"base64url": url}) + + +def payload_to_placeholder(payload_bytes: bytes) -> str: + return "" diff --git a/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py index b93125ba..d220b39f 100644 --- a/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +++ b/src/lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py @@ -301,7 +301,9 @@ def set_data_attributes(traced_response: TracedData, span: Span): prompt_index += 1 elif block_dict.get("type") == "computer_call_output": _set_span_attribute( - span, f"{GEN_AI_PROMPT}.{prompt_index}.role", "computer-call" + span, + f"{GEN_AI_PROMPT}.{prompt_index}.role", + "computer_call_output", ) output_image_url = block_dict.get("output", {}).get("image_url") if output_image_url: @@ -325,16 +327,45 @@ def set_data_attributes(traced_response: TracedData, span: Span): call_content = {} if block_dict.get("id"): call_content["id"] = block_dict.get("id") - if block_dict.get("call_id"): - call_content["call_id"] = block_dict.get("call_id") if block_dict.get("action"): call_content["action"] = block_dict.get("action") _set_span_attribute( span, - f"{GEN_AI_PROMPT}.{prompt_index}.content", + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.arguments", json.dumps(call_content), ) + _set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.id", + block_dict.get("call_id"), + ) + _set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.name", + "computer_call", + ) prompt_index += 1 + elif block_dict.get("type") == "reasoning": + reasoning_summary = block_dict.get("summary") + if reasoning_summary and isinstance(reasoning_summary, list): + processed_chunks = [ + {"type": "text", "text": chunk.get("text")} + for chunk in reasoning_summary + if isinstance(chunk, dict) + and chunk.get("type") == "summary_text" + ] + _set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.reasoning", + json_dumps(processed_chunks), + ) + _set_span_attribute( + span, + f"{GEN_AI_PROMPT}.{prompt_index}.role", + "assistant", + ) + # reasoning is followed by other content parts in the same messge, + # so we don't increment the prompt index # TODO: handle other block types _set_span_attribute(span, f"{GEN_AI_COMPLETION}.0.role", "assistant") @@ -408,14 +439,18 @@ def set_data_attributes(traced_response: TracedData, span: Span): tool_call_index += 1 elif block_dict.get("type") == "reasoning": reasoning_summary = block_dict.get("summary") - if reasoning_summary: - if isinstance(reasoning_summary, (list, dict)): - reasoning_value = json_dumps(reasoning_summary) - else: - reasoning_value = reasoning_summary - _set_span_attribute( - span, f"{GEN_AI_COMPLETION}.0.reasoning", reasoning_value - ) + if reasoning_summary and isinstance(reasoning_summary, list): + processed_chunks = [ + {"type": "text", "text": chunk.get("text")} + for chunk in reasoning_summary + if isinstance(chunk, dict) + and chunk.get("type") == "summary_text" + ] + _set_span_attribute( + span, + "gen_ai.completion.0.reasoning", + json_dumps(processed_chunks), + ) # TODO: handle other block types, in particular other calls diff --git a/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py b/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py index bed77114..352f9a02 100644 --- a/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +++ b/src/lmnr/opentelemetry_lib/tracing/_instrument_initializers.py @@ -142,6 +142,26 @@ def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None: return CrewAiInstrumentor() +class CuaAgentInstrumentorInitializer(InstrumentorInitializer): + def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None: + if not is_package_installed("cua-agent"): + return None + + from ..opentelemetry.instrumentation.cua_agent import CuaAgentInstrumentor + + return CuaAgentInstrumentor() + + +class CuaComputerInstrumentorInitializer(InstrumentorInitializer): + def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None: + if not is_package_installed("cua-computer"): + return None + + from ..opentelemetry.instrumentation.cua_computer import CuaComputerInstrumentor + + return CuaComputerInstrumentor() + + class GoogleGenAIInstrumentorInitializer(InstrumentorInitializer): def init_instrumentor(self, *args, **kwargs) -> BaseInstrumentor | None: if not is_package_installed("google-genai"): diff --git a/src/lmnr/opentelemetry_lib/tracing/instruments.py b/src/lmnr/opentelemetry_lib/tracing/instruments.py index 416057d3..e3042ff5 100644 --- a/src/lmnr/opentelemetry_lib/tracing/instruments.py +++ b/src/lmnr/opentelemetry_lib/tracing/instruments.py @@ -22,6 +22,8 @@ class Instruments(Enum): CHROMA = "chroma" COHERE = "cohere" CREWAI = "crewai" + CUA_AGENT = "cua_agent" + CUA_COMPUTER = "cua_computer" GOOGLE_GENAI = "google_genai" GROQ = "groq" HAYSTACK = "haystack" @@ -67,6 +69,8 @@ class Instruments(Enum): Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(), Instruments.COHERE: initializers.CohereInstrumentorInitializer(), Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(), + Instruments.CUA_AGENT: initializers.CuaAgentInstrumentorInitializer(), + Instruments.CUA_COMPUTER: initializers.CuaComputerInstrumentorInitializer(), Instruments.GOOGLE_GENAI: initializers.GoogleGenAIInstrumentorInitializer(), Instruments.GROQ: initializers.GroqInstrumentorInitializer(), Instruments.HAYSTACK: initializers.HaystackInstrumentorInitializer(), diff --git a/tests/cassettes/test_litellm_openai/test_litellm_openai_responses_with_computer_tools.yaml b/tests/cassettes/test_litellm_openai/test_litellm_openai_responses_with_computer_tools.yaml index 7e8bd934..b99d4f0a 100644 --- a/tests/cassettes/test_litellm_openai/test_litellm_openai_responses_with_computer_tools.yaml +++ b/tests/cassettes/test_litellm_openai/test_litellm_openai_responses_with_computer_tools.yaml @@ -1,6 +1,7 @@ interactions: - request: - body: '{"model":"computer-use-preview","input":"Take a screenshot of the desktop.","tools":[{"type":"computer_use_preview","display_width":1024,"display_height":768,"environment":"linux"}],"truncation":"auto"}' + body: '{"model":"computer-use-preview","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Take + a screenshot of the desktop."}]}],"tools":[{"type":"computer_use_preview","display_width":1024,"display_height":768,"environment":"linux"}],"truncation":"auto"}' headers: accept: - '*/*' @@ -9,33 +10,33 @@ interactions: connection: - keep-alive content-length: - - '202' + - '278' content-type: - application/json host: - api.openai.com user-agent: - - litellm/1.76.2 + - litellm/1.76.3 method: POST uri: https://api.openai.com/v1/responses response: body: string: !!binary | - H4sIAAAAAAAAA3RUTW+jMBC951cgnzcrAgGS3Pa0h95X2q0qy9gDuDE2ssdpoyr/fYUhfDTtBaH3 - mPGbNw9/bKKISEFOEbHgOpofyio+7itxqOLD7shKscvzY1bGQogjryDO0zQ/JlkJLM7SPCE/+gam - fAWO9yZGOxhwboEhCMp6bldkRZrFSbIPnEOG3vU13LSdAgQxFJWMn2trvO5VVUw5CDBYayw5Rdor - FQCp74VUADKp3Jp1aD1HafQKb9k7NR47jxTNGR5JNEZRztS6XWsEqLtYj2C33sG2s3CR8LZN4iTb - xul2txv9CAeQU/S8iaIo+gjPyWjuR5uzJC9ZsJkd0xh2eVHskyop+P5Lm0MLvHaw1BGkzvR3rgaS - BTvIaRK06Oe4BdCuMUhG8jbV9UfQUXv/qtI/hrt/Fft70EX6lNdw+P3LlU/zSR1oIXVNHasAr5Q3 - wM+9queX8MVtE0UvwamOWaYUqLXtaP2w9GCw8Y7eczXImNbSWdN2SDnjDdAzXJecBeaMlrqeBiZQ - VcaGnLYgpG9HwaQGDZYhUOfbltllm97TFbgZnSHjbFKARllJWIXTgb1IDhQHnAiomFdIxugbC8sx - EdquF+ADvPsZj+g7ztorY1uGi+VNqwvfbRYrIxewpXESr4tRJ92D042RPFQzj4ZMhHsM7UPivAM6 - Jn/euJCuU+xKG5B108ss8sMD+SYFNv2E8XAJDEvRF2mNbkGHzSip/Tv5FBM0HVWm7qwpe4XxBHZL - v6zXnI0RX8zlHathNlLq1c+/P446V8TiSvmYfwPegJgr45Xnny+V4iv8q7ZTTr/rjAaZmskszqZd - ereOXQvIBEPW979tbv8BAAD//wMAqwTTEN4FAAA= + H4sIAAAAAAAAA3RUy46jMBC85yuQz5sVkIGE3Fb7+oI9jUaWYzfgibEtu52ZaJR/X+EQHpPMBaEq + d7u6uuBjlSRECrJPiANvabnjmajKioks3WXV9lDsyjrNRc2rlOf5Jq2rjNcVh7IqIecl+dY3MIdX + 4HhrYrSHK84dMARBWc9l22JbbJ6KsoqcR4bB9zXcdFYBgrgWHRg/Ns4E3auqmfIQYXDOOLJPdFAq + AlLfCqkAZFL5JevRBY7S6AXesXdqAtqAFM0R7kk0RlHO1LJdZwSom9iA4NbBw9o6OEl4W+dpXqzT + zTrLBj/iBWSfPK+SJEk+4nM0moebzYdMiF1v8y5nUGzzHQdWbngFD22OLfBsYa4jSp3or1yNJIt2 + kP0oaNbPcwegfWuQDORlrOuvoIP2/lXw3P343WzyovS/5F/R/vlnw0/7Ot1kQQupG+pZDXimvAV+ + 7FU9v8QTl1WSvESnLHNMKVBL29GF69KjwSZ4esvVVca4FutMZ5FyxlugRzjPOQfMGy11Mw5MoK6N + izntQMjQDYJJAxocQ6A+dB1z8za9pwtwNThDhtmkAI2ylrAIpwd3khwoXnEioGZBIRmibxzMx0To + bC8gRDj7ng7oO07aa+M6hrPljauL51azlZETuIPxEs+zUUfdV6dbI3msZgENGQl/H9q7xAUPdEj+ + tHEhvVXsTFuQTdvL3Ja7O/JNCmz7CdP8aSRBn6QzugMdN6OkDu/kU0zQWKpMY5059ArTEbRzv1zQ + nA0Rn80VPGtgMlLqxcf/VA06F8Tsl/IxfQa8BTFVpgvPP/9Uto/wR23HnH7VGQ0yNZFFWoy7DH4Z + uw6QCYas739ZXf4DAAD//wMAOkjV5d4FAAA= headers: CF-RAY: - - 97bff1d08d3337b6-LHR + - 97d1866399e55265-LHR Connection: - keep-alive Content-Encoding: @@ -43,14 +44,14 @@ interactions: Content-Type: - application/json Date: - - Mon, 08 Sep 2025 16:50:27 GMT + - Wed, 10 Sep 2025 20:02:51 GMT Server: - cloudflare Set-Cookie: - - __cf_bm=1IpLDEAHBpODP3gGfolB_.aLH432VxzW_Jtd_phON1A-1757350227-1.0.1.1-0BK8G6Jcf.CbaIUkSQ5De62rd2mFmd6CUIc0xLXbVInqxRvf74ciAoHVQzYwISXUJPLIXin6ZUCYuoms4vt.gyDIL1E_Q7xZzVqLk5j8n2c; - path=/; expires=Mon, 08-Sep-25 17:20:27 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=dn6kJJsk8xXGdHIu6EOhj0fTrS6XtY8KqiIwFlvrotE-1757534571-1.0.1.1-X9nsHOIPs8C1cCphpA_kPPeaYs8nJb7NGmgiz1AMx6q4rvCuNwLIKsyGFaKPmMEJUHm7a__yLSdlPvdYgDp0y4NVQzue4my4tjioQv8kHfk; + path=/; expires=Wed, 10-Sep-25 20:32:51 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=g3Tf1nr3bPN5CgAUfz.vqm11CmsfXek7aagD9zkvSN8-1757350227096-0.0.1.1-604800000; + - _cfuvid=FMJbKL.ufOkV8X2I7a8ZwJbctbcaaH8jwkSY5ndJPD8-1757534571541-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Transfer-Encoding: - chunked @@ -63,7 +64,7 @@ interactions: openai-organization: - user-xzaeeoqlanzncr8pomspsknu openai-processing-ms: - - '3191' + - '1667' openai-project: - proj_aT4OgTR5NJ9iNjg4xWc82hiE openai-version: @@ -71,7 +72,7 @@ interactions: strict-transport-security: - max-age=31536000; includeSubDomains; preload x-envoy-upstream-service-time: - - '3198' + - '1673' x-ratelimit-limit-requests: - '3000' x-ratelimit-remaining-requests: @@ -79,7 +80,96 @@ interactions: x-ratelimit-reset-requests: - 20ms x-request-id: - - req_fdd881d1bd82204de4d8077852a0c4af + - req_f72108e38452e04fb611184ecd67ff7e + status: + code: 200 + message: OK +- request: + body: '{"model":"computer-use-preview","input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Take + a screenshot of the desktop."}]},{"type":"computer_call","call_id":"call_dc2rAEg3256sDiGdhFUpuCpj","id":"cu_68c1d96b1dd8819782ae5728cea63c9e0f91cf9ce696e2c6","action":{"type":"screenshot"},"status":"completed"},{"type":"computer_call_output","call_id":"call_dc2rAEg3256sDiGdhFUpuCpj","output":{"type":"computer_screenshot","image_url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAgAElEQVR4Xu3YQREAIAwDQepfNDDI4LYOsukrs+8tR4AAAQIECBAgQIAAAQIECHwtMAaAr/sVjgABAgQIECBAgAABAgQIPAEDgEcgQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIDzMcNwAABvvSURBVECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgebs/emYSkLUgAAAABJRU5ErkJggg=="}}],"tools":[{"type":"computer_use_preview","display_width":1024,"display_height":768,"environment":"linux"}],"truncation":"auto"}' + headers: + accept: + - '*/*' + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '21159' + content-type: + - application/json + cookie: + - __cf_bm=dn6kJJsk8xXGdHIu6EOhj0fTrS6XtY8KqiIwFlvrotE-1757534571-1.0.1.1-X9nsHOIPs8C1cCphpA_kPPeaYs8nJb7NGmgiz1AMx6q4rvCuNwLIKsyGFaKPmMEJUHm7a__yLSdlPvdYgDp0y4NVQzue4my4tjioQv8kHfk; + _cfuvid=FMJbKL.ufOkV8X2I7a8ZwJbctbcaaH8jwkSY5ndJPD8-1757534571541-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - litellm/1.76.3 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAAwAAAP//hFTLbtswELz7KwSd44KSJVnysUGABi2CIm0ObREQNLmyWVOkwIdjI8i/ + F6LesYNeBGGG3J2d3eXrIghCzsJNEGowNc5yGrEio9Ea0Twq1kVKckqjLCYZK1GyQmUR0bKgkBUZ + xDQLb5oAavsXqO2DKGmgxakGYoFh0nDROl2nqyRdx54zllhnmjtUVbUAC6y9tCX0sNPKyUZVSYQB + D4PWSoebQDohPMBlfxEzsIQLM2eN1Y5aruQMr8gJK2drZ7FVB7gkrVICUyLm4SrFQPRinQW9dAaW + tYYjh5dljOJ0iVbLKOr88AnCTfBnEQRB8Oq/o9GmtxniOEOtzWUapVFKSgKIInbVZh/CnmtojSZG + SS53I2VcVRF9btI+e+zt5lp66rr06xjlcdKkJzTP0xwSlCdJRpLi/+l7G7xTEwkfNNWTxHcj3AyC + JvEOcK41GDOcDjxmBg877Pvj/cPPH7ePd3cP4YA/d39vQ6pGFe7KbX6/RN+ePv++r5508fW2+sVy + l584fUGjuBok43KHDSnBnjHdAz2YiZWLIHj2va2JJkKAmA+K1a4dUz8Syhncb0IrYxikWquqtpgS + ugd8gPOUG3vaexRCWSrtN6sCxl3VCQ53IEETC3hseh9mNgkNuOicCbvaOANpeclhtk4G9JFTwLbF + QwYlccKG3bIqDdMyLVR1I8B5OPqEOvRkR+2l0hWxk34P3fbnpiMaHkFvleH2PCl10N06vVec+tvE + WRUOhLlcs4shdQZwt6tjxxk3tSBnvAe+2zcy11l+Qb5wZvdNhShOBhLkkWslK5C+M4JLdwrfjYlV + NRZqV2u1bRSiAaynfmknKem2YlKXM2QHo5Fczp6rOI7im0tm8gq+jntA98DGq2hm+vt3cJVfI67F + HSb1o9BWWSKmklM0tNOZ+eRVYAkjljQJ3hZv/wAAAP//AwDM90h9kwYAAA== + headers: + CF-RAY: + - 97d18680cfdc5265-LHR + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 10 Sep 2025 20:02:59 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - user-xzaeeoqlanzncr8pomspsknu + openai-processing-ms: + - '6941' + openai-project: + - proj_aT4OgTR5NJ9iNjg4xWc82hiE + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '6947' + x-ratelimit-limit-requests: + - '3000' + x-ratelimit-limit-tokens: + - '20000000' + x-ratelimit-remaining-requests: + - '2999' + x-ratelimit-remaining-tokens: + - '19999235' + x-ratelimit-reset-requests: + - 20ms + x-ratelimit-reset-tokens: + - 2ms + x-request-id: + - req_a19771393f1e283f4699b13f0c26ac2f status: code: 200 message: OK diff --git a/tests/data/base64_png_blank_1024_768.txt b/tests/data/base64_png_blank_1024_768.txt new file mode 100644 index 00000000..0cf3899a --- /dev/null +++ b/tests/data/base64_png_blank_1024_768.txt @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAABAAAAAMACAYAAAC6uhUNAAAgAElEQVR4Xu3YQREAIAwDQepfNDDI4LYOsukrs+8tR4AAAQIECBAgQIAAAQIECHwtMAaAr/sVjgABAgQIECBAgAABAgQIPAEDgEcgQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIDzMcNwAABvvSURBVECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgYAP0CAAAECBAgQIECAAAECBAICBoBAySISIECAAAECBAgQIECAAAEDgB8gQIAAAQIECBAgQIAAAQIBAQNAoGQRCRAgQIAAAQIECBAgQICAAcAPECBAgAABAgQIECBAgACBgIABIFCyiAQIECBAgAABAgQIECBAwADgBwgQIECAAAECBAgQIECAQEDAABAoWUQCBAgQIECAAAECBAgQIGAA8AMECBAgQIAAAQIECBAgQCAgYAAIlCwiAQIECBAgQIAAAQIECBAwAPgBAgQIECBAgAABAgQIECAQEDAABEoWkQABAgQIECBAgAABAgQIGAD8AAECBAgQIECAAAECBAgQCAgYAAIli0iAAAECBAgQIECAAAECBAwAfoAAAQIECBAgQIAAAQIECAQEDACBkkUkQIAAAQIECBAgQIAAAQIGAD9AgAABAgQIECBAgAABAgQCAgaAQMkiEiBAgAABAgQIECBAgAABA4AfIECAAAECBAgQIECAAAECAQEDQKBkEQkQIECAAAECBAgQIECAgAHADxAgQIAAAQIECBAgQIAAgYCAASBQsogECBAgQIAAAQIECBAgQMAA4AcIECBAgAABAgQIECBAgEBAwAAQKFlEAgQIECBAgAABAgQIECBgAPADBAgQIECAAAECBAgQIEAgIGAACJQsIgECBAgQIECAAAECBAgQMAD4AQIECBAgQIAAAQIECBAgEBAwAARKFpEAAQIECBAgQIAAAQIECBgA/AABAgQIECBAgAABAgQIEAgIGAACJYtIgAABAgQIECBAgAABAgQMAH6AAAECBAgQIECAAAECBAgEBAwAgZJFJECAAAECBAgQIECAAAECBgA/QIAAAQIECBAgQIAAAQIEAgIGgEDJIhIgQIAAAQIECBAgQIAAAQOAHyBAgAABAgQIECBAgAABAgEBA0CgZBEJECBAgAABAgQIECBAgIABwA8QIECAAAECBAgQIECAAIGAgAEgULKIBAgQIECAAAECBAgQIEDAAOAHCBAgQIAAAQIECBAgQIBAQMAAEChZRAIECBAgQIAAAQIECBAgYADwAwQIECBAgAABAgQIECBAICBgAAiULCIBAgQIECBAgAABAgQIEDAA+AECBAgQIECAAAECBAgQIBAQMAAEShaRAAECBAgQIECAAAECBAgYAPwAAQIECBAgQIAAAQIECBAICBgAAiWLSIAAAQIECBAgQIAAAQIEDAB+gAABAgQIECBAgAABAgQIBAQMAIGSRSRAgAABAgQIECBAgAABAgebs/emYSkLUgAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/tests/test_litellm_anthropic.py b/tests/test_litellm_anthropic.py index 91d8961a..8babacdd 100644 --- a/tests/test_litellm_anthropic.py +++ b/tests/test_litellm_anthropic.py @@ -466,6 +466,25 @@ def test_litellm_anthropic_with_chat_history_and_tools( second_span.attributes["gen_ai.prompt.1.content"] == first_response.choices[0].message.content ) + assert second_span.attributes["gen_ai.prompt.1.tool_calls.0.name"] == "get_weather" + assert ( + first_span.attributes["gen_ai.completion.0.tool_calls.0.name"] == "get_weather" + ) + assert json.loads( + second_span.attributes["gen_ai.prompt.1.tool_calls.0.arguments"] + ) == {"city": "San Francisco"} + assert json.loads( + first_span.attributes["gen_ai.completion.0.tool_calls.0.arguments"] + ) == {"city": "San Francisco"} + assert ( + second_span.attributes["gen_ai.prompt.1.tool_calls.0.id"] + == first_response.choices[0].message.tool_calls[0]["id"] + ) + assert ( + first_span.attributes["gen_ai.completion.0.tool_calls.0.id"] + == first_response.choices[0].message.tool_calls[0]["id"] + ) + assert second_span.attributes["gen_ai.prompt.1.role"] == "assistant" assert second_span.attributes["gen_ai.prompt.2.content"] == "Sunny as always!" assert second_span.attributes["gen_ai.prompt.2.role"] == "tool" diff --git a/tests/test_litellm_openai.py b/tests/test_litellm_openai.py index 4d7a7fc1..32d99f35 100644 --- a/tests/test_litellm_openai.py +++ b/tests/test_litellm_openai.py @@ -15,6 +15,9 @@ SLEEP_TO_FLUSH_SECONDS = 0.05 +BASE64_IMAGE = "" +with open("tests/data/base64_png_blank_1024_768.txt", "r") as f: + BASE64_IMAGE = f.read().strip() EVENT_JSON_SCHEMA = { "name": "event", @@ -956,9 +959,64 @@ def test_litellm_openai_responses_with_computer_tools( # to the VCR cassette. os.environ["OPENAI_API_KEY"] = "test-key" litellm.callbacks = [litellm_callback] + + user_prompt = "Take a screenshot of the desktop." + first_response = litellm.responses( + model="openai/computer-use-preview", + input=[ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_prompt, + } + ], + } + ], + truncation="auto", + tools=[ + { + "type": "computer_use_preview", + "display_width": 1024, + "display_height": 768, + "environment": "linux", + } + ], + ) + litellm.responses( model="openai/computer-use-preview", - input="Take a screenshot of the desktop.", + input=[ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": user_prompt, + } + ], + }, + { + "type": "computer_call", + "call_id": first_response.output[-1].call_id, + "id": first_response.output[-1].id, + "action": { + "type": "screenshot", + }, + "status": "completed", + }, + { + "type": "computer_call_output", + "call_id": first_response.output[-1].call_id, + "output": { + "type": "computer_screenshot", + "image_url": f"data:image/png;base64,{BASE64_IMAGE}", + }, + }, + ], truncation="auto", tools=[ { @@ -974,38 +1032,84 @@ def test_litellm_openai_responses_with_computer_tools( Laminar.flush() time.sleep(SLEEP_TO_FLUSH_SECONDS) spans = span_exporter.get_finished_spans() - assert len(spans) == 1 - assert spans[0].name == "litellm.responses" - assert spans[0].attributes["gen_ai.request.model"] == "computer-use-preview" - assert spans[0].attributes["gen_ai.usage.input_tokens"] == 498 - assert spans[0].attributes["gen_ai.usage.output_tokens"] == 7 - assert spans[0].attributes["llm.usage.total_tokens"] == 505 + assert len(spans) == 2 + sorted_spans: list[ReadableSpan] = sorted(list(spans), key=lambda s: s.start_time) + first_span = sorted_spans[0] + second_span = sorted_spans[1] + assert first_span.name == "litellm.responses" + assert first_span.attributes["gen_ai.request.model"] == "computer-use-preview" + assert first_span.attributes["gen_ai.usage.input_tokens"] == 498 + assert first_span.attributes["gen_ai.usage.output_tokens"] == 7 + assert first_span.attributes["llm.usage.total_tokens"] == 505 + assert first_span.attributes["gen_ai.prompt.0.role"] == "user" + assert json.loads(first_span.attributes["gen_ai.prompt.0.content"]) == [ + {"type": "input_text", "text": user_prompt} + ] + assert first_span.attributes["gen_ai.completion.0.role"] == "assistant" + assert first_span.attributes["gen_ai.system"] == "openai" assert ( - spans[0].attributes["gen_ai.prompt.0.content"] - == "Take a screenshot of the desktop." + first_span.attributes["llm.request.functions.0.name"] == "computer_use_preview" ) - assert spans[0].attributes["gen_ai.prompt.0.role"] == "user" - assert spans[0].attributes["gen_ai.completion.0.role"] == "assistant" - assert spans[0].attributes["gen_ai.system"] == "openai" - assert spans[0].attributes["llm.request.functions.0.name"] == "computer_use_preview" - assert json.loads(spans[0].attributes["llm.request.functions.0.parameters"]) == { + assert json.loads(first_span.attributes["llm.request.functions.0.parameters"]) == { "display_width": 1024, "display_height": 768, "environment": "linux", } assert ( - spans[0].attributes["gen_ai.completion.0.tool_calls.0.name"] == "computer_call" + first_span.attributes["gen_ai.completion.0.tool_calls.0.name"] + == "computer_call" ) assert json.loads( - spans[0].attributes["gen_ai.completion.0.tool_calls.0.arguments"] + first_span.attributes["gen_ai.completion.0.tool_calls.0.arguments"] ) == { "type": "screenshot", } assert ( - spans[0].attributes["gen_ai.completion.0.tool_calls.0.id"] - == "call_l3VocsZfaY8n73K6ge8GAsbK" + first_span.attributes["gen_ai.completion.0.tool_calls.0.id"] + == first_response.output[-1].call_id ) + assert second_span.name == "litellm.responses" + assert second_span.attributes["gen_ai.request.model"] == "computer-use-preview" + assert second_span.attributes["gen_ai.usage.input_tokens"] == 2212 + assert second_span.attributes["gen_ai.usage.output_tokens"] == 38 + assert second_span.attributes["llm.usage.total_tokens"] == 2250 + assert second_span.attributes["gen_ai.prompt.0.role"] == "user" + assert json.loads(second_span.attributes["gen_ai.prompt.0.content"]) == [ + {"type": "input_text", "text": user_prompt} + ] + assert second_span.attributes["gen_ai.completion.0.role"] == "assistant" + assert second_span.attributes["gen_ai.system"] == "openai" + assert ( + second_span.attributes["llm.request.functions.0.name"] == "computer_use_preview" + ) + assert json.loads(second_span.attributes["llm.request.functions.0.parameters"]) == { + "display_width": 1024, + "display_height": 768, + "environment": "linux", + } + assert ( + second_span.attributes["gen_ai.prompt.1.tool_calls.0.name"] == "computer_call" + ) + assert json.loads( + second_span.attributes["gen_ai.prompt.1.tool_calls.0.arguments"] + ) == { + "action": {"type": "screenshot"}, + "id": first_response.output[-1].id, + } + assert ( + second_span.attributes["gen_ai.prompt.1.tool_calls.0.id"] + == first_response.output[-1].call_id + ) + assert second_span.attributes["gen_ai.prompt.1.role"] == "assistant" + assert json.loads(second_span.attributes["gen_ai.prompt.2.content"]) == [ + { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{BASE64_IMAGE}"}, + } + ] + assert second_span.attributes["gen_ai.prompt.2.role"] == "computer_call_output" + @pytest.mark.vcr def test_litellm_openai_with_structured_output_and_streaming(