Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
# pyproject floors by UV_RESOLUTION=lowest-direct on the oldest tox factor, so they are NOT pinned
# here to avoid drift between the declared bound and the tested version.

# Exception: this change depends on the fully-qualified error.type behavior added in
# opentelemetry-util-genai 1.1b0, which is not on PyPI yet. Install it from the workspace so the
# oldest env exercises the required behavior. Remove this once 1.1b0 is released and bump the
# pyproject floor to >= 1.1b0.
-e ./util/opentelemetry-util-genai

langchain-openai==0.2.0
langchain-aws==0.2.2
langchain-google-genai==2.0.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,9 @@ def assert_openai_completion_attributes_with_error(
assert span is not None
assert span.name == "chat gpt-3.5-turbo"
attributes = span.attributes
assert attributes[error_attributes.ERROR_TYPE] == "AuthenticationError"
assert (
attributes[error_attributes.ERROR_TYPE] == "openai.AuthenticationError"
)
assert attributes[gen_ai_attributes.GEN_AI_OPERATION_NAME] == "chat"
assert (
attributes[gen_ai_attributes.GEN_AI_REQUEST_MODEL] == "gpt-3.5-turbo"
Expand Down Expand Up @@ -490,7 +492,9 @@ def assert_duration_metric_when_error(metric, parent_span):

def assert_duration_metric_attributes_when_error(attributes, parent_span):
assert len(attributes) == 4
assert attributes[error_attributes.ERROR_TYPE] == "AuthenticationError"
assert (
attributes[error_attributes.ERROR_TYPE] == "openai.AuthenticationError"
)
assert attributes.get(gen_ai_attributes.GEN_AI_PROVIDER_NAME) == "openai"
assert (
attributes.get(gen_ai_attributes.GEN_AI_OPERATION_NAME)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Responses that fail now record the actual error type (`error` event code or the response's error code) instead of a generic `RuntimeError`, for both streaming and non-streaming calls. Responses that finish as incomplete (`max_output_tokens`/`content_filter`) are recorded as a finish reason rather than an error.
Comment thread
lmolkova marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
EmbeddingInvocation,
InferenceInvocation,
)
from opentelemetry.util.genai.types import (
Error,
)

from .chat_wrappers import AsyncChatStreamWrapper, ChatStreamWrapper
from .utils import (
Expand Down Expand Up @@ -89,7 +86,7 @@ def traced_method(wrapped, instance, args, kwargs):
chat_invocation.stop()
return result
except Exception as error:
chat_invocation.fail(Error(type=type(error), message=str(error)))
chat_invocation.fail(error)
raise

return traced_method
Expand Down Expand Up @@ -125,7 +122,7 @@ async def traced_method(wrapped, instance, args, kwargs):
return result

except Exception as error:
chat_invocation.fail(Error(type=type(error), message=str(error)))
chat_invocation.fail(error)
raise

return traced_method
Expand All @@ -140,7 +137,7 @@ def traced_method(wrapped, instance, args, kwargs):
try:
result = wrapped(*args, **kwargs)
except Exception as error:
invocation.fail(Error(type=type(error), message=str(error)))
invocation.fail(error)
raise

_safe_set_embeddings_response_properties(invocation, result)
Expand All @@ -159,7 +156,7 @@ async def traced_method(wrapped, instance, args, kwargs):
try:
result = await wrapped(*args, **kwargs)
except Exception as error:
invocation.fail(Error(type=type(error), message=str(error)))
invocation.fail(error)
raise

_safe_set_embeddings_response_properties(invocation, result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
apply_request_attributes,
extract_params,
get_inference_creation_kwargs,
get_response_error,
set_invocation_response_attributes,
)
from .response_wrappers import (
Expand Down Expand Up @@ -115,12 +116,15 @@ def traced_method(
capture_content,
)

response = cast("ResponseResult", parsed_result)
set_invocation_response_attributes(
invocation,
cast("ResponseResult", parsed_result),
capture_content,
invocation, response, capture_content
)
invocation.stop()
error = get_response_error(response)
if error is not None:
invocation.fail(error)
else:
invocation.stop()
return result
except Exception as error:
invocation.fail(error)
Expand Down Expand Up @@ -194,12 +198,15 @@ async def traced_method(
capture_content,
)

response = cast("ResponseResult", parsed_result)
set_invocation_response_attributes(
invocation,
cast("ResponseResult", parsed_result),
capture_content,
invocation, response, capture_content
)
invocation.stop()
error = get_response_error(response)
if error is not None:
invocation.fail(error)
else:
invocation.stop()
return result
except Exception as error:
invocation.fail(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from openai.types.responses.response_usage import ResponseUsage

from opentelemetry.util.genai.types import (
Error,
InputMessage,
OutputMessage,
Text,
Expand Down Expand Up @@ -57,6 +58,7 @@

try:
from opentelemetry.util.genai.types import (
Error,
InputMessage,
OutputMessage,
Reasoning,
Expand All @@ -66,6 +68,7 @@
ToolCallRequest as ToolCall,
)
except ImportError:
Error = None
InputMessage = None
OutputMessage = None
Reasoning = None
Expand Down Expand Up @@ -365,6 +368,21 @@ def extract_finish_reasons(response: "Response | None") -> list[str]:
return list(dict.fromkeys(finish_reasons))


def get_response_error(response: "Response | None") -> "Error | None":
"""Return an ``Error`` when the response failed, else ``None``.

A failed response carries a ``ResponseError`` (``code`` + ``message``).
Incomplete responses (``incomplete_details``) are *not* errors — they
surface as a finish reason instead.
"""
if Response is None or Error is None or not isinstance(response, Response):
return None
error = response.error
if error is None or not getattr(error, "code", None):
return None
return Error(type=error.code, message=getattr(error, "message", None))


def get_inference_creation_kwargs(
params: ResponseRequestParams,
client_instance: object,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@

try:
from opentelemetry.instrumentation.genai.openai.response_extractors import ( # pylint: disable=no-name-in-module
get_response_error,
set_invocation_response_attributes,
)
except ImportError:
get_response_error = None
set_invocation_response_attributes = None

if TYPE_CHECKING:
Expand Down Expand Up @@ -140,10 +142,10 @@ def _stop(
self._self_invocation.stop()
self._self_response_telemetry_finalized = True

def _fail(self, message: str, error_type: type[BaseException]) -> None:
def _fail(self, error: Error) -> None:
if self._self_response_telemetry_finalized:
return
self._self_invocation.fail(Error(message=message, type=error_type))
self._self_invocation.fail(error)
self._self_response_telemetry_finalized = True

def _process_chunk(
Expand All @@ -155,7 +157,10 @@ def _on_stream_end(self) -> None:
self._stop(None)

def _on_stream_error(self, error: BaseException) -> None:
self._fail(str(error), type(error))
if self._self_response_telemetry_finalized:
return
self._self_invocation.fail(error)
self._self_response_telemetry_finalized = True

def get_final_response(self) -> "ParsedResponse[TextFormatT]":
self.until_done()
Expand Down Expand Up @@ -188,23 +193,26 @@ def process_event(self, event: "ResponseStreamEvent[TextFormatT]") -> None:
if model:
self._self_invocation.request_model = model

if event_type == "response.completed":
if event_type in {"response.completed", "response.incomplete"}:
Comment thread
eternalcuriouslearner marked this conversation as resolved.
self._stop(response)
return

if event_type in {"response.failed", "response.incomplete"}:
if event_type == "response.failed":
_set_response_attributes(
self._self_invocation,
response,
self._self_capture_content,
)
self._fail(event_type, RuntimeError)
error = (
get_response_error(response) if get_response_error else None
)
self._fail(error or Error(type=event_type, message=None))
return

if event_type == "response.error":
error_type = getattr(event, "code", None) or "response.error"
message = getattr(event, "message", None) or error_type
self._fail(message, RuntimeError)
if event_type == "error":
error_type = getattr(event, "code", None) or "error"
message = getattr(event, "message", None)
self._fail(Error(type=error_type, message=message))


class ResponseStreamWrapper(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
# OpenTelemetry SDK and test utilities come transitively from opentelemetry-test-util-genai, which
# every oldest env installs. Pin here only test-only deps that nothing else already provides.

# Exception: this change depends on the fully-qualified error.type behavior added in
# opentelemetry-util-genai 1.1b0, which is not on PyPI yet. Install it from the workspace so the
# oldest env exercises the required behavior. Remove this once 1.1b0 is released and bump the
# pyproject floor to >= 1.1b0.
-e ./util/opentelemetry-util-genai
Comment thread
lmolkova marked this conversation as resolved.

numpy==2.3.1 ; python_version >= "3.11"
numpy==2.1.0 ; python_version == "3.10"
pydantic==2.12.5
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ async def test_async_chat_completion_bad_endpoint(
)
assert 4242 == spans[0].attributes[ServerAttributes.SERVER_PORT]
assert (
"APIConnectionError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
"openai.APIConnectionError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)
Comment thread
lmolkova marked this conversation as resolved.


Expand All @@ -184,7 +185,10 @@ async def test_async_chat_completion_404(
assert_all_attributes(
spans[0], llm_model_value, latest_experimental_enabled
)
assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
assert (
"openai.NotFoundError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)


@pytest.mark.asyncio()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,10 @@ async def test_async_embeddings_error_handling(
latest_experimental_enabled,
operation_name="embeddings",
)
assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
assert (
"openai.NotFoundError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)


@pytest.mark.asyncio
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,10 @@ async def test_async_responses_create_connection_error(
)
assert span.attributes[ServerAttributes.SERVER_ADDRESS] == "localhost"
assert span.attributes[ServerAttributes.SERVER_PORT] == 4242
assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError"
assert (
span.attributes[ErrorAttributes.ERROR_TYPE]
== "openai.APIConnectionError"
)


@pytest.mark.asyncio()
Expand All @@ -381,7 +384,7 @@ async def test_async_responses_create_api_error(
)
assert (
span.attributes[ErrorAttributes.ERROR_TYPE]
== type(exc_info.value).__name__
== f"openai.{type(exc_info.value).__name__}"
)
Comment thread
lmolkova marked this conversation as resolved.


Expand Down Expand Up @@ -437,7 +440,10 @@ async def test_async_responses_stream_connection_error(
assert (
span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL
)
assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError"
assert (
span.attributes[ErrorAttributes.ERROR_TYPE]
== "openai.APIConnectionError"
)


@pytest.mark.asyncio()
Expand Down Expand Up @@ -655,7 +661,10 @@ async def test_async_responses_create_streaming_connection_error(
assert (
span.attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] == DEFAULT_MODEL
)
assert span.attributes[ErrorAttributes.ERROR_TYPE] == "APIConnectionError"
assert (
span.attributes[ErrorAttributes.ERROR_TYPE]
== "openai.APIConnectionError"
)


@pytest.mark.asyncio()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,7 @@ async def test_async_structured_output_404(
assert_all_attributes(
spans[0], llm_model_value, latest_experimental_enabled
)
assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
assert (
"openai.NotFoundError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ def test_chat_completion_bad_endpoint(
)
assert 4242 == spans[0].attributes[ServerAttributes.SERVER_PORT]
assert (
"APIConnectionError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
"openai.APIConnectionError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)
Comment thread
lmolkova marked this conversation as resolved.

metrics = metric_reader.get_metrics_data().resource_metrics
Expand All @@ -231,7 +232,7 @@ def test_chat_completion_bad_endpoint(
duration_metric.data.data_points[0].attributes[
ErrorAttributes.ERROR_TYPE
]
== "APIConnectionError"
== "openai.APIConnectionError"
)


Expand All @@ -253,7 +254,10 @@ def test_chat_completion_404(
assert_all_attributes(
spans[0], llm_model_value, latest_experimental_enabled
)
assert "NotFoundError" == spans[0].attributes[ErrorAttributes.ERROR_TYPE]
assert (
"openai.NotFoundError"
== spans[0].attributes[ErrorAttributes.ERROR_TYPE]
)

metrics = metric_reader.get_metrics_data().resource_metrics
assert len(metrics) == 1
Expand All @@ -273,7 +277,7 @@ def test_chat_completion_404(
duration_metric.data.data_points[0].attributes[
ErrorAttributes.ERROR_TYPE
]
== "NotFoundError"
== "openai.NotFoundError"
)


Expand Down
Loading