Skip to content

Commit 7e07feb

Browse files
authored
openai-v2: default empty string for GEN_AI_REQUEST_MODEL on missing model (#4494)
1 parent b51543a commit 7e07feb

4 files changed

Lines changed: 131 additions & 5 deletions

File tree

instrumentation-genai/opentelemetry-instrumentation-openai-v2/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
- Add strongly typed Responses API extractors with validation and content
3636
extraction improvements
3737
([#4337](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4337))
38+
- Default empty string for `gen_ai.request.model` attribute on missing model.
39+
([#4494](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/4494))
3840

3941
## Version 2.3b0 (2025-12-24)
4042

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/patch.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ def traced_method(wrapped, instance, args, kwargs):
6868
**get_llm_request_attributes(kwargs, instance, False)
6969
}
7070

71-
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
71+
operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]
72+
model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
73+
span_name = f"{operation_name} {model}" if model else operation_name
7274
with tracer.start_as_current_span(
7375
name=span_name,
7476
kind=SpanKind.CLIENT,
@@ -169,7 +171,9 @@ async def traced_method(wrapped, instance, args, kwargs):
169171
**get_llm_request_attributes(kwargs, instance, False)
170172
}
171173

172-
span_name = f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
174+
operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]
175+
model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
176+
span_name = f"{operation_name} {model}" if model else operation_name
173177
with tracer.start_as_current_span(
174178
name=span_name,
175179
kind=SpanKind.CLIENT,
@@ -365,7 +369,9 @@ async def traced_method(wrapped, instance, args, kwargs):
365369

366370
def _get_embeddings_span_name(span_attributes):
367371
"""Get span name for embeddings operations."""
368-
return f"{span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]} {span_attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL]}"
372+
operation_name = span_attributes[GenAIAttributes.GEN_AI_OPERATION_NAME]
373+
model = span_attributes.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
374+
return f"{operation_name} {model}" if model else operation_name
369375

370376

371377
def _record_metrics(

instrumentation-genai/opentelemetry-instrumentation-openai-v2/src/opentelemetry/instrumentation/openai_v2/utils.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,16 @@ def get_llm_request_attributes(
211211
latest_experimental_enabled,
212212
operation_name=GenAIAttributes.GenAiOperationNameValues.CHAT.value,
213213
):
214-
# pylint: disable=too-many-branches
214+
# pylint: disable=too-many-branches,too-many-locals
215215

216216
attributes = {
217217
GenAIAttributes.GEN_AI_OPERATION_NAME: operation_name,
218-
GenAIAttributes.GEN_AI_REQUEST_MODEL: kwargs.get("model"),
219218
}
220219

220+
model = kwargs.get("model")
221+
if model:
222+
attributes[GenAIAttributes.GEN_AI_REQUEST_MODEL] = model
223+
221224
if latest_experimental_enabled:
222225
attributes.update(
223226
{
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Unit tests for get_llm_request_attributes and span name logic in utils.py."""
16+
17+
from types import SimpleNamespace
18+
19+
import pytest
20+
21+
from opentelemetry.instrumentation.openai_v2.patch import (
22+
_get_embeddings_span_name,
23+
)
24+
from opentelemetry.instrumentation.openai_v2.utils import (
25+
get_llm_request_attributes,
26+
)
27+
from opentelemetry.semconv._incubating.attributes import (
28+
gen_ai_attributes as GenAIAttributes,
29+
)
30+
31+
32+
@pytest.fixture(autouse=True)
33+
def fixture_vcr():
34+
"""No VCR needed for these unit tests."""
35+
yield
36+
37+
38+
def _make_client(base_url=None):
39+
return SimpleNamespace(_base_url=base_url)
40+
41+
42+
def test_model_omitted_when_missing():
43+
"""When 'model' is not in kwargs, GEN_AI_REQUEST_MODEL should be absent."""
44+
attrs = get_llm_request_attributes(
45+
kwargs={},
46+
client_instance=_make_client(),
47+
latest_experimental_enabled=False,
48+
)
49+
assert GenAIAttributes.GEN_AI_REQUEST_MODEL not in attrs
50+
51+
52+
def test_model_omitted_when_missing_experimental():
53+
"""Same as above but with latest_experimental_enabled=True."""
54+
attrs = get_llm_request_attributes(
55+
kwargs={},
56+
client_instance=_make_client(),
57+
latest_experimental_enabled=True,
58+
)
59+
assert GenAIAttributes.GEN_AI_REQUEST_MODEL not in attrs
60+
61+
62+
def test_model_preserved_when_provided():
63+
"""When 'model' is in kwargs, GEN_AI_REQUEST_MODEL should be set to its value."""
64+
attrs = get_llm_request_attributes(
65+
kwargs={"model": "gpt-4o-mini"},
66+
client_instance=_make_client(),
67+
latest_experimental_enabled=False,
68+
)
69+
assert attrs[GenAIAttributes.GEN_AI_REQUEST_MODEL] == "gpt-4o-mini"
70+
71+
72+
def test_span_name_includes_model_when_present():
73+
"""Span name should be '{operation} {model}' when model is provided."""
74+
attrs = get_llm_request_attributes(
75+
kwargs={"model": "gpt-4o"},
76+
client_instance=_make_client(),
77+
latest_experimental_enabled=False,
78+
)
79+
operation_name = attrs[GenAIAttributes.GEN_AI_OPERATION_NAME]
80+
model = attrs.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
81+
span_name = f"{operation_name} {model}" if model else operation_name
82+
assert span_name == "chat gpt-4o"
83+
84+
85+
def test_span_name_uses_operation_only_when_model_missing():
86+
"""Span name should be just '{operation}' when model is not provided."""
87+
attrs = get_llm_request_attributes(
88+
kwargs={},
89+
client_instance=_make_client(),
90+
latest_experimental_enabled=False,
91+
)
92+
operation_name = attrs[GenAIAttributes.GEN_AI_OPERATION_NAME]
93+
model = attrs.get(GenAIAttributes.GEN_AI_REQUEST_MODEL)
94+
span_name = f"{operation_name} {model}" if model else operation_name
95+
assert span_name == "chat"
96+
97+
98+
def test_embeddings_span_name_includes_model():
99+
"""Embeddings span name should be '{operation} {model}' when model is present."""
100+
span_attributes = {
101+
GenAIAttributes.GEN_AI_OPERATION_NAME: "embeddings",
102+
GenAIAttributes.GEN_AI_REQUEST_MODEL: "text-embedding-3-small",
103+
}
104+
assert (
105+
_get_embeddings_span_name(span_attributes)
106+
== "embeddings text-embedding-3-small"
107+
)
108+
109+
110+
def test_embeddings_span_name_without_model():
111+
"""Embeddings span name should be just '{operation}' when model is absent."""
112+
span_attributes = {
113+
GenAIAttributes.GEN_AI_OPERATION_NAME: "embeddings",
114+
}
115+
assert _get_embeddings_span_name(span_attributes) == "embeddings"

0 commit comments

Comments
 (0)