opentelemetry-instrumentation-openai 0.44.1__tar.gz → 0.44.3__tar.gz

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.
Files changed (22) hide show
  1. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/PKG-INFO +1 -1
  2. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +167 -135
  3. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +39 -34
  4. opentelemetry_instrumentation_openai-0.44.3/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +326 -0
  5. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +19 -2
  6. opentelemetry_instrumentation_openai-0.44.3/opentelemetry/instrumentation/openai/version.py +1 -0
  7. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/pyproject.toml +2 -2
  8. opentelemetry_instrumentation_openai-0.44.1/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -318
  9. opentelemetry_instrumentation_openai-0.44.1/opentelemetry/instrumentation/openai/version.py +0 -1
  10. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/README.md +0 -0
  11. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/__init__.py +0 -0
  12. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/__init__.py +0 -0
  13. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/config.py +0 -0
  14. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +0 -0
  15. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/event_emitter.py +0 -0
  16. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/event_models.py +0 -0
  17. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +0 -0
  18. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
  19. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/utils.py +0 -0
  20. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v0/__init__.py +0 -0
  21. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/__init__.py +0 -0
  22. {opentelemetry_instrumentation_openai-0.44.1 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +0 -0
@@ -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
@@ -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
@@ -0,0 +1,326 @@
1
+ import logging
2
+ import time
3
+
4
+ from opentelemetry import context as context_api
5
+ from opentelemetry import trace
6
+ from opentelemetry.instrumentation.openai.shared import (
7
+ _set_span_attribute,
8
+ model_as_dict,
9
+ )
10
+ from opentelemetry.instrumentation.openai.shared.config import Config
11
+ from opentelemetry.instrumentation.openai.shared.event_emitter import emit_event
12
+ from opentelemetry.instrumentation.openai.shared.event_models import (
13
+ ChoiceEvent,
14
+ MessageEvent,
15
+ )
16
+ from opentelemetry.instrumentation.openai.utils import (
17
+ _with_tracer_wrapper,
18
+ dont_throw,
19
+ should_emit_events,
20
+ )
21
+ from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
22
+ from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
23
+ from opentelemetry.semconv_ai import LLMRequestTypeValues, SpanAttributes
24
+ from opentelemetry.trace import SpanKind, Status, StatusCode
25
+
26
+ from openai._legacy_response import LegacyAPIResponse
27
+ from openai.types.beta.threads.run import Run
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ assistants = {}
32
+ runs = {}
33
+
34
+
35
+ @_with_tracer_wrapper
36
+ def assistants_create_wrapper(tracer, wrapped, instance, args, kwargs):
37
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
38
+ return wrapped(*args, **kwargs)
39
+
40
+ response = wrapped(*args, **kwargs)
41
+
42
+ assistants[response.id] = {
43
+ "model": kwargs.get("model"),
44
+ "instructions": kwargs.get("instructions"),
45
+ }
46
+
47
+ return response
48
+
49
+
50
+ @_with_tracer_wrapper
51
+ def runs_create_wrapper(tracer, wrapped, instance, args, kwargs):
52
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
53
+ return wrapped(*args, **kwargs)
54
+
55
+ thread_id = kwargs.get("thread_id")
56
+ instructions = kwargs.get("instructions")
57
+
58
+ try:
59
+ response = wrapped(*args, **kwargs)
60
+ response_dict = model_as_dict(response)
61
+
62
+ runs[thread_id] = {
63
+ "start_time": time.time_ns(),
64
+ "assistant_id": kwargs.get("assistant_id"),
65
+ "instructions": instructions,
66
+ "run_id": response_dict.get("id"),
67
+ }
68
+
69
+ return response
70
+ except Exception as e:
71
+ runs[thread_id] = {
72
+ "exception": e,
73
+ "end_time": time.time_ns(),
74
+ }
75
+ raise
76
+
77
+
78
+ @_with_tracer_wrapper
79
+ def runs_retrieve_wrapper(tracer, wrapped, instance, args, kwargs):
80
+ @dont_throw
81
+ def process_response(response):
82
+ if type(response) is LegacyAPIResponse:
83
+ parsed_response = response.parse()
84
+ else:
85
+ parsed_response = response
86
+ assert type(parsed_response) is Run
87
+
88
+ if parsed_response.thread_id in runs:
89
+ thread_id = parsed_response.thread_id
90
+ runs[thread_id]["end_time"] = time.time_ns()
91
+ if parsed_response.usage:
92
+ runs[thread_id]["usage"] = parsed_response.usage
93
+
94
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
95
+ return wrapped(*args, **kwargs)
96
+
97
+ try:
98
+ response = wrapped(*args, **kwargs)
99
+ process_response(response)
100
+ return response
101
+ except Exception as e:
102
+ thread_id = kwargs.get("thread_id")
103
+ if thread_id in runs:
104
+ runs[thread_id]["exception"] = e
105
+ runs[thread_id]["end_time"] = time.time_ns()
106
+ raise
107
+
108
+
109
+ @_with_tracer_wrapper
110
+ def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
111
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
112
+ return wrapped(*args, **kwargs)
113
+
114
+ id = kwargs.get("thread_id")
115
+
116
+ response = wrapped(*args, **kwargs)
117
+
118
+ response_dict = model_as_dict(response)
119
+ if id not in runs:
120
+ return response
121
+
122
+ run = runs[id]
123
+ messages = sorted(response_dict["data"], key=lambda x: x["created_at"])
124
+
125
+ span = tracer.start_span(
126
+ "openai.assistant.run",
127
+ kind=SpanKind.CLIENT,
128
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
129
+ start_time=run.get("start_time"),
130
+ )
131
+
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
+
151
+ _set_span_attribute(
152
+ span,
153
+ SpanAttributes.LLM_SYSTEM,
154
+ "openai",
155
+ )
156
+ _set_span_attribute(
157
+ span,
158
+ SpanAttributes.LLM_REQUEST_MODEL,
159
+ assistant["model"],
160
+ )
161
+ _set_span_attribute(
162
+ span,
163
+ SpanAttributes.LLM_RESPONSE_MODEL,
164
+ assistant["model"],
165
+ )
166
+ if should_emit_events():
167
+ emit_event(MessageEvent(content=assistant["instructions"], role="system"))
168
+ else:
169
+ _set_span_attribute(
170
+ span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
171
+ )
172
+ _set_span_attribute(
173
+ span,
174
+ f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
175
+ assistant["instructions"],
176
+ )
177
+ prompt_index += 1
178
+ _set_span_attribute(
179
+ span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
180
+ )
181
+ _set_span_attribute(
182
+ span,
183
+ f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
184
+ run["instructions"],
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
+ )
240
+
241
+ span.end(run.get("end_time"))
242
+
243
+ return response
244
+
245
+
246
+ @_with_tracer_wrapper
247
+ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
248
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
249
+ return wrapped(*args, **kwargs)
250
+
251
+ assistant_id = kwargs.get("assistant_id")
252
+ instructions = kwargs.get("instructions")
253
+
254
+ span = tracer.start_span(
255
+ "openai.assistant.run_stream",
256
+ kind=SpanKind.CLIENT,
257
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
258
+ )
259
+
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)
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"]
274
+ )
275
+ _set_span_attribute(
276
+ span,
277
+ SpanAttributes.LLM_SYSTEM,
278
+ "openai",
279
+ )
280
+ _set_span_attribute(
281
+ span,
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
307
+ )
308
+
309
+ from opentelemetry.instrumentation.openai.v1.event_handler_wrapper import (
310
+ EventHandleWrapper,
311
+ )
312
+
313
+ kwargs["event_handler"] = EventHandleWrapper(
314
+ original_handler=kwargs["event_handler"],
315
+ span=span,
316
+ )
317
+
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
  )
@@ -8,7 +8,7 @@ show_missing = true
8
8
 
9
9
  [tool.poetry]
10
10
  name = "opentelemetry-instrumentation-openai"
11
- version = "0.44.1"
11
+ version = "0.44.3"
12
12
  description = "OpenTelemetry OpenAI instrumentation"
13
13
  authors = [
14
14
  "Gal Kleinman <gal@traceloop.com>",
@@ -38,7 +38,7 @@ pytest = "^8.2.2"
38
38
  pytest-sugar = "1.0.0"
39
39
  vcrpy = "^6.0.1"
40
40
  pytest-recording = "^0.13.1"
41
- openai = { extras = ["datalib"], version = ">=1.66.0" }
41
+ openai = { extras = ["datalib"], version = "1.99.7" }
42
42
  opentelemetry-sdk = "^1.27.0"
43
43
  pytest-asyncio = "^0.23.7"
44
44
  requests = "^2.31.0"
@@ -1,318 +0,0 @@
1
- import logging
2
- import time
3
-
4
- from opentelemetry import context as context_api
5
- from opentelemetry.instrumentation.openai.shared import (
6
- _set_span_attribute,
7
- model_as_dict,
8
- )
9
- from opentelemetry.instrumentation.openai.shared.config import Config
10
- from opentelemetry.instrumentation.openai.shared.event_emitter import emit_event
11
- from opentelemetry.instrumentation.openai.shared.event_models import (
12
- ChoiceEvent,
13
- MessageEvent,
14
- )
15
- from opentelemetry.instrumentation.openai.utils import (
16
- _with_tracer_wrapper,
17
- dont_throw,
18
- should_emit_events,
19
- )
20
- from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
21
- from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
22
- from opentelemetry.semconv_ai import LLMRequestTypeValues, SpanAttributes
23
- from opentelemetry.trace import SpanKind, Status, StatusCode
24
-
25
- from openai._legacy_response import LegacyAPIResponse
26
- from openai.types.beta.threads.run import Run
27
-
28
- logger = logging.getLogger(__name__)
29
-
30
- assistants = {}
31
- runs = {}
32
-
33
-
34
- @_with_tracer_wrapper
35
- def assistants_create_wrapper(tracer, wrapped, instance, args, kwargs):
36
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
37
- return wrapped(*args, **kwargs)
38
-
39
- response = wrapped(*args, **kwargs)
40
-
41
- assistants[response.id] = {
42
- "model": kwargs.get("model"),
43
- "instructions": kwargs.get("instructions"),
44
- }
45
-
46
- return response
47
-
48
-
49
- @_with_tracer_wrapper
50
- def runs_create_wrapper(tracer, wrapped, instance, args, kwargs):
51
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
52
- return wrapped(*args, **kwargs)
53
-
54
- thread_id = kwargs.get("thread_id")
55
- instructions = kwargs.get("instructions")
56
-
57
- try:
58
- response = wrapped(*args, **kwargs)
59
- response_dict = model_as_dict(response)
60
-
61
- runs[thread_id] = {
62
- "start_time": time.time_ns(),
63
- "assistant_id": kwargs.get("assistant_id"),
64
- "instructions": instructions,
65
- "run_id": response_dict.get("id"),
66
- }
67
-
68
- return response
69
- except Exception as e:
70
- runs[thread_id] = {
71
- "exception": e,
72
- "end_time": time.time_ns(),
73
- }
74
- raise
75
-
76
-
77
- @_with_tracer_wrapper
78
- def runs_retrieve_wrapper(tracer, wrapped, instance, args, kwargs):
79
- @dont_throw
80
- def process_response(response):
81
- if type(response) is LegacyAPIResponse:
82
- parsed_response = response.parse()
83
- else:
84
- parsed_response = response
85
- assert type(parsed_response) is Run
86
-
87
- if parsed_response.thread_id in runs:
88
- thread_id = parsed_response.thread_id
89
- runs[thread_id]["end_time"] = time.time_ns()
90
- if parsed_response.usage:
91
- runs[thread_id]["usage"] = parsed_response.usage
92
-
93
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
94
- return wrapped(*args, **kwargs)
95
-
96
- try:
97
- response = wrapped(*args, **kwargs)
98
- process_response(response)
99
- return response
100
- except Exception as e:
101
- thread_id = kwargs.get("thread_id")
102
- if thread_id in runs:
103
- runs[thread_id]["exception"] = e
104
- runs[thread_id]["end_time"] = time.time_ns()
105
- raise
106
-
107
-
108
- @_with_tracer_wrapper
109
- def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
110
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
111
- return wrapped(*args, **kwargs)
112
-
113
- id = kwargs.get("thread_id")
114
-
115
- response = wrapped(*args, **kwargs)
116
-
117
- response_dict = model_as_dict(response)
118
- if id not in runs:
119
- return response
120
-
121
- run = runs[id]
122
- messages = sorted(response_dict["data"], key=lambda x: x["created_at"])
123
-
124
- span = tracer.start_span(
125
- "openai.assistant.run",
126
- kind=SpanKind.CLIENT,
127
- attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
128
- start_time=run.get("start_time"),
129
- )
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
-
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:
164
- _set_span_attribute(
165
- span, f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role", "system"
166
- )
167
- _set_span_attribute(
168
- span,
169
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
170
- assistant["instructions"],
171
- )
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
- if should_emit_events():
193
- emit_event(MessageEvent(content=message_content, role=message_role))
194
- else:
195
- _set_span_attribute(
196
- span,
197
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.role",
198
- message_role,
199
- )
200
- _set_span_attribute(
201
- span,
202
- f"{SpanAttributes.LLM_PROMPTS}.{prompt_index}.content",
203
- message_content,
204
- )
205
- 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
- _set_span_attribute(
225
- span,
226
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
227
- usage_dict.get("completion_tokens"),
228
- )
229
- _set_span_attribute(
230
- span,
231
- SpanAttributes.LLM_USAGE_PROMPT_TOKENS,
232
- usage_dict.get("prompt_tokens"),
233
- )
234
-
235
- span.end(run.get("end_time"))
236
-
237
- return response
238
-
239
-
240
- @_with_tracer_wrapper
241
- def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
242
- if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
243
- return wrapped(*args, **kwargs)
244
-
245
- assistant_id = kwargs.get("assistant_id")
246
- instructions = kwargs.get("instructions")
247
-
248
- span = tracer.start_span(
249
- "openai.assistant.run_stream",
250
- kind=SpanKind.CLIENT,
251
- attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
252
- )
253
-
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"
281
- )
282
- )
283
- else:
284
- _set_span_attribute(
285
- span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", "system"
286
- )
287
- _set_span_attribute(
288
- span,
289
- f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
290
- assistants[assistant_id]["instructions"],
291
- )
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
-
301
- from opentelemetry.instrumentation.openai.v1.event_handler_wrapper import (
302
- EventHandleWrapper,
303
- )
304
-
305
- kwargs["event_handler"] = EventHandleWrapper(
306
- original_handler=kwargs["event_handler"],
307
- span=span,
308
- )
309
-
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