opentelemetry-instrumentation-groq 0.33.6__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,41 +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
33
+ from opentelemetry.semconv._incubating.attributes import (
34
+ gen_ai_attributes as GenAIAttributes,
35
+ )
24
36
  from opentelemetry.semconv_ai import (
25
37
  SUPPRESS_LANGUAGE_MODEL_INSTRUMENTATION_KEY,
26
38
  LLMRequestTypeValues,
27
- SpanAttributes,
28
39
  Meters,
40
+ SpanAttributes,
29
41
  )
30
42
  from opentelemetry.trace import SpanKind, Tracer, get_tracer
31
43
  from opentelemetry.trace.status import Status, StatusCode
32
44
  from wrapt import wrap_function_wrapper
33
45
 
46
+ from groq._streaming import AsyncStream, Stream
47
+
34
48
  logger = logging.getLogger(__name__)
35
49
 
36
50
  _instruments = ("groq >= 0.9.0",)
37
51
 
38
- CONTENT_FILTER_KEY = "content_filter_results"
39
52
 
40
53
  WRAPPED_METHODS = [
41
54
  {
@@ -59,163 +72,6 @@ def is_streaming_response(response):
59
72
  return isinstance(response, Stream) or isinstance(response, AsyncStream)
60
73
 
61
74
 
62
- def _dump_content(content):
63
- if isinstance(content, str):
64
- return content
65
- json_serializable = []
66
- for item in content:
67
- if item.get("type") == "text":
68
- json_serializable.append({"type": "text", "text": item.get("text")})
69
- elif item.get("type") == "image":
70
- json_serializable.append(
71
- {
72
- "type": "image",
73
- "source": {
74
- "type": item.get("source").get("type"),
75
- "media_type": item.get("source").get("media_type"),
76
- "data": str(item.get("source").get("data")),
77
- },
78
- }
79
- )
80
- return json.dumps(json_serializable)
81
-
82
-
83
- @dont_throw
84
- def _set_input_attributes(span, kwargs):
85
- set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, kwargs.get("model"))
86
- set_span_attribute(
87
- span, SpanAttributes.LLM_REQUEST_MAX_TOKENS, kwargs.get("max_tokens_to_sample")
88
- )
89
- set_span_attribute(
90
- span, SpanAttributes.LLM_REQUEST_TEMPERATURE, kwargs.get("temperature")
91
- )
92
- set_span_attribute(span, SpanAttributes.LLM_REQUEST_TOP_P, kwargs.get("top_p"))
93
- set_span_attribute(
94
- span, SpanAttributes.LLM_FREQUENCY_PENALTY, kwargs.get("frequency_penalty")
95
- )
96
- set_span_attribute(
97
- span, SpanAttributes.LLM_PRESENCE_PENALTY, kwargs.get("presence_penalty")
98
- )
99
- set_span_attribute(span, SpanAttributes.LLM_IS_STREAMING, kwargs.get("stream") or False)
100
-
101
- if should_send_prompts():
102
- if kwargs.get("prompt") is not None:
103
- set_span_attribute(
104
- span, f"{SpanAttributes.LLM_PROMPTS}.0.user", kwargs.get("prompt")
105
- )
106
-
107
- elif kwargs.get("messages") is not None:
108
- for i, message in enumerate(kwargs.get("messages")):
109
- set_span_attribute(
110
- span,
111
- f"{SpanAttributes.LLM_PROMPTS}.{i}.content",
112
- _dump_content(message.get("content")),
113
- )
114
- set_span_attribute(
115
- span, f"{SpanAttributes.LLM_PROMPTS}.{i}.role", message.get("role")
116
- )
117
-
118
-
119
- def _set_completions(span, choices):
120
- if choices is None:
121
- return
122
-
123
- for choice in choices:
124
- index = choice.get("index")
125
- prefix = f"{SpanAttributes.LLM_COMPLETIONS}.{index}"
126
- set_span_attribute(
127
- span, f"{prefix}.finish_reason", choice.get("finish_reason")
128
- )
129
-
130
- if choice.get("content_filter_results"):
131
- set_span_attribute(
132
- span,
133
- f"{prefix}.{CONTENT_FILTER_KEY}",
134
- json.dumps(choice.get("content_filter_results")),
135
- )
136
-
137
- if choice.get("finish_reason") == "content_filter":
138
- set_span_attribute(span, f"{prefix}.role", "assistant")
139
- set_span_attribute(span, f"{prefix}.content", "FILTERED")
140
-
141
- return
142
-
143
- message = choice.get("message")
144
- if not message:
145
- return
146
-
147
- set_span_attribute(span, f"{prefix}.role", message.get("role"))
148
- set_span_attribute(span, f"{prefix}.content", message.get("content"))
149
-
150
- function_call = message.get("function_call")
151
- if function_call:
152
- set_span_attribute(
153
- span, f"{prefix}.tool_calls.0.name", function_call.get("name")
154
- )
155
- set_span_attribute(
156
- span,
157
- f"{prefix}.tool_calls.0.arguments",
158
- function_call.get("arguments"),
159
- )
160
-
161
- tool_calls = message.get("tool_calls")
162
- if tool_calls:
163
- for i, tool_call in enumerate(tool_calls):
164
- function = tool_call.get("function")
165
- set_span_attribute(
166
- span,
167
- f"{prefix}.tool_calls.{i}.id",
168
- tool_call.get("id"),
169
- )
170
- set_span_attribute(
171
- span,
172
- f"{prefix}.tool_calls.{i}.name",
173
- function.get("name"),
174
- )
175
- set_span_attribute(
176
- span,
177
- f"{prefix}.tool_calls.{i}.arguments",
178
- function.get("arguments"),
179
- )
180
-
181
-
182
- @dont_throw
183
- def _set_response_attributes(span, response):
184
- response = model_as_dict(response)
185
-
186
- set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, response.get("model"))
187
-
188
- usage = response.get("usage")
189
- if usage:
190
- set_span_attribute(
191
- span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, usage.get("total_tokens")
192
- )
193
- set_span_attribute(
194
- span,
195
- SpanAttributes.LLM_USAGE_COMPLETION_TOKENS,
196
- usage.get("completion_tokens"),
197
- )
198
- set_span_attribute(
199
- span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, usage.get("prompt_tokens")
200
- )
201
-
202
- choices = response.get("choices")
203
- if should_send_prompts() and choices:
204
- _set_completions(span, choices)
205
-
206
-
207
- def _with_tracer_wrapper(func):
208
- """Helper for providing tracer for wrapper functions."""
209
-
210
- def _with_tracer(tracer, to_wrap):
211
- def wrapper(wrapped, instance, args, kwargs):
212
- return func(tracer, to_wrap, wrapped, instance, args, kwargs)
213
-
214
- return wrapper
215
-
216
- return _with_tracer
217
-
218
-
219
75
  def _with_chat_telemetry_wrapper(func):
220
76
  """Helper for providing tracer for wrapper functions. Includes metric collectors."""
221
77
 
@@ -224,6 +80,7 @@ def _with_chat_telemetry_wrapper(func):
224
80
  token_histogram,
225
81
  choice_counter,
226
82
  duration_histogram,
83
+ event_logger,
227
84
  to_wrap,
228
85
  ):
229
86
  def wrapper(wrapped, instance, args, kwargs):
@@ -232,6 +89,7 @@ def _with_chat_telemetry_wrapper(func):
232
89
  token_histogram,
233
90
  choice_counter,
234
91
  duration_histogram,
92
+ event_logger,
235
93
  to_wrap,
236
94
  wrapped,
237
95
  instance,
@@ -266,12 +124,110 @@ def _create_metrics(meter: Meter):
266
124
  return token_histogram, choice_counter, duration_histogram
267
125
 
268
126
 
127
+ def _process_streaming_chunk(chunk):
128
+ """Extract content, finish_reason and usage from a streaming chunk."""
129
+ if not chunk.choices:
130
+ return None, None, None
131
+
132
+ delta = chunk.choices[0].delta
133
+ content = delta.content if hasattr(delta, "content") else None
134
+ finish_reason = chunk.choices[0].finish_reason
135
+
136
+ # Extract usage from x_groq if present in the final chunk
137
+ usage = None
138
+ if hasattr(chunk, "x_groq") and chunk.x_groq and chunk.x_groq.usage:
139
+ usage = chunk.x_groq.usage
140
+
141
+ return content, finish_reason, usage
142
+
143
+
144
+ def _handle_streaming_response(
145
+ span, accumulated_content, finish_reason, usage, event_logger
146
+ ):
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
153
+ )
154
+
155
+
156
+ def _create_stream_processor(response, span, event_logger):
157
+ """Create a generator that processes a stream while collecting telemetry."""
158
+ accumulated_content = ""
159
+ finish_reason = None
160
+ usage = None
161
+
162
+ for chunk in response:
163
+ content, chunk_finish_reason, chunk_usage = _process_streaming_chunk(chunk)
164
+ if content:
165
+ accumulated_content += content
166
+ if chunk_finish_reason:
167
+ finish_reason = chunk_finish_reason
168
+ if chunk_usage:
169
+ usage = chunk_usage
170
+ yield chunk
171
+
172
+ _handle_streaming_response(
173
+ span, accumulated_content, finish_reason, usage, event_logger
174
+ )
175
+
176
+ if span.is_recording():
177
+ span.set_status(Status(StatusCode.OK))
178
+
179
+ span.end()
180
+
181
+
182
+ async def _create_async_stream_processor(response, span, event_logger):
183
+ """Create an async generator that processes a stream while collecting telemetry."""
184
+ accumulated_content = ""
185
+ finish_reason = None
186
+ usage = None
187
+
188
+ async for chunk in response:
189
+ content, chunk_finish_reason, chunk_usage = _process_streaming_chunk(chunk)
190
+ if content:
191
+ accumulated_content += content
192
+ if chunk_finish_reason:
193
+ finish_reason = chunk_finish_reason
194
+ if chunk_usage:
195
+ usage = chunk_usage
196
+ yield chunk
197
+
198
+ _handle_streaming_response(
199
+ span, accumulated_content, finish_reason, usage, event_logger
200
+ )
201
+
202
+ if span.is_recording():
203
+ span.set_status(Status(StatusCode.OK))
204
+
205
+ span.end()
206
+
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
+
269
224
  @_with_chat_telemetry_wrapper
270
225
  def _wrap(
271
226
  tracer: Tracer,
272
227
  token_histogram: Histogram,
273
228
  choice_counter: Counter,
274
229
  duration_histogram: Histogram,
230
+ event_logger: Union[EventLogger, None],
275
231
  to_wrap,
276
232
  wrapped,
277
233
  instance,
@@ -289,13 +245,12 @@ def _wrap(
289
245
  name,
290
246
  kind=SpanKind.CLIENT,
291
247
  attributes={
292
- SpanAttributes.LLM_SYSTEM: "Groq",
248
+ GenAIAttributes.GEN_AI_SYSTEM: "groq",
293
249
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
294
250
  },
295
251
  )
296
252
 
297
- if span.is_recording():
298
- _set_input_attributes(span, kwargs)
253
+ _handle_input(span, kwargs, event_logger)
299
254
 
300
255
  start_time = time.time()
301
256
  try:
@@ -313,8 +268,16 @@ def _wrap(
313
268
  end_time = time.time()
314
269
 
315
270
  if is_streaming_response(response):
316
- # TODO: implement streaming
317
- pass
271
+ try:
272
+ return _create_stream_processor(response, span, event_logger)
273
+ except Exception as ex:
274
+ logger.warning(
275
+ "Failed to process streaming response for groq span, error: %s",
276
+ str(ex),
277
+ )
278
+ span.set_status(Status(StatusCode.ERROR))
279
+ span.end()
280
+ raise
318
281
  elif response:
319
282
  try:
320
283
  metric_attributes = shared_metrics_attributes(response)
@@ -326,14 +289,14 @@ def _wrap(
326
289
  attributes=metric_attributes,
327
290
  )
328
291
 
329
- if span.is_recording():
330
- _set_response_attributes(span, response)
292
+ _handle_response(span, response, token_histogram, event_logger)
331
293
 
332
294
  except Exception as ex: # pylint: disable=broad-except
333
295
  logger.warning(
334
296
  "Failed to set response attributes for groq span, error: %s",
335
297
  str(ex),
336
298
  )
299
+
337
300
  if span.is_recording():
338
301
  span.set_status(Status(StatusCode.OK))
339
302
  span.end()
@@ -346,6 +309,7 @@ async def _awrap(
346
309
  token_histogram: Histogram,
347
310
  choice_counter: Counter,
348
311
  duration_histogram: Histogram,
312
+ event_logger: Union[EventLogger, None],
349
313
  to_wrap,
350
314
  wrapped,
351
315
  instance,
@@ -363,20 +327,15 @@ async def _awrap(
363
327
  name,
364
328
  kind=SpanKind.CLIENT,
365
329
  attributes={
366
- SpanAttributes.LLM_SYSTEM: "Groq",
330
+ GenAIAttributes.GEN_AI_SYSTEM: "groq",
367
331
  SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
368
332
  },
369
333
  )
370
- try:
371
- if span.is_recording():
372
- _set_input_attributes(span, kwargs)
373
334
 
374
- except Exception as ex: # pylint: disable=broad-except
375
- logger.warning(
376
- "Failed to set input attributes for groq span, error: %s", str(ex)
377
- )
335
+ _handle_input(span, kwargs, event_logger)
378
336
 
379
337
  start_time = time.time()
338
+
380
339
  try:
381
340
  response = await wrapped(*args, **kwargs)
382
341
  except Exception as e: # pylint: disable=broad-except
@@ -389,9 +348,19 @@ async def _awrap(
389
348
 
390
349
  raise e
391
350
 
351
+ end_time = time.time()
352
+
392
353
  if is_streaming_response(response):
393
- # TODO: implement streaming
394
- pass
354
+ try:
355
+ return await _create_async_stream_processor(response, span, event_logger)
356
+ except Exception as ex:
357
+ logger.warning(
358
+ "Failed to process streaming response for groq span, error: %s",
359
+ str(ex),
360
+ )
361
+ span.set_status(Status(StatusCode.ERROR))
362
+ span.end()
363
+ raise
395
364
  elif response:
396
365
  metric_attributes = shared_metrics_attributes(response)
397
366
 
@@ -402,8 +371,7 @@ async def _awrap(
402
371
  attributes=metric_attributes,
403
372
  )
404
373
 
405
- if span.is_recording():
406
- _set_response_attributes(span, response)
374
+ _handle_response(span, response, token_histogram, event_logger)
407
375
 
408
376
  if span.is_recording():
409
377
  span.set_status(Status(StatusCode.OK))
@@ -420,14 +388,14 @@ class GroqInstrumentor(BaseInstrumentor):
420
388
 
421
389
  def __init__(
422
390
  self,
423
- enrich_token_usage: bool = False,
424
391
  exception_logger=None,
392
+ use_legacy_attributes: bool = True,
425
393
  get_common_metrics_attributes: Callable[[], dict] = lambda: {},
426
394
  ):
427
395
  super().__init__()
428
396
  Config.exception_logger = exception_logger
429
- Config.enrich_token_usage = enrich_token_usage
430
397
  Config.get_common_metrics_attributes = get_common_metrics_attributes
398
+ Config.use_legacy_attributes = use_legacy_attributes
431
399
 
432
400
  def instrumentation_dependencies(self) -> Collection[str]:
433
401
  return _instruments
@@ -451,7 +419,14 @@ class GroqInstrumentor(BaseInstrumentor):
451
419
  token_histogram,
452
420
  choice_counter,
453
421
  duration_histogram,
454
- ) = (None, None, None, None)
422
+ ) = (None, None, None)
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
+ )
455
430
 
456
431
  for wrapped_method in WRAPPED_METHODS:
457
432
  wrap_package = wrapped_method.get("package")
@@ -467,6 +442,7 @@ class GroqInstrumentor(BaseInstrumentor):
467
442
  token_histogram,
468
443
  choice_counter,
469
444
  duration_histogram,
445
+ event_logger,
470
446
  wrapped_method,
471
447
  ),
472
448
  )
@@ -486,6 +462,7 @@ class GroqInstrumentor(BaseInstrumentor):
486
462
  token_histogram,
487
463
  choice_counter,
488
464
  duration_histogram,
465
+ event_logger,
489
466
  wrapped_method,
490
467
  ),
491
468
  )
@@ -501,8 +478,9 @@ class GroqInstrumentor(BaseInstrumentor):
501
478
  wrapped_method.get("method"),
502
479
  )
503
480
  for wrapped_method in WRAPPED_AMETHODS:
481
+ wrap_package = wrapped_method.get("package")
504
482
  wrap_object = wrapped_method.get("object")
505
483
  unwrap(
506
- f"groq.resources.completions.{wrap_object}",
484
+ f"{wrap_package}.{wrap_object}",
507
485
  wrapped_method.get("method"),
508
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,14 +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
 
15
+ _PYDANTIC_VERSION = version("pydantic")
16
+
17
+ TRACELOOP_TRACE_CONTENT = "TRACELOOP_TRACE_CONTENT"
18
+
12
19
 
13
20
  def set_span_attribute(span, name, value):
14
21
  if value is not None and value != "":
@@ -17,7 +24,7 @@ def set_span_attribute(span, name, value):
17
24
 
18
25
  def should_send_prompts():
19
26
  return (
20
- os.getenv("TRACELOOP_TRACE_CONTENT") or "true"
27
+ os.getenv(TRACELOOP_TRACE_CONTENT) or "true"
21
28
  ).lower() == "true" or context_api.get_value("override_enable_content_tracing")
22
29
 
23
30
 
@@ -55,7 +62,7 @@ def shared_metrics_attributes(response):
55
62
  return {
56
63
  **common_attributes,
57
64
  GEN_AI_SYSTEM: GEN_AI_SYSTEM_GROQ,
58
- SpanAttributes.LLM_RESPONSE_MODEL: response_dict.get("model"),
65
+ GenAIAttributes.GEN_AI_RESPONSE_MODEL: response_dict.get("model"),
59
66
  }
60
67
 
61
68
 
@@ -68,7 +75,7 @@ def error_metrics_attributes(exception):
68
75
 
69
76
 
70
77
  def model_as_dict(model):
71
- if version("pydantic") < "2.0.0":
78
+ if _PYDANTIC_VERSION < "2.0.0":
72
79
  return model.dict()
73
80
  if hasattr(model, "model_dump"):
74
81
  return model.model_dump()
@@ -76,3 +83,12 @@ def model_as_dict(model):
76
83
  return model_as_dict(model.parse())
77
84
  else:
78
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.33.6"
1
+ __version__ = "0.48.0"
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: opentelemetry-instrumentation-groq
3
- Version: 0.33.6
3
+ Version: 0.48.0
4
4
  Summary: OpenTelemetry Groq instrumentation
5
- Home-page: https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-groq
6
5
  License: Apache-2.0
7
6
  Author: Gal Kleinman
8
7
  Author-email: gal@traceloop.com
@@ -14,11 +13,12 @@ Classifier: Programming Language :: Python :: 3.10
14
13
  Classifier: Programming Language :: Python :: 3.11
15
14
  Classifier: Programming Language :: Python :: 3.12
16
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
17
  Provides-Extra: instruments
18
- Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0)
19
- Requires-Dist: opentelemetry-instrumentation (>=0.48b0,<0.49)
20
- Requires-Dist: opentelemetry-semantic-conventions (>=0.48b0,<0.49)
21
- Requires-Dist: opentelemetry-semantic-conventions-ai (==0.4.2)
18
+ Requires-Dist: opentelemetry-api (>=1.28.0,<2.0.0)
19
+ Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
20
+ Requires-Dist: opentelemetry-semantic-conventions (>=0.50b0)
21
+ Requires-Dist: opentelemetry-semantic-conventions-ai (>=0.4.13,<0.5.0)
22
22
  Project-URL: Repository, https://github.com/traceloop/openllmetry/tree/main/packages/opentelemetry-instrumentation-groq
23
23
  Description-Content-Type: text/markdown
24
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 1.9.1
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=JllxoBhhu4XKVTAeRE6_KvCNwvzJz-hQmNfQqHU4O_8,15885
2
- opentelemetry/instrumentation/groq/config.py,sha256=eN2YxQdWlAF-qWPwZZr0xFM-8tx9zUjmiparuB64jcU,170
3
- opentelemetry/instrumentation/groq/utils.py,sha256=goqxOrjnQgklCclkVjEekZWxUakuDgV_StB4XmP67sY,2112
4
- opentelemetry/instrumentation/groq/version.py,sha256=5Y0NtoRrd9Mt0esLjATqHKGOX_Jre-ex7yMzoNdSwtY,23
5
- opentelemetry_instrumentation_groq-0.33.6.dist-info/METADATA,sha256=RgoRVhcLpbC85wz6l5kNRx2-NHz9mIGGB-NWc8rjeqs,2235
6
- opentelemetry_instrumentation_groq-0.33.6.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
7
- opentelemetry_instrumentation_groq-0.33.6.dist-info/entry_points.txt,sha256=uezQe06CpIK8xTZZSK0lF29nOKkz_w6VR4sQnb4IAFQ,87
8
- opentelemetry_instrumentation_groq-0.33.6.dist-info/RECORD,,