Skip to content

Commit 0abc6bd

Browse files
authored
fix(llamaindex): structured llm model and temperature parsing (#3159)
1 parent 865f5cb commit 0abc6bd

File tree

4 files changed

+341
-0
lines changed

4 files changed

+341
-0
lines changed

packages/opentelemetry-instrumentation-llamaindex/opentelemetry/instrumentation/llamaindex/span_utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def set_llm_chat_request_model_attributes(event, span):
3131

3232
model_dict = event.model_dict
3333
span.set_attribute(SpanAttributes.LLM_REQUEST_TYPE, LLMRequestTypeValues.CHAT.value)
34+
35+
# For StructuredLLM, the model and temperature are nested under model_dict.llm
36+
if "llm" in model_dict:
37+
model_dict = model_dict.get("llm", {})
38+
3439
span.set_attribute(SpanAttributes.LLM_REQUEST_MODEL, model_dict.get("model"))
3540
span.set_attribute(
3641
SpanAttributes.LLM_REQUEST_TEMPERATURE, model_dict.get("temperature")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"system","content":"Extract invoice information from
4+
the following text."},{"role":"user","content":"Invoice #67890 for $299.99 to
5+
Jane Doe"}],"model":"gpt-4o","stream":false,"temperature":0.5,"tool_choice":"required","tools":[{"type":"function","function":{"name":"Invoice","description":"Example
6+
model for structured output testing.","parameters":{"properties":{"invoice_id":{"description":"Invoice
7+
identifier","title":"Invoice Id","type":"string"},"amount":{"description":"Invoice
8+
amount","title":"Amount","type":"number"},"customer_name":{"description":"Customer
9+
name","title":"Customer Name","type":"string"}},"required":["invoice_id","amount","customer_name"],"type":"object","additionalProperties":false},"strict":false}}]}'
10+
headers:
11+
accept:
12+
- application/json
13+
accept-encoding:
14+
- gzip, deflate
15+
connection:
16+
- keep-alive
17+
content-length:
18+
- '751'
19+
content-type:
20+
- application/json
21+
host:
22+
- api.openai.com
23+
traceparent:
24+
- 00-8a61275987df582f1f4634434316e052-29c62ce5233ca6c1-01
25+
user-agent:
26+
- AsyncOpenAI/Python 1.58.1
27+
x-stainless-arch:
28+
- arm64
29+
x-stainless-async:
30+
- async:asyncio
31+
x-stainless-lang:
32+
- python
33+
x-stainless-os:
34+
- MacOS
35+
x-stainless-package-version:
36+
- 1.58.1
37+
x-stainless-retry-count:
38+
- '0'
39+
x-stainless-runtime:
40+
- CPython
41+
x-stainless-runtime-version:
42+
- 3.9.5
43+
method: POST
44+
uri: https://api.openai.com/v1/chat/completions
45+
response:
46+
body:
47+
string: !!binary |
48+
H4sIAAAAAAAAAwAAAP//jFNRb5swEH7nV1j3nFQhJITwOG2Tmm1au2mqlFEh1xzgxdjMNlnbKP99
49+
MiSBpJk0HpB13313330+7zxCgGcQE2AltayqxfjdH/97Pt8+LMJv4X1w+/l+XX35+PvDWr2Wn0oY
50+
OYZ6+oXMHlk3TFW1QMuV7GCmkVp0Vf3FPPDDKJj5LVCpDIWjFbUdz9R4OpnOxpNoPAkPxFJxhgZi
51+
8tMjhJBd+3cSZYbPEJPJ6Bip0BhaIMSnJEJAK+EiQI3hxlJpYdSDTEmL0qmWjRADwColUkaF6Bt3
52+
325w7n2iQqTB5us6Kld3myh6jrTmP+4eypfVPB/060q/1K2gvJHs5M8AP8Xji2aEgKRVy72VW+fK
53+
BZUQoLpoKpTWyYZdArxLTHmWQJxAuIiWkwRGCdBKNdImEE+Xy5vlcpQAa4xVFerUNWmzV1Qiea8w
54+
gT2cNdp7186PA/805o2h4q2xVEplqZuvdfbxgOxPlyhUUWv1ZC6okHPJTZlqpKb1ZnhF3lFIKwGa
55+
sy2AWquqtqlVG2ybLmddUeh3tAeniwNolaWij/tTf3SlXJqhpbzdktNiMspKzHpqv6C0ybgaAN5g
56+
9LdqrtXuxuey+J/yPcAY1haztNaYcXY+cZ+m0T3hf6WdTG4Fg0G9dZtlOWp3HRnmtBHd6wLzYixW
57+
ac5lgbrWvH1ikNdpEC7mURhk0RS8vfcXAAD//wMAz9iN+WsEAAA=
58+
headers:
59+
CF-RAY:
60+
- 9631210b48c59b09-TLV
61+
Connection:
62+
- keep-alive
63+
Content-Encoding:
64+
- gzip
65+
Content-Type:
66+
- application/json
67+
Date:
68+
- Tue, 22 Jul 2025 07:12:22 GMT
69+
Server:
70+
- cloudflare
71+
Set-Cookie:
72+
- __cf_bm=85Pnuwdxp60XG9gTYk6PvKiayYA.O7_GyYvIqszoFK4-1753168342-1.0.1.1-U3UrVzfu85BkIH.H5MYad4ZmN7puPBD7KQW.azMffta5E4H.xC_CG5z8TrUQgzbuCSgy2usWVYL2yAcSoMeLGGO4EEBzHVfENLWDNLBsUK8;
73+
path=/; expires=Tue, 22-Jul-25 07:42:22 GMT; domain=.api.openai.com; HttpOnly;
74+
Secure; SameSite=None
75+
- _cfuvid=8vwo6_jaBCZTKBaXzggpnVdGNTrb_E1G2_w3V4_aTfw-1753168342175-0.0.1.1-604800000;
76+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
77+
Strict-Transport-Security:
78+
- max-age=31536000; includeSubDomains; preload
79+
Transfer-Encoding:
80+
- chunked
81+
X-Content-Type-Options:
82+
- nosniff
83+
access-control-expose-headers:
84+
- X-Request-ID
85+
alt-svc:
86+
- h3=":443"; ma=86400
87+
cf-cache-status:
88+
- DYNAMIC
89+
openai-organization:
90+
- traceloop
91+
openai-processing-ms:
92+
- '741'
93+
openai-project:
94+
- proj_tzz1TbPPOXaf6j9tEkVUBIAa
95+
openai-version:
96+
- '2020-10-01'
97+
x-envoy-upstream-service-time:
98+
- '744'
99+
x-ratelimit-limit-requests:
100+
- '10000'
101+
x-ratelimit-limit-tokens:
102+
- '30000000'
103+
x-ratelimit-remaining-requests:
104+
- '9999'
105+
x-ratelimit-remaining-tokens:
106+
- '29999973'
107+
x-ratelimit-reset-requests:
108+
- 6ms
109+
x-ratelimit-reset-tokens:
110+
- 0s
111+
x-request-id:
112+
- 9a674902-3edd-41ec-b409-4d348446f812
113+
status:
114+
code: 200
115+
message: OK
116+
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
interactions:
2+
- request:
3+
body: '{"messages":[{"role":"system","content":"Extract invoice information from
4+
the following text."},{"role":"user","content":"Invoice #12345 for $199.99 to
5+
John Smith"}],"model":"gpt-4o","stream":false,"temperature":0.7,"tool_choice":"required","tools":[{"type":"function","function":{"name":"Invoice","description":"Example
6+
model for structured output testing.","parameters":{"properties":{"invoice_id":{"description":"Invoice
7+
identifier","title":"Invoice Id","type":"string"},"amount":{"description":"Invoice
8+
amount","title":"Amount","type":"number"},"customer_name":{"description":"Customer
9+
name","title":"Customer Name","type":"string"}},"required":["invoice_id","amount","customer_name"],"type":"object","additionalProperties":false},"strict":false}}]}'
10+
headers:
11+
accept:
12+
- application/json
13+
accept-encoding:
14+
- gzip, deflate
15+
connection:
16+
- keep-alive
17+
content-length:
18+
- '753'
19+
content-type:
20+
- application/json
21+
host:
22+
- api.openai.com
23+
traceparent:
24+
- 00-fea82171c54bdef7cc2453298f3b9c96-2e3d335209cf620a-01
25+
user-agent:
26+
- OpenAI/Python 1.58.1
27+
x-stainless-arch:
28+
- arm64
29+
x-stainless-async:
30+
- 'false'
31+
x-stainless-lang:
32+
- python
33+
x-stainless-os:
34+
- MacOS
35+
x-stainless-package-version:
36+
- 1.58.1
37+
x-stainless-retry-count:
38+
- '0'
39+
x-stainless-runtime:
40+
- CPython
41+
x-stainless-runtime-version:
42+
- 3.9.5
43+
method: POST
44+
uri: https://api.openai.com/v1/chat/completions
45+
response:
46+
body:
47+
string: !!binary |
48+
H4sIAAAAAAAAAwAAAP//jFPRbtowFH3PV1j3OVQkAQJ56zpNo1pVTUjbtKWKXOdCTB07s50Oivj3
49+
yQmQQJm0PETWPT7nHh9f7zxCgOeQEGAFtaysxODDn2DBNl8+vf14GG43xe0LXdR1/PPjOg5/fwff
50+
MdTzGpk9sm6YKiuBlivZwkwjtehUg3gcBZNpFE0boFQ5CkdbVXYwUoNwGI4Gw+lgODkQC8UZGkjI
51+
L48QQnbN31mUOW4gIUP/WCnRGLpCSE6bCAGthKsANYYbS6UFvwOZkhalcy1rIXqAVUpkjArRNW6/
52+
XW/d5USFyPJq/rmOH9nD/Nvdbayi+0cze17ffe31a6W3VWNoWUt2yqeHn+rJRTNCQNKy4c7lq0vl
53+
gkoIUL2qS5TW2YZdCrRUtbQpJMFsdjOb+Smw2lhVos6cVgpJCveqkGRRcluk4KfAW+2M5w0ahNFo
54+
nMIezlrtvWvrp16CGpe1oeJ9tFRKZak7YZPt0wHZn65RqFWl1bO5oMKSS26KTCM1TTr9S/KORhoL
55+
UJ/NAVRalZXNrHrBpuls1IpCN6UdGMYH0CpLRVcPwsC/IpflaClv5uQ0moyyAvOO2o0orXOueoDX
56+
O/p7N9e02+Nzufof+Q5gDCuLeVZpzDk7P3G3TaN7xP/adgq5MQwG9asbFMtRu+vIcUlr0b4vMFtj
57+
scyWXK5QV5o3jwyWVRZN4vF0EuXTELy99xcAAP//AwAYSN8XbQQAAA==
58+
headers:
59+
CF-RAY:
60+
- 963120fffc22efea-TLV
61+
Connection:
62+
- keep-alive
63+
Content-Encoding:
64+
- gzip
65+
Content-Type:
66+
- application/json
67+
Date:
68+
- Tue, 22 Jul 2025 07:12:19 GMT
69+
Server:
70+
- cloudflare
71+
Set-Cookie:
72+
- __cf_bm=KVzJcC68SQuziL071b4WwO20k3mVcAnXeO74ikQvbTk-1753168339-1.0.1.1-VjNOeRtShSwdWPgUcGHVVipel_nbk82XRkGT5m__qbU7aBiCFLU6Oj8DNlL53ZnnKptoG4CaJSvVt6Zh5YLChq9Ad7vpo5IhXwUtoeR3ws0;
73+
path=/; expires=Tue, 22-Jul-25 07:42:19 GMT; domain=.api.openai.com; HttpOnly;
74+
Secure; SameSite=None
75+
- _cfuvid=B236WyNvITlBmvDyAycWz.GvuU_hKcSHN1HBQ8O2fa4-1753168339363-0.0.1.1-604800000;
76+
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
77+
Transfer-Encoding:
78+
- chunked
79+
X-Content-Type-Options:
80+
- nosniff
81+
access-control-expose-headers:
82+
- X-Request-ID
83+
alt-svc:
84+
- h3=":443"; ma=86400
85+
cf-cache-status:
86+
- DYNAMIC
87+
openai-organization:
88+
- traceloop
89+
openai-processing-ms:
90+
- '890'
91+
openai-project:
92+
- proj_tzz1TbPPOXaf6j9tEkVUBIAa
93+
openai-version:
94+
- '2020-10-01'
95+
strict-transport-security:
96+
- max-age=31536000; includeSubDomains; preload
97+
x-envoy-upstream-service-time:
98+
- '897'
99+
x-ratelimit-limit-requests:
100+
- '10000'
101+
x-ratelimit-limit-tokens:
102+
- '30000000'
103+
x-ratelimit-remaining-requests:
104+
- '9999'
105+
x-ratelimit-remaining-tokens:
106+
- '29999973'
107+
x-ratelimit-reset-requests:
108+
- 6ms
109+
x-ratelimit-reset-tokens:
110+
- 0s
111+
x-request-id:
112+
- req_d35a94342d3f797254ffcd31f2a6b423
113+
status:
114+
code: 200
115+
message: OK
116+
version: 1
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import pytest
2+
from llama_index.core.llms import ChatMessage
3+
from llama_index.llms.openai import OpenAI
4+
from pydantic import BaseModel, Field
5+
from opentelemetry.semconv_ai import SpanAttributes, LLMRequestTypeValues
6+
7+
8+
class Invoice(BaseModel):
9+
"""Example model for structured output testing."""
10+
11+
invoice_id: str = Field(description="Invoice identifier")
12+
amount: float = Field(description="Invoice amount")
13+
customer_name: str = Field(description="Customer name")
14+
15+
16+
@pytest.mark.vcr()
17+
def test_structured_llm_model_attributes(instrument_with_content, span_exporter):
18+
"""
19+
Test that StructuredLLM correctly sets model attributes.
20+
21+
This test reproduces the issue where set_llm_chat_request_model_attributes
22+
fails to access model and temperature from StructuredLLM because it tries
23+
to access model_dict.model instead of model_dict.llm.model.
24+
"""
25+
llm = OpenAI(model="gpt-4o", temperature=0.7)
26+
structured_llm = llm.as_structured_llm(Invoice)
27+
28+
messages = [
29+
ChatMessage(
30+
role="system",
31+
content="Extract invoice information from the following text.",
32+
),
33+
ChatMessage(role="user", content="Invoice #12345 for $199.99 to John Smith"),
34+
]
35+
36+
response = structured_llm.chat(messages)
37+
38+
assert response is not None
39+
40+
spans = span_exporter.get_finished_spans()
41+
assert len(spans) > 0
42+
43+
llm_span = None
44+
for span in spans:
45+
if (
46+
span.attributes.get(SpanAttributes.LLM_REQUEST_TYPE)
47+
== LLMRequestTypeValues.CHAT.value
48+
):
49+
llm_span = span
50+
break
51+
52+
assert llm_span is not None, "Should have an LLM span"
53+
54+
attributes = llm_span.attributes
55+
assert "gen_ai.request.model" in attributes
56+
assert attributes["gen_ai.request.model"] == "gpt-4o"
57+
assert "gen_ai.request.temperature" in attributes
58+
assert attributes["gen_ai.request.temperature"] == 0.7
59+
60+
61+
@pytest.mark.vcr()
62+
@pytest.mark.asyncio
63+
async def test_structured_llm_achat_model_attributes(
64+
instrument_with_content, span_exporter
65+
):
66+
"""
67+
Test that StructuredLLM achat method correctly sets model attributes.
68+
69+
This is the async version of the test that reproduces the original issue.
70+
"""
71+
llm = OpenAI(model="gpt-4o", temperature=0.5)
72+
structured_llm = llm.as_structured_llm(Invoice)
73+
74+
messages = [
75+
ChatMessage(
76+
role="system",
77+
content="Extract invoice information from the following text.",
78+
),
79+
ChatMessage(role="user", content="Invoice #67890 for $299.99 to Jane Doe"),
80+
]
81+
82+
response = await structured_llm.achat(messages)
83+
84+
assert response is not None
85+
86+
spans = span_exporter.get_finished_spans()
87+
assert len(spans) > 0
88+
89+
llm_span = None
90+
for span in spans:
91+
if (
92+
span.attributes.get(SpanAttributes.LLM_REQUEST_TYPE)
93+
== LLMRequestTypeValues.CHAT.value
94+
):
95+
llm_span = span
96+
break
97+
98+
assert llm_span is not None, "Should have an LLM span"
99+
100+
attributes = llm_span.attributes
101+
assert "gen_ai.request.model" in attributes
102+
assert attributes["gen_ai.request.model"] == "gpt-4o"
103+
assert "gen_ai.request.temperature" in attributes
104+
assert attributes["gen_ai.request.temperature"] == 0.5

0 commit comments

Comments
 (0)