opentelemetry-instrumentation-openai 0.15.10__tar.gz → 0.15.12__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.

Potentially problematic release.


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

Files changed (17) hide show
  1. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/PKG-INFO +1 -1
  2. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +166 -49
  3. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/v1/__init__.py +9 -1
  4. opentelemetry_instrumentation_openai-0.15.12/opentelemetry/instrumentation/openai/version.py +1 -0
  5. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/pyproject.toml +2 -2
  6. opentelemetry_instrumentation_openai-0.15.10/opentelemetry/instrumentation/openai/version.py +0 -1
  7. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/README.md +0 -0
  8. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/__init__.py +0 -0
  9. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/__init__.py +0 -0
  10. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +0 -0
  11. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/config.py +0 -0
  12. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +0 -0
  13. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +0 -0
  14. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/utils.py +0 -0
  15. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/v0/__init__.py +0 -0
  16. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +0 -0
  17. {opentelemetry_instrumentation_openai-0.15.10 → opentelemetry_instrumentation_openai-0.15.12}/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: opentelemetry-instrumentation-openai
3
- Version: 0.15.10
3
+ Version: 0.15.12
4
4
  Summary: OpenTelemetry OpenAI instrumentation
5
5
  Home-page: https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-openai
6
6
  License: Apache-2.0
@@ -8,7 +8,6 @@ from opentelemetry.semconv.ai import SpanAttributes, LLMRequestTypeValues
8
8
 
9
9
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
10
10
  from opentelemetry.instrumentation.openai.utils import (
11
- _with_tracer_wrapper,
12
11
  _with_chat_telemetry_wrapper,
13
12
  )
14
13
  from opentelemetry.instrumentation.openai.shared import (
@@ -95,7 +94,7 @@ def chat_wrapper(
95
94
  streaming_time_to_first_token,
96
95
  streaming_time_to_generate,
97
96
  start_time,
98
- kwargs
97
+ kwargs,
99
98
  )
100
99
 
101
100
  duration = end_time - start_time
@@ -114,8 +113,20 @@ def chat_wrapper(
114
113
  return response
115
114
 
116
115
 
117
- @_with_tracer_wrapper
118
- async def achat_wrapper(tracer, wrapped, instance, args, kwargs):
116
+ @_with_chat_telemetry_wrapper
117
+ async def achat_wrapper(
118
+ tracer: Tracer,
119
+ token_counter: Counter,
120
+ choice_counter: Counter,
121
+ duration_histogram: Histogram,
122
+ exception_counter: Counter,
123
+ streaming_time_to_first_token: Histogram,
124
+ streaming_time_to_generate: Histogram,
125
+ wrapped,
126
+ instance,
127
+ args,
128
+ kwargs,
129
+ ):
119
130
  if context_api.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
120
131
  return wrapped(*args, **kwargs)
121
132
 
@@ -125,13 +136,52 @@ async def achat_wrapper(tracer, wrapped, instance, args, kwargs):
125
136
  attributes={SpanAttributes.LLM_REQUEST_TYPE: LLM_REQUEST_TYPE.value},
126
137
  )
127
138
  _handle_request(span, kwargs, instance)
128
- response = await wrapped(*args, **kwargs)
139
+
140
+ try:
141
+ start_time = time.time()
142
+ response = await wrapped(*args, **kwargs)
143
+ end_time = time.time()
144
+ except Exception as e: # pylint: disable=broad-except
145
+ end_time = time.time()
146
+ duration = end_time - start_time if "start_time" in locals() else 0
147
+
148
+ attributes = {
149
+ "error.type": e.__class__.__name__,
150
+ }
151
+
152
+ if duration > 0 and duration_histogram:
153
+ duration_histogram.record(duration, attributes=attributes)
154
+ if exception_counter:
155
+ exception_counter.add(1, attributes=attributes)
156
+
157
+ raise e
129
158
 
130
159
  if is_streaming_response(response):
131
160
  # span will be closed after the generator is done
132
- return _abuild_from_streaming_response(span, response)
161
+ return _abuild_from_streaming_response(
162
+ span,
163
+ response,
164
+ instance,
165
+ token_counter,
166
+ choice_counter,
167
+ duration_histogram,
168
+ streaming_time_to_first_token,
169
+ streaming_time_to_generate,
170
+ start_time,
171
+ kwargs,
172
+ )
133
173
 
134
- _handle_response(response, span)
174
+ duration = end_time - start_time
175
+
176
+ _handle_response(
177
+ response,
178
+ span,
179
+ instance,
180
+ token_counter,
181
+ choice_counter,
182
+ duration_histogram,
183
+ duration,
184
+ )
135
185
  span.end()
136
186
 
137
187
  return response
@@ -285,6 +335,61 @@ def _set_completions(span, choices):
285
335
  )
286
336
 
287
337
 
338
+ def _set_streaming_token_metrics(
339
+ request_kwargs, complete_response, span, token_counter, shared_attributes
340
+ ):
341
+ # use tiktoken calculate token usage
342
+ if not should_record_stream_token_usage():
343
+ return
344
+
345
+ # kwargs={'model': 'gpt-3.5', 'messages': [{'role': 'user', 'content': '...'}], 'stream': True}
346
+ prompt_usage = -1
347
+ completion_usage = -1
348
+
349
+ # prompt_usage
350
+ if request_kwargs and request_kwargs.get("messages"):
351
+ prompt_content = ""
352
+ model_name = request_kwargs.get("model") or None
353
+ for msg in request_kwargs.get("messages"):
354
+ if msg.get("content"):
355
+ prompt_content += msg.get("content")
356
+ if model_name:
357
+ prompt_usage = get_token_count_from_string(prompt_content, model_name)
358
+
359
+ # completion_usage
360
+ if complete_response.get("choices"):
361
+ completion_content = ""
362
+ model_name = complete_response.get("model") or None
363
+
364
+ for choice in complete_response.get("choices"): # type: dict
365
+ if choice.get("message") and choice.get("message").get("content"):
366
+ completion_content += choice["message"]["content"]
367
+
368
+ if model_name:
369
+ completion_usage = get_token_count_from_string(
370
+ completion_content, model_name
371
+ )
372
+
373
+ # span record
374
+ _set_span_stream_usage(span, prompt_usage, completion_usage)
375
+
376
+ # metrics record
377
+ if token_counter:
378
+ if type(prompt_usage) is int and prompt_usage >= 0:
379
+ attributes_with_token_type = {
380
+ **shared_attributes,
381
+ "llm.usage.token_type": "prompt",
382
+ }
383
+ token_counter.add(prompt_usage, attributes=attributes_with_token_type)
384
+
385
+ if type(completion_usage) is int and completion_usage >= 0:
386
+ attributes_with_token_type = {
387
+ **shared_attributes,
388
+ "llm.usage.token_type": "completion",
389
+ }
390
+ token_counter.add(completion_usage, attributes=attributes_with_token_type)
391
+
392
+
288
393
  def _build_from_streaming_response(
289
394
  span,
290
395
  response,
@@ -295,7 +400,7 @@ def _build_from_streaming_response(
295
400
  streaming_time_to_first_token=None,
296
401
  streaming_time_to_generate=None,
297
402
  start_time=None,
298
- request_kwargs=None
403
+ request_kwargs=None,
299
404
  ):
300
405
  complete_response = {"choices": [], "model": ""}
301
406
 
@@ -322,46 +427,9 @@ def _build_from_streaming_response(
322
427
  "stream": True,
323
428
  }
324
429
 
325
- # use tiktoken calculate token usage
326
- if should_record_stream_token_usage():
327
- # kwargs={'model': 'gpt-3.5', 'messages': [{'role': 'user', 'content': '...'}], 'stream': True}
328
- prompt_usage = -1
329
- completion_usage = -1
330
-
331
- # prompt_usage
332
- if request_kwargs and request_kwargs.get("messages"):
333
- prompt_content = ""
334
- model_name = request_kwargs.get("model") or None
335
- for msg in request_kwargs.get("messages"):
336
- if msg.get("content"):
337
- prompt_content += msg.get("content")
338
- if model_name:
339
- prompt_usage = get_token_count_from_string(prompt_content, model_name)
340
-
341
- # completion_usage
342
- if complete_response.get("choices"):
343
- completion_content = ""
344
- model_name = complete_response.get("model") or None
345
-
346
- for choice in complete_response.get("choices"): # type: dict
347
- if choice.get("message") and choice.get("message").get("content"):
348
- completion_content += choice["message"]["content"]
349
-
350
- if model_name:
351
- completion_usage = get_token_count_from_string(completion_content, model_name)
352
-
353
- # span record
354
- _set_span_stream_usage(span, prompt_usage, completion_usage)
355
-
356
- # metrics record
357
- if token_counter:
358
- if type(prompt_usage) is int and prompt_usage >= 0:
359
- attributes_with_token_type = {**shared_attributes, "llm.usage.token_type": "prompt"}
360
- token_counter.add(prompt_usage, attributes=attributes_with_token_type)
361
-
362
- if type(completion_usage) is int and completion_usage >= 0:
363
- attributes_with_token_type = {**shared_attributes, "llm.usage.token_type": "completion"}
364
- token_counter.add(completion_usage, attributes=attributes_with_token_type)
430
+ _set_streaming_token_metrics(
431
+ request_kwargs, complete_response, span, token_counter, shared_attributes
432
+ )
365
433
 
366
434
  # choice metrics
367
435
  if choice_counter and complete_response.get("choices"):
@@ -388,14 +456,63 @@ def _build_from_streaming_response(
388
456
  span.end()
389
457
 
390
458
 
391
- async def _abuild_from_streaming_response(span, response):
459
+ async def _abuild_from_streaming_response(
460
+ span,
461
+ response,
462
+ instance=None,
463
+ token_counter=None,
464
+ choice_counter=None,
465
+ duration_histogram=None,
466
+ streaming_time_to_first_token=None,
467
+ streaming_time_to_generate=None,
468
+ start_time=None,
469
+ request_kwargs=None,
470
+ ):
392
471
  complete_response = {"choices": [], "model": ""}
472
+
473
+ first_token = True
474
+ time_of_first_token = start_time # will be updated when first token is received
475
+
393
476
  async for item in response:
477
+ span.add_event(name="llm.content.completion.chunk")
478
+
394
479
  item_to_yield = item
480
+
481
+ if first_token and streaming_time_to_first_token:
482
+ time_of_first_token = time.time()
483
+ streaming_time_to_first_token.record(time_of_first_token - start_time)
484
+ first_token = False
485
+
395
486
  _accumulate_stream_items(item, complete_response)
396
487
 
397
488
  yield item_to_yield
398
489
 
490
+ shared_attributes = {
491
+ "llm.response.model": complete_response.get("model") or None,
492
+ "server.address": _get_openai_base_url(instance),
493
+ "stream": True,
494
+ }
495
+
496
+ _set_streaming_token_metrics(
497
+ request_kwargs, complete_response, span, token_counter, shared_attributes
498
+ )
499
+
500
+ # choice metrics
501
+ if choice_counter and complete_response.get("choices"):
502
+ _set_choice_counter_metrics(
503
+ choice_counter, complete_response.get("choices"), shared_attributes
504
+ )
505
+
506
+ # duration metrics
507
+ if start_time and isinstance(start_time, (float, int)):
508
+ duration = time.time() - start_time
509
+ else:
510
+ duration = None
511
+ if duration and isinstance(duration, (float, int)) and duration_histogram:
512
+ duration_histogram.record(duration, attributes=shared_attributes)
513
+ if streaming_time_to_generate and time_of_first_token:
514
+ streaming_time_to_generate.record(time.time() - time_of_first_token)
515
+
399
516
  _set_response_attributes(span, complete_response)
400
517
 
401
518
  if should_send_prompts():
@@ -160,7 +160,15 @@ class OpenAIV1Instrumentor(BaseInstrumentor):
160
160
  wrap_function_wrapper(
161
161
  "openai.resources.chat.completions",
162
162
  "AsyncCompletions.create",
163
- achat_wrapper(tracer),
163
+ achat_wrapper(
164
+ tracer,
165
+ chat_token_counter,
166
+ chat_choice_counter,
167
+ chat_duration_histogram,
168
+ chat_exception_counter,
169
+ streaming_time_to_first_token,
170
+ streaming_time_to_generate,
171
+ ),
164
172
  )
165
173
  wrap_function_wrapper(
166
174
  "openai.resources.completions",
@@ -8,7 +8,7 @@ show_missing = true
8
8
 
9
9
  [tool.poetry]
10
10
  name = "opentelemetry-instrumentation-openai"
11
- version = "0.15.10"
11
+ version = "0.15.12"
12
12
  description = "OpenTelemetry OpenAI instrumentation"
13
13
  authors = [
14
14
  "Gal Kleinman <gal@traceloop.com>",
@@ -34,7 +34,7 @@ autopep8 = "2.1.0"
34
34
  flake8 = "7.0.0"
35
35
 
36
36
  [tool.poetry.group.test.dependencies]
37
- pytest = "8.1.0"
37
+ pytest = "8.1.1"
38
38
  pytest-sugar = "1.0.0"
39
39
  vcrpy = "^6.0.1"
40
40
  pytest-recording = "^0.13.1"