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.
- judgeval/api/__init__.py +4 -18
- judgeval/api/api_types.py +18 -2
- judgeval/data/judgment_types.py +18 -2
- judgeval/logger.py +1 -1
- judgeval/tracer/__init__.py +10 -7
- judgeval/tracer/keys.py +7 -3
- judgeval/tracer/llm/__init__.py +2 -1227
- judgeval/tracer/llm/config.py +110 -0
- judgeval/tracer/llm/constants.py +10 -0
- judgeval/tracer/llm/llm_anthropic/__init__.py +3 -0
- judgeval/tracer/llm/llm_anthropic/wrapper.py +611 -0
- judgeval/tracer/llm/llm_google/__init__.py +0 -0
- judgeval/tracer/llm/llm_google/config.py +24 -0
- judgeval/tracer/llm/llm_google/wrapper.py +426 -0
- judgeval/tracer/llm/llm_groq/__init__.py +0 -0
- judgeval/tracer/llm/llm_groq/config.py +23 -0
- judgeval/tracer/llm/llm_groq/wrapper.py +477 -0
- judgeval/tracer/llm/llm_openai/__init__.py +3 -0
- judgeval/tracer/llm/llm_openai/wrapper.py +637 -0
- judgeval/tracer/llm/llm_together/__init__.py +0 -0
- judgeval/tracer/llm/llm_together/config.py +23 -0
- judgeval/tracer/llm/llm_together/wrapper.py +478 -0
- judgeval/tracer/llm/providers.py +5 -5
- judgeval/tracer/processors/__init__.py +1 -1
- judgeval/trainer/console.py +1 -1
- judgeval/utils/decorators/__init__.py +0 -0
- judgeval/utils/decorators/dont_throw.py +21 -0
- judgeval/utils/{decorators.py → decorators/use_once.py} +0 -11
- judgeval/utils/meta.py +1 -1
- judgeval/utils/version_check.py +1 -1
- judgeval/version.py +1 -1
- judgeval-0.16.1.dist-info/METADATA +266 -0
- {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/RECORD +38 -24
- judgeval/tracer/llm/google/__init__.py +0 -21
- judgeval/tracer/llm/groq/__init__.py +0 -20
- judgeval/tracer/llm/together/__init__.py +0 -20
- judgeval-0.15.0.dist-info/METADATA +0 -158
- /judgeval/tracer/llm/{anthropic/__init__.py → llm_anthropic/config.py} +0 -0
- /judgeval/tracer/llm/{openai/__init__.py → llm_openai/config.py} +0 -0
- {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/WHEEL +0 -0
- {judgeval-0.15.0.dist-info → judgeval-0.16.1.dist-info}/entry_points.txt +0 -0
- {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
|
+
]
|