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.
Files changed (113) hide show
  1. lmnr/__init__.py +6 -15
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/{cli.py → cli/evals.py} +20 -102
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +9 -2
  7. lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
  8. lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
  9. lmnr/opentelemetry_lib/litellm/utils.py +82 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  56. lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
  57. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
  58. lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
  59. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  60. lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
  61. lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
  62. lmnr/opentelemetry_lib/tracing/processor.py +128 -30
  63. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  64. lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
  65. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  66. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  67. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  68. lmnr/sdk/browser/background_send_events.py +158 -0
  69. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  70. lmnr/sdk/browser/browser_use_otel.py +12 -12
  71. lmnr/sdk/browser/bubus_otel.py +71 -0
  72. lmnr/sdk/browser/cdp_utils.py +518 -0
  73. lmnr/sdk/browser/inject_script.js +514 -0
  74. lmnr/sdk/browser/patchright_otel.py +18 -44
  75. lmnr/sdk/browser/playwright_otel.py +104 -187
  76. lmnr/sdk/browser/pw_utils.py +249 -210
  77. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  78. lmnr/sdk/browser/utils.py +1 -1
  79. lmnr/sdk/client/asynchronous/async_client.py +47 -15
  80. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
  81. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  82. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  83. lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
  84. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  85. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  86. lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
  87. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  88. lmnr/sdk/client/synchronous/resources/evals.py +83 -17
  89. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  90. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  91. lmnr/sdk/client/synchronous/sync_client.py +47 -15
  92. lmnr/sdk/datasets/__init__.py +94 -0
  93. lmnr/sdk/datasets/file_utils.py +91 -0
  94. lmnr/sdk/decorators.py +103 -23
  95. lmnr/sdk/evaluations.py +122 -33
  96. lmnr/sdk/laminar.py +816 -333
  97. lmnr/sdk/log.py +7 -2
  98. lmnr/sdk/types.py +124 -143
  99. lmnr/sdk/utils.py +115 -2
  100. lmnr/version.py +1 -1
  101. {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
  102. lmnr-0.7.26.dist-info/RECORD +116 -0
  103. lmnr-0.7.26.dist-info/WHEEL +4 -0
  104. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  105. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  106. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  107. lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
  108. lmnr/sdk/client/synchronous/resources/agent.py +0 -323
  109. lmnr/sdk/datasets.py +0 -60
  110. lmnr-0.6.16.dist-info/LICENSE +0 -75
  111. lmnr-0.6.16.dist-info/RECORD +0 -61
  112. lmnr-0.6.16.dist-info/WHEEL +0 -4
  113. 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("TRACELOOP_TRACE_CONTENT") or "true"
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
- json.dumps(tool_dict.get("parameters")),
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
- content_str = get_content(processed_content)
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
- content_str
187
- if isinstance(content_str, str)
188
- else json.dumps(content_str)
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
- json.dumps(function_call.get("arguments")),
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
- usage_dict.get("candidates_token_count"),
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
- for i, candidate in enumerate(candidates_list):
319
+ i = 0
320
+ for candidate in candidates_list:
321
+ has_content = False
274
322
  processed_content = process_content_union(candidate.content)
275
- content_str = get_content(processed_content)
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
- set_span_attribute(
281
- span,
282
- f"{gen_ai_attributes.GEN_AI_COMPLETION}.{i}.content",
283
- (
284
- content_str
285
- if isinstance(content_str, str)
286
- else json.dumps(content_str)
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
- json.dumps(function_call.get("arguments")),
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
- if chunk.model_version:
333
- model_version = chunk.model_version
334
-
335
- if chunk.candidates:
336
- # Currently gemini throws an error if you pass more than one candidate
337
- # with streaming
338
- if chunk.candidates and len(chunk.candidates) > 0:
339
- final_parts += chunk.candidates[0].content.parts or []
340
- role = chunk.candidates[0].content.role or role
341
- if chunk.usage_metadata:
342
- usage_dict = to_dict(chunk.usage_metadata)
343
- # prompt token count is sent in every chunk
344
- # (and is less by 1 in the last chunk, so we set it once);
345
- # total token count in every chunk is greater by prompt token count than it should be,
346
- # thus this awkward logic here
347
- if aggregated_usage_metadata.get("prompt_token_count") is None:
348
- aggregated_usage_metadata["prompt_token_count"] = (
349
- usage_dict.get("prompt_token_count") or 0
350
- )
351
- aggregated_usage_metadata["total_token_count"] = (
352
- usage_dict.get("total_token_count") or 0
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
- compound_response = types.GenerateContentResponse(
363
- candidates=[
364
- {
365
- "content": {
366
- "parts": final_parts,
367
- "role": role,
368
- },
369
- }
370
- ],
371
- usage_metadata=types.GenerateContentResponseUsageMetadataDict(
372
- **aggregated_usage_metadata
373
- ),
374
- model_version=model_version,
375
- )
376
- if span.is_recording():
377
- _set_response_attributes(span, compound_response)
378
- span.end()
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
- if chunk.candidates:
391
- # Currently gemini throws an error if you pass more than one candidate
392
- # with streaming
393
- if chunk.candidates and len(chunk.candidates) > 0:
394
- final_parts += chunk.candidates[0].content.parts or []
395
- role = chunk.candidates[0].content.role or role
396
- if chunk.model_version:
397
- model_version = chunk.model_version
398
- if chunk.usage_metadata:
399
- usage_dict = to_dict(chunk.usage_metadata)
400
- # prompt token count is sent in every chunk
401
- # (and is less by 1 in the last chunk, so we set it once);
402
- # total token count in every chunk is greater by prompt token count than it should be,
403
- # thus this awkward logic here
404
- if aggregated_usage_metadata.get("prompt_token_count") is None:
405
- aggregated_usage_metadata["prompt_token_count"] = usage_dict.get(
406
- "prompt_token_count"
407
- )
408
- aggregated_usage_metadata["total_token_count"] = usage_dict.get(
409
- "total_token_count"
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
- compound_response = types.GenerateContentResponse(
420
- candidates=[
421
- {
422
- "content": {
423
- "parts": final_parts,
424
- "role": role,
425
- },
426
- }
427
- ],
428
- usage_metadata=types.GenerateContentResponseUsageMetadataDict(
429
- **aggregated_usage_metadata
430
- ),
431
- model_version=model_version,
432
- )
433
- if span.is_recording():
434
- _set_response_attributes(span, compound_response)
435
- span.end()
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
- if to_wrap.get("is_streaming"):
458
- return _build_from_streaming_response(span, wrapped(*args, **kwargs))
459
- else:
506
+ try:
460
507
  response = wrapped(*args, **kwargs)
461
-
462
- if span.is_recording():
463
- _set_response_attributes(span, response)
464
-
465
- span.end()
466
- return response
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
- if to_wrap.get("is_streaming"):
489
- return _abuild_from_streaming_response(span, await wrapped(*args, **kwargs))
490
- else:
543
+ try:
491
544
  response = await wrapped(*args, **kwargs)
492
-
493
- if span.is_recording():
494
- _set_response_attributes(span, response)
495
-
496
- span.end()
497
- return response
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)