paid-python 0.3.4__py3-none-any.whl → 0.3.5__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.
Files changed (25) hide show
  1. paid/_vendor/__init__.py +0 -0
  2. paid/_vendor/opentelemetry/__init__.py +0 -0
  3. paid/_vendor/opentelemetry/instrumentation/__init__.py +0 -0
  4. paid/_vendor/opentelemetry/instrumentation/openai/__init__.py +54 -0
  5. paid/_vendor/opentelemetry/instrumentation/openai/shared/__init__.py +399 -0
  6. paid/_vendor/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1192 -0
  7. paid/_vendor/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +292 -0
  8. paid/_vendor/opentelemetry/instrumentation/openai/shared/config.py +15 -0
  9. paid/_vendor/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +311 -0
  10. paid/_vendor/opentelemetry/instrumentation/openai/shared/event_emitter.py +108 -0
  11. paid/_vendor/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  12. paid/_vendor/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  13. paid/_vendor/opentelemetry/instrumentation/openai/shared/span_utils.py +0 -0
  14. paid/_vendor/opentelemetry/instrumentation/openai/utils.py +190 -0
  15. paid/_vendor/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  16. paid/_vendor/opentelemetry/instrumentation/openai/v1/__init__.py +358 -0
  17. paid/_vendor/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +329 -0
  18. paid/_vendor/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +134 -0
  19. paid/_vendor/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +996 -0
  20. paid/_vendor/opentelemetry/instrumentation/openai/version.py +1 -0
  21. paid/tracing/autoinstrumentation.py +2 -1
  22. {paid_python-0.3.4.dist-info → paid_python-0.3.5.dist-info}/METADATA +1 -2
  23. {paid_python-0.3.4.dist-info → paid_python-0.3.5.dist-info}/RECORD +25 -5
  24. {paid_python-0.3.4.dist-info → paid_python-0.3.5.dist-info}/LICENSE +0 -0
  25. {paid_python-0.3.4.dist-info → paid_python-0.3.5.dist-info}/WHEEL +0 -0
@@ -0,0 +1,358 @@
1
+ from typing import Collection
2
+
3
+ from opentelemetry._logs import get_logger
4
+ from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
5
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.chat_wrappers import (
6
+ achat_wrapper,
7
+ chat_wrapper,
8
+ )
9
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.completion_wrappers import (
10
+ acompletion_wrapper,
11
+ completion_wrapper,
12
+ )
13
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.config import Config
14
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.embeddings_wrappers import (
15
+ aembeddings_wrapper,
16
+ embeddings_wrapper,
17
+ )
18
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.image_gen_wrappers import (
19
+ image_gen_metrics_wrapper,
20
+ )
21
+ from paid._vendor.opentelemetry.instrumentation.openai.utils import is_metrics_enabled
22
+ from paid._vendor.opentelemetry.instrumentation.openai.v1.assistant_wrappers import (
23
+ assistants_create_wrapper,
24
+ messages_list_wrapper,
25
+ runs_create_and_stream_wrapper,
26
+ runs_create_wrapper,
27
+ runs_retrieve_wrapper,
28
+ )
29
+
30
+ from paid._vendor.opentelemetry.instrumentation.openai.v1.responses_wrappers import (
31
+ async_responses_cancel_wrapper,
32
+ async_responses_get_or_create_wrapper,
33
+ responses_cancel_wrapper,
34
+ responses_get_or_create_wrapper,
35
+ )
36
+
37
+ from paid._vendor.opentelemetry.instrumentation.openai.version import __version__
38
+ from opentelemetry.instrumentation.utils import unwrap
39
+ from opentelemetry.metrics import get_meter
40
+ from opentelemetry.semconv._incubating.metrics import gen_ai_metrics as GenAIMetrics
41
+ from opentelemetry.semconv_ai import Meters
42
+ from opentelemetry.trace import get_tracer
43
+ from wrapt import wrap_function_wrapper
44
+
45
+
46
+ _instruments = ("openai >= 1.0.0",)
47
+
48
+
49
+ class OpenAIV1Instrumentor(BaseInstrumentor):
50
+ def instrumentation_dependencies(self) -> Collection[str]:
51
+ return _instruments
52
+
53
+ def _try_wrap(self, module, function, wrapper):
54
+ """
55
+ Wrap a function if it exists, otherwise do nothing.
56
+ This is useful for handling cases where the function is not available in
57
+ the older versions of the library.
58
+
59
+ Args:
60
+ module (str): The module to wrap, e.g. "openai.resources.chat.completions"
61
+ function (str): "Object.function" to wrap, e.g. "Completions.parse"
62
+ wrapper (callable): The wrapper to apply to the function.
63
+ """
64
+ try:
65
+ wrap_function_wrapper(module, function, wrapper)
66
+ except (AttributeError, ModuleNotFoundError):
67
+ pass
68
+
69
+ def _instrument(self, **kwargs):
70
+ tracer_provider = kwargs.get("tracer_provider")
71
+ tracer = get_tracer(__name__, __version__, tracer_provider)
72
+
73
+ # meter and counters are inited here
74
+ meter_provider = kwargs.get("meter_provider")
75
+ meter = get_meter(__name__, __version__, meter_provider)
76
+
77
+ if not Config.use_legacy_attributes:
78
+ logger_provider = kwargs.get("logger_provider")
79
+ Config.event_logger = get_logger(
80
+ __name__, __version__, logger_provider=logger_provider
81
+ )
82
+
83
+ if is_metrics_enabled():
84
+ tokens_histogram = meter.create_histogram(
85
+ name=Meters.LLM_TOKEN_USAGE,
86
+ unit="token",
87
+ description="Measures number of input and output tokens used",
88
+ )
89
+
90
+ chat_choice_counter = meter.create_counter(
91
+ name=Meters.LLM_GENERATION_CHOICES,
92
+ unit="choice",
93
+ description="Number of choices returned by chat completions call",
94
+ )
95
+
96
+ duration_histogram = meter.create_histogram(
97
+ name=Meters.LLM_OPERATION_DURATION,
98
+ unit="s",
99
+ description="GenAI operation duration",
100
+ )
101
+
102
+ chat_exception_counter = meter.create_counter(
103
+ name=Meters.LLM_COMPLETIONS_EXCEPTIONS,
104
+ unit="time",
105
+ description="Number of exceptions occurred during chat completions",
106
+ )
107
+
108
+ streaming_time_to_first_token = meter.create_histogram(
109
+ name=GenAIMetrics.GEN_AI_SERVER_TIME_TO_FIRST_TOKEN,
110
+ unit="s",
111
+ description="Time to first token in streaming chat completions",
112
+ )
113
+ streaming_time_to_generate = meter.create_histogram(
114
+ name=Meters.LLM_STREAMING_TIME_TO_GENERATE,
115
+ unit="s",
116
+ description="Time between first token and completion in streaming chat completions",
117
+ )
118
+ else:
119
+ (
120
+ tokens_histogram,
121
+ chat_choice_counter,
122
+ duration_histogram,
123
+ chat_exception_counter,
124
+ streaming_time_to_first_token,
125
+ streaming_time_to_generate,
126
+ ) = (None, None, None, None, None, None)
127
+
128
+ wrap_function_wrapper(
129
+ "openai.resources.chat.completions",
130
+ "Completions.create",
131
+ chat_wrapper(
132
+ tracer,
133
+ tokens_histogram,
134
+ chat_choice_counter,
135
+ duration_histogram,
136
+ chat_exception_counter,
137
+ streaming_time_to_first_token,
138
+ streaming_time_to_generate,
139
+ ),
140
+ )
141
+
142
+ wrap_function_wrapper(
143
+ "openai.resources.completions",
144
+ "Completions.create",
145
+ completion_wrapper(tracer),
146
+ )
147
+
148
+ if is_metrics_enabled():
149
+ embeddings_vector_size_counter = meter.create_counter(
150
+ name=Meters.LLM_EMBEDDINGS_VECTOR_SIZE,
151
+ unit="element",
152
+ description="he size of returned vector",
153
+ )
154
+ embeddings_exception_counter = meter.create_counter(
155
+ name=Meters.LLM_EMBEDDINGS_EXCEPTIONS,
156
+ unit="time",
157
+ description="Number of exceptions occurred during embeddings operation",
158
+ )
159
+ else:
160
+ (
161
+ tokens_histogram,
162
+ embeddings_vector_size_counter,
163
+ embeddings_exception_counter,
164
+ ) = (None, None, None)
165
+
166
+ wrap_function_wrapper(
167
+ "openai.resources.embeddings",
168
+ "Embeddings.create",
169
+ embeddings_wrapper(
170
+ tracer,
171
+ tokens_histogram,
172
+ embeddings_vector_size_counter,
173
+ duration_histogram,
174
+ embeddings_exception_counter,
175
+ ),
176
+ )
177
+
178
+ wrap_function_wrapper(
179
+ "openai.resources.chat.completions",
180
+ "AsyncCompletions.create",
181
+ achat_wrapper(
182
+ tracer,
183
+ tokens_histogram,
184
+ chat_choice_counter,
185
+ duration_histogram,
186
+ chat_exception_counter,
187
+ streaming_time_to_first_token,
188
+ streaming_time_to_generate,
189
+ ),
190
+ )
191
+ wrap_function_wrapper(
192
+ "openai.resources.completions",
193
+ "AsyncCompletions.create",
194
+ acompletion_wrapper(tracer),
195
+ )
196
+ wrap_function_wrapper(
197
+ "openai.resources.embeddings",
198
+ "AsyncEmbeddings.create",
199
+ aembeddings_wrapper(
200
+ tracer,
201
+ tokens_histogram,
202
+ embeddings_vector_size_counter,
203
+ duration_histogram,
204
+ embeddings_exception_counter,
205
+ ),
206
+ )
207
+ # in newer versions, Completions.parse are out of beta
208
+ self._try_wrap(
209
+ "openai.resources.chat.completions",
210
+ "Completions.parse",
211
+ chat_wrapper(
212
+ tracer,
213
+ tokens_histogram,
214
+ chat_choice_counter,
215
+ duration_histogram,
216
+ chat_exception_counter,
217
+ streaming_time_to_first_token,
218
+ streaming_time_to_generate,
219
+ ),
220
+ )
221
+ self._try_wrap(
222
+ "openai.resources.chat.completions",
223
+ "AsyncCompletions.parse",
224
+ achat_wrapper(
225
+ tracer,
226
+ tokens_histogram,
227
+ chat_choice_counter,
228
+ duration_histogram,
229
+ chat_exception_counter,
230
+ streaming_time_to_first_token,
231
+ streaming_time_to_generate,
232
+ ),
233
+ )
234
+
235
+ if is_metrics_enabled():
236
+ image_gen_exception_counter = meter.create_counter(
237
+ name=Meters.LLM_IMAGE_GENERATIONS_EXCEPTIONS,
238
+ unit="time",
239
+ description="Number of exceptions occurred during image generations operation",
240
+ )
241
+ else:
242
+ image_gen_exception_counter = None
243
+
244
+ wrap_function_wrapper(
245
+ "openai.resources.images",
246
+ "Images.generate",
247
+ image_gen_metrics_wrapper(duration_histogram, image_gen_exception_counter),
248
+ )
249
+
250
+ # Beta APIs may not be available consistently in all versions
251
+ self._try_wrap(
252
+ "openai.resources.beta.assistants",
253
+ "Assistants.create",
254
+ assistants_create_wrapper(tracer),
255
+ )
256
+ self._try_wrap(
257
+ "openai.resources.beta.chat.completions",
258
+ "Completions.parse",
259
+ chat_wrapper(
260
+ tracer,
261
+ tokens_histogram,
262
+ chat_choice_counter,
263
+ duration_histogram,
264
+ chat_exception_counter,
265
+ streaming_time_to_first_token,
266
+ streaming_time_to_generate,
267
+ ),
268
+ )
269
+ self._try_wrap(
270
+ "openai.resources.beta.chat.completions",
271
+ "AsyncCompletions.parse",
272
+ achat_wrapper(
273
+ tracer,
274
+ tokens_histogram,
275
+ chat_choice_counter,
276
+ duration_histogram,
277
+ chat_exception_counter,
278
+ streaming_time_to_first_token,
279
+ streaming_time_to_generate,
280
+ ),
281
+ )
282
+ self._try_wrap(
283
+ "openai.resources.beta.threads.runs",
284
+ "Runs.create",
285
+ runs_create_wrapper(tracer),
286
+ )
287
+ self._try_wrap(
288
+ "openai.resources.beta.threads.runs",
289
+ "Runs.retrieve",
290
+ runs_retrieve_wrapper(tracer),
291
+ )
292
+ self._try_wrap(
293
+ "openai.resources.beta.threads.runs",
294
+ "Runs.create_and_stream",
295
+ runs_create_and_stream_wrapper(tracer),
296
+ )
297
+ self._try_wrap(
298
+ "openai.resources.beta.threads.messages",
299
+ "Messages.list",
300
+ messages_list_wrapper(tracer),
301
+ )
302
+ self._try_wrap(
303
+ "openai.resources.responses",
304
+ "Responses.create",
305
+ responses_get_or_create_wrapper(tracer),
306
+ )
307
+ self._try_wrap(
308
+ "openai.resources.responses",
309
+ "Responses.retrieve",
310
+ responses_get_or_create_wrapper(tracer),
311
+ )
312
+ self._try_wrap(
313
+ "openai.resources.responses",
314
+ "Responses.cancel",
315
+ responses_cancel_wrapper(tracer),
316
+ )
317
+ self._try_wrap(
318
+ "openai.resources.responses",
319
+ "AsyncResponses.create",
320
+ async_responses_get_or_create_wrapper(tracer),
321
+ )
322
+ self._try_wrap(
323
+ "openai.resources.responses",
324
+ "AsyncResponses.retrieve",
325
+ async_responses_get_or_create_wrapper(tracer),
326
+ )
327
+ self._try_wrap(
328
+ "openai.resources.responses",
329
+ "AsyncResponses.cancel",
330
+ async_responses_cancel_wrapper(tracer),
331
+ )
332
+
333
+ def _uninstrument(self, **kwargs):
334
+ unwrap("openai.resources.chat.completions", "Completions.create")
335
+ unwrap("openai.resources.completions", "Completions.create")
336
+ unwrap("openai.resources.embeddings", "Embeddings.create")
337
+ unwrap("openai.resources.chat.completions", "AsyncCompletions.create")
338
+ unwrap("openai.resources.completions", "AsyncCompletions.create")
339
+ unwrap("openai.resources.embeddings", "AsyncEmbeddings.create")
340
+ unwrap("openai.resources.images", "Images.generate")
341
+
342
+ # Beta APIs may not be available consistently in all versions
343
+ try:
344
+ unwrap("openai.resources.beta.assistants", "Assistants.create")
345
+ unwrap("openai.resources.beta.chat.completions", "Completions.parse")
346
+ unwrap("openai.resources.beta.chat.completions", "AsyncCompletions.parse")
347
+ unwrap("openai.resources.beta.threads.runs", "Runs.create")
348
+ unwrap("openai.resources.beta.threads.runs", "Runs.retrieve")
349
+ unwrap("openai.resources.beta.threads.runs", "Runs.create_and_stream")
350
+ unwrap("openai.resources.beta.threads.messages", "Messages.list")
351
+ unwrap("openai.resources.responses", "Responses.create")
352
+ unwrap("openai.resources.responses", "Responses.retrieve")
353
+ unwrap("openai.resources.responses", "Responses.cancel")
354
+ unwrap("openai.resources.responses", "AsyncResponses.create")
355
+ unwrap("openai.resources.responses", "AsyncResponses.retrieve")
356
+ unwrap("openai.resources.responses", "AsyncResponses.cancel")
357
+ except ImportError:
358
+ pass
@@ -0,0 +1,329 @@
1
+ import logging
2
+ import time
3
+
4
+ from opentelemetry import context as context_api
5
+ from opentelemetry import trace
6
+ from paid._vendor.opentelemetry.instrumentation.openai.shared import (
7
+ _set_span_attribute,
8
+ model_as_dict,
9
+ )
10
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.config import Config
11
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.event_emitter import emit_event
12
+ from paid._vendor.opentelemetry.instrumentation.openai.shared.event_models import (
13
+ ChoiceEvent,
14
+ MessageEvent,
15
+ )
16
+ from paid._vendor.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._incubating.attributes import (
24
+ gen_ai_attributes as GenAIAttributes,
25
+ )
26
+ from opentelemetry.semconv_ai import LLMRequestTypeValues, SpanAttributes
27
+ from opentelemetry.trace import SpanKind, Status, StatusCode
28
+
29
+ from openai._legacy_response import LegacyAPIResponse
30
+ from openai.types.beta.threads.run import Run
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ assistants = {}
35
+ runs = {}
36
+
37
+
38
+ @_with_tracer_wrapper
39
+ def assistants_create_wrapper(tracer, wrapped, instance, args, kwargs):
40
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
41
+ return wrapped(*args, **kwargs)
42
+
43
+ response = wrapped(*args, **kwargs)
44
+
45
+ assistants[response.id] = {
46
+ "model": kwargs.get("model"),
47
+ "instructions": kwargs.get("instructions"),
48
+ }
49
+
50
+ return response
51
+
52
+
53
+ @_with_tracer_wrapper
54
+ def runs_create_wrapper(tracer, wrapped, instance, args, kwargs):
55
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
56
+ return wrapped(*args, **kwargs)
57
+
58
+ thread_id = kwargs.get("thread_id")
59
+ instructions = kwargs.get("instructions")
60
+
61
+ try:
62
+ response = wrapped(*args, **kwargs)
63
+ response_dict = model_as_dict(response)
64
+
65
+ runs[thread_id] = {
66
+ "start_time": time.time_ns(),
67
+ "assistant_id": kwargs.get("assistant_id"),
68
+ "instructions": instructions,
69
+ "run_id": response_dict.get("id"),
70
+ }
71
+
72
+ return response
73
+ except Exception as e:
74
+ runs[thread_id] = {
75
+ "exception": e,
76
+ "end_time": time.time_ns(),
77
+ }
78
+ raise
79
+
80
+
81
+ @_with_tracer_wrapper
82
+ def runs_retrieve_wrapper(tracer, wrapped, instance, args, kwargs):
83
+ @dont_throw
84
+ def process_response(response):
85
+ if type(response) is LegacyAPIResponse:
86
+ parsed_response = response.parse()
87
+ else:
88
+ parsed_response = response
89
+ assert type(parsed_response) is Run
90
+
91
+ if parsed_response.thread_id in runs:
92
+ thread_id = parsed_response.thread_id
93
+ runs[thread_id]["end_time"] = time.time_ns()
94
+ if parsed_response.usage:
95
+ runs[thread_id]["usage"] = parsed_response.usage
96
+
97
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
98
+ return wrapped(*args, **kwargs)
99
+
100
+ try:
101
+ response = wrapped(*args, **kwargs)
102
+ process_response(response)
103
+ return response
104
+ except Exception as e:
105
+ thread_id = kwargs.get("thread_id")
106
+ if thread_id in runs:
107
+ runs[thread_id]["exception"] = e
108
+ runs[thread_id]["end_time"] = time.time_ns()
109
+ raise
110
+
111
+
112
+ @_with_tracer_wrapper
113
+ def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
114
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
115
+ return wrapped(*args, **kwargs)
116
+
117
+ id = kwargs.get("thread_id")
118
+
119
+ response = wrapped(*args, **kwargs)
120
+
121
+ response_dict = model_as_dict(response)
122
+ if id not in runs:
123
+ return response
124
+
125
+ run = runs[id]
126
+ messages = sorted(response_dict["data"], key=lambda x: x["created_at"])
127
+
128
+ span = tracer.start_span(
129
+ "openai.assistant.run",
130
+ kind=SpanKind.CLIENT,
131
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
132
+ start_time=run.get("start_time"),
133
+ )
134
+
135
+ # Use the span as current context to ensure events get proper trace context
136
+ with trace.use_span(span, end_on_exit=False):
137
+ if exception := run.get("exception"):
138
+ span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
139
+ span.record_exception(exception)
140
+ span.set_status(Status(StatusCode.ERROR, str(exception)))
141
+ span.end()
142
+ return response
143
+
144
+ prompt_index = 0
145
+ if assistants.get(run["assistant_id"]) is not None or Config.enrich_assistant:
146
+ if Config.enrich_assistant:
147
+ assistant = model_as_dict(
148
+ instance._client.beta.assistants.retrieve(run["assistant_id"])
149
+ )
150
+ assistants[run["assistant_id"]] = assistant
151
+ else:
152
+ assistant = assistants[run["assistant_id"]]
153
+
154
+ _set_span_attribute(
155
+ span,
156
+ GenAIAttributes.GEN_AI_SYSTEM,
157
+ "openai",
158
+ )
159
+ _set_span_attribute(
160
+ span,
161
+ GenAIAttributes.GEN_AI_REQUEST_MODEL,
162
+ assistant["model"],
163
+ )
164
+ _set_span_attribute(
165
+ span,
166
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL,
167
+ assistant["model"],
168
+ )
169
+ if should_emit_events():
170
+ emit_event(MessageEvent(content=assistant["instructions"], role="system"))
171
+ else:
172
+ _set_span_attribute(
173
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.role", "system"
174
+ )
175
+ _set_span_attribute(
176
+ span,
177
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.content",
178
+ assistant["instructions"],
179
+ )
180
+ prompt_index += 1
181
+ _set_span_attribute(
182
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.role", "system"
183
+ )
184
+ _set_span_attribute(
185
+ span,
186
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.content",
187
+ run["instructions"],
188
+ )
189
+ if should_emit_events():
190
+ emit_event(MessageEvent(content=run["instructions"], role="system"))
191
+ prompt_index += 1
192
+
193
+ completion_index = 0
194
+ for msg in messages:
195
+ prefix = f"{GenAIAttributes.GEN_AI_COMPLETION}.{completion_index}"
196
+ content = msg.get("content")
197
+
198
+ message_content = content[0].get("text").get("value")
199
+ message_role = msg.get("role")
200
+ if message_role in ["user", "system"]:
201
+ if should_emit_events():
202
+ emit_event(MessageEvent(content=message_content, role=message_role))
203
+ else:
204
+ _set_span_attribute(
205
+ span,
206
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.role",
207
+ message_role,
208
+ )
209
+ _set_span_attribute(
210
+ span,
211
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{prompt_index}.content",
212
+ message_content,
213
+ )
214
+ prompt_index += 1
215
+ else:
216
+ if should_emit_events():
217
+ emit_event(
218
+ ChoiceEvent(
219
+ index=completion_index,
220
+ message={"content": message_content, "role": message_role},
221
+ )
222
+ )
223
+ else:
224
+ _set_span_attribute(span, f"{prefix}.role", msg.get("role"))
225
+ _set_span_attribute(span, f"{prefix}.content", message_content)
226
+ _set_span_attribute(
227
+ span, f"gen_ai.response.{completion_index}.id", msg.get("id")
228
+ )
229
+ completion_index += 1
230
+
231
+ if run.get("usage"):
232
+ usage_dict = model_as_dict(run.get("usage"))
233
+ _set_span_attribute(
234
+ span,
235
+ GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS,
236
+ usage_dict.get("completion_tokens"),
237
+ )
238
+ _set_span_attribute(
239
+ span,
240
+ GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS,
241
+ usage_dict.get("prompt_tokens"),
242
+ )
243
+
244
+ span.end(run.get("end_time"))
245
+
246
+ return response
247
+
248
+
249
+ @_with_tracer_wrapper
250
+ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
251
+ if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
252
+ return wrapped(*args, **kwargs)
253
+
254
+ assistant_id = kwargs.get("assistant_id")
255
+ instructions = kwargs.get("instructions")
256
+
257
+ span = tracer.start_span(
258
+ "openai.assistant.run_stream",
259
+ kind=SpanKind.CLIENT,
260
+ attributes={SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.CHAT.value},
261
+ )
262
+
263
+ # Use the span as current context to ensure events get proper trace context
264
+ with trace.use_span(span, end_on_exit=False):
265
+ i = 0
266
+ if assistants.get(assistant_id) is not None or Config.enrich_assistant:
267
+ if Config.enrich_assistant:
268
+ assistant = model_as_dict(
269
+ instance._client.beta.assistants.retrieve(assistant_id)
270
+ )
271
+ assistants[assistant_id] = assistant
272
+ else:
273
+ assistant = assistants[assistant_id]
274
+
275
+ _set_span_attribute(
276
+ span, GenAIAttributes.GEN_AI_REQUEST_MODEL, assistants[assistant_id]["model"]
277
+ )
278
+ _set_span_attribute(
279
+ span,
280
+ GenAIAttributes.GEN_AI_SYSTEM,
281
+ "openai",
282
+ )
283
+ _set_span_attribute(
284
+ span,
285
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL,
286
+ assistants[assistant_id]["model"],
287
+ )
288
+ if should_emit_events():
289
+ emit_event(
290
+ MessageEvent(
291
+ content=assistants[assistant_id]["instructions"], role="system"
292
+ )
293
+ )
294
+ else:
295
+ _set_span_attribute(
296
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.role", "system"
297
+ )
298
+ _set_span_attribute(
299
+ span,
300
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.content",
301
+ assistants[assistant_id]["instructions"],
302
+ )
303
+ i += 1
304
+ if should_emit_events():
305
+ emit_event(MessageEvent(content=instructions, role="system"))
306
+ else:
307
+ _set_span_attribute(span, f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.role", "system")
308
+ _set_span_attribute(
309
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.content", instructions
310
+ )
311
+
312
+ from paid._vendor.opentelemetry.instrumentation.openai.v1.event_handler_wrapper import (
313
+ EventHandleWrapper,
314
+ )
315
+
316
+ kwargs["event_handler"] = EventHandleWrapper(
317
+ original_handler=kwargs["event_handler"],
318
+ span=span,
319
+ )
320
+
321
+ try:
322
+ response = wrapped(*args, **kwargs)
323
+ return response
324
+ except Exception as e:
325
+ span.set_attribute(ERROR_TYPE, e.__class__.__name__)
326
+ span.record_exception(e)
327
+ span.set_status(Status(StatusCode.ERROR, str(e)))
328
+ span.end()
329
+ raise