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.
- opentelemetry/instrumentation/groq/__init__.py +84 -241
- opentelemetry/instrumentation/groq/config.py +1 -1
- opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- opentelemetry/instrumentation/groq/event_models.py +41 -0
- opentelemetry/instrumentation/groq/span_utils.py +233 -0
- opentelemetry/instrumentation/groq/utils.py +19 -5
- opentelemetry/instrumentation/groq/version.py +1 -1
- {opentelemetry_instrumentation_groq-0.40.2.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/METADATA +4 -3
- opentelemetry_instrumentation_groq-0.48.0.dist-info/RECORD +11 -0
- {opentelemetry_instrumentation_groq-0.40.2.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/WHEEL +1 -1
- opentelemetry_instrumentation_groq-0.40.2.dist-info/RECORD +0 -8
- {opentelemetry_instrumentation_groq-0.40.2.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
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
|
|
25
|
-
|
|
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
|
|
314
|
-
span, accumulated_content, finish_reason
|
|
144
|
+
def _handle_streaming_response(
|
|
145
|
+
span, accumulated_content, finish_reason, usage, event_logger
|
|
315
146
|
):
|
|
316
|
-
|
|
317
|
-
if
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
248
|
+
GenAIAttributes.GEN_AI_SYSTEM: "groq",
|
|
410
249
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
411
250
|
},
|
|
412
251
|
)
|
|
413
252
|
|
|
414
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"
|
|
484
|
+
f"{wrap_package}.{wrap_object}",
|
|
642
485
|
wrapped_method.get("method"),
|
|
643
486
|
)
|
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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.
|
|
1
|
+
__version__ = "0.48.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: opentelemetry-instrumentation-groq
|
|
3
|
-
Version: 0.
|
|
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 (
|
|
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,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,,
|