opentelemetry-instrumentation-openai 0.44.2__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.2 → opentelemetry_instrumentation_openai-0.44.3}/PKG-INFO +1 -1
  2. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +133 -128
  3. {opentelemetry_instrumentation_openai-0.44.2 → 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.3/opentelemetry/instrumentation/openai/version.py +1 -0
  6. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/pyproject.toml +1 -1
  7. opentelemetry_instrumentation_openai-0.44.2/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -318
  8. opentelemetry_instrumentation_openai-0.44.2/opentelemetry/instrumentation/openai/version.py +0 -1
  9. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/README.md +0 -0
  10. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/__init__.py +0 -0
  11. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/__init__.py +0 -0
  12. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/config.py +0 -0
  13. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +0 -0
  14. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/event_emitter.py +0 -0
  15. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/event_models.py +0 -0
  16. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +0 -0
  17. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
  18. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/utils.py +0 -0
  19. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v0/__init__.py +0 -0
  20. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/__init__.py +0 -0
  21. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +0 -0
  22. {opentelemetry_instrumentation_openai-0.44.2 → opentelemetry_instrumentation_openai-0.44.3}/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: opentelemetry-instrumentation-openai
3
- Version: 0.44.2
3
+ Version: 0.44.3
4
4
  Summary: OpenTelemetry OpenAI instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -48,6 +48,7 @@ from opentelemetry.semconv_ai import (
48
48
  SpanAttributes,
49
49
  )
50
50
  from opentelemetry.trace import SpanKind, Tracer
51
+ from opentelemetry import trace
51
52
  from opentelemetry.trace.status import Status, StatusCode
52
53
  from wrapt import ObjectProxy
53
54
 
@@ -86,75 +87,77 @@ def chat_wrapper(
86
87
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
87
88
  )
88
89
 
89
- run_async(_handle_request(span, kwargs, instance))
90
- try:
91
- start_time = time.time()
92
- response = wrapped(*args, **kwargs)
93
- end_time = time.time()
94
- except Exception as e: # pylint: disable=broad-except
95
- end_time = time.time()
96
- duration = end_time - start_time if "start_time" in locals() else 0
97
-
98
- attributes = {
99
- "error.type": e.__class__.__name__,
100
- }
101
-
102
- if duration > 0 and duration_histogram:
103
- duration_histogram.record(duration, attributes=attributes)
104
- if exception_counter:
105
- exception_counter.add(1, attributes=attributes)
106
-
107
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
108
- span.record_exception(e)
109
- span.set_status(Status(StatusCode.ERROR, str(e)))
110
- 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
+ }
111
104
 
112
- 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)
113
109
 
114
- if is_streaming_response(response):
115
- # span will be closed after the generator is done
116
- if is_openai_v1():
117
- return ChatStream(
118
- span,
119
- response,
120
- instance,
121
- token_counter,
122
- choice_counter,
123
- duration_histogram,
124
- streaming_time_to_first_token,
125
- streaming_time_to_generate,
126
- start_time,
127
- kwargs,
128
- )
129
- else:
130
- return _build_from_streaming_response(
131
- span,
132
- response,
133
- instance,
134
- token_counter,
135
- choice_counter,
136
- duration_histogram,
137
- streaming_time_to_first_token,
138
- streaming_time_to_generate,
139
- start_time,
140
- kwargs,
141
- )
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()
142
114
 
143
- duration = end_time - start_time
115
+ raise
144
116
 
145
- _handle_response(
146
- response,
147
- span,
148
- instance,
149
- token_counter,
150
- choice_counter,
151
- duration_histogram,
152
- duration,
153
- )
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
+ )
154
145
 
155
- span.end()
146
+ duration = end_time - start_time
156
147
 
157
- 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
158
161
 
159
162
 
160
163
  @_with_chat_telemetry_wrapper
@@ -182,78 +185,80 @@ async def achat_wrapper(
182
185
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
183
186
  )
184
187
 
185
- 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)
186
191
 
187
- try:
188
- start_time = time.time()
189
- response = await wrapped(*args, **kwargs)
190
- end_time = time.time()
191
- except Exception as e: # pylint: disable=broad-except
192
- end_time = time.time()
193
- duration = end_time - start_time if "start_time" in locals() else 0
194
-
195
- common_attributes = Config.get_common_metrics_attributes()
196
- attributes = {
197
- **common_attributes,
198
- "error.type": e.__class__.__name__,
199
- }
200
-
201
- if duration > 0 and duration_histogram:
202
- duration_histogram.record(duration, attributes=attributes)
203
- if exception_counter:
204
- exception_counter.add(1, attributes=attributes)
205
-
206
- span.set_attribute(ERROR_TYPE, e.__class__.__name__)
207
- span.record_exception(e)
208
- span.set_status(Status(StatusCode.ERROR, str(e)))
209
- 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
+ }
210
205
 
211
- 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)
212
210
 
213
- if is_streaming_response(response):
214
- # span will be closed after the generator is done
215
- if is_openai_v1():
216
- return ChatStream(
217
- span,
218
- response,
219
- instance,
220
- token_counter,
221
- choice_counter,
222
- duration_histogram,
223
- streaming_time_to_first_token,
224
- streaming_time_to_generate,
225
- start_time,
226
- kwargs,
227
- )
228
- else:
229
- return _abuild_from_streaming_response(
230
- span,
231
- response,
232
- instance,
233
- token_counter,
234
- choice_counter,
235
- duration_histogram,
236
- streaming_time_to_first_token,
237
- streaming_time_to_generate,
238
- start_time,
239
- kwargs,
240
- )
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()
241
215
 
242
- duration = end_time - start_time
216
+ raise
243
217
 
244
- _handle_response(
245
- response,
246
- span,
247
- instance,
248
- token_counter,
249
- choice_counter,
250
- duration_histogram,
251
- duration,
252
- )
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
+ )
253
246
 
254
- span.end()
247
+ duration = end_time - start_time
255
248
 
256
- 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
257
262
 
258
263
 
259
264
  @dont_throw
@@ -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
@@ -8,7 +8,7 @@ show_missing = true
8
8
 
9
9
  [tool.poetry]
10
10
  name = "opentelemetry-instrumentation-openai"
11
- version = "0.44.2"
11
+ version = "0.44.3"
12
12
  description = "OpenTelemetry OpenAI instrumentation"
13
13
  authors = [
14
14
  "Gal Kleinman <gal@traceloop.com>",
@@ -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