diff --git a/logfire/_internal/integrations/llm_providers/anthropic.py b/logfire/_internal/integrations/llm_providers/anthropic.py index 35c03df7e..1c4c525ef 100644 --- a/logfire/_internal/integrations/llm_providers/anthropic.py +++ b/logfire/_internal/integrations/llm_providers/anthropic.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast import anthropic from anthropic.types import Message, TextBlock, TextDelta @@ -25,15 +25,20 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig: """Returns the endpoint config for Anthropic or Bedrock depending on the url.""" url = options.url - json_data = options.json_data - if not isinstance(json_data, dict): # pragma: no cover + raw_json_data = options.json_data + if not isinstance(raw_json_data, dict): # pragma: no cover # Ensure that `{request_data[model]!r}` doesn't raise an error, just a warning about `model` missing. - json_data = {} + raw_json_data = {} + json_data = cast('dict[str, Any]', raw_json_data) if url == '/v1/messages': return EndpointConfig( message_template='Message with {request_data[model]!r}', - span_data={'request_data': json_data}, + span_data={ + 'request_data': json_data, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'chat', + }, stream_state_cls=AnthropicMessageStreamState, ) else: diff --git a/logfire/_internal/integrations/llm_providers/llm_provider.py b/logfire/_internal/integrations/llm_providers/llm_provider.py index 1514ca2bb..921844d2b 100644 --- a/logfire/_internal/integrations/llm_providers/llm_provider.py +++ b/logfire/_internal/integrations/llm_providers/llm_provider.py @@ -4,12 +4,18 @@ from contextlib import AbstractContextManager, ExitStack, contextmanager, nullcontext from typing import TYPE_CHECKING, Any, Callable, cast +from opentelemetry.trace import SpanKind + from logfire import attach_context, get_context from logfire.propagate import ContextCarrier from ...constants import ONE_SECOND_IN_NANOSECONDS from ...utils import is_instrumentation_suppressed, log_internal_error, suppress_instrumentation +# GenAI semantic convention attribute names for span naming +OPERATION_NAME = 'gen_ai.operation.name' +REQUEST_MODEL = 'gen_ai.request.model' + if TYPE_CHECKING: from ...main import Logfire, LogfireSpan from .types import EndpointConfig, StreamState @@ -132,11 +138,20 @@ def __stream__(self) -> Iterator[Any]: # In these methods, `*args` is only expected to be `(self,)` # in the case where we instrument classes rather than client instances. + def _get_otel_span_name(span_data: dict[str, Any]) -> str | None: + """Construct OTel-compliant span name as '{operation} {model}'.""" + operation = span_data.get(OPERATION_NAME) + model = span_data.get(REQUEST_MODEL) + if operation and model: + return f'{operation} {model}' + return None + def instrumented_llm_request_sync(*args: Any, **kwargs: Any) -> Any: message_template, span_data, kwargs = _instrumentation_setup(*args, **kwargs) if message_template is None: return original_request_method(*args, **kwargs) - with logfire_llm.span(message_template, **span_data) as span: + span_name = _get_otel_span_name(span_data) + with logfire_llm.span(message_template, _span_kind=SpanKind.CLIENT, _span_name=span_name, **span_data) as span: with maybe_suppress_instrumentation(suppress_otel): if kwargs.get('stream'): return original_request_method(*args, **kwargs) @@ -148,7 +163,8 @@ async def instrumented_llm_request_async(*args: Any, **kwargs: Any) -> Any: message_template, span_data, kwargs = _instrumentation_setup(*args, **kwargs) if message_template is None: return await original_request_method(*args, **kwargs) - with logfire_llm.span(message_template, **span_data) as span: + span_name = _get_otel_span_name(span_data) + with logfire_llm.span(message_template, _span_kind=SpanKind.CLIENT, _span_name=span_name, **span_data) as span: with maybe_suppress_instrumentation(suppress_otel): if kwargs.get('stream'): return await original_request_method(*args, **kwargs) diff --git a/logfire/_internal/integrations/llm_providers/openai.py b/logfire/_internal/integrations/llm_providers/openai.py index 40c53d8bd..8beceff60 100644 --- a/logfire/_internal/integrations/llm_providers/openai.py +++ b/logfire/_internal/integrations/llm_providers/openai.py @@ -37,10 +37,11 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig: """Returns the endpoint config for OpenAI depending on the url.""" url = options.url - json_data = options.json_data - if not isinstance(json_data, dict): # pragma: no cover + raw_json_data = options.json_data + if not isinstance(raw_json_data, dict): # pragma: no cover # Ensure that `{request_data[model]!r}` doesn't raise an error, just a warning about `model` missing. - json_data = {} + raw_json_data = {} + json_data = cast('dict[str, Any]', raw_json_data) if url == '/chat/completions': if is_current_agent_span('Chat completion with {gen_ai.request.model!r}'): @@ -48,20 +49,25 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig: return EndpointConfig( message_template='Chat Completion with {request_data[model]!r}', - span_data={'request_data': json_data, 'gen_ai.request.model': json_data['model']}, + span_data={ + 'request_data': json_data, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'chat', + }, stream_state_cls=OpenaiChatCompletionStreamState, ) elif url == '/responses': if is_current_agent_span('Responses API', 'Responses API with {gen_ai.request.model!r}'): return EndpointConfig(message_template='', span_data={}) - stream = json_data.get('stream', False) # type: ignore + stream = json_data.get('stream', False) span_data: dict[str, Any] = { - 'gen_ai.request.model': json_data['model'], - 'request_data': {'model': json_data['model'], 'stream': stream}, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'chat', + 'request_data': {'model': json_data.get('model'), 'stream': stream}, 'events': inputs_to_events( - json_data['input'], # type: ignore - json_data.get('instructions'), # type: ignore + json_data.get('input'), + json_data.get('instructions'), ), } @@ -73,18 +79,30 @@ def get_endpoint_config(options: FinalRequestOptions) -> EndpointConfig: elif url == '/completions': return EndpointConfig( message_template='Completion with {request_data[model]!r}', - span_data={'request_data': json_data, 'gen_ai.request.model': json_data['model']}, + span_data={ + 'request_data': json_data, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'text_completion', + }, stream_state_cls=OpenaiCompletionStreamState, ) elif url == '/embeddings': return EndpointConfig( message_template='Embedding Creation with {request_data[model]!r}', - span_data={'request_data': json_data, 'gen_ai.request.model': json_data['model']}, + span_data={ + 'request_data': json_data, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'embeddings', + }, ) elif url == '/images/generations': return EndpointConfig( message_template='Image Generation with {request_data[model]!r}', - span_data={'request_data': json_data, 'gen_ai.request.model': json_data['model']}, + span_data={ + 'request_data': json_data, + 'gen_ai.request.model': json_data.get('model'), + 'gen_ai.operation.name': 'image_generation', + }, ) else: span_data = {'request_data': json_data, 'url': url} @@ -183,7 +201,8 @@ def on_response(response: ResponseT, span: LogfireSpan) -> ResponseT: on_response(response.parse(), span) # type: ignore return cast('ResponseT', response) - span.set_attribute('gen_ai.system', 'openai') + # Note: gen_ai.system is deprecated in favor of gen_ai.provider.name + # It was removed as a breaking change for OTel GenAI semantic convention compliance if isinstance(response_model := getattr(response, 'model', None), str): span.set_attribute('gen_ai.response.model', response_model) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index 75ad44384..b67094232 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -26,7 +26,7 @@ from opentelemetry.context import Context from opentelemetry.metrics import CallbackT, Counter, Histogram, UpDownCounter from opentelemetry.sdk.trace import ReadableSpan, Span -from opentelemetry.trace import SpanContext +from opentelemetry.trace import SpanContext, SpanKind from opentelemetry.util import types as otel_types from typing_extensions import LiteralString, ParamSpec @@ -188,6 +188,7 @@ def _span( _span_name: str | None = None, _level: LevelName | int | None = None, _links: Sequence[tuple[SpanContext, otel_types.Attributes]] = (), + _span_kind: SpanKind | None = None, ) -> LogfireSpan: try: if _level is not None: @@ -243,6 +244,7 @@ def _span( self._spans_tracer, json_schema_properties, links=_links, + span_kind=_span_kind, ) except Exception: log_internal_error() @@ -540,6 +542,7 @@ def span( _span_name: str | None = None, _level: LevelName | None = None, _links: Sequence[tuple[SpanContext, otel_types.Attributes]] = (), + _span_kind: SpanKind | None = None, **attributes: Any, ) -> LogfireSpan: """Context manager for creating a span. @@ -559,6 +562,7 @@ def span( _tags: An optional sequence of tags to include in the span. _level: An optional log level name. _links: An optional sequence of links to other spans. Each link is a tuple of a span context and attributes. + _span_kind: An optional span kind (e.g., SpanKind.CLIENT for external calls). attributes: The arguments to include in the span and format the message template with. Attributes starting with an underscore are not allowed. """ @@ -571,6 +575,7 @@ def span( _span_name=_span_name, _level=_level, _links=_links, + _span_kind=_span_kind, ) @overload @@ -2382,12 +2387,14 @@ def __init__( tracer: _ProxyTracer, json_schema_properties: JsonSchemaProperties, links: Sequence[tuple[SpanContext, otel_types.Attributes]], + span_kind: SpanKind | None = None, ) -> None: self._span_name = span_name self._otlp_attributes = otlp_attributes self._tracer = tracer self._json_schema_properties = json_schema_properties self._links = list(trace_api.Link(context=context, attributes=attributes) for context, attributes in links) + self._span_kind = span_kind self._added_attributes = False self._token: None | Token[Context] = None @@ -2402,11 +2409,14 @@ def __getattr__(self, name: str) -> Any: def _start(self): if self._span is not None: return - self._span = self._tracer.start_span( - name=self._span_name, - attributes=self._otlp_attributes, - links=self._links, - ) + kwargs: dict[str, Any] = { + 'name': self._span_name, + 'attributes': self._otlp_attributes, + 'links': self._links, + } + if self._span_kind is not None: + kwargs['kind'] = self._span_kind + self._span = self._tracer.start_span(**kwargs) @handle_internal_errors def _attach(self): diff --git a/tests/otel_integrations/test_anthropic.py b/tests/otel_integrations/test_anthropic.py index 24cbaca09..37d74362d 100644 --- a/tests/otel_integrations/test_anthropic.py +++ b/tests/otel_integrations/test_anthropic.py @@ -141,7 +141,7 @@ def test_sync_messages(instrumented_client: anthropic.Anthropic, exporter: TestE assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -160,6 +160,8 @@ def test_sync_messages(instrumented_client: anthropic.Anthropic, exporter: TestE } ) ), + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", @@ -192,6 +194,8 @@ def test_sync_messages(instrumented_client: anthropic.Anthropic, exporter: TestE 'type': 'object', 'properties': { 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, 'response_data': { 'type': 'object', @@ -207,6 +211,7 @@ def test_sync_messages(instrumented_client: anthropic.Anthropic, exporter: TestE } ) ), + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, } ] @@ -225,7 +230,7 @@ async def test_async_messages(instrumented_async_client: anthropic.AsyncAnthropi assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -242,6 +247,8 @@ async def test_async_messages(instrumented_async_client: anthropic.AsyncAnthropi 'model': 'claude-3-haiku-20240307', } ), + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': True, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", @@ -273,6 +280,8 @@ async def test_async_messages(instrumented_async_client: anthropic.AsyncAnthropi 'type': 'object', 'properties': { 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, 'response_data': { 'type': 'object', @@ -287,6 +296,7 @@ async def test_async_messages(instrumented_async_client: anthropic.AsyncAnthropi }, }, ), + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, } ] @@ -306,7 +316,7 @@ def test_sync_message_empty_response_chunk(instrumented_client: anthropic.Anthro assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -316,12 +326,15 @@ def test_sync_message_empty_response_chunk(instrumented_client: anthropic.Anthro 'code.function': 'test_sync_message_empty_response_chunk', 'code.lineno': 123, 'request_data': '{"max_tokens":1000,"messages":[],"model":"claude-3-haiku-20240307","stream":true,"system":"empty response chunk"}', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", - 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{}}}', 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, { @@ -340,10 +353,13 @@ def test_sync_message_empty_response_chunk(instrumented_client: anthropic.Anthro 'code.lineno': 123, 'logfire.msg': "streaming response from 'claude-3-haiku-20240307' took 1.00s", 'logfire.span_type': 'log', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': '{"combined_chunk_content":"","chunk_count":0}', - 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{},"response_data":{"type":"object"}}}', + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, ] @@ -368,7 +384,7 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -378,12 +394,15 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter 'code.function': 'test_sync_messages_stream', 'code.lineno': 123, 'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","stream":true,"system":"You are a helpful assistant."}', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", - 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{}}}', 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, { @@ -402,10 +421,13 @@ def test_sync_messages_stream(instrumented_client: anthropic.Anthropic, exporter 'code.lineno': 123, 'logfire.msg': "streaming response from 'claude-3-haiku-20240307' took 1.00s", 'logfire.span_type': 'log', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': '{"combined_chunk_content":"The answer is secret","chunk_count":2}', - 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{},"response_data":{"type":"object"}}}', + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, ] @@ -433,7 +455,7 @@ async def test_async_messages_stream( assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -443,12 +465,15 @@ async def test_async_messages_stream( 'code.function': 'test_async_messages_stream', 'code.lineno': 123, 'request_data': '{"max_tokens":1000,"messages":[{"role":"user","content":"What is four plus five?"}],"model":"claude-3-haiku-20240307","stream":true,"system":"You are a helpful assistant."}', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': True, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", - 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"async":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{}}}', 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, { @@ -467,10 +492,13 @@ async def test_async_messages_stream( 'code.lineno': 123, 'logfire.msg': "streaming response from 'claude-3-haiku-20240307' took 1.00s", 'logfire.span_type': 'log', + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': '{"combined_chunk_content":"The answer is secret","chunk_count":2}', - 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"async":{},"response_data":{"type":"object"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"duration":{},"request_data":{"type":"object"},"gen_ai.request.model":{},"gen_ai.operation.name":{},"async":{},"response_data":{"type":"object"}}}', + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, }, ] @@ -489,7 +517,7 @@ def test_tool_messages(instrumented_client: anthropic.Anthropic, exporter: TestE assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat claude-3-haiku-20240307', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -504,6 +532,8 @@ def test_tool_messages(instrumented_client: anthropic.Anthropic, exporter: TestE 'model': 'claude-3-haiku-20240307', 'system': 'tool response', }, + 'gen_ai.request.model': 'claude-3-haiku-20240307', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': "Message with 'claude-3-haiku-20240307'", @@ -530,6 +560,8 @@ def test_tool_messages(instrumented_client: anthropic.Anthropic, exporter: TestE 'type': 'object', 'properties': { 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, 'response_data': { 'type': 'object', @@ -539,6 +571,7 @@ def test_tool_messages(instrumented_client: anthropic.Anthropic, exporter: TestE }, }, }, + 'gen_ai.response.model': 'claude-3-haiku-20240307', }, } ] diff --git a/tests/otel_integrations/test_anthropic_bedrock.py b/tests/otel_integrations/test_anthropic_bedrock.py index ab7884f6c..74b938847 100644 --- a/tests/otel_integrations/test_anthropic_bedrock.py +++ b/tests/otel_integrations/test_anthropic_bedrock.py @@ -72,7 +72,7 @@ def test_sync_messages(mock_client: AnthropicBedrock, exporter: TestExporter): assert exporter.exported_spans_as_dict() == snapshot( [ { - 'name': 'Message with {request_data[model]!r}', + 'name': 'chat anthropic.claude-3-haiku-20240307-v1:0', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -89,6 +89,8 @@ def test_sync_messages(mock_client: AnthropicBedrock, exporter: TestExporter): 'model': model_id, } ), + 'gen_ai.request.model': 'anthropic.claude-3-haiku-20240307-v1:0', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Message with {request_data[model]!r}', 'logfire.msg': f"Message with '{model_id}'", @@ -120,6 +122,8 @@ def test_sync_messages(mock_client: AnthropicBedrock, exporter: TestExporter): 'type': 'object', 'properties': { 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, 'response_data': { 'type': 'object', @@ -134,6 +138,7 @@ def test_sync_messages(mock_client: AnthropicBedrock, exporter: TestExporter): }, } ), + 'gen_ai.response.model': 'anthropic.claude-3-haiku-20240307-v1:0', }, } ] diff --git a/tests/otel_integrations/test_openai.py b/tests/otel_integrations/test_openai.py index 7b36ad7ac..60e5029c4 100644 --- a/tests/otel_integrations/test_openai.py +++ b/tests/otel_integrations/test_openai.py @@ -403,7 +403,7 @@ def test_sync_chat_completions(instrumented_client: openai.Client, exporter: Tes assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -422,11 +422,11 @@ def test_sync_chat_completions(instrumented_client: openai.Client, exporter: Tes } ), 'async': False, + 'gen_ai.operation.name': 'chat', 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-4', 'gen_ai.response.model': 'gpt-4', 'gen_ai.usage.input_tokens': 2, @@ -458,7 +458,7 @@ def test_sync_chat_completions(instrumented_client: openai.Client, exporter: Tes 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -500,7 +500,7 @@ async def test_async_chat_completions(instrumented_async_client: openai.AsyncCli assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -519,11 +519,11 @@ async def test_async_chat_completions(instrumented_async_client: openai.AsyncCli } ), 'async': True, + 'gen_ai.operation.name': 'chat', 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-4', 'gen_ai.response.model': 'gpt-4', 'gen_ai.usage.input_tokens': 2, @@ -555,7 +555,7 @@ async def test_async_chat_completions(instrumented_async_client: openai.AsyncCli 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -596,7 +596,7 @@ def test_sync_chat_empty_response_chunk(instrumented_client: openai.Client, expo assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -611,12 +611,18 @@ def test_sync_chat_empty_response_chunk(instrumented_client: openai.Client, expo 'stream': True, }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), @@ -644,6 +650,7 @@ def test_sync_chat_empty_response_chunk(instrumented_client: openai.Client, expo 'logfire.msg': "streaming response from 'gpt-4' took 1.00s", 'gen_ai.request.model': 'gpt-4', 'logfire.span_type': 'log', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': {'combined_chunk_content': '', 'chunk_count': 0}, @@ -653,6 +660,7 @@ def test_sync_chat_empty_response_chunk(instrumented_client: openai.Client, expo 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': {'type': 'object'}, }, @@ -676,7 +684,7 @@ def test_sync_chat_empty_response_choices(instrumented_client: openai.Client, ex assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -691,12 +699,18 @@ def test_sync_chat_empty_response_choices(instrumented_client: openai.Client, ex 'stream': True, }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), @@ -724,6 +738,7 @@ def test_sync_chat_empty_response_choices(instrumented_client: openai.Client, ex 'logfire.msg': "streaming response from 'gpt-4' took 1.00s", 'gen_ai.request.model': 'gpt-4', 'logfire.span_type': 'log', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': {'message': None, 'usage': None}, @@ -733,6 +748,7 @@ def test_sync_chat_empty_response_choices(instrumented_client: openai.Client, ex 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': {'type': 'object'}, }, @@ -784,7 +800,7 @@ def test_sync_chat_tool_call_stream(instrumented_client: openai.Client, exporter assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -821,12 +837,18 @@ def test_sync_chat_tool_call_stream(instrumented_client: openai.Client, exporter ], }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.tags': ('LLM',), 'logfire.span_type': 'span', @@ -876,6 +898,7 @@ def test_sync_chat_tool_call_stream(instrumented_client: openai.Client, exporter }, 'gen_ai.request.model': 'gpt-4', 'async': False, + 'gen_ai.operation.name': 'chat', 'duration': 1.0, 'response_data': { 'message': { @@ -913,6 +936,7 @@ def test_sync_chat_tool_call_stream(instrumented_client: openai.Client, exporter 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': { 'type': 'object', @@ -1000,7 +1024,7 @@ async def test_async_chat_tool_call_stream( assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1037,12 +1061,18 @@ async def test_async_chat_tool_call_stream( ], }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': True, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.tags': ('LLM',), 'logfire.span_type': 'span', @@ -1092,6 +1122,7 @@ async def test_async_chat_tool_call_stream( }, 'gen_ai.request.model': 'gpt-4', 'async': True, + 'gen_ai.operation.name': 'chat', 'duration': 1.0, 'response_data': { 'message': { @@ -1129,6 +1160,7 @@ async def test_async_chat_tool_call_stream( 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': { 'type': 'object', @@ -1186,7 +1218,7 @@ def test_sync_chat_completions_stream(instrumented_client: openai.Client, export assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1204,12 +1236,18 @@ def test_sync_chat_completions_stream(instrumented_client: openai.Client, export 'stream': True, }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), @@ -1240,6 +1278,7 @@ def test_sync_chat_completions_stream(instrumented_client: openai.Client, export 'logfire.msg': "streaming response from 'gpt-4' took 1.00s", 'gen_ai.request.model': 'gpt-4', 'logfire.span_type': 'log', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': { @@ -1261,6 +1300,7 @@ def test_sync_chat_completions_stream(instrumented_client: openai.Client, export 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': { 'type': 'object', @@ -1298,7 +1338,7 @@ async def test_async_chat_completions_stream( assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat gpt-4', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1316,12 +1356,18 @@ async def test_async_chat_completions_stream( 'stream': True, }, 'gen_ai.request.model': 'gpt-4', + 'gen_ai.operation.name': 'chat', 'async': True, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'gpt-4'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), @@ -1352,6 +1398,7 @@ async def test_async_chat_completions_stream( 'logfire.msg': "streaming response from 'gpt-4' took 1.00s", 'gen_ai.request.model': 'gpt-4', 'logfire.span_type': 'log', + 'gen_ai.operation.name': 'chat', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': { @@ -1373,6 +1420,7 @@ async def test_async_chat_completions_stream( 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': { 'type': 'object', @@ -1402,7 +1450,7 @@ def test_completions(instrumented_client: openai.Client, exporter: TestExporter) assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Completion with {request_data[model]!r}', + 'name': 'text_completion gpt-3.5-turbo-instruct', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1413,11 +1461,11 @@ def test_completions(instrumented_client: openai.Client, exporter: TestExporter) 'code.lineno': 123, 'request_data': {'model': 'gpt-3.5-turbo-instruct', 'prompt': 'What is four plus five?'}, 'async': False, + 'gen_ai.operation.name': 'text_completion', 'logfire.msg_template': 'Completion with {request_data[model]!r}', 'logfire.msg': "Completion with 'gpt-3.5-turbo-instruct'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-3.5-turbo-instruct', 'gen_ai.response.model': 'gpt-3.5-turbo-instruct', 'gen_ai.usage.input_tokens': 2, @@ -1439,7 +1487,7 @@ def test_completions(instrumented_client: openai.Client, exporter: TestExporter) 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -1480,7 +1528,7 @@ def test_responses_stream(exporter: TestExporter) -> None: assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Responses API with {gen_ai.request.model!r}', + 'name': 'chat gpt-4.1', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1490,6 +1538,7 @@ def test_responses_stream(exporter: TestExporter) -> None: 'code.function': 'test_responses_stream', 'code.lineno': 123, 'request_data': {'model': 'gpt-4.1', 'stream': True}, + 'gen_ai.operation.name': 'chat', 'gen_ai.request.model': 'gpt-4.1', 'events': [ {'event.name': 'gen_ai.user.message', 'content': 'What is four plus five?', 'role': 'user'} @@ -1501,6 +1550,7 @@ def test_responses_stream(exporter: TestExporter) -> None: 'type': 'object', 'properties': { 'request_data': {'type': 'object'}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'events': {'type': 'array'}, 'async': {}, @@ -1527,6 +1577,7 @@ def test_responses_stream(exporter: TestExporter) -> None: 'code.lineno': 123, 'request_data': {'model': 'gpt-4.1', 'stream': True}, 'gen_ai.request.model': 'gpt-4.1', + 'gen_ai.operation.name': 'chat', 'async': False, 'duration': 1.0, 'events': [ @@ -1546,6 +1597,7 @@ def test_responses_stream(exporter: TestExporter) -> None: 'properties': { 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, 'events': {'type': 'array'}, 'duration': {}, @@ -1570,7 +1622,7 @@ def test_completions_stream(instrumented_client: openai.Client, exporter: TestEx assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Completion with {request_data[model]!r}', + 'name': 'text_completion gpt-3.5-turbo-instruct', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1585,12 +1637,18 @@ def test_completions_stream(instrumented_client: openai.Client, exporter: TestEx 'stream': True, }, 'gen_ai.request.model': 'gpt-3.5-turbo-instruct', + 'gen_ai.operation.name': 'text_completion', 'async': False, 'logfire.msg_template': 'Completion with {request_data[model]!r}', 'logfire.msg': "Completion with 'gpt-3.5-turbo-instruct'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), @@ -1618,6 +1676,7 @@ def test_completions_stream(instrumented_client: openai.Client, exporter: TestEx 'logfire.msg': "streaming response from 'gpt-3.5-turbo-instruct' took 1.00s", 'gen_ai.request.model': 'gpt-3.5-turbo-instruct', 'logfire.span_type': 'log', + 'gen_ai.operation.name': 'text_completion', 'logfire.tags': ('LLM',), 'duration': 1.0, 'response_data': {'combined_chunk_content': 'The answer is Nine', 'chunk_count': 2}, @@ -1627,6 +1686,7 @@ def test_completions_stream(instrumented_client: openai.Client, exporter: TestEx 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': {'type': 'object'}, }, @@ -1647,7 +1707,7 @@ def test_embeddings(instrumented_client: openai.Client, exporter: TestExporter) assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Embedding Creation with {request_data[model]!r}', + 'name': 'embeddings text-embedding-3-small', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1662,11 +1722,11 @@ def test_embeddings(instrumented_client: openai.Client, exporter: TestExporter) 'encoding_format': 'base64', }, 'async': False, + 'gen_ai.operation.name': 'embeddings', 'logfire.msg_template': 'Embedding Creation with {request_data[model]!r}', 'logfire.msg': "Embedding Creation with 'text-embedding-3-small'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'text-embedding-3-small', 'gen_ai.response.model': 'text-embedding-3-small', 'gen_ai.usage.input_tokens': 1, @@ -1676,7 +1736,7 @@ def test_embeddings(instrumented_client: openai.Client, exporter: TestExporter) 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -1705,7 +1765,7 @@ def test_images(instrumented_client: openai.Client, exporter: TestExporter) -> N assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Image Generation with {request_data[model]!r}', + 'name': 'image_generation dall-e-3', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1716,12 +1776,12 @@ def test_images(instrumented_client: openai.Client, exporter: TestExporter) -> N 'code.lineno': 123, 'request_data': {'prompt': 'A picture of a cat.', 'model': 'dall-e-3'}, 'gen_ai.request.model': 'dall-e-3', + 'gen_ai.operation.name': 'image_generation', 'async': False, 'logfire.msg_template': 'Image Generation with {request_data[model]!r}', 'logfire.msg': "Image Generation with 'dall-e-3'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'response_data': { 'images': [ { @@ -1736,8 +1796,8 @@ def test_images(instrumented_client: openai.Client, exporter: TestExporter) -> N 'properties': { 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'async': {}, - 'gen_ai.system': {}, 'response_data': { 'type': 'object', 'properties': { @@ -1829,7 +1889,7 @@ def test_dont_suppress_httpx(exporter: TestExporter) -> None: }, }, { - 'name': 'Completion with {request_data[model]!r}', + 'name': 'text_completion gpt-3.5-turbo-instruct', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1841,11 +1901,11 @@ def test_dont_suppress_httpx(exporter: TestExporter) -> None: 'code.lineno': 123, 'request_data': {'model': 'gpt-3.5-turbo-instruct', 'prompt': 'xxx'}, 'async': False, + 'gen_ai.operation.name': 'text_completion', 'logfire.msg_template': 'Completion with {request_data[model]!r}', 'logfire.msg': "Completion with 'gpt-3.5-turbo-instruct'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-3.5-turbo-instruct', 'gen_ai.response.model': 'gpt-3.5-turbo-instruct', 'gen_ai.usage.input_tokens': 2, @@ -1867,7 +1927,7 @@ def test_dont_suppress_httpx(exporter: TestExporter) -> None: 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -1936,7 +1996,7 @@ def test_suppress_httpx(exporter: TestExporter) -> None: assert exporter.exported_spans_as_dict(parse_json_attributes=True, include_instrumentation_scope=True) == snapshot( [ { - 'name': 'Completion with {request_data[model]!r}', + 'name': 'text_completion gpt-3.5-turbo-instruct', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -1948,11 +2008,11 @@ def test_suppress_httpx(exporter: TestExporter) -> None: 'code.lineno': 123, 'request_data': {'model': 'gpt-3.5-turbo-instruct', 'prompt': 'xxx'}, 'async': False, + 'gen_ai.operation.name': 'text_completion', 'logfire.msg_template': 'Completion with {request_data[model]!r}', 'logfire.msg': "Completion with 'gpt-3.5-turbo-instruct'", 'logfire.span_type': 'span', 'logfire.tags': ('LLM',), - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-3.5-turbo-instruct', 'gen_ai.response.model': 'gpt-3.5-turbo-instruct', 'gen_ai.usage.input_tokens': 2, @@ -1974,7 +2034,7 @@ def test_suppress_httpx(exporter: TestExporter) -> None: 'properties': { 'request_data': {'type': 'object'}, 'async': {}, - 'gen_ai.system': {}, + 'gen_ai.operation.name': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, @@ -2044,10 +2104,9 @@ def test_create_files(instrumented_client: openai.Client, exporter: TestExporter 'code.filepath': 'test_openai.py', 'code.function': 'test_create_files', 'code.lineno': 123, - 'gen_ai.system': 'openai', 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}, 'gen_ai.system': {}}, + 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}}, }, }, } @@ -2077,10 +2136,9 @@ async def test_create_files_async(instrumented_async_client: openai.AsyncClient, 'code.filepath': 'test_openai.py', 'code.function': 'test_create_files_async', 'code.lineno': 123, - 'gen_ai.system': 'openai', 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}, 'gen_ai.system': {}}, + 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}}, }, }, } @@ -2122,7 +2180,6 @@ def test_create_assistant(instrumented_client: openai.Client, exporter: TestExpo 'code.filepath': 'test_openai.py', 'code.function': 'test_create_assistant', 'code.lineno': 123, - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-4o', 'gen_ai.response.model': 'gpt-4-turbo', 'logfire.json_schema': { @@ -2131,7 +2188,6 @@ def test_create_assistant(instrumented_client: openai.Client, exporter: TestExpo 'request_data': {'type': 'object'}, 'url': {}, 'async': {}, - 'gen_ai.system': {}, 'gen_ai.request.model': {}, 'gen_ai.response.model': {}, }, @@ -2165,10 +2221,9 @@ def test_create_thread(instrumented_client: openai.Client, exporter: TestExporte 'code.filepath': 'test_openai.py', 'code.function': 'test_create_thread', 'code.lineno': 123, - 'gen_ai.system': 'openai', 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}, 'gen_ai.system': {}}, + 'properties': {'request_data': {'type': 'object'}, 'url': {}, 'async': {}}, }, }, } @@ -2208,7 +2263,7 @@ def test_responses_api(exporter: TestExporter) -> None: assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Responses API with {gen_ai.request.model!r}', + 'name': 'chat gpt-4.1', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -2218,12 +2273,12 @@ def test_responses_api(exporter: TestExporter) -> None: 'code.function': 'test_responses_api', 'code.lineno': 123, 'async': False, + 'gen_ai.operation.name': 'chat', 'request_data': {'model': 'gpt-4.1', 'stream': False}, 'logfire.msg_template': 'Responses API with {gen_ai.request.model!r}', 'logfire.msg': "Responses API with 'gpt-4.1'", 'logfire.tags': ('LLM',), 'logfire.span_type': 'span', - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-4.1', 'gen_ai.response.model': 'gpt-4.1-2025-04-14', 'gen_ai.usage.input_tokens': 65, @@ -2252,10 +2307,10 @@ def test_responses_api(exporter: TestExporter) -> None: 'type': 'object', 'properties': { 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'request_data': {'type': 'object'}, 'events': {'type': 'array'}, 'async': {}, - 'gen_ai.system': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, 'gen_ai.usage.output_tokens': {}, @@ -2265,7 +2320,7 @@ def test_responses_api(exporter: TestExporter) -> None: }, }, { - 'name': 'Responses API with {gen_ai.request.model!r}', + 'name': 'chat gpt-4.1', 'context': {'trace_id': 2, 'span_id': 3, 'is_remote': False}, 'parent': None, 'start_time': 3000000000, @@ -2275,12 +2330,12 @@ def test_responses_api(exporter: TestExporter) -> None: 'code.function': 'test_responses_api', 'code.lineno': 123, 'async': False, + 'gen_ai.operation.name': 'chat', 'request_data': {'model': 'gpt-4.1', 'stream': False}, 'logfire.msg_template': 'Responses API with {gen_ai.request.model!r}', 'logfire.msg': "Responses API with 'gpt-4.1'", 'logfire.tags': ('LLM',), 'logfire.span_type': 'span', - 'gen_ai.system': 'openai', 'gen_ai.request.model': 'gpt-4.1', 'gen_ai.response.model': 'gpt-4.1-2025-04-14', 'gen_ai.usage.input_tokens': 43, @@ -2320,10 +2375,10 @@ def test_responses_api(exporter: TestExporter) -> None: 'type': 'object', 'properties': { 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, 'request_data': {'type': 'object'}, 'events': {'type': 'array'}, 'async': {}, - 'gen_ai.system': {}, 'gen_ai.response.model': {}, 'gen_ai.usage.input_tokens': {}, 'gen_ai.usage.output_tokens': {}, @@ -2366,7 +2421,7 @@ def test_openrouter_streaming_reasoning(exporter: TestExporter) -> None: assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( [ { - 'name': 'Chat Completion with {request_data[model]!r}', + 'name': 'chat google/gemini-2.5-flash', 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, 'parent': None, 'start_time': 1000000000, @@ -2381,12 +2436,18 @@ def test_openrouter_streaming_reasoning(exporter: TestExporter) -> None: 'stream': True, }, 'gen_ai.request.model': 'google/gemini-2.5-flash', + 'gen_ai.operation.name': 'chat', 'async': False, 'logfire.msg_template': 'Chat Completion with {request_data[model]!r}', 'logfire.msg': "Chat Completion with 'google/gemini-2.5-flash'", 'logfire.json_schema': { 'type': 'object', - 'properties': {'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}}, + 'properties': { + 'request_data': {'type': 'object'}, + 'gen_ai.request.model': {}, + 'gen_ai.operation.name': {}, + 'async': {}, + }, }, 'logfire.tags': ('LLM',), 'logfire.span_type': 'span', @@ -2414,6 +2475,7 @@ def test_openrouter_streaming_reasoning(exporter: TestExporter) -> None: }, 'gen_ai.request.model': 'google/gemini-2.5-flash', 'async': False, + 'gen_ai.operation.name': 'chat', 'duration': 1.0, 'response_data': { 'message': { @@ -2468,6 +2530,7 @@ def test_openrouter_streaming_reasoning(exporter: TestExporter) -> None: 'request_data': {'type': 'object'}, 'gen_ai.request.model': {}, 'async': {}, + 'gen_ai.operation.name': {}, 'duration': {}, 'response_data': { 'type': 'object',