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.
- opentelemetry/instrumentation/groq/__init__.py +167 -189
- 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 +22 -6
- opentelemetry/instrumentation/groq/version.py +1 -1
- {opentelemetry_instrumentation_groq-0.33.6.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/METADATA +7 -7
- opentelemetry_instrumentation_groq-0.48.0.dist-info/RECORD +11 -0
- {opentelemetry_instrumentation_groq-0.33.6.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/WHEEL +1 -1
- opentelemetry_instrumentation_groq-0.33.6.dist-info/RECORD +0 -8
- {opentelemetry_instrumentation_groq-0.33.6.dist-info → opentelemetry_instrumentation_groq-0.48.0.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
248
|
+
GenAIAttributes.GEN_AI_SYSTEM: "groq",
|
|
293
249
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
294
250
|
},
|
|
295
251
|
)
|
|
296
252
|
|
|
297
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
484
|
+
f"{wrap_package}.{wrap_object}",
|
|
507
485
|
wrapped_method.get("method"),
|
|
508
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,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.
|
|
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(
|
|
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
|
-
|
|
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
|
|
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.
|
|
1
|
+
__version__ = "0.48.0"
|
|
@@ -1,8 +1,7 @@
|
|
|
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
|
-
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.
|
|
19
|
-
Requires-Dist: opentelemetry-instrumentation (>=0.
|
|
20
|
-
Requires-Dist: opentelemetry-semantic-conventions (>=0.
|
|
21
|
-
Requires-Dist: opentelemetry-semantic-conventions-ai (
|
|
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,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,,
|