opentelemetry-instrumentation-groq 0.40.2__py3-none-any.whl → 0.48.0__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.
@@ -1,44 +1,54 @@
1
1
  """OpenTelemetry Groq instrumentation"""
2
2
 
3
- import json
4
3
  import logging
5
4
  import os
6
5
  import time
7
- from typing import Callable, Collection
6
+ from typing import Callable, Collection, Union
8
7
 
9
- from groq._streaming import AsyncStream, Stream
10
8
  from opentelemetry import context as context_api
9
+ from opentelemetry._events import EventLogger, get_event_logger
11
10
  from opentelemetry.instrumentation.groq.config import Config
11
+ from opentelemetry.instrumentation.groq.event_emitter import (
12
+ emit_choice_events,
13
+ emit_message_events,
14
+ emit_streaming_response_events,
15
+ )
16
+ from opentelemetry.instrumentation.groq.span_utils import (
17
+ set_input_attributes,
18
+ set_model_input_attributes,
19
+ set_model_response_attributes,
20
+ set_model_streaming_response_attributes,
21
+ set_response_attributes,
22
+ set_streaming_response_attributes,
23
+ )
12
24
  from opentelemetry.instrumentation.groq.utils import (
13
- dont_throw,
14
25
  error_metrics_attributes,
15
- model_as_dict,
16
- set_span_attribute,
17
26
  shared_metrics_attributes,
18
- should_send_prompts,
27
+ should_emit_events,
19
28
  )
20
29
  from opentelemetry.instrumentation.groq.version import __version__
21
30
  from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
22
31
  from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
23
32
  from opentelemetry.metrics import Counter, Histogram, Meter, get_meter
24
- from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
25
- GEN_AI_RESPONSE_ID,
33
+ from opentelemetry.semconv._incubating.attributes import (
34
+ gen_ai_attributes as GenAIAttributes,
26
35
  )
27
36
  from opentelemetry.semconv_ai import (
28
37
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
29
38
  LLMRequestTypeValues,
30
- SpanAttributes,
31
39
  Meters,
40
+ SpanAttributes,
32
41
  )
33
42
  from opentelemetry.trace import SpanKind, Tracer, get_tracer
34
43
  from opentelemetry.trace.status import Status, StatusCode
35
44
  from wrapt import wrap_function_wrapper
36
45
 
46
+ from groq._streaming import AsyncStream, Stream
47
+
37
48
  logger = logging.getLogger(__name__)
38
49
 
39
50
  _instruments = ("groq >= 0.9.0",)
40
51
 
41
- CONTENT_FILTER_KEY = "content_filter_results"
42
52
 
43
53
  WRAPPED_METHODS = [
44
54
  {
@@ -62,187 +72,6 @@ def is_streaming_response(response):
62
72
  return isinstance(response, Stream) or isinstance(response, AsyncStream)
63
73
 
64
74
 
65
- def _dump_content(content):
66
- if isinstance(content, str):
67
- return content
68
- json_serializable = []
69
- for item in content:
70
- if item.get("type") == "text":
71
- json_serializable.append({"type": "text", "text": item.get("text")})
72
- elif item.get("type") == "image":
73
- json_serializable.append(
74
- {
75
- "type": "image",
76
- "source": {
77
- "type": item.get("source").get("type"),
78
- "media_type": item.get("source").get("media_type"),
79
- "data": str(item.get("source").get("data")),
80
- },
81
- }
82
- )
83
- return json.dumps(json_serializable)
84
-
85
-
86
- @dont_throw
87
- def _set_input_attributes(span, kwargs):
88
- set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model"))
89
- set_span_attribute(
90
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens_to_sample")
91
- )
92
- set_span_attribute(
93
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
94
- )
95
- set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
96
- set_span_attribute(
97
- span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
98
- )
99
- set_span_attribute(
100
- span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
101
- )
102
- set_span_attribute(
103
- span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
104
- )
105
-
106
- if should_send_prompts():
107
- if kwargs.get("prompt") is not None:
108
- set_span_attribute(
109
- span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
110
- )
111
-
112
- elif kwargs.get("messages") is not None:
113
- for i, message in enumerate(kwargs.get("messages")):
114
- set_span_attribute(
115
- span,
116
- f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
117
- _dump_content(message.get("content")),
118
- )
119
- set_span_attribute(
120
- span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message.get("role")
121
- )
122
-
123
-
124
- def _set_completions(span, choices):
125
- if choices is None:
126
- return
127
-
128
- for choice in choices:
129
- index = choice.get("index")
130
- prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
131
- set_span_attribute(span, f"{prefix}.finish_reason", choice.get("finish_reason"))
132
-
133
- if choice.get("content_filter_results"):
134
- set_span_attribute(
135
- span,
136
- f"{prefix}.{CONTENT_FILTER_KEY}",
137
- json.dumps(choice.get("content_filter_results")),
138
- )
139
-
140
- if choice.get("finish_reason") == "content_filter":
141
- set_span_attribute(span, f"{prefix}.role", "assistant")
142
- set_span_attribute(span, f"{prefix}.content", "FILTERED")
143
-
144
- return
145
-
146
- message = choice.get("message")
147
- if not message:
148
- return
149
-
150
- set_span_attribute(span, f"{prefix}.role", message.get("role"))
151
- set_span_attribute(span, f"{prefix}.content", message.get("content"))
152
-
153
- function_call = message.get("function_call")
154
- if function_call:
155
- set_span_attribute(
156
- span, f"{prefix}.tool_calls.0.name", function_call.get("name")
157
- )
158
- set_span_attribute(
159
- span,
160
- f"{prefix}.tool_calls.0.arguments",
161
- function_call.get("arguments"),
162
- )
163
-
164
- tool_calls = message.get("tool_calls")
165
- if tool_calls:
166
- for i, tool_call in enumerate(tool_calls):
167
- function = tool_call.get("function")
168
- set_span_attribute(
169
- span,
170
- f"{prefix}.tool_calls.{i}.id",
171
- tool_call.get("id"),
172
- )
173
- set_span_attribute(
174
- span,
175
- f"{prefix}.tool_calls.{i}.name",
176
- function.get("name"),
177
- )
178
- set_span_attribute(
179
- span,
180
- f"{prefix}.tool_calls.{i}.arguments",
181
- function.get("arguments"),
182
- )
183
-
184
-
185
- @dont_throw
186
- def _set_response_attributes(span, response, token_histogram):
187
- response = model_as_dict(response)
188
- set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
189
- set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
190
-
191
- usage = response.get("usage") or {}
192
- prompt_tokens = usage.get("prompt_tokens")
193
- completion_tokens = usage.get("completion_tokens")
194
- if usage:
195
- set_span_attribute(
196
- span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.get("total_tokens")
197
- )
198
- set_span_attribute(
199
- span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
200
- )
201
- set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, prompt_tokens)
202
-
203
- if (
204
- isinstance(prompt_tokens, int)
205
- and prompt_tokens >= 0
206
- and token_histogram is not None
207
- ):
208
- token_histogram.record(
209
- prompt_tokens,
210
- attributes={
211
- SpanAttributes.LLM_TOKEN_TYPE: "input",
212
- SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
213
- },
214
- )
215
-
216
- if (
217
- isinstance(completion_tokens, int)
218
- and completion_tokens >= 0
219
- and token_histogram is not None
220
- ):
221
- token_histogram.record(
222
- completion_tokens,
223
- attributes={
224
- SpanAttributes.LLM_TOKEN_TYPE: "output",
225
- SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
226
- },
227
- )
228
-
229
- choices = response.get("choices")
230
- if should_send_prompts() and choices:
231
- _set_completions(span, choices)
232
-
233
-
234
- def _with_tracer_wrapper(func):
235
- """Helper for providing tracer for wrapper functions."""
236
-
237
- def _with_tracer(tracer, to_wrap):
238
- def wrapper(wrapped, instance, args, kwargs):
239
- return func(tracer, to_wrap, wrapped, instance, args, kwargs)
240
-
241
- return wrapper
242
-
243
- return _with_tracer
244
-
245
-
246
75
  def _with_chat_telemetry_wrapper(func):
247
76
  """Helper for providing tracer for wrapper functions. Includes metric collectors."""
248
77
 
@@ -251,6 +80,7 @@ def _with_chat_telemetry_wrapper(func):
251
80
  token_histogram,
252
81
  choice_counter,
253
82
  duration_histogram,
83
+ event_logger,
254
84
  to_wrap,
255
85
  ):
256
86
  def wrapper(wrapped, instance, args, kwargs):
@@ -259,6 +89,7 @@ def _with_chat_telemetry_wrapper(func):
259
89
  token_histogram,
260
90
  choice_counter,
261
91
  duration_histogram,
92
+ event_logger,
262
93
  to_wrap,
263
94
  wrapped,
264
95
  instance,
@@ -310,32 +141,19 @@ def _process_streaming_chunk(chunk):
310
141
  return content, finish_reason, usage
311
142
 
312
143
 
313
- def _set_streaming_response_attributes(
314
- span, accumulated_content, finish_reason=None, usage=None
144
+ def _handle_streaming_response(
145
+ span, accumulated_content, finish_reason, usage, event_logger
315
146
  ):
316
- """Set span attributes for accumulated streaming response."""
317
- if not span.is_recording():
318
- return
319
-
320
- prefix = f"{SpanAttributes.LLM_COMPLETIONS}.0"
321
- set_span_attribute(span, f"{prefix}.role", "assistant")
322
- set_span_attribute(span, f"{prefix}.content", accumulated_content)
323
- if finish_reason:
324
- set_span_attribute(span, f"{prefix}.finish_reason", finish_reason)
325
-
326
- if usage:
327
- set_span_attribute(
328
- span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, usage.completion_tokens
329
- )
330
- set_span_attribute(
331
- span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage.prompt_tokens
332
- )
333
- set_span_attribute(
334
- span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.total_tokens
147
+ set_model_streaming_response_attributes(span, usage)
148
+ if should_emit_events() and event_logger:
149
+ emit_streaming_response_events(accumulated_content, finish_reason, event_logger)
150
+ else:
151
+ set_streaming_response_attributes(
152
+ span, accumulated_content, finish_reason, usage
335
153
  )
336
154
 
337
155
 
338
- def _create_stream_processor(response, span):
156
+ def _create_stream_processor(response, span, event_logger):
339
157
  """Create a generator that processes a stream while collecting telemetry."""
340
158
  accumulated_content = ""
341
159
  finish_reason = None
@@ -351,15 +169,17 @@ def _create_stream_processor(response, span):
351
169
  usage = chunk_usage
352
170
  yield chunk
353
171
 
172
+ _handle_streaming_response(
173
+ span, accumulated_content, finish_reason, usage, event_logger
174
+ )
175
+
354
176
  if span.is_recording():
355
- _set_streaming_response_attributes(
356
- span, accumulated_content, finish_reason, usage
357
- )
358
177
  span.set_status(Status(StatusCode.OK))
178
+
359
179
  span.end()
360
180
 
361
181
 
362
- async def _create_async_stream_processor(response, span):
182
+ async def _create_async_stream_processor(response, span, event_logger):
363
183
  """Create an async generator that processes a stream while collecting telemetry."""
364
184
  accumulated_content = ""
365
185
  finish_reason = None
@@ -375,20 +195,39 @@ async def _create_async_stream_processor(response, span):
375
195
  usage = chunk_usage
376
196
  yield chunk
377
197
 
198
+ _handle_streaming_response(
199
+ span, accumulated_content, finish_reason, usage, event_logger
200
+ )
201
+
378
202
  if span.is_recording():
379
- _set_streaming_response_attributes(
380
- span, accumulated_content, finish_reason, usage
381
- )
382
203
  span.set_status(Status(StatusCode.OK))
204
+
383
205
  span.end()
384
206
 
385
207
 
208
+ def _handle_input(span, kwargs, event_logger):
209
+ set_model_input_attributes(span, kwargs)
210
+ if should_emit_events() and event_logger:
211
+ emit_message_events(kwargs, event_logger)
212
+ else:
213
+ set_input_attributes(span, kwargs)
214
+
215
+
216
+ def _handle_response(span, response, token_histogram, event_logger):
217
+ set_model_response_attributes(span, response, token_histogram)
218
+ if should_emit_events() and event_logger:
219
+ emit_choice_events(response, event_logger)
220
+ else:
221
+ set_response_attributes(span, response)
222
+
223
+
386
224
  @_with_chat_telemetry_wrapper
387
225
  def _wrap(
388
226
  tracer: Tracer,
389
227
  token_histogram: Histogram,
390
228
  choice_counter: Counter,
391
229
  duration_histogram: Histogram,
230
+ event_logger: Union[EventLogger, None],
392
231
  to_wrap,
393
232
  wrapped,
394
233
  instance,
@@ -406,13 +245,12 @@ def _wrap(
406
245
  name,
407
246
  kind=SpanKind.CLIENT,
408
247
  attributes={
409
- SpanAttributes.LLM_SYSTEM: "Groq",
248
+ GenAIAttributes.GEN_AI_SYSTEM: "groq",
410
249
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
411
250
  },
412
251
  )
413
252
 
414
- if span.is_recording():
415
- _set_input_attributes(span, kwargs)
253
+ _handle_input(span, kwargs, event_logger)
416
254
 
417
255
  start_time = time.time()
418
256
  try:
@@ -431,7 +269,7 @@ def _wrap(
431
269
 
432
270
  if is_streaming_response(response):
433
271
  try:
434
- return _create_stream_processor(response, span)
272
+ return _create_stream_processor(response, span, event_logger)
435
273
  except Exception as ex:
436
274
  logger.warning(
437
275
  "Failed to process streaming response for groq span, error: %s",
@@ -451,14 +289,14 @@ def _wrap(
451
289
  attributes=metric_attributes,
452
290
  )
453
291
 
454
- if span.is_recording():
455
- _set_response_attributes(span, response, token_histogram)
292
+ _handle_response(span, response, token_histogram, event_logger)
456
293
 
457
294
  except Exception as ex: # pylint: disable=broad-except
458
295
  logger.warning(
459
296
  "Failed to set response attributes for groq span, error: %s",
460
297
  str(ex),
461
298
  )
299
+
462
300
  if span.is_recording():
463
301
  span.set_status(Status(StatusCode.OK))
464
302
  span.end()
@@ -471,6 +309,7 @@ async def _awrap(
471
309
  token_histogram: Histogram,
472
310
  choice_counter: Counter,
473
311
  duration_histogram: Histogram,
312
+ event_logger: Union[EventLogger, None],
474
313
  to_wrap,
475
314
  wrapped,
476
315
  instance,
@@ -488,20 +327,15 @@ async def _awrap(
488
327
  name,
489
328
  kind=SpanKind.CLIENT,
490
329
  attributes={
491
- SpanAttributes.LLM_SYSTEM: "Groq",
330
+ GenAIAttributes.GEN_AI_SYSTEM: "groq",
492
331
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
493
332
  },
494
333
  )
495
- try:
496
- if span.is_recording():
497
- _set_input_attributes(span, kwargs)
498
334
 
499
- except Exception as ex: # pylint: disable=broad-except
500
- logger.warning(
501
- "Failed to set input attributes for groq span, error: %s", str(ex)
502
- )
335
+ _handle_input(span, kwargs, event_logger)
503
336
 
504
337
  start_time = time.time()
338
+
505
339
  try:
506
340
  response = await wrapped(*args, **kwargs)
507
341
  except Exception as e: # pylint: disable=broad-except
@@ -518,7 +352,7 @@ async def _awrap(
518
352
 
519
353
  if is_streaming_response(response):
520
354
  try:
521
- return await _create_async_stream_processor(response, span)
355
+ return await _create_async_stream_processor(response, span, event_logger)
522
356
  except Exception as ex:
523
357
  logger.warning(
524
358
  "Failed to process streaming response for groq span, error: %s",
@@ -537,8 +371,7 @@ async def _awrap(
537
371
  attributes=metric_attributes,
538
372
  )
539
373
 
540
- if span.is_recording():
541
- _set_response_attributes(span, response, token_histogram)
374
+ _handle_response(span, response, token_histogram, event_logger)
542
375
 
543
376
  if span.is_recording():
544
377
  span.set_status(Status(StatusCode.OK))
@@ -555,14 +388,14 @@ class GroqInstrumentor(BaseInstrumentor):
555
388
 
556
389
  def __init__(
557
390
  self,
558
- enrich_token_usage: bool = False,
559
391
  exception_logger=None,
392
+ use_legacy_attributes: bool = True,
560
393
  get_common_metrics_attributes: Callable[[], dict] = lambda: {},
561
394
  ):
562
395
  super().__init__()
563
396
  Config.exception_logger = exception_logger
564
- Config.enrich_token_usage = enrich_token_usage
565
397
  Config.get_common_metrics_attributes = get_common_metrics_attributes
398
+ Config.use_legacy_attributes = use_legacy_attributes
566
399
 
567
400
  def instrumentation_dependencies(self) -> Collection[str]:
568
401
  return _instruments
@@ -588,6 +421,13 @@ class GroqInstrumentor(BaseInstrumentor):
588
421
  duration_histogram,
589
422
  ) = (None, None, None)
590
423
 
424
+ event_logger = None
425
+ if not Config.use_legacy_attributes:
426
+ event_logger_provider = kwargs.get("event_logger_provider")
427
+ event_logger = get_event_logger(
428
+ __name__, __version__, event_logger_provider=event_logger_provider
429
+ )
430
+
591
431
  for wrapped_method in WRAPPED_METHODS:
592
432
  wrap_package = wrapped_method.get("package")
593
433
  wrap_object = wrapped_method.get("object")
@@ -602,6 +442,7 @@ class GroqInstrumentor(BaseInstrumentor):
602
442
  token_histogram,
603
443
  choice_counter,
604
444
  duration_histogram,
445
+ event_logger,
605
446
  wrapped_method,
606
447
  ),
607
448
  )
@@ -621,6 +462,7 @@ class GroqInstrumentor(BaseInstrumentor):
621
462
  token_histogram,
622
463
  choice_counter,
623
464
  duration_histogram,
465
+ event_logger,
624
466
  wrapped_method,
625
467
  ),
626
468
  )
@@ -636,8 +478,9 @@ class GroqInstrumentor(BaseInstrumentor):
636
478
  wrapped_method.get("method"),
637
479
  )
638
480
  for wrapped_method in WRAPPED_AMETHODS:
481
+ wrap_package = wrapped_method.get("package")
639
482
  wrap_object = wrapped_method.get("object")
640
483
  unwrap(
641
- f"groq.resources.completions.{wrap_object}",
484
+ f"{wrap_package}.{wrap_object}",
642
485
  wrapped_method.get("method"),
643
486
  )
@@ -2,6 +2,6 @@ from typing import Callable
2
2
 
3
3
 
4
4
  class Config:
5
- enrich_token_usage = False
6
5
  exception_logger = None
7
6
  get_common_metrics_attributes: Callable[[], dict] = lambda: {}
7
+ use_legacy_attributes = True
@@ -0,0 +1,143 @@
1
+ from dataclasses import asdict
2
+ from enum import Enum
3
+ from typing import Union
4
+
5
+ from opentelemetry._events import Event, EventLogger
6
+ from opentelemetry.instrumentation.groq.event_models import ChoiceEvent, MessageEvent
7
+ from opentelemetry.instrumentation.groq.utils import (
8
+ dont_throw,
9
+ should_emit_events,
10
+ should_send_prompts,
11
+ )
12
+ from opentelemetry.semconv._incubating.attributes import (
13
+ gen_ai_attributes as GenAIAttributes,
14
+ )
15
+
16
+ from groq.types.chat.chat_completion import ChatCompletion
17
+
18
+
19
+ class Roles(Enum):
20
+ USER = "user"
21
+ ASSISTANT = "assistant"
22
+ SYSTEM = "system"
23
+ TOOL = "tool"
24
+
25
+
26
+ VALID_MESSAGE_ROLES = {role.value for role in Roles}
27
+ """The valid roles for naming the message event."""
28
+
29
+ EVENT_ATTRIBUTES = {
30
+ # Should be GenAIAttributes.GenAiSystemValues.GROQ.value but it's not defined in the opentelemetry-semconv package
31
+ GenAIAttributes.GEN_AI_SYSTEM: "groq"
32
+ }
33
+ """The attributes to be used for the event."""
34
+
35
+
36
+ @dont_throw
37
+ def emit_message_events(kwargs: dict, event_logger):
38
+ for message in kwargs.get("messages", []):
39
+ emit_event(
40
+ MessageEvent(
41
+ content=message.get("content"), role=message.get("role", "unknown")
42
+ ),
43
+ event_logger=event_logger,
44
+ )
45
+
46
+
47
+ @dont_throw
48
+ def emit_choice_events(response: ChatCompletion, event_logger):
49
+ for choice in response.choices:
50
+ emit_event(
51
+ ChoiceEvent(
52
+ index=choice.index,
53
+ message={
54
+ "content": choice.message.content,
55
+ "role": choice.message.role or "unknown",
56
+ },
57
+ finish_reason=choice.finish_reason,
58
+ ),
59
+ event_logger=event_logger,
60
+ )
61
+
62
+
63
+ @dont_throw
64
+ def emit_streaming_response_events(
65
+ accumulated_content: str, finish_reason: Union[str, None], event_logger
66
+ ):
67
+ """Emit events for streaming response."""
68
+ emit_event(
69
+ ChoiceEvent(
70
+ index=0,
71
+ message={"content": accumulated_content, "role": "assistant"},
72
+ finish_reason=finish_reason or "unknown",
73
+ ),
74
+ event_logger,
75
+ )
76
+
77
+
78
+ def emit_event(
79
+ event: Union[MessageEvent, ChoiceEvent], event_logger: Union[EventLogger, None]
80
+ ) -> None:
81
+ """
82
+ Emit an event to the OpenTelemetry SDK.
83
+
84
+ Args:
85
+ event: The event to emit.
86
+ """
87
+ if not should_emit_events() or event_logger is None:
88
+ return
89
+
90
+ if isinstance(event, MessageEvent):
91
+ _emit_message_event(event, event_logger)
92
+ elif isinstance(event, ChoiceEvent):
93
+ _emit_choice_event(event, event_logger)
94
+ else:
95
+ raise TypeError("Unsupported event type")
96
+
97
+
98
+ def _emit_message_event(event: MessageEvent, event_logger: EventLogger) -> None:
99
+ body = asdict(event)
100
+
101
+ if event.role in VALID_MESSAGE_ROLES:
102
+ name = "gen_ai.{}.message".format(event.role)
103
+ # According to the semantic conventions, the role is conditionally required if available
104
+ # and not equal to the "role" in the message name. So, remove the role from the body if
105
+ # it is the same as the in the event name.
106
+ body.pop("role", None)
107
+ else:
108
+ name = "gen_ai.user.message"
109
+
110
+ # According to the semantic conventions, only the assistant role has tool call
111
+ if event.role != Roles.ASSISTANT.value and event.tool_calls is not None:
112
+ del body["tool_calls"]
113
+ elif event.tool_calls is None:
114
+ del body["tool_calls"]
115
+
116
+ if not should_send_prompts():
117
+ del body["content"]
118
+ if body.get("tool_calls") is not None:
119
+ for tool_call in body["tool_calls"]:
120
+ tool_call["function"].pop("arguments", None)
121
+
122
+ event_logger.emit(Event(name=name, body=body, attributes=EVENT_ATTRIBUTES))
123
+
124
+
125
+ def _emit_choice_event(event: ChoiceEvent, event_logger: EventLogger) -> None:
126
+ body = asdict(event)
127
+ if event.message["role"] == Roles.ASSISTANT.value:
128
+ # According to the semantic conventions, the role is conditionally required if available
129
+ # and not equal to "assistant", so remove the role from the body if it is "assistant".
130
+ body["message"].pop("role", None)
131
+
132
+ if event.tool_calls is None:
133
+ del body["tool_calls"]
134
+
135
+ if not should_send_prompts():
136
+ body["message"].pop("content", None)
137
+ if body.get("tool_calls") is not None:
138
+ for tool_call in body["tool_calls"]:
139
+ tool_call["function"].pop("arguments", None)
140
+
141
+ event_logger.emit(
142
+ Event(name="gen_ai.choice", body=body, attributes=EVENT_ATTRIBUTES)
143
+ )
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, List, Literal, Optional, TypedDict
3
+
4
+
5
+ class _FunctionToolCall(TypedDict):
6
+ function_name: str
7
+ arguments: Optional[dict[str, Any]]
8
+
9
+
10
+ class ToolCall(TypedDict):
11
+ """Represents a tool call in the AI model."""
12
+
13
+ id: str
14
+ function: _FunctionToolCall
15
+ type: Literal["function"]
16
+
17
+
18
+ class CompletionMessage(TypedDict):
19
+ """Represents a message in the AI model."""
20
+
21
+ content: Any
22
+ role: str = "assistant"
23
+
24
+
25
+ @dataclass
26
+ class MessageEvent:
27
+ """Represents an input event for the AI model."""
28
+
29
+ content: Any
30
+ role: str = "user"
31
+ tool_calls: Optional[List[ToolCall]] = None
32
+
33
+
34
+ @dataclass
35
+ class ChoiceEvent:
36
+ """Represents a completion event for the AI model."""
37
+
38
+ index: int
39
+ message: CompletionMessage
40
+ finish_reason: str = "unknown"
41
+ tool_calls: Optional[List[ToolCall]] = None
@@ -0,0 +1,233 @@
1
+ import json
2
+
3
+ from opentelemetry.instrumentation.groq.utils import (
4
+ dont_throw,
5
+ model_as_dict,
6
+ set_span_attribute,
7
+ should_send_prompts,
8
+ )
9
+ from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
10
+ GEN_AI_RESPONSE_ID,
11
+ )
12
+ from opentelemetry.semconv._incubating.attributes import (
13
+ gen_ai_attributes as GenAIAttributes,
14
+ )
15
+ from opentelemetry.semconv_ai import (
16
+ SpanAttributes,
17
+ )
18
+
19
+ CONTENT_FILTER_KEY = "content_filter_results"
20
+
21
+
22
+ @dont_throw
23
+ def set_input_attributes(span, kwargs):
24
+ if not span.is_recording():
25
+ return
26
+
27
+ if should_send_prompts():
28
+ if kwargs.get("prompt") is not None:
29
+ set_span_attribute(
30
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.0.user", kwargs.get("prompt")
31
+ )
32
+
33
+ elif kwargs.get("messages") is not None:
34
+ for i, message in enumerate(kwargs.get("messages")):
35
+ set_span_attribute(
36
+ span,
37
+ f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.content",
38
+ _dump_content(message.get("content")),
39
+ )
40
+ set_span_attribute(
41
+ span, f"{GenAIAttributes.GEN_AI_PROMPT}.{i}.role", message.get("role")
42
+ )
43
+
44
+
45
+ @dont_throw
46
+ def set_model_input_attributes(span, kwargs):
47
+ if not span.is_recording():
48
+ return
49
+
50
+ set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_MODEL, kwargs.get("model"))
51
+ set_span_attribute(
52
+ span, GenAIAttributes.GEN_AI_REQUEST_MAX_TOKENS, kwargs.get("max_tokens_to_sample")
53
+ )
54
+ set_span_attribute(
55
+ span, GenAIAttributes.GEN_AI_REQUEST_TEMPERATURE, kwargs.get("temperature")
56
+ )
57
+ set_span_attribute(span, GenAIAttributes.GEN_AI_REQUEST_TOP_P, kwargs.get("top_p"))
58
+ set_span_attribute(
59
+ span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
60
+ )
61
+ set_span_attribute(
62
+ span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
63
+ )
64
+ set_span_attribute(
65
+ span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False
66
+ )
67
+
68
+
69
+ def set_streaming_response_attributes(
70
+ span, accumulated_content, finish_reason=None, usage=None
71
+ ):
72
+ """Set span attributes for accumulated streaming response."""
73
+ if not span.is_recording() or not should_send_prompts():
74
+ return
75
+
76
+ prefix = f"{GenAIAttributes.GEN_AI_COMPLETION}.0"
77
+ set_span_attribute(span, f"{prefix}.role", "assistant")
78
+ set_span_attribute(span, f"{prefix}.content", accumulated_content)
79
+ if finish_reason:
80
+ set_span_attribute(span, f"{prefix}.finish_reason", finish_reason)
81
+
82
+
83
+ def set_model_streaming_response_attributes(span, usage):
84
+ if not span.is_recording():
85
+ return
86
+
87
+ if usage:
88
+ set_span_attribute(
89
+ span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, usage.completion_tokens
90
+ )
91
+ set_span_attribute(
92
+ span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, usage.prompt_tokens
93
+ )
94
+ set_span_attribute(
95
+ span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.total_tokens
96
+ )
97
+
98
+
99
+ @dont_throw
100
+ def set_model_response_attributes(span, response, token_histogram):
101
+ if not span.is_recording():
102
+ return
103
+ response = model_as_dict(response)
104
+ set_span_attribute(span, GenAIAttributes.GEN_AI_RESPONSE_MODEL, response.get("model"))
105
+ set_span_attribute(span, GEN_AI_RESPONSE_ID, response.get("id"))
106
+
107
+ usage = response.get("usage") or {}
108
+ prompt_tokens = usage.get("prompt_tokens")
109
+ completion_tokens = usage.get("completion_tokens")
110
+ if usage:
111
+ set_span_attribute(
112
+ span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.get("total_tokens")
113
+ )
114
+ set_span_attribute(
115
+ span, GenAIAttributes.GEN_AI_USAGE_OUTPUT_TOKENS, completion_tokens
116
+ )
117
+ set_span_attribute(span, GenAIAttributes.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens)
118
+
119
+ if (
120
+ isinstance(prompt_tokens, int)
121
+ and prompt_tokens >= 0
122
+ and token_histogram is not None
123
+ ):
124
+ token_histogram.record(
125
+ prompt_tokens,
126
+ attributes={
127
+ GenAIAttributes.GEN_AI_TOKEN_TYPE: "input",
128
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"),
129
+ },
130
+ )
131
+
132
+ if (
133
+ isinstance(completion_tokens, int)
134
+ and completion_tokens >= 0
135
+ and token_histogram is not None
136
+ ):
137
+ token_histogram.record(
138
+ completion_tokens,
139
+ attributes={
140
+ GenAIAttributes.GEN_AI_TOKEN_TYPE: "output",
141
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response.get("model"),
142
+ },
143
+ )
144
+
145
+
146
+ def set_response_attributes(span, response):
147
+ if not span.is_recording():
148
+ return
149
+ choices = model_as_dict(response).get("choices")
150
+ if should_send_prompts() and choices:
151
+ _set_completions(span, choices)
152
+
153
+
154
+ def _set_completions(span, choices):
155
+ if choices is None or not should_send_prompts():
156
+ return
157
+
158
+ for choice in choices:
159
+ index = choice.get("index")
160
+ prefix = f"{GenAIAttributes.GEN_AI_COMPLETION}.{index}"
161
+ set_span_attribute(span, f"{prefix}.finish_reason", choice.get("finish_reason"))
162
+
163
+ if choice.get("content_filter_results"):
164
+ set_span_attribute(
165
+ span,
166
+ f"{prefix}.{CONTENT_FILTER_KEY}",
167
+ json.dumps(choice.get("content_filter_results")),
168
+ )
169
+
170
+ if choice.get("finish_reason") == "content_filter":
171
+ set_span_attribute(span, f"{prefix}.role", "assistant")
172
+ set_span_attribute(span, f"{prefix}.content", "FILTERED")
173
+
174
+ return
175
+
176
+ message = choice.get("message")
177
+ if not message:
178
+ return
179
+
180
+ set_span_attribute(span, f"{prefix}.role", message.get("role"))
181
+ set_span_attribute(span, f"{prefix}.content", message.get("content"))
182
+
183
+ function_call = message.get("function_call")
184
+ if function_call:
185
+ set_span_attribute(
186
+ span, f"{prefix}.tool_calls.0.name", function_call.get("name")
187
+ )
188
+ set_span_attribute(
189
+ span,
190
+ f"{prefix}.tool_calls.0.arguments",
191
+ function_call.get("arguments"),
192
+ )
193
+
194
+ tool_calls = message.get("tool_calls")
195
+ if tool_calls:
196
+ for i, tool_call in enumerate(tool_calls):
197
+ function = tool_call.get("function")
198
+ set_span_attribute(
199
+ span,
200
+ f"{prefix}.tool_calls.{i}.id",
201
+ tool_call.get("id"),
202
+ )
203
+ set_span_attribute(
204
+ span,
205
+ f"{prefix}.tool_calls.{i}.name",
206
+ function.get("name"),
207
+ )
208
+ set_span_attribute(
209
+ span,
210
+ f"{prefix}.tool_calls.{i}.arguments",
211
+ function.get("arguments"),
212
+ )
213
+
214
+
215
+ def _dump_content(content):
216
+ if isinstance(content, str):
217
+ return content
218
+ json_serializable = []
219
+ for item in content:
220
+ if item.get("type") == "text":
221
+ json_serializable.append({"type": "text", "text": item.get("text")})
222
+ elif item.get("type") == "image":
223
+ json_serializable.append(
224
+ {
225
+ "type": "image",
226
+ "source": {
227
+ "type": item.get("source").get("type"),
228
+ "media_type": item.get("source").get("media_type"),
229
+ "data": str(item.get("source").get("data")),
230
+ },
231
+ }
232
+ )
233
+ return json.dumps(json_serializable)
@@ -1,16 +1,21 @@
1
- from importlib.metadata import version
2
- import os
3
1
  import logging
2
+ import os
4
3
  import traceback
4
+ from importlib.metadata import version
5
+
5
6
  from opentelemetry import context as context_api
6
7
  from opentelemetry.instrumentation.groq.config import Config
7
- from opentelemetry.semconv_ai import SpanAttributes
8
+ from opentelemetry.semconv._incubating.attributes import (
9
+ gen_ai_attributes as GenAIAttributes,
10
+ )
8
11
 
9
12
  GEN_AI_SYSTEM = "gen_ai.system"
10
13
  GEN_AI_SYSTEM_GROQ = "groq"
11
14
 
12
15
  _PYDANTIC_VERSION = version("pydantic")
13
16
 
17
+ TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"
18
+
14
19
 
15
20
  def set_span_attribute(span, name, value):
16
21
  if value is not None and value != "":
@@ -19,7 +24,7 @@ def set_span_attribute(span, name, value):
19
24
 
20
25
  def should_send_prompts():
21
26
  return (
22
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
27
+ os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
23
28
  ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
24
29
 
25
30
 
@@ -57,7 +62,7 @@ def shared_metrics_attributes(response):
57
62
  return {
58
63
  **common_attributes,
59
64
  GEN_AI_SYSTEM: GEN_AI_SYSTEM_GROQ,
60
- SpanAttributes.LLM_RESPONSE_MODEL: response_dict.get("model"),
65
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_dict.get("model"),
61
66
  }
62
67
 
63
68
 
@@ -78,3 +83,12 @@ def model_as_dict(model):
78
83
  return model_as_dict(model.parse())
79
84
  else:
80
85
  return model
86
+
87
+
88
+ def should_emit_events() -> bool:
89
+ """
90
+ Checks if the instrumentation isn't using the legacy attributes
91
+ and if the event logger is not None.
92
+ """
93
+
94
+ return not Config.use_legacy_attributes
@@ -1 +1 @@
1
- __version__ = "0.40.2"
1
+ __version__ = "0.48.0"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-groq
3
- Version: 0.40.2
3
+ Version: 0.48.0
4
4
  Summary: OpenTelemetry Groq instrumentation
5
5
  License: Apache-2.0
6
6
  Author: Gal Kleinman
@@ -13,11 +13,12 @@ Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
16
17
  Provides-Extra: instruments
17
18
  Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
18
19
  Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
19
20
  Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
20
- Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.5)
21
+ Requires-Dist: opentelemetry-semantic-conventions-ai (>=0.4.13,<0.5.0)
21
22
  Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-groq
22
23
  Description-Content-Type: text/markdown
23
24
 
@@ -0,0 +1,11 @@
1
+ opentelemetry/instrumentation/groq/__init__.py,sha256=Be0lZGwA528PZRC3SmrELzZygeVBW4JVEGmdw2jOPd8,15021
2
+ opentelemetry/instrumentation/groq/config.py,sha256=c11xP2YLnXpKFwVlLh23_a-DV96dsfqRqOg6IWACNBI,172
3
+ opentelemetry/instrumentation/groq/event_emitter.py,sha256=m29pi5dSBIN1PGek6Mo_exifBy9vxECes1hdIEa1YaE,4549
4
+ opentelemetry/instrumentation/groq/event_models.py,sha256=PCfCGxrrArwZqR-4wFcXrhwQq0sBMAxmSrpC4PUMtaM,876
5
+ opentelemetry/instrumentation/groq/span_utils.py,sha256=D6PP9kljSmROGNFBkSaccUCh-d3_V0SUEQbwpjTjI2o,7738
6
+ opentelemetry/instrumentation/groq/utils.py,sha256=jSqYCmESVXDCOfJEPIvF6y5M5QoFx2qf7Jnw-r9TTyc,2464
7
+ opentelemetry/instrumentation/groq/version.py,sha256=bkYe4lEQZCEmFm0XRZaZkxTV1niMqR_lbp-tzKL6s6c,23
8
+ opentelemetry_instrumentation_groq-0.48.0.dist-info/METADATA,sha256=uw3O490pTgYTppJq5urVIScLUbfk16Kam-8FcqlJZ68,2176
9
+ opentelemetry_instrumentation_groq-0.48.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
10
+ opentelemetry_instrumentation_groq-0.48.0.dist-info/entry_points.txt,sha256=uezQe06CpIK8xTZZSK0lF29nOKkz_w6VR4sQnb4IAFQ,87
11
+ opentelemetry_instrumentation_groq-0.48.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,8 +0,0 @@
1
- opentelemetry/instrumentation/groq/__init__.py,sha256=E1blGTym8YomMT-bsUCrVSneSy-CZmHdtCWup19OpFw,20433
2
- opentelemetry/instrumentation/groq/config.py,sha256=eN2YxQdWlAF-qWPwZZr0xFM-8tx9zUjmiparuB64jcU,170
3
- opentelemetry/instrumentation/groq/utils.py,sha256=1ESL4NCp8Mjww8cGEzQO_AEqGiSK4JSiMFYUhwBnuao,2151
4
- opentelemetry/instrumentation/groq/version.py,sha256=GgBV43P17gCEFbfEWV0eXof7RjJb8K6WBCun28R_eEw,23
5
- opentelemetry_instrumentation_groq-0.40.2.dist-info/METADATA,sha256=rA1UQTy0-HB3cYLkuKMf2XaKfV-Z9YuaZ_Ul9nnQ-FI,2117
6
- opentelemetry_instrumentation_groq-0.40.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
7
- opentelemetry_instrumentation_groq-0.40.2.dist-info/entry_points.txt,sha256=uezQe06CpIK8xTZZSK0lF29nOKkz_w6VR4sQnb4IAFQ,87
8
- opentelemetry_instrumentation_groq-0.40.2.dist-info/RECORD,,