lmnr 0.6.16__py3-none-any.whl → 0.7.26__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.
- lmnr/__init__.py +6 -15
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/{cli.py → cli/evals.py} +20 -102
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +9 -2
- lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
- lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
- lmnr/opentelemetry_lib/litellm/utils.py +82 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
- lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
- lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
- lmnr/opentelemetry_lib/tracing/processor.py +128 -30
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +12 -12
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +18 -44
- lmnr/sdk/browser/playwright_otel.py +104 -187
- lmnr/sdk/browser/pw_utils.py +249 -210
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +1 -1
- lmnr/sdk/client/asynchronous/async_client.py +47 -15
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
- lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +83 -17
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +4 -10
- lmnr/sdk/client/synchronous/sync_client.py +47 -15
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +103 -23
- lmnr/sdk/evaluations.py +122 -33
- lmnr/sdk/laminar.py +816 -333
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +124 -143
- lmnr/sdk/utils.py +115 -2
- lmnr/version.py +1 -1
- {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
- lmnr/sdk/client/synchronous/resources/agent.py +0 -323
- lmnr/sdk/datasets.py +0 -60
- lmnr-0.6.16.dist-info/LICENSE +0 -75
- lmnr-0.6.16.dist-info/RECORD +0 -61
- lmnr-0.6.16.dist-info/WHEEL +0 -4
- lmnr-0.6.16.dist-info/entry_points.txt +0 -3
|
@@ -8,15 +8,24 @@ from typing import AsyncGenerator, Callable, Collection, Generator
|
|
|
8
8
|
|
|
9
9
|
from google.genai import types
|
|
10
10
|
|
|
11
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
|
12
|
+
get_current_context,
|
|
13
|
+
get_event_attributes_from_context,
|
|
14
|
+
)
|
|
15
|
+
from lmnr.sdk.utils import json_dumps
|
|
16
|
+
|
|
11
17
|
from .config import (
|
|
12
18
|
Config,
|
|
13
19
|
)
|
|
20
|
+
from .schema_utils import SchemaJSONEncoder, process_schema
|
|
14
21
|
from .utils import (
|
|
15
22
|
dont_throw,
|
|
16
23
|
get_content,
|
|
24
|
+
merge_text_parts,
|
|
25
|
+
process_content_union,
|
|
26
|
+
process_stream_chunk,
|
|
17
27
|
role_from_content_union,
|
|
18
28
|
set_span_attribute,
|
|
19
|
-
process_content_union,
|
|
20
29
|
to_dict,
|
|
21
30
|
with_tracer_wrapper,
|
|
22
31
|
)
|
|
@@ -24,8 +33,9 @@ from opentelemetry.trace import Tracer
|
|
|
24
33
|
from wrapt import wrap_function_wrapper
|
|
25
34
|
|
|
26
35
|
from opentelemetry import context as context_api
|
|
27
|
-
from opentelemetry.trace import get_tracer, SpanKind, Span
|
|
36
|
+
from opentelemetry.trace import get_tracer, SpanKind, Span, Status, StatusCode
|
|
28
37
|
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes
|
|
38
|
+
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
|
29
39
|
|
|
30
40
|
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
31
41
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY, unwrap
|
|
@@ -78,7 +88,7 @@ WRAPPED_METHODS = [
|
|
|
78
88
|
|
|
79
89
|
def should_send_prompts():
|
|
80
90
|
return (
|
|
81
|
-
os.getenv("
|
|
91
|
+
os.getenv("LAMINAR_TRACE_CONTENT") or "true"
|
|
82
92
|
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
|
|
83
93
|
|
|
84
94
|
|
|
@@ -128,6 +138,25 @@ def _set_request_attributes(span, args, kwargs):
|
|
|
128
138
|
span, gen_ai_attributes.GEN_AI_REQUEST_SEED, config_dict.get("seed")
|
|
129
139
|
)
|
|
130
140
|
|
|
141
|
+
if schema := config_dict.get("response_schema"):
|
|
142
|
+
try:
|
|
143
|
+
set_span_attribute(
|
|
144
|
+
span,
|
|
145
|
+
SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA,
|
|
146
|
+
json.dumps(process_schema(schema), cls=SchemaJSONEncoder),
|
|
147
|
+
)
|
|
148
|
+
except Exception:
|
|
149
|
+
pass
|
|
150
|
+
elif json_schema := config_dict.get("response_json_schema"):
|
|
151
|
+
try:
|
|
152
|
+
set_span_attribute(
|
|
153
|
+
span,
|
|
154
|
+
SpanAttributes.LLM_REQUEST_STRUCTURED_OUTPUT_SCHEMA,
|
|
155
|
+
json_dumps(json_schema),
|
|
156
|
+
)
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
|
|
131
160
|
tools: list[types.FunctionDeclaration] = []
|
|
132
161
|
arg_tools = config_dict.get("tools", kwargs.get("tools"))
|
|
133
162
|
if arg_tools:
|
|
@@ -152,7 +181,7 @@ def _set_request_attributes(span, args, kwargs):
|
|
|
152
181
|
set_span_attribute(
|
|
153
182
|
span,
|
|
154
183
|
f"{SpanAttributes.LLM_REQUEST_FUNCTIONS}.{tool_num}.parameters",
|
|
155
|
-
|
|
184
|
+
json_dumps(tool_dict.get("parameters")),
|
|
156
185
|
)
|
|
157
186
|
|
|
158
187
|
if should_send_prompts():
|
|
@@ -177,15 +206,17 @@ def _set_request_attributes(span, args, kwargs):
|
|
|
177
206
|
contents = [contents]
|
|
178
207
|
for content in contents:
|
|
179
208
|
processed_content = process_content_union(content)
|
|
180
|
-
|
|
209
|
+
content_payload = get_content(processed_content)
|
|
210
|
+
if isinstance(content_payload, dict):
|
|
211
|
+
content_payload = [content_payload]
|
|
181
212
|
|
|
182
213
|
set_span_attribute(
|
|
183
214
|
span,
|
|
184
215
|
f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.content",
|
|
185
216
|
(
|
|
186
|
-
|
|
187
|
-
if isinstance(
|
|
188
|
-
else
|
|
217
|
+
content_payload
|
|
218
|
+
if isinstance(content_payload, str)
|
|
219
|
+
else json_dumps(content_payload)
|
|
189
220
|
),
|
|
190
221
|
)
|
|
191
222
|
blocks = (
|
|
@@ -218,7 +249,7 @@ def _set_request_attributes(span, args, kwargs):
|
|
|
218
249
|
set_span_attribute(
|
|
219
250
|
span,
|
|
220
251
|
f"{gen_ai_attributes.GEN_AI_PROMPT}.{i}.tool_calls.{tool_call_index}.arguments",
|
|
221
|
-
|
|
252
|
+
json_dumps(function_call.get("arguments")),
|
|
222
253
|
)
|
|
223
254
|
tool_call_index += 1
|
|
224
255
|
|
|
@@ -244,6 +275,16 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
244
275
|
|
|
245
276
|
if response.usage_metadata:
|
|
246
277
|
usage_dict = to_dict(response.usage_metadata)
|
|
278
|
+
candidates_token_count = usage_dict.get("candidates_token_count")
|
|
279
|
+
# unlike OpenAI, and unlike input cached tokens, thinking tokens are
|
|
280
|
+
# not counted as part of candidates token count, so we need to add them
|
|
281
|
+
# separately for consistency with other instrumentations
|
|
282
|
+
thoughts_token_count = usage_dict.get("thoughts_token_count")
|
|
283
|
+
output_token_count = (
|
|
284
|
+
(candidates_token_count or 0) + (thoughts_token_count or 0)
|
|
285
|
+
if candidates_token_count is not None or thoughts_token_count is not None
|
|
286
|
+
else None
|
|
287
|
+
)
|
|
247
288
|
set_span_attribute(
|
|
248
289
|
span,
|
|
249
290
|
gen_ai_attributes.GEN_AI_USAGE_INPUT_TOKENS,
|
|
@@ -252,7 +293,7 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
252
293
|
set_span_attribute(
|
|
253
294
|
span,
|
|
254
295
|
gen_ai_attributes.GEN_AI_USAGE_OUTPUT_TOKENS,
|
|
255
|
-
|
|
296
|
+
output_token_count,
|
|
256
297
|
)
|
|
257
298
|
set_span_attribute(
|
|
258
299
|
span,
|
|
@@ -264,28 +305,39 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
264
305
|
SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS,
|
|
265
306
|
usage_dict.get("cached_content_token_count"),
|
|
266
307
|
)
|
|
308
|
+
set_span_attribute(
|
|
309
|
+
span,
|
|
310
|
+
SpanAttributes.LLM_USAGE_REASONING_TOKENS,
|
|
311
|
+
thoughts_token_count,
|
|
312
|
+
)
|
|
267
313
|
|
|
268
314
|
if should_send_prompts():
|
|
269
315
|
set_span_attribute(
|
|
270
316
|
span, f"{gen_ai_attributes.GEN_AI_COMPLETION}.0.role", "model"
|
|
271
317
|
)
|
|
272
318
|
candidates_list = candidates if isinstance(candidates, list) else [candidates]
|
|
273
|
-
|
|
319
|
+
i = 0
|
|
320
|
+
for candidate in candidates_list:
|
|
321
|
+
has_content = False
|
|
274
322
|
processed_content = process_content_union(candidate.content)
|
|
275
|
-
|
|
323
|
+
content_payload = get_content(processed_content)
|
|
324
|
+
if isinstance(content_payload, dict):
|
|
325
|
+
content_payload = [content_payload]
|
|
276
326
|
|
|
277
327
|
set_span_attribute(
|
|
278
328
|
span, f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.role", "model"
|
|
279
329
|
)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
330
|
+
if content_payload:
|
|
331
|
+
has_content = True
|
|
332
|
+
set_span_attribute(
|
|
333
|
+
span,
|
|
334
|
+
f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.content",
|
|
335
|
+
(
|
|
336
|
+
content_payload
|
|
337
|
+
if isinstance(content_payload, str)
|
|
338
|
+
else json_dumps(content_payload)
|
|
339
|
+
),
|
|
340
|
+
)
|
|
289
341
|
blocks = (
|
|
290
342
|
processed_content
|
|
291
343
|
if isinstance(processed_content, list)
|
|
@@ -298,6 +350,7 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
298
350
|
if not block_dict.get("function_call"):
|
|
299
351
|
continue
|
|
300
352
|
function_call = to_dict(block_dict.get("function_call", {}))
|
|
353
|
+
has_content = True
|
|
301
354
|
set_span_attribute(
|
|
302
355
|
span,
|
|
303
356
|
f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{tool_call_index}.name",
|
|
@@ -315,9 +368,11 @@ def _set_response_attributes(span, response: types.GenerateContentResponse):
|
|
|
315
368
|
set_span_attribute(
|
|
316
369
|
span,
|
|
317
370
|
f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.tool_calls.{tool_call_index}.arguments",
|
|
318
|
-
|
|
371
|
+
json_dumps(function_call.get("arguments")),
|
|
319
372
|
)
|
|
320
373
|
tool_call_index += 1
|
|
374
|
+
if has_content:
|
|
375
|
+
i += 1
|
|
321
376
|
|
|
322
377
|
|
|
323
378
|
@dont_throw
|
|
@@ -329,53 +384,49 @@ def _build_from_streaming_response(
|
|
|
329
384
|
aggregated_usage_metadata = defaultdict(int)
|
|
330
385
|
model_version = None
|
|
331
386
|
for chunk in response:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
aggregated_usage_metadata["candidates_token_count"] += (
|
|
355
|
-
usage_dict.get("candidates_token_count") or 0
|
|
356
|
-
)
|
|
357
|
-
aggregated_usage_metadata["total_token_count"] += (
|
|
358
|
-
usage_dict.get("candidates_token_count") or 0
|
|
359
|
-
)
|
|
387
|
+
try:
|
|
388
|
+
span.add_event("llm.content.completion.chunk")
|
|
389
|
+
except Exception:
|
|
390
|
+
pass
|
|
391
|
+
# Important: do all processing in a separate sync function, that is
|
|
392
|
+
# wrapped in @dont_throw. If we did it here, the @dont_throw on top of
|
|
393
|
+
# this function would not be able to catch the errors, as they are
|
|
394
|
+
# raised later, after the generator is returned, and when it is being
|
|
395
|
+
# consumed.
|
|
396
|
+
chunk_result = process_stream_chunk(
|
|
397
|
+
chunk,
|
|
398
|
+
role,
|
|
399
|
+
model_version,
|
|
400
|
+
aggregated_usage_metadata,
|
|
401
|
+
final_parts,
|
|
402
|
+
)
|
|
403
|
+
# even though process_stream_chunk can't return None, the result can be
|
|
404
|
+
# None, if the processing throws an error (see @dont_throw)
|
|
405
|
+
if chunk_result:
|
|
406
|
+
role = chunk_result["role"]
|
|
407
|
+
model_version = chunk_result["model_version"]
|
|
360
408
|
yield chunk
|
|
361
409
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
410
|
+
try:
|
|
411
|
+
compound_response = types.GenerateContentResponse(
|
|
412
|
+
candidates=[
|
|
413
|
+
{
|
|
414
|
+
"content": {
|
|
415
|
+
"parts": merge_text_parts(final_parts),
|
|
416
|
+
"role": role,
|
|
417
|
+
},
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
usage_metadata=types.GenerateContentResponseUsageMetadataDict(
|
|
421
|
+
**aggregated_usage_metadata
|
|
422
|
+
),
|
|
423
|
+
model_version=model_version,
|
|
424
|
+
)
|
|
425
|
+
if span.is_recording():
|
|
426
|
+
_set_response_attributes(span, compound_response)
|
|
427
|
+
finally:
|
|
428
|
+
if span.is_recording():
|
|
429
|
+
span.end()
|
|
379
430
|
|
|
380
431
|
|
|
381
432
|
@dont_throw
|
|
@@ -387,52 +438,49 @@ async def _abuild_from_streaming_response(
|
|
|
387
438
|
aggregated_usage_metadata = defaultdict(int)
|
|
388
439
|
model_version = None
|
|
389
440
|
async for chunk in response:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
aggregated_usage_metadata["candidates_token_count"] += (
|
|
412
|
-
usage_dict.get("candidates_token_count") or 0
|
|
413
|
-
)
|
|
414
|
-
aggregated_usage_metadata["total_token_count"] += (
|
|
415
|
-
usage_dict.get("candidates_token_count") or 0
|
|
416
|
-
)
|
|
441
|
+
try:
|
|
442
|
+
span.add_event("llm.content.completion.chunk")
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
# Important: do all processing in a separate sync function, that is
|
|
446
|
+
# wrapped in @dont_throw. If we did it here, the @dont_throw on top of
|
|
447
|
+
# this function would not be able to catch the errors, as they are
|
|
448
|
+
# raised later, after the generator is returned, and when it is being
|
|
449
|
+
# consumed.
|
|
450
|
+
chunk_result = process_stream_chunk(
|
|
451
|
+
chunk,
|
|
452
|
+
role,
|
|
453
|
+
model_version,
|
|
454
|
+
aggregated_usage_metadata,
|
|
455
|
+
final_parts,
|
|
456
|
+
)
|
|
457
|
+
# even though process_stream_chunk can't return None, the result can be
|
|
458
|
+
# None, if the processing throws an error (see @dont_throw)
|
|
459
|
+
if chunk_result:
|
|
460
|
+
role = chunk_result["role"]
|
|
461
|
+
model_version = chunk_result["model_version"]
|
|
417
462
|
yield chunk
|
|
418
463
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
"
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
464
|
+
try:
|
|
465
|
+
compound_response = types.GenerateContentResponse(
|
|
466
|
+
candidates=[
|
|
467
|
+
{
|
|
468
|
+
"content": {
|
|
469
|
+
"parts": merge_text_parts(final_parts),
|
|
470
|
+
"role": role,
|
|
471
|
+
},
|
|
472
|
+
}
|
|
473
|
+
],
|
|
474
|
+
usage_metadata=types.GenerateContentResponseUsageMetadataDict(
|
|
475
|
+
**aggregated_usage_metadata
|
|
476
|
+
),
|
|
477
|
+
model_version=model_version,
|
|
478
|
+
)
|
|
479
|
+
if span.is_recording():
|
|
480
|
+
_set_response_attributes(span, compound_response)
|
|
481
|
+
finally:
|
|
482
|
+
if span.is_recording():
|
|
483
|
+
span.end()
|
|
436
484
|
|
|
437
485
|
|
|
438
486
|
@with_tracer_wrapper
|
|
@@ -449,21 +497,27 @@ def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
|
449
497
|
SpanAttributes.LLM_SYSTEM: "gemini",
|
|
450
498
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
451
499
|
},
|
|
500
|
+
context=get_current_context(),
|
|
452
501
|
)
|
|
453
502
|
|
|
454
503
|
if span.is_recording():
|
|
455
504
|
_set_request_attributes(span, args, kwargs)
|
|
456
505
|
|
|
457
|
-
|
|
458
|
-
return _build_from_streaming_response(span, wrapped(*args, **kwargs))
|
|
459
|
-
else:
|
|
506
|
+
try:
|
|
460
507
|
response = wrapped(*args, **kwargs)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
508
|
+
if to_wrap.get("is_streaming"):
|
|
509
|
+
return _build_from_streaming_response(span, response)
|
|
510
|
+
if span.is_recording():
|
|
511
|
+
_set_response_attributes(span, response)
|
|
512
|
+
span.end()
|
|
513
|
+
return response
|
|
514
|
+
except Exception as e:
|
|
515
|
+
attributes = get_event_attributes_from_context()
|
|
516
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
517
|
+
span.record_exception(e, attributes=attributes)
|
|
518
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
519
|
+
span.end()
|
|
520
|
+
raise
|
|
467
521
|
|
|
468
522
|
|
|
469
523
|
@with_tracer_wrapper
|
|
@@ -480,21 +534,29 @@ async def _awrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
|
480
534
|
SpanAttributes.LLM_SYSTEM: "gemini",
|
|
481
535
|
SpanAttributes.LLM_REQUEST_TYPE: LLMRequestTypeValues.COMPLETION.value,
|
|
482
536
|
},
|
|
537
|
+
context=get_current_context(),
|
|
483
538
|
)
|
|
484
539
|
|
|
485
540
|
if span.is_recording():
|
|
486
541
|
_set_request_attributes(span, args, kwargs)
|
|
487
542
|
|
|
488
|
-
|
|
489
|
-
return _abuild_from_streaming_response(span, await wrapped(*args, **kwargs))
|
|
490
|
-
else:
|
|
543
|
+
try:
|
|
491
544
|
response = await wrapped(*args, **kwargs)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
545
|
+
if to_wrap.get("is_streaming"):
|
|
546
|
+
return _abuild_from_streaming_response(span, response)
|
|
547
|
+
else:
|
|
548
|
+
if span.is_recording():
|
|
549
|
+
_set_response_attributes(span, response)
|
|
550
|
+
|
|
551
|
+
span.end()
|
|
552
|
+
return response
|
|
553
|
+
except Exception as e:
|
|
554
|
+
attributes = get_event_attributes_from_context()
|
|
555
|
+
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
|
556
|
+
span.record_exception(e, attributes=attributes)
|
|
557
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
558
|
+
span.end()
|
|
559
|
+
raise
|
|
498
560
|
|
|
499
561
|
|
|
500
562
|
class GoogleGenAiSdkInstrumentor(BaseInstrumentor):
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from google.genai._api_client import BaseApiClient
|
|
3
|
+
from google.genai._transformers import t_schema
|
|
4
|
+
from google.genai.types import JSONSchemaType
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
DUMMY_CLIENT = BaseApiClient(api_key="dummy")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def process_schema(schema: Any) -> dict[str, Any]:
|
|
12
|
+
# The only thing we need from the client is the t_schema function
|
|
13
|
+
try:
|
|
14
|
+
json_schema = t_schema(DUMMY_CLIENT, schema).json_schema.model_dump(
|
|
15
|
+
exclude_unset=True, exclude_none=True
|
|
16
|
+
)
|
|
17
|
+
except Exception:
|
|
18
|
+
json_schema = {}
|
|
19
|
+
return json_schema
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SchemaJSONEncoder(json.JSONEncoder):
|
|
23
|
+
def default(self, o: Any) -> Any:
|
|
24
|
+
if isinstance(o, JSONSchemaType):
|
|
25
|
+
return o.value
|
|
26
|
+
return super().default(o)
|