judgeval 0.15.0__py3-none-any.whl → 0.16.1__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 (42) hide show
  1. judgeval/api/__init__.py +4 -18
  2. judgeval/api/api_types.py +18 -2
  3. judgeval/data/judgment_types.py +18 -2
  4. judgeval/logger.py +1 -1
  5. judgeval/tracer/__init__.py +10 -7
  6. judgeval/tracer/keys.py +7 -3
  7. judgeval/tracer/llm/__init__.py +2 -1227
  8. judgeval/tracer/llm/config.py +110 -0
  9. judgeval/tracer/llm/constants.py +10 -0
  10. judgeval/tracer/llm/llm_anthropic/__init__.py +3 -0
  11. judgeval/tracer/llm/llm_anthropic/wrapper.py +611 -0
  12. judgeval/tracer/llm/llm_google/__init__.py +0 -0
  13. judgeval/tracer/llm/llm_google/config.py +24 -0
  14. judgeval/tracer/llm/llm_google/wrapper.py +426 -0
  15. judgeval/tracer/llm/llm_groq/__init__.py +0 -0
  16. judgeval/tracer/llm/llm_groq/config.py +23 -0
  17. judgeval/tracer/llm/llm_groq/wrapper.py +477 -0
  18. judgeval/tracer/llm/llm_openai/__init__.py +3 -0
  19. judgeval/tracer/llm/llm_openai/wrapper.py +637 -0
  20. judgeval/tracer/llm/llm_together/__init__.py +0 -0
  21. judgeval/tracer/llm/llm_together/config.py +23 -0
  22. judgeval/tracer/llm/llm_together/wrapper.py +478 -0
  23. judgeval/tracer/llm/providers.py +5 -5
  24. judgeval/tracer/processors/__init__.py +1 -1
  25. judgeval/trainer/console.py +1 -1
  26. judgeval/utils/decorators/__init__.py +0 -0
  27. judgeval/utils/decorators/dont_throw.py +21 -0
  28. judgeval/utils/{decorators.py → decorators/use_once.py} +0 -11
  29. judgeval/utils/meta.py +1 -1
  30. judgeval/utils/version_check.py +1 -1
  31. judgeval/version.py +1 -1
  32. judgeval-0.16.1.dist-info/METADATA +266 -0
  33. {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/RECORD +38 -24
  34. judgeval/tracer/llm/google/__init__.py +0 -21
  35. judgeval/tracer/llm/groq/__init__.py +0 -20
  36. judgeval/tracer/llm/together/__init__.py +0 -20
  37. judgeval-0.15.0.dist-info/METADATA +0 -158
  38. /judgeval/tracer/llm/{anthropic/__init__.py → llm_anthropic/config.py} +0 -0
  39. /judgeval/tracer/llm/{openai/__init__.py → llm_openai/config.py} +0 -0
  40. {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/WHEEL +0 -0
  41. {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/entry_points.txt +0 -0
  42. {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/licenses/LICENSE.md +0 -0
@@ -0,0 +1,637 @@
1
+ from __future__ import annotations
2
+ import functools
3
+ import orjson
4
+ from typing import (
5
+ TYPE_CHECKING,
6
+ Any,
7
+ Optional,
8
+ Tuple,
9
+ Protocol,
10
+ TypeVar,
11
+ Union,
12
+ Sequence,
13
+ Callable,
14
+ Iterator,
15
+ AsyncIterator,
16
+ runtime_checkable,
17
+ )
18
+
19
+ from judgeval.tracer.llm.llm_openai.config import (
20
+ HAS_OPENAI,
21
+ openai_OpenAI,
22
+ openai_AsyncOpenAI,
23
+ )
24
+ from judgeval.tracer.managers import sync_span_context, async_span_context
25
+ from judgeval.tracer.keys import AttributeKeys
26
+ from judgeval.tracer.utils import set_span_attribute
27
+ from judgeval.utils.serialize import safe_serialize
28
+
29
+ if TYPE_CHECKING:
30
+ from judgeval.tracer import Tracer
31
+ from opentelemetry.trace import Span
32
+
33
+
34
+ @runtime_checkable
35
+ class OpenAIPromptTokensDetails(Protocol):
36
+ cached_tokens: Optional[int]
37
+
38
+
39
+ @runtime_checkable
40
+ class OpenAIUsage(Protocol):
41
+ prompt_tokens: Optional[int]
42
+ completion_tokens: Optional[int]
43
+ total_tokens: Optional[int]
44
+ prompt_tokens_details: Optional[OpenAIPromptTokensDetails]
45
+
46
+
47
+ @runtime_checkable
48
+ class OpenAIResponseUsage(Protocol):
49
+ input_tokens: Optional[int]
50
+ output_tokens: Optional[int]
51
+ total_tokens: Optional[int]
52
+
53
+
54
+ @runtime_checkable
55
+ class OpenAIUnifiedUsage(Protocol):
56
+ prompt_tokens: Optional[int]
57
+ completion_tokens: Optional[int]
58
+
59
+ input_tokens: Optional[int]
60
+ output_tokens: Optional[int]
61
+
62
+ total_tokens: Optional[int]
63
+ prompt_tokens_details: Optional[OpenAIPromptTokensDetails]
64
+
65
+
66
+ @runtime_checkable
67
+ class OpenAIMessage(Protocol):
68
+ content: Optional[str]
69
+ role: str
70
+
71
+
72
+ @runtime_checkable
73
+ class OpenAIParsedMessage(Protocol):
74
+ parsed: Optional[str]
75
+ content: Optional[str]
76
+ role: str
77
+
78
+
79
+ @runtime_checkable
80
+ class OpenAIChoice(Protocol):
81
+ index: int
82
+ message: OpenAIMessage
83
+ finish_reason: Optional[str]
84
+
85
+
86
+ @runtime_checkable
87
+ class OpenAIParsedChoice(Protocol):
88
+ index: int
89
+ message: OpenAIParsedMessage
90
+ finish_reason: Optional[str]
91
+
92
+
93
+ @runtime_checkable
94
+ class OpenAIResponseContent(Protocol):
95
+ text: str
96
+
97
+
98
+ @runtime_checkable
99
+ class OpenAIResponseOutput(Protocol):
100
+ content: Sequence[OpenAIResponseContent]
101
+
102
+
103
+ @runtime_checkable
104
+ class OpenAIChatCompletionBase(Protocol):
105
+ id: str
106
+ object: str
107
+ created: int
108
+ model: str
109
+ choices: Sequence[Union[OpenAIChoice, OpenAIParsedChoice]]
110
+ usage: Optional[OpenAIUnifiedUsage]
111
+
112
+
113
+ OpenAIChatCompletion = OpenAIChatCompletionBase
114
+ OpenAIParsedChatCompletion = OpenAIChatCompletionBase
115
+
116
+
117
+ @runtime_checkable
118
+ class OpenAIResponse(Protocol):
119
+ id: str
120
+ object: str
121
+ created: int
122
+ model: str
123
+ output: Sequence[OpenAIResponseOutput]
124
+ usage: Optional[OpenAIUnifiedUsage]
125
+
126
+
127
+ @runtime_checkable
128
+ class OpenAIStreamDelta(Protocol):
129
+ content: Optional[str]
130
+
131
+
132
+ @runtime_checkable
133
+ class OpenAIStreamChoice(Protocol):
134
+ index: int
135
+ delta: OpenAIStreamDelta
136
+
137
+
138
+ @runtime_checkable
139
+ class OpenAIStreamChunk(Protocol):
140
+ choices: Sequence[OpenAIStreamChoice]
141
+ usage: Optional[OpenAIUnifiedUsage]
142
+
143
+
144
+ @runtime_checkable
145
+ class OpenAIClient(Protocol):
146
+ pass
147
+
148
+
149
+ @runtime_checkable
150
+ class OpenAIAsyncClient(Protocol):
151
+ pass
152
+
153
+
154
+ OpenAIResponseType = Union[OpenAIChatCompletionBase, OpenAIResponse]
155
+ OpenAIStreamType = Union[Iterator[OpenAIStreamChunk], AsyncIterator[OpenAIStreamChunk]]
156
+
157
+
158
+ def _extract_openai_content(chunk: OpenAIStreamChunk) -> str:
159
+ if chunk.choices and len(chunk.choices) > 0:
160
+ delta_content = chunk.choices[0].delta.content
161
+ if delta_content:
162
+ return delta_content
163
+ return ""
164
+
165
+
166
+ def _extract_openai_tokens(usage_data: OpenAIUnifiedUsage) -> Tuple[int, int, int, int]:
167
+ if hasattr(usage_data, "prompt_tokens") and usage_data.prompt_tokens is not None:
168
+ prompt_tokens = usage_data.prompt_tokens
169
+ completion_tokens = usage_data.completion_tokens or 0
170
+
171
+ elif hasattr(usage_data, "input_tokens") and usage_data.input_tokens is not None:
172
+ prompt_tokens = usage_data.input_tokens
173
+ completion_tokens = usage_data.output_tokens or 0
174
+ else:
175
+ prompt_tokens = 0
176
+ completion_tokens = 0
177
+
178
+ # Extract cached tokens
179
+ cache_read_input_tokens = 0
180
+ if (
181
+ usage_data.prompt_tokens_details
182
+ and usage_data.prompt_tokens_details.cached_tokens
183
+ ):
184
+ cache_read_input_tokens = usage_data.prompt_tokens_details.cached_tokens
185
+
186
+ cache_creation_input_tokens = 0 # OpenAI doesn't have cache creation tokens
187
+
188
+ return (
189
+ prompt_tokens,
190
+ completion_tokens,
191
+ cache_read_input_tokens,
192
+ cache_creation_input_tokens,
193
+ )
194
+
195
+
196
+ def _format_openai_output(
197
+ response: OpenAIResponseType,
198
+ ) -> Tuple[Optional[Union[str, list[dict[str, Any]]]], Optional[OpenAIUnifiedUsage]]:
199
+ message_content: Optional[Union[str, list[dict[str, Any]]]] = None
200
+ usage_data: Optional[OpenAIUnifiedUsage] = None
201
+
202
+ try:
203
+ if isinstance(response, OpenAIResponse):
204
+ usage_data = response.usage
205
+ if response.output and len(response.output) > 0:
206
+ output0 = response.output[0]
207
+ if output0.content and len(output0.content) > 0:
208
+ try:
209
+ content_blocks = []
210
+ for seg in output0.content:
211
+ if hasattr(seg, "type"):
212
+ seg_type = getattr(seg, "type", None)
213
+ if seg_type == "text" and hasattr(seg, "text"):
214
+ block_data = {
215
+ "type": "text",
216
+ "text": getattr(seg, "text", ""),
217
+ }
218
+ elif seg_type == "function_call":
219
+ block_data = {
220
+ "type": "function_call",
221
+ "name": getattr(seg, "name", None),
222
+ "call_id": getattr(seg, "call_id", None),
223
+ "arguments": getattr(seg, "arguments", None),
224
+ }
225
+ else:
226
+ # Handle unknown types
227
+ block_data = {"type": seg_type}
228
+ for attr in [
229
+ "text",
230
+ "name",
231
+ "call_id",
232
+ "arguments",
233
+ "content",
234
+ ]:
235
+ if hasattr(seg, attr):
236
+ block_data[attr] = getattr(seg, attr)
237
+ content_blocks.append(block_data)
238
+ elif hasattr(seg, "text") and seg.text:
239
+ # Fallback for segments without type
240
+ content_blocks.append(
241
+ {"type": "text", "text": seg.text}
242
+ )
243
+
244
+ message_content = (
245
+ content_blocks if content_blocks else str(output0.content)
246
+ )
247
+ except (TypeError, AttributeError):
248
+ message_content = str(output0.content)
249
+ elif isinstance(response, OpenAIChatCompletionBase):
250
+ usage_data = response.usage
251
+ if response.choices and len(response.choices) > 0:
252
+ message = response.choices[0].message
253
+
254
+ if (
255
+ hasattr(message, "parsed")
256
+ and getattr(message, "parsed", None) is not None
257
+ ):
258
+ # For parsed responses, return as structured data
259
+ parsed_data = getattr(message, "parsed")
260
+ message_content = [{"type": "parsed", "content": parsed_data}]
261
+ else:
262
+ content_blocks = []
263
+
264
+ # Handle regular content
265
+ if hasattr(message, "content") and message.content:
266
+ content_blocks.append(
267
+ {"type": "text", "text": str(message.content)}
268
+ )
269
+
270
+ # Handle tool calls (standard Chat Completions API)
271
+ if hasattr(message, "tool_calls") and message.tool_calls:
272
+ for tool_call in message.tool_calls:
273
+ tool_call_data = {
274
+ "type": "tool_call",
275
+ "id": getattr(tool_call, "id", None),
276
+ "function": {
277
+ "name": getattr(tool_call.function, "name", None)
278
+ if hasattr(tool_call, "function")
279
+ else None,
280
+ "arguments": getattr(
281
+ tool_call.function, "arguments", None
282
+ )
283
+ if hasattr(tool_call, "function")
284
+ else None,
285
+ },
286
+ }
287
+ content_blocks.append(tool_call_data)
288
+
289
+ message_content = content_blocks if content_blocks else None
290
+ except (AttributeError, IndexError, TypeError):
291
+ pass
292
+
293
+ return message_content, usage_data
294
+
295
+
296
+ class TracedOpenAIGenerator:
297
+ def __init__(
298
+ self,
299
+ tracer: Tracer,
300
+ generator: Iterator[OpenAIStreamChunk],
301
+ client: OpenAIClient,
302
+ span: Span,
303
+ model_name: str,
304
+ ):
305
+ self.tracer = tracer
306
+ self.generator = generator
307
+ self.client = client
308
+ self.span = span
309
+ self.model_name = model_name
310
+ self.accumulated_content = ""
311
+
312
+ def __iter__(self) -> Iterator[OpenAIStreamChunk]:
313
+ return self
314
+
315
+ def __next__(self) -> OpenAIStreamChunk:
316
+ try:
317
+ chunk = next(self.generator)
318
+ content = _extract_openai_content(chunk)
319
+ if content:
320
+ self.accumulated_content += content
321
+ if chunk.usage:
322
+ prompt_tokens, completion_tokens, cache_read, cache_creation = (
323
+ _extract_openai_tokens(chunk.usage)
324
+ )
325
+ set_span_attribute(
326
+ self.span, AttributeKeys.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens
327
+ )
328
+ set_span_attribute(
329
+ self.span,
330
+ AttributeKeys.GEN_AI_USAGE_OUTPUT_TOKENS,
331
+ completion_tokens,
332
+ )
333
+ set_span_attribute(
334
+ self.span,
335
+ AttributeKeys.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
336
+ cache_read,
337
+ )
338
+ set_span_attribute(
339
+ self.span,
340
+ AttributeKeys.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
341
+ cache_creation,
342
+ )
343
+ set_span_attribute(
344
+ self.span,
345
+ AttributeKeys.JUDGMENT_USAGE_METADATA,
346
+ safe_serialize(chunk.usage),
347
+ )
348
+ return chunk
349
+ except StopIteration:
350
+ set_span_attribute(
351
+ self.span, AttributeKeys.GEN_AI_COMPLETION, self.accumulated_content
352
+ )
353
+ self.span.end()
354
+ raise
355
+ except Exception as e:
356
+ if self.span:
357
+ self.span.record_exception(e)
358
+ self.span.end()
359
+ raise
360
+
361
+
362
+ class TracedOpenAIAsyncGenerator:
363
+ def __init__(
364
+ self,
365
+ tracer: Tracer,
366
+ async_generator: AsyncIterator[OpenAIStreamChunk],
367
+ client: OpenAIAsyncClient,
368
+ span: Span,
369
+ model_name: str,
370
+ ):
371
+ self.tracer = tracer
372
+ self.async_generator = async_generator
373
+ self.client = client
374
+ self.span = span
375
+ self.model_name = model_name
376
+ self.accumulated_content = ""
377
+
378
+ def __aiter__(self) -> AsyncIterator[OpenAIStreamChunk]:
379
+ return self
380
+
381
+ async def __anext__(self) -> OpenAIStreamChunk:
382
+ try:
383
+ chunk = await self.async_generator.__anext__()
384
+ content = _extract_openai_content(chunk)
385
+ if content:
386
+ self.accumulated_content += content
387
+ if chunk.usage:
388
+ prompt_tokens, completion_tokens, cache_read, cache_creation = (
389
+ _extract_openai_tokens(chunk.usage)
390
+ )
391
+ set_span_attribute(
392
+ self.span, AttributeKeys.GEN_AI_USAGE_INPUT_TOKENS, prompt_tokens
393
+ )
394
+ set_span_attribute(
395
+ self.span,
396
+ AttributeKeys.GEN_AI_USAGE_OUTPUT_TOKENS,
397
+ completion_tokens,
398
+ )
399
+ set_span_attribute(
400
+ self.span,
401
+ AttributeKeys.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
402
+ cache_read,
403
+ )
404
+ set_span_attribute(
405
+ self.span,
406
+ AttributeKeys.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
407
+ cache_creation,
408
+ )
409
+
410
+ set_span_attribute(
411
+ self.span,
412
+ AttributeKeys.JUDGMENT_USAGE_METADATA,
413
+ safe_serialize(chunk.usage),
414
+ )
415
+ return chunk
416
+ except StopAsyncIteration:
417
+ set_span_attribute(
418
+ self.span, AttributeKeys.GEN_AI_COMPLETION, self.accumulated_content
419
+ )
420
+ self.span.end()
421
+ raise
422
+ except Exception as e:
423
+ if self.span:
424
+ self.span.record_exception(e)
425
+ self.span.end()
426
+ raise
427
+
428
+
429
+ TClient = TypeVar("TClient", bound=OpenAIClient)
430
+
431
+
432
+ def wrap_openai_client(tracer: Tracer, client: TClient) -> TClient:
433
+ if not HAS_OPENAI:
434
+ return client
435
+
436
+ assert openai_OpenAI is not None
437
+ assert openai_AsyncOpenAI is not None
438
+
439
+ def wrapped(function: Callable, span_name: str):
440
+ @functools.wraps(function)
441
+ def wrapper(*args, **kwargs):
442
+ if kwargs.get("stream", False):
443
+ span = tracer.get_tracer().start_span(
444
+ span_name, attributes={AttributeKeys.JUDGMENT_SPAN_KIND: "llm"}
445
+ )
446
+ tracer.add_agent_attributes_to_span(span)
447
+ set_span_attribute(
448
+ span, AttributeKeys.GEN_AI_PROMPT, safe_serialize(kwargs)
449
+ )
450
+ model_name = kwargs.get("model", "")
451
+ set_span_attribute(span, AttributeKeys.GEN_AI_REQUEST_MODEL, model_name)
452
+ stream_response = function(*args, **kwargs)
453
+ return TracedOpenAIGenerator(
454
+ tracer, stream_response, client, span, model_name
455
+ )
456
+ else:
457
+ with sync_span_context(
458
+ tracer, span_name, {AttributeKeys.JUDGMENT_SPAN_KIND: "llm"}
459
+ ) as span:
460
+ tracer.add_agent_attributes_to_span(span)
461
+ set_span_attribute(
462
+ span, AttributeKeys.GEN_AI_PROMPT, safe_serialize(kwargs)
463
+ )
464
+ model_name = kwargs.get("model", "")
465
+ set_span_attribute(
466
+ span, AttributeKeys.GEN_AI_REQUEST_MODEL, model_name
467
+ )
468
+ response = function(*args, **kwargs)
469
+
470
+ if isinstance(response, (OpenAIChatCompletionBase, OpenAIResponse)):
471
+ output, usage_data = _format_openai_output(response)
472
+ # Serialize structured data to JSON for span attribute
473
+ if isinstance(output, list):
474
+ output_str = orjson.dumps(
475
+ output, option=orjson.OPT_INDENT_2
476
+ ).decode()
477
+ else:
478
+ output_str = str(output) if output is not None else None
479
+ set_span_attribute(
480
+ span, AttributeKeys.GEN_AI_COMPLETION, output_str
481
+ )
482
+ if usage_data:
483
+ (
484
+ prompt_tokens,
485
+ completion_tokens,
486
+ cache_read,
487
+ cache_creation,
488
+ ) = _extract_openai_tokens(usage_data)
489
+ set_span_attribute(
490
+ span,
491
+ AttributeKeys.GEN_AI_USAGE_INPUT_TOKENS,
492
+ prompt_tokens,
493
+ )
494
+ set_span_attribute(
495
+ span,
496
+ AttributeKeys.GEN_AI_USAGE_OUTPUT_TOKENS,
497
+ completion_tokens,
498
+ )
499
+ set_span_attribute(
500
+ span,
501
+ AttributeKeys.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
502
+ cache_read,
503
+ )
504
+ set_span_attribute(
505
+ span,
506
+ AttributeKeys.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
507
+ cache_creation,
508
+ )
509
+ set_span_attribute(
510
+ span,
511
+ AttributeKeys.JUDGMENT_USAGE_METADATA,
512
+ safe_serialize(usage_data),
513
+ )
514
+ set_span_attribute(
515
+ span,
516
+ AttributeKeys.GEN_AI_RESPONSE_MODEL,
517
+ getattr(response, "model", model_name),
518
+ )
519
+ return response
520
+
521
+ return wrapper
522
+
523
+ def wrapped_async(function: Callable, span_name: str):
524
+ @functools.wraps(function)
525
+ async def wrapper(*args, **kwargs):
526
+ if kwargs.get("stream", False):
527
+ span = tracer.get_tracer().start_span(
528
+ span_name, attributes={AttributeKeys.JUDGMENT_SPAN_KIND: "llm"}
529
+ )
530
+ tracer.add_agent_attributes_to_span(span)
531
+ set_span_attribute(
532
+ span, AttributeKeys.GEN_AI_PROMPT, safe_serialize(kwargs)
533
+ )
534
+ model_name = kwargs.get("model", "")
535
+ set_span_attribute(span, AttributeKeys.GEN_AI_REQUEST_MODEL, model_name)
536
+ stream_response = await function(*args, **kwargs)
537
+ return TracedOpenAIAsyncGenerator(
538
+ tracer, stream_response, client, span, model_name
539
+ )
540
+ else:
541
+ async with async_span_context(
542
+ tracer, span_name, {AttributeKeys.JUDGMENT_SPAN_KIND: "llm"}
543
+ ) as span:
544
+ tracer.add_agent_attributes_to_span(span)
545
+ set_span_attribute(
546
+ span, AttributeKeys.GEN_AI_PROMPT, safe_serialize(kwargs)
547
+ )
548
+ model_name = kwargs.get("model", "")
549
+ set_span_attribute(
550
+ span, AttributeKeys.GEN_AI_REQUEST_MODEL, model_name
551
+ )
552
+ response = await function(*args, **kwargs)
553
+
554
+ if isinstance(response, (OpenAIChatCompletionBase, OpenAIResponse)):
555
+ output, usage_data = _format_openai_output(response)
556
+ # Serialize structured data to JSON for span attribute
557
+ if isinstance(output, list):
558
+ output_str = orjson.dumps(
559
+ output, option=orjson.OPT_INDENT_2
560
+ ).decode()
561
+ else:
562
+ output_str = str(output) if output is not None else None
563
+ set_span_attribute(
564
+ span, AttributeKeys.GEN_AI_COMPLETION, output_str
565
+ )
566
+ if usage_data:
567
+ (
568
+ prompt_tokens,
569
+ completion_tokens,
570
+ cache_read,
571
+ cache_creation,
572
+ ) = _extract_openai_tokens(usage_data)
573
+ set_span_attribute(
574
+ span,
575
+ AttributeKeys.GEN_AI_USAGE_INPUT_TOKENS,
576
+ prompt_tokens,
577
+ )
578
+ set_span_attribute(
579
+ span,
580
+ AttributeKeys.GEN_AI_USAGE_OUTPUT_TOKENS,
581
+ completion_tokens,
582
+ )
583
+ set_span_attribute(
584
+ span,
585
+ AttributeKeys.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS,
586
+ cache_read,
587
+ )
588
+ set_span_attribute(
589
+ span,
590
+ AttributeKeys.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS,
591
+ cache_creation,
592
+ )
593
+ set_span_attribute(
594
+ span,
595
+ AttributeKeys.JUDGMENT_USAGE_METADATA,
596
+ safe_serialize(usage_data),
597
+ )
598
+ set_span_attribute(
599
+ span,
600
+ AttributeKeys.GEN_AI_RESPONSE_MODEL,
601
+ getattr(response, "model", model_name),
602
+ )
603
+ return response
604
+
605
+ return wrapper
606
+
607
+ span_name = "OPENAI_API_CALL"
608
+ if isinstance(client, openai_OpenAI):
609
+ setattr(
610
+ client.chat.completions,
611
+ "create",
612
+ wrapped(client.chat.completions.create, span_name),
613
+ )
614
+ setattr(client.responses, "create", wrapped(client.responses.create, span_name))
615
+ setattr(
616
+ client.beta.chat.completions,
617
+ "parse",
618
+ wrapped(client.beta.chat.completions.parse, span_name),
619
+ )
620
+ elif isinstance(client, openai_AsyncOpenAI):
621
+ setattr(
622
+ client.chat.completions,
623
+ "create",
624
+ wrapped_async(client.chat.completions.create, span_name),
625
+ )
626
+ setattr(
627
+ client.responses,
628
+ "create",
629
+ wrapped_async(client.responses.create, span_name),
630
+ )
631
+ setattr(
632
+ client.beta.chat.completions,
633
+ "parse",
634
+ wrapped_async(client.beta.chat.completions.parse, span_name),
635
+ )
636
+
637
+ return client
File without changes
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from together import Together, AsyncTogether # type: ignore[import-untyped]
6
+
7
+ try:
8
+ from together import Together, AsyncTogether # type: ignore[import-untyped]
9
+
10
+ HAS_TOGETHER = True
11
+ except ImportError:
12
+ HAS_TOGETHER = False
13
+ Together = AsyncTogether = None # type: ignore[misc,assignment]
14
+
15
+ # Export the classes for runtime use
16
+ together_Together = Together
17
+ together_AsyncTogether = AsyncTogether
18
+
19
+ __all__ = [
20
+ "HAS_TOGETHER",
21
+ "together_Together",
22
+ "together_AsyncTogether",
23
+ ]