opentelemetry-instrumentation-openai 0.44.1__py3-none-any.whl → 0.44.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of opentelemetry-instrumentation-openai might be problematic. Click here for more details.

@@ -7,6 +7,7 @@ from functools import singledispatch
7
7
  from typing import List, Optional, Union
8
8
 
9
9
  from opentelemetry import context as context_api
10
+ import pydantic
10
11
  from opentelemetry.instrumentation.openai.shared import (
11
12
  OPENAI_LLM_USAGE_TOKEN_TYPES,
12
13
  _get_openai_base_url,
@@ -47,12 +48,10 @@ from opentelemetry.semconv_ai import (
47
48
  SpanAttributes,
48
49
  )
49
50
  from opentelemetry.trace import SpanKind, Tracer
51
+ from opentelemetry import trace
50
52
  from opentelemetry.trace.status import Status, StatusCode
51
53
  from wrapt import ObjectProxy
52
54
 
53
- from openai.types.chat import ChatCompletionMessageToolCall
54
- from openai.types.chat.chat_completion_message import FunctionCall
55
-
56
55
  SPAN_NAME = "openai.chat"
57
56
  PROMPT_FILTER_KEY = "prompt_filter_results"
58
57
  CONTENT_FILTER_KEY = "content_filter_results"
@@ -88,75 +87,77 @@ def chat_wrapper(
88
87
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
89
88
  )
90
89
 
91
- run_async(_handle_request(span, kwargs, instance))
92
- try:
93
- start_time = time.time()
94
- response = wrapped(*args, **kwargs)
95
- end_time = time.time()
96
- except Exception as e: # pylint: disable=broad-except
97
- end_time = time.time()
98
- duration = end_time - start_time if "start_time" in locals() else 0
99
-
100
- attributes = {
101
- "error.type": e.__class__.__name__,
102
- }
103
-
104
- if duration > 0 and duration_histogram:
105
- duration_histogram.record(duration, attributes=attributes)
106
- if exception_counter:
107
- exception_counter.add(1, attributes=attributes)
108
-
109
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
110
- span.record_exception(e)
111
- span.set_status(Status(StatusCode.ERROR, str(e)))
112
- span.end()
90
+ # Use the span as current context to ensure events get proper trace context
91
+ with trace.use_span(span, end_on_exit=False):
92
+ run_async(_handle_request(span, kwargs, instance))
93
+ try:
94
+ start_time = time.time()
95
+ response = wrapped(*args, **kwargs)
96
+ end_time = time.time()
97
+ except Exception as e: # pylint: disable=broad-except
98
+ end_time = time.time()
99
+ duration = end_time - start_time if "start_time" in locals() else 0
100
+
101
+ attributes = {
102
+ "error.type": e.__class__.__name__,
103
+ }
113
104
 
114
- raise
105
+ if duration > 0 and duration_histogram:
106
+ duration_histogram.record(duration, attributes=attributes)
107
+ if exception_counter:
108
+ exception_counter.add(1, attributes=attributes)
115
109
 
116
- if is_streaming_response(response):
117
- # span will be closed after the generator is done
118
- if is_openai_v1():
119
- return ChatStream(
120
- span,
121
- response,
122
- instance,
123
- token_counter,
124
- choice_counter,
125
- duration_histogram,
126
- streaming_time_to_first_token,
127
- streaming_time_to_generate,
128
- start_time,
129
- kwargs,
130
- )
131
- else:
132
- return _build_from_streaming_response(
133
- span,
134
- response,
135
- instance,
136
- token_counter,
137
- choice_counter,
138
- duration_histogram,
139
- streaming_time_to_first_token,
140
- streaming_time_to_generate,
141
- start_time,
142
- kwargs,
143
- )
110
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
111
+ span.record_exception(e)
112
+ span.set_status(Status(StatusCode.ERROR, str(e)))
113
+ span.end()
144
114
 
145
- duration = end_time - start_time
115
+ raise
146
116
 
147
- _handle_response(
148
- response,
149
- span,
150
- instance,
151
- token_counter,
152
- choice_counter,
153
- duration_histogram,
154
- duration,
155
- )
117
+ if is_streaming_response(response):
118
+ # span will be closed after the generator is done
119
+ if is_openai_v1():
120
+ return ChatStream(
121
+ span,
122
+ response,
123
+ instance,
124
+ token_counter,
125
+ choice_counter,
126
+ duration_histogram,
127
+ streaming_time_to_first_token,
128
+ streaming_time_to_generate,
129
+ start_time,
130
+ kwargs,
131
+ )
132
+ else:
133
+ return _build_from_streaming_response(
134
+ span,
135
+ response,
136
+ instance,
137
+ token_counter,
138
+ choice_counter,
139
+ duration_histogram,
140
+ streaming_time_to_first_token,
141
+ streaming_time_to_generate,
142
+ start_time,
143
+ kwargs,
144
+ )
156
145
 
157
- span.end()
146
+ duration = end_time - start_time
158
147
 
159
- return response
148
+ _handle_response(
149
+ response,
150
+ span,
151
+ instance,
152
+ token_counter,
153
+ choice_counter,
154
+ duration_histogram,
155
+ duration,
156
+ )
157
+
158
+ span.end()
159
+
160
+ return response
160
161
 
161
162
 
162
163
  @_with_chat_telemetry_wrapper
@@ -184,78 +185,80 @@ async def achat_wrapper(
184
185
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
185
186
  )
186
187
 
187
- await _handle_request(span, kwargs, instance)
188
+ # Use the span as current context to ensure events get proper trace context
189
+ with trace.use_span(span, end_on_exit=False):
190
+ await _handle_request(span, kwargs, instance)
188
191
 
189
- try:
190
- start_time = time.time()
191
- response = await wrapped(*args, **kwargs)
192
- end_time = time.time()
193
- except Exception as e: # pylint: disable=broad-except
194
- end_time = time.time()
195
- duration = end_time - start_time if "start_time" in locals() else 0
196
-
197
- common_attributes = Config.get_common_metrics_attributes()
198
- attributes = {
199
- **common_attributes,
200
- "error.type": e.__class__.__name__,
201
- }
202
-
203
- if duration > 0 and duration_histogram:
204
- duration_histogram.record(duration, attributes=attributes)
205
- if exception_counter:
206
- exception_counter.add(1, attributes=attributes)
207
-
208
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
209
- span.record_exception(e)
210
- span.set_status(Status(StatusCode.ERROR, str(e)))
211
- span.end()
192
+ try:
193
+ start_time = time.time()
194
+ response = await wrapped(*args, **kwargs)
195
+ end_time = time.time()
196
+ except Exception as e: # pylint: disable=broad-except
197
+ end_time = time.time()
198
+ duration = end_time - start_time if "start_time" in locals() else 0
199
+
200
+ common_attributes = Config.get_common_metrics_attributes()
201
+ attributes = {
202
+ **common_attributes,
203
+ "error.type": e.__class__.__name__,
204
+ }
212
205
 
213
- raise
206
+ if duration > 0 and duration_histogram:
207
+ duration_histogram.record(duration, attributes=attributes)
208
+ if exception_counter:
209
+ exception_counter.add(1, attributes=attributes)
214
210
 
215
- if is_streaming_response(response):
216
- # span will be closed after the generator is done
217
- if is_openai_v1():
218
- return ChatStream(
219
- span,
220
- response,
221
- instance,
222
- token_counter,
223
- choice_counter,
224
- duration_histogram,
225
- streaming_time_to_first_token,
226
- streaming_time_to_generate,
227
- start_time,
228
- kwargs,
229
- )
230
- else:
231
- return _abuild_from_streaming_response(
232
- span,
233
- response,
234
- instance,
235
- token_counter,
236
- choice_counter,
237
- duration_histogram,
238
- streaming_time_to_first_token,
239
- streaming_time_to_generate,
240
- start_time,
241
- kwargs,
242
- )
211
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
212
+ span.record_exception(e)
213
+ span.set_status(Status(StatusCode.ERROR, str(e)))
214
+ span.end()
243
215
 
244
- duration = end_time - start_time
216
+ raise
245
217
 
246
- _handle_response(
247
- response,
248
- span,
249
- instance,
250
- token_counter,
251
- choice_counter,
252
- duration_histogram,
253
- duration,
254
- )
218
+ if is_streaming_response(response):
219
+ # span will be closed after the generator is done
220
+ if is_openai_v1():
221
+ return ChatStream(
222
+ span,
223
+ response,
224
+ instance,
225
+ token_counter,
226
+ choice_counter,
227
+ duration_histogram,
228
+ streaming_time_to_first_token,
229
+ streaming_time_to_generate,
230
+ start_time,
231
+ kwargs,
232
+ )
233
+ else:
234
+ return _abuild_from_streaming_response(
235
+ span,
236
+ response,
237
+ instance,
238
+ token_counter,
239
+ choice_counter,
240
+ duration_histogram,
241
+ streaming_time_to_first_token,
242
+ streaming_time_to_generate,
243
+ start_time,
244
+ kwargs,
245
+ )
255
246
 
256
- span.end()
247
+ duration = end_time - start_time
257
248
 
258
- return response
249
+ _handle_response(
250
+ response,
251
+ span,
252
+ instance,
253
+ token_counter,
254
+ choice_counter,
255
+ duration_histogram,
256
+ duration,
257
+ )
258
+
259
+ span.end()
260
+
261
+ return response
259
262
 
260
263
 
261
264
  @dont_throw
@@ -961,8 +964,10 @@ async def _abuild_from_streaming_response(
961
964
  span.end()
962
965
 
963
966
 
967
+ # pydantic.BaseModel here is ChatCompletionMessageFunctionToolCall (as of openai 1.99.7)
968
+ # but we keep to a parent type to support older versions
964
969
  def _parse_tool_calls(
965
- tool_calls: Optional[List[Union[dict, ChatCompletionMessageToolCall]]],
970
+ tool_calls: Optional[List[Union[dict, pydantic.BaseModel]]],
966
971
  ) -> Union[List[ToolCall], None]:
967
972
  """
968
973
  Util to correctly parse the tool calls data from the OpenAI API to this module's
@@ -976,12 +981,11 @@ def _parse_tool_calls(
976
981
  for tool_call in tool_calls:
977
982
  tool_call_data = None
978
983
 
979
- # Handle dict or ChatCompletionMessageToolCall
980
984
  if isinstance(tool_call, dict):
981
985
  tool_call_data = copy.deepcopy(tool_call)
982
- elif isinstance(tool_call, ChatCompletionMessageToolCall):
986
+ elif _is_chat_message_function_tool_call(tool_call):
983
987
  tool_call_data = tool_call.model_dump()
984
- elif isinstance(tool_call, FunctionCall):
988
+ elif _is_function_call(tool_call):
985
989
  function_call = tool_call.model_dump()
986
990
  tool_call_data = ToolCall(
987
991
  id="",
@@ -996,6 +1000,34 @@ def _parse_tool_calls(
996
1000
  return result
997
1001
 
998
1002
 
1003
+ def _is_chat_message_function_tool_call(model: Union[dict, pydantic.BaseModel]) -> bool:
1004
+ try:
1005
+ from openai.types.chat.chat_completion_message_function_tool_call import (
1006
+ ChatCompletionMessageFunctionToolCall,
1007
+ )
1008
+
1009
+ return isinstance(model, ChatCompletionMessageFunctionToolCall)
1010
+ except Exception:
1011
+ try:
1012
+ # Since OpenAI 1.99.3, ChatCompletionMessageToolCall is a Union,
1013
+ # and the isinstance check will fail. This is fine, because in all
1014
+ # those versions, the check above will succeed.
1015
+ from openai.types.chat.chat_completion_message_tool_call import (
1016
+ ChatCompletionMessageToolCall,
1017
+ )
1018
+ return isinstance(model, ChatCompletionMessageToolCall)
1019
+ except Exception:
1020
+ return False
1021
+
1022
+
1023
+ def _is_function_call(model: Union[dict, pydantic.BaseModel]) -> bool:
1024
+ try:
1025
+ from openai.types.chat.chat_completion_message import FunctionCall
1026
+ return isinstance(model, FunctionCall)
1027
+ except Exception:
1028
+ return False
1029
+
1030
+
999
1031
  @singledispatch
1000
1032
  def _parse_choice_event(choice) -> ChoiceEvent:
1001
1033
  has_message = choice.message is not None
@@ -1,6 +1,7 @@
1
1
  import logging
2
2
 
3
3
  from opentelemetry import context as context_api
4
+ from opentelemetry import trace
4
5
  from opentelemetry.instrumentation.openai.shared import (
5
6
  _set_client_attributes,
6
7
  _set_functions_attributes,
@@ -55,25 +56,27 @@ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
55
56
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
56
57
  )
57
58
 
58
- _handle_request(span, kwargs, instance)
59
+ # Use the span as current context to ensure events get proper trace context
60
+ with trace.use_span(span, end_on_exit=False):
61
+ _handle_request(span, kwargs, instance)
62
+
63
+ try:
64
+ response = wrapped(*args, **kwargs)
65
+ except Exception as e:
66
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
67
+ span.record_exception(e)
68
+ span.set_status(Status(StatusCode.ERROR, str(e)))
69
+ span.end()
70
+ raise
71
+
72
+ if is_streaming_response(response):
73
+ # span will be closed after the generator is done
74
+ return _build_from_streaming_response(span, kwargs, response)
75
+ else:
76
+ _handle_response(response, span, instance)
59
77
 
60
- try:
61
- response = wrapped(*args, **kwargs)
62
- except Exception as e:
63
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
64
- span.record_exception(e)
65
- span.set_status(Status(StatusCode.ERROR, str(e)))
66
78
  span.end()
67
- raise
68
-
69
- if is_streaming_response(response):
70
- # span will be closed after the generator is done
71
- return _build_from_streaming_response(span, kwargs, response)
72
- else:
73
- _handle_response(response, span, instance)
74
-
75
- span.end()
76
- return response
79
+ return response
77
80
 
78
81
 
79
82
  @_with_tracer_wrapper
@@ -89,25 +92,27 @@ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
89
92
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
90
93
  )
91
94
 
92
- _handle_request(span, kwargs, instance)
95
+ # Use the span as current context to ensure events get proper trace context
96
+ with trace.use_span(span, end_on_exit=False):
97
+ _handle_request(span, kwargs, instance)
98
+
99
+ try:
100
+ response = await wrapped(*args, **kwargs)
101
+ except Exception as e:
102
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
103
+ span.record_exception(e)
104
+ span.set_status(Status(StatusCode.ERROR, str(e)))
105
+ span.end()
106
+ raise
107
+
108
+ if is_streaming_response(response):
109
+ # span will be closed after the generator is done
110
+ return _abuild_from_streaming_response(span, kwargs, response)
111
+ else:
112
+ _handle_response(response, span, instance)
93
113
 
94
- try:
95
- response = await wrapped(*args, **kwargs)
96
- except Exception as e:
97
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
98
- span.record_exception(e)
99
- span.set_status(Status(StatusCode.ERROR, str(e)))
100
114
  span.end()
101
- raise
102
-
103
- if is_streaming_response(response):
104
- # span will be closed after the generator is done
105
- return _abuild_from_streaming_response(span, kwargs, response)
106
- else:
107
- _handle_response(response, span, instance)
108
-
109
- span.end()
110
- return response
115
+ return response
111
116
 
112
117
 
113
118
  @dont_throw
@@ -2,6 +2,7 @@ import logging
2
2
  import time
3
3
 
4
4
  from opentelemetry import context as context_api
5
+ from opentelemetry import trace
5
6
  from opentelemetry.instrumentation.openai.shared import (
6
7
  _set_span_attribute,
7
8
  model_as_dict,
@@ -127,110 +128,115 @@ def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
127
128
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
128
129
  start_time=run.get("start_time"),
129
130
  )
130
- if exception := run.get("exception"):
131
- span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
132
- span.record_exception(exception)
133
- span.set_status(Status(StatusCode.ERROR, str(exception)))
134
- span.end(run.get("end_time"))
135
-
136
- prompt_index = 0
137
- if assistants.get(run["assistant_id"]) is not None or Config.enrich_assistant:
138
- if Config.enrich_assistant:
139
- assistant = model_as_dict(
140
- instance._client.beta.assistants.retrieve(run["assistant_id"])
141
- )
142
- assistants[run["assistant_id"]] = assistant
143
- else:
144
- assistant = assistants[run["assistant_id"]]
145
131
 
146
- _set_span_attribute(
147
- span,
148
- SpanAttributes.LLM_SYSTEM,
149
- "openai",
150
- )
151
- _set_span_attribute(
152
- span,
153
- SpanAttributes.LLM_REQUEST_MODEL,
154
- assistant["model"],
155
- )
156
- _set_span_attribute(
157
- span,
158
- SpanAttributes.LLM_RESPONSE_MODEL,
159
- assistant["model"],
160
- )
161
- if should_emit_events():
162
- emit_event(MessageEvent(content=assistant["instructions"], role="system"))
163
- else:
132
+ # Use the span as current context to ensure events get proper trace context
133
+ with trace.use_span(span, end_on_exit=False):
134
+ if exception := run.get("exception"):
135
+ span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
136
+ span.record_exception(exception)
137
+ span.set_status(Status(StatusCode.ERROR, str(exception)))
138
+ span.end()
139
+ return response
140
+
141
+ prompt_index = 0
142
+ if assistants.get(run["assistant_id"]) is not None or Config.enrich_assistant:
143
+ if Config.enrich_assistant:
144
+ assistant = model_as_dict(
145
+ instance._client.beta.assistants.retrieve(run["assistant_id"])
146
+ )
147
+ assistants[run["assistant_id"]] = assistant
148
+ else:
149
+ assistant = assistants[run["assistant_id"]]
150
+
164
151
  _set_span_attribute(
165
- span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
152
+ span,
153
+ SpanAttributes.LLM_SYSTEM,
154
+ "openai",
166
155
  )
167
156
  _set_span_attribute(
168
157
  span,
169
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
170
- assistant["instructions"],
158
+ SpanAttributes.LLM_REQUEST_MODEL,
159
+ assistant["model"],
160
+ )
161
+ _set_span_attribute(
162
+ span,
163
+ SpanAttributes.LLM_RESPONSE_MODEL,
164
+ assistant["model"],
171
165
  )
172
- prompt_index += 1
173
- _set_span_attribute(
174
- span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
175
- )
176
- _set_span_attribute(
177
- span,
178
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
179
- run["instructions"],
180
- )
181
- emit_event(MessageEvent(content=run["instructions"], role="system"))
182
- prompt_index += 1
183
-
184
- completion_index = 0
185
- for msg in messages:
186
- prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{completion_index}"
187
- content = msg.get("content")
188
-
189
- message_content = content[0].get("text").get("value")
190
- message_role = msg.get("role")
191
- if message_role in ["user", "system"]:
192
166
  if should_emit_events():
193
- emit_event(MessageEvent(content=message_content, role=message_role))
167
+ emit_event(MessageEvent(content=assistant["instructions"], role="system"))
194
168
  else:
195
169
  _set_span_attribute(
196
- span,
197
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
198
- message_role,
170
+ span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
199
171
  )
200
172
  _set_span_attribute(
201
173
  span,
202
174
  f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
203
- message_content,
175
+ assistant["instructions"],
204
176
  )
205
177
  prompt_index += 1
206
- else:
207
- if should_emit_events():
208
- emit_event(
209
- ChoiceEvent(
210
- index=completion_index,
211
- message={"content": message_content, "role": message_role},
212
- )
213
- )
214
- else:
215
- _set_span_attribute(span, f"{prefix}.role", msg.get("role"))
216
- _set_span_attribute(span, f"{prefix}.content", message_content)
217
- _set_span_attribute(
218
- span, f"gen_ai.response.{completion_index}.id", msg.get("id")
219
- )
220
- completion_index += 1
221
-
222
- if run.get("usage"):
223
- usage_dict = model_as_dict(run.get("usage"))
224
178
  _set_span_attribute(
225
- span,
226
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
227
- usage_dict.get("completion_tokens"),
179
+ span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
228
180
  )
229
181
  _set_span_attribute(
230
182
  span,
231
- SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
232
- usage_dict.get("prompt_tokens"),
183
+ f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
184
+ run["instructions"],
233
185
  )
186
+ if should_emit_events():
187
+ emit_event(MessageEvent(content=run["instructions"], role="system"))
188
+ prompt_index += 1
189
+
190
+ completion_index = 0
191
+ for msg in messages:
192
+ prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{completion_index}"
193
+ content = msg.get("content")
194
+
195
+ message_content = content[0].get("text").get("value")
196
+ message_role = msg.get("role")
197
+ if message_role in ["user", "system"]:
198
+ if should_emit_events():
199
+ emit_event(MessageEvent(content=message_content, role=message_role))
200
+ else:
201
+ _set_span_attribute(
202
+ span,
203
+ f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
204
+ message_role,
205
+ )
206
+ _set_span_attribute(
207
+ span,
208
+ f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
209
+ message_content,
210
+ )
211
+ prompt_index += 1
212
+ else:
213
+ if should_emit_events():
214
+ emit_event(
215
+ ChoiceEvent(
216
+ index=completion_index,
217
+ message={"content": message_content, "role": message_role},
218
+ )
219
+ )
220
+ else:
221
+ _set_span_attribute(span, f"{prefix}.role", msg.get("role"))
222
+ _set_span_attribute(span, f"{prefix}.content", message_content)
223
+ _set_span_attribute(
224
+ span, f"gen_ai.response.{completion_index}.id", msg.get("id")
225
+ )
226
+ completion_index += 1
227
+
228
+ if run.get("usage"):
229
+ usage_dict = model_as_dict(run.get("usage"))
230
+ _set_span_attribute(
231
+ span,
232
+ SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
233
+ usage_dict.get("completion_tokens"),
234
+ )
235
+ _set_span_attribute(
236
+ span,
237
+ SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
238
+ usage_dict.get("prompt_tokens"),
239
+ )
234
240
 
235
241
  span.end(run.get("end_time"))
236
242
 
@@ -251,68 +257,70 @@ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
251
257
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
252
258
  )
253
259
 
254
- i = 0
255
- if assistants.get(assistant_id) is not None or Config.enrich_assistant:
256
- if Config.enrich_assistant:
257
- assistant = model_as_dict(
258
- instance._client.beta.assistants.retrieve(assistant_id)
259
- )
260
- assistants[assistant_id] = assistant
261
- else:
262
- assistant = assistants[assistant_id]
263
-
264
- _set_span_attribute(
265
- span, SpanAttributes.LLM_REQUEST_MODEL, assistants[assistant_id]["model"]
266
- )
267
- _set_span_attribute(
268
- span,
269
- SpanAttributes.LLM_SYSTEM,
270
- "openai",
271
- )
272
- _set_span_attribute(
273
- span,
274
- SpanAttributes.LLM_RESPONSE_MODEL,
275
- assistants[assistant_id]["model"],
276
- )
277
- if should_emit_events():
278
- emit_event(
279
- MessageEvent(
280
- content=assistants[assistant_id]["instructions"], role="system"
260
+ # Use the span as current context to ensure events get proper trace context
261
+ with trace.use_span(span, end_on_exit=False):
262
+ i = 0
263
+ if assistants.get(assistant_id) is not None or Config.enrich_assistant:
264
+ if Config.enrich_assistant:
265
+ assistant = model_as_dict(
266
+ instance._client.beta.assistants.retrieve(assistant_id)
281
267
  )
268
+ assistants[assistant_id] = assistant
269
+ else:
270
+ assistant = assistants[assistant_id]
271
+
272
+ _set_span_attribute(
273
+ span, SpanAttributes.LLM_REQUEST_MODEL, assistants[assistant_id]["model"]
282
274
  )
283
- else:
284
275
  _set_span_attribute(
285
- span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", "system"
276
+ span,
277
+ SpanAttributes.LLM_SYSTEM,
278
+ "openai",
286
279
  )
287
280
  _set_span_attribute(
288
281
  span,
289
- f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
290
- assistants[assistant_id]["instructions"],
282
+ SpanAttributes.LLM_RESPONSE_MODEL,
283
+ assistants[assistant_id]["model"],
284
+ )
285
+ if should_emit_events():
286
+ emit_event(
287
+ MessageEvent(
288
+ content=assistants[assistant_id]["instructions"], role="system"
289
+ )
290
+ )
291
+ else:
292
+ _set_span_attribute(
293
+ span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", "system"
294
+ )
295
+ _set_span_attribute(
296
+ span,
297
+ f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
298
+ assistants[assistant_id]["instructions"],
299
+ )
300
+ i += 1
301
+ if should_emit_events():
302
+ emit_event(MessageEvent(content=instructions, role="system"))
303
+ else:
304
+ _set_span_attribute(span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", "system")
305
+ _set_span_attribute(
306
+ span, f"{SpanAttributes.LLM_PROMPTS}.{i}.content", instructions
291
307
  )
292
- i += 1
293
- if should_emit_events():
294
- emit_event(MessageEvent(content=instructions, role="system"))
295
- else:
296
- _set_span_attribute(span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", "system")
297
- _set_span_attribute(
298
- span, f"{SpanAttributes.LLM_PROMPTS}.{i}.content", instructions
299
- )
300
308
 
301
- from opentelemetry.instrumentation.openai.v1.event_handler_wrapper import (
302
- EventHandleWrapper,
303
- )
309
+ from opentelemetry.instrumentation.openai.v1.event_handler_wrapper import (
310
+ EventHandleWrapper,
311
+ )
304
312
 
305
- kwargs["event_handler"] = EventHandleWrapper(
306
- original_handler=kwargs["event_handler"],
307
- span=span,
308
- )
313
+ kwargs["event_handler"] = EventHandleWrapper(
314
+ original_handler=kwargs["event_handler"],
315
+ span=span,
316
+ )
309
317
 
310
- try:
311
- response = wrapped(*args, **kwargs)
312
- return response
313
- except Exception as e:
314
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
315
- span.record_exception(e)
316
- span.set_status(Status(StatusCode.ERROR, str(e)))
317
- span.end()
318
- raise
318
+ try:
319
+ response = wrapped(*args, **kwargs)
320
+ return response
321
+ except Exception as e:
322
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
323
+ span.record_exception(e)
324
+ span.set_status(Status(StatusCode.ERROR, str(e)))
325
+ span.end()
326
+ raise
@@ -447,6 +447,14 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
447
447
  merged_tools = existing_data.get("tools", []) + request_tools
448
448
 
449
449
  try:
450
+ parsed_response_output_text = None
451
+ if hasattr(parsed_response, "output_text"):
452
+ parsed_response_output_text = parsed_response.output_text
453
+ else:
454
+ try:
455
+ parsed_response_output_text = parsed_response.output[0].content[0].text
456
+ except Exception:
457
+ pass
450
458
  traced_data = TracedData(
451
459
  start_time=existing_data.get("start_time", start_time),
452
460
  response_id=parsed_response.id,
@@ -456,7 +464,7 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
456
464
  output_blocks={block.id: block for block in parsed_response.output}
457
465
  | existing_data.get("output_blocks", {}),
458
466
  usage=existing_data.get("usage", parsed_response.usage),
459
- output_text=existing_data.get("output_text", parsed_response.output_text),
467
+ output_text=existing_data.get("output_text", parsed_response_output_text),
460
468
  request_model=existing_data.get("request_model", kwargs.get("model")),
461
469
  response_model=existing_data.get("response_model", parsed_response.model),
462
470
  )
@@ -541,6 +549,15 @@ async def async_responses_get_or_create_wrapper(
541
549
  merged_tools = existing_data.get("tools", []) + request_tools
542
550
 
543
551
  try:
552
+ parsed_response_output_text = None
553
+ if hasattr(parsed_response, "output_text"):
554
+ parsed_response_output_text = parsed_response.output_text
555
+ else:
556
+ try:
557
+ parsed_response_output_text = parsed_response.output[0].content[0].text
558
+ except Exception:
559
+ pass
560
+
544
561
  traced_data = TracedData(
545
562
  start_time=existing_data.get("start_time", start_time),
546
563
  response_id=parsed_response.id,
@@ -550,7 +567,7 @@ async def async_responses_get_or_create_wrapper(
550
567
  output_blocks={block.id: block for block in parsed_response.output}
551
568
  | existing_data.get("output_blocks", {}),
552
569
  usage=existing_data.get("usage", parsed_response.usage),
553
- output_text=existing_data.get("output_text", parsed_response.output_text),
570
+ output_text=existing_data.get("output_text", parsed_response_output_text),
554
571
  request_model=existing_data.get("request_model", kwargs.get("model")),
555
572
  response_model=existing_data.get("response_model", parsed_response.model),
556
573
  )
@@ -1 +1 @@
1
- __version__ = "0.44.1"
1
+ __version__ = "0.44.3"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opentelemetry-instrumentation-openai
3
- Version: 0.44.1
3
+ Version: 0.44.3
4
4
  Summary: OpenTelemetry OpenAI instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -1,7 +1,7 @@
1
1
  opentelemetry/instrumentation/openai/__init__.py,sha256=Mx_nwMl0TlhUjrQOR4qdx6MEhBUKp5cuUIIXFzi3mXo,2093
2
2
  opentelemetry/instrumentation/openai/shared/__init__.py,sha256=Ba429tv5NPuQN7RoLzaj00K9oj88BaUBdPmUUsZ-7ic,12346
3
- opentelemetry/instrumentation/openai/shared/chat_wrappers.py,sha256=SH_IJMOIE1uqpn5f2-mlqdBBH_YvJqLG9p5amJSWTm0,37517
4
- opentelemetry/instrumentation/openai/shared/completion_wrappers.py,sha256=LH90ZPytXD4_yXAfRkw7N7BimkVCvMzcuMCYu0RdNVo,8824
3
+ opentelemetry/instrumentation/openai/shared/chat_wrappers.py,sha256=whdVqpTBFyWXITuY2pJYzVx1PF7kt5pQXvG7Z0nZV8Y,39325
4
+ opentelemetry/instrumentation/openai/shared/completion_wrappers.py,sha256=600McQXNCPFaifeD4yFq00beZ9XjGEbYT_3XVojHQT4,9244
5
5
  opentelemetry/instrumentation/openai/shared/config.py,sha256=nQfVXiznVUIv2_BHSUQpaoCnxysG3XpaYpIZdxi0mxM,477
6
6
  opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py,sha256=oRHghd4vSDJ6fNHjL9G8QfKnPnp_NkZfVmTDSgZScVU,9251
7
7
  opentelemetry/instrumentation/openai/shared/event_emitter.py,sha256=iXUoyEHbC9DqwjnudkPOZlDBC4qLdEBmfsxB_q0nzbQ,3113
@@ -11,11 +11,11 @@ opentelemetry/instrumentation/openai/shared/span_utils.py,sha256=47DEQpj8HBSa-_T
11
11
  opentelemetry/instrumentation/openai/utils.py,sha256=-0ugLRCR50v25KncuOq4tXHHPzdsH5PjS4Qd_8PP0TQ,4684
12
12
  opentelemetry/instrumentation/openai/v0/__init__.py,sha256=FhpVbP8NqjN2We_srppZ_U-0-Vbk-A15VSQp3zUnW3k,6353
13
13
  opentelemetry/instrumentation/openai/v1/__init__.py,sha256=oLst4xav77tTteZKXo59uyb-2IWqw_xOafaSMzTxq9g,13255
14
- opentelemetry/instrumentation/openai/v1/assistant_wrappers.py,sha256=ZBbJHgsY3_OuTb9n-WN-XGK4xsH4b2zy6fO6IkElxhk,10318
14
+ opentelemetry/instrumentation/openai/v1/assistant_wrappers.py,sha256=oa5xYEDELFN9luvSn3y1xhSs37yRYY_Pwh6htqOs8gc,11297
15
15
  opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py,sha256=AT-eDZOFP-K_mm-ecdgQaILoIsEiItZmtwzwAuse86Q,4350
16
- opentelemetry/instrumentation/openai/v1/responses_wrappers.py,sha256=YQt2fdsdyJXczfQ5rqkOJ6CJtvF_Q4be0yX6hYpfdno,23800
17
- opentelemetry/instrumentation/openai/version.py,sha256=ZDD-b1AJzaso91jggbcesxnf2F7iAueyqfbMaJ4TrtA,23
18
- opentelemetry_instrumentation_openai-0.44.1.dist-info/METADATA,sha256=EGPJJxdzR5cjMZ4_V4ittCWDEi1WrPajSkxS4-gpSko,2150
19
- opentelemetry_instrumentation_openai-0.44.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
20
- opentelemetry_instrumentation_openai-0.44.1.dist-info/entry_points.txt,sha256=vTBfiX5yXji5YHikuJHEOoBZ1TFdPQ1EI4ctd2pZSeE,93
21
- opentelemetry_instrumentation_openai-0.44.1.dist-info/RECORD,,
16
+ opentelemetry/instrumentation/openai/v1/responses_wrappers.py,sha256=NSty_lrL5HJVt88d_keV-wQ17-4XGVzc9ukMLaITAug,24471
17
+ opentelemetry/instrumentation/openai/version.py,sha256=Fgvc0TeIXcwh7WOr-X3Z00Z33fH6ATWZZONRmU5RM7c,23
18
+ opentelemetry_instrumentation_openai-0.44.3.dist-info/METADATA,sha256=y4pHNuysf2K7XMuErYnP_aDV67b9fMptG-qq5Un6yf4,2150
19
+ opentelemetry_instrumentation_openai-0.44.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
20
+ opentelemetry_instrumentation_openai-0.44.3.dist-info/entry_points.txt,sha256=vTBfiX5yXji5YHikuJHEOoBZ1TFdPQ1EI4ctd2pZSeE,93
21
+ opentelemetry_instrumentation_openai-0.44.3.dist-info/RECORD,,