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
|
@@ -3,16 +3,28 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
|
|
6
|
+
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_PROMPT
|
|
6
7
|
from opentelemetry.trace import SpanKind, Status, StatusCode, Tracer
|
|
7
|
-
from lmnr.opentelemetry_lib.litellm.utils import
|
|
8
|
+
from lmnr.opentelemetry_lib.litellm.utils import (
|
|
9
|
+
get_tool_definition,
|
|
10
|
+
is_validator_iterator,
|
|
11
|
+
model_as_dict,
|
|
12
|
+
set_span_attribute,
|
|
13
|
+
)
|
|
8
14
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
|
9
15
|
|
|
16
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
|
17
|
+
get_current_context,
|
|
18
|
+
get_event_attributes_from_context,
|
|
19
|
+
)
|
|
20
|
+
from lmnr.opentelemetry_lib.tracing.attributes import ASSOCIATION_PROPERTIES
|
|
10
21
|
from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
|
|
11
22
|
from lmnr.sdk.log import get_default_logger
|
|
23
|
+
from lmnr.sdk.utils import json_dumps
|
|
12
24
|
|
|
13
25
|
logger = get_default_logger(__name__)
|
|
14
26
|
|
|
15
|
-
SUPPORTED_CALL_TYPES = ["completion", "acompletion"]
|
|
27
|
+
SUPPORTED_CALL_TYPES = ["completion", "acompletion", "responses", "aresponses"]
|
|
16
28
|
|
|
17
29
|
# Try to import the necessary LiteLLM components and gracefully handle ImportError
|
|
18
30
|
try:
|
|
@@ -35,11 +47,29 @@ try:
|
|
|
35
47
|
litellm.callbacks = [LaminarLiteLLMCallback()]
|
|
36
48
|
"""
|
|
37
49
|
|
|
50
|
+
logged_openai_responses: set[str]
|
|
51
|
+
|
|
38
52
|
def __init__(self, **kwargs):
|
|
39
53
|
super().__init__(**kwargs)
|
|
40
54
|
if not hasattr(TracerWrapper, "instance") or TracerWrapper.instance is None:
|
|
41
55
|
raise ValueError("Laminar must be initialized before LiteLLM callback")
|
|
42
56
|
|
|
57
|
+
self.logged_openai_responses = set()
|
|
58
|
+
if is_package_installed("openai"):
|
|
59
|
+
from lmnr.opentelemetry_lib.opentelemetry.instrumentation.openai import (
|
|
60
|
+
OpenAIInstrumentor,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
openai_instrumentor = OpenAIInstrumentor()
|
|
64
|
+
if (
|
|
65
|
+
openai_instrumentor
|
|
66
|
+
and openai_instrumentor.is_instrumented_by_opentelemetry
|
|
67
|
+
):
|
|
68
|
+
logger.debug(
|
|
69
|
+
"Disabling OpenTelemetry instrumentation for OpenAI to avoid double-instrumentation of LiteLLM."
|
|
70
|
+
)
|
|
71
|
+
openai_instrumentor.uninstrument()
|
|
72
|
+
|
|
43
73
|
def _get_tracer(self) -> Tracer:
|
|
44
74
|
if not hasattr(TracerWrapper, "instance") or TracerWrapper.instance is None:
|
|
45
75
|
raise ValueError("Laminar must be initialized before LiteLLM callback")
|
|
@@ -50,6 +80,14 @@ try:
|
|
|
50
80
|
):
|
|
51
81
|
if kwargs.get("call_type") not in SUPPORTED_CALL_TYPES:
|
|
52
82
|
return
|
|
83
|
+
if kwargs.get("call_type") in ["responses", "aresponses"]:
|
|
84
|
+
# responses API may be called multiple times with the same response_obj
|
|
85
|
+
response_id = getattr(response_obj, "id", None)
|
|
86
|
+
if response_id in self.logged_openai_responses:
|
|
87
|
+
return
|
|
88
|
+
if response_id:
|
|
89
|
+
self.logged_openai_responses.add(response_id)
|
|
90
|
+
self.logged_openai_responses.add(response_obj.id)
|
|
53
91
|
try:
|
|
54
92
|
self._create_span(
|
|
55
93
|
kwargs, response_obj, start_time, end_time, is_success=True
|
|
@@ -88,12 +126,18 @@ try:
|
|
|
88
126
|
is_success: bool,
|
|
89
127
|
):
|
|
90
128
|
"""Create an OpenTelemetry span for the LiteLLM call"""
|
|
91
|
-
|
|
129
|
+
call_type = kwargs.get("call_type", "completion")
|
|
130
|
+
if call_type == "aresponses":
|
|
131
|
+
call_type = "responses"
|
|
132
|
+
if call_type == "acompletion":
|
|
133
|
+
call_type = "completion"
|
|
134
|
+
span_name = f"litellm.{call_type}"
|
|
92
135
|
try:
|
|
93
136
|
tracer = self._get_tracer()
|
|
94
137
|
except Exception as e:
|
|
95
138
|
logger.error(f"Error getting tracer: {e}")
|
|
96
139
|
return
|
|
140
|
+
|
|
97
141
|
span = tracer.start_span(
|
|
98
142
|
span_name,
|
|
99
143
|
kind=SpanKind.CLIENT,
|
|
@@ -101,6 +145,7 @@ try:
|
|
|
101
145
|
attributes={
|
|
102
146
|
"lmnr.internal.provider": "litellm",
|
|
103
147
|
},
|
|
148
|
+
context=get_current_context(),
|
|
104
149
|
)
|
|
105
150
|
try:
|
|
106
151
|
model = kwargs.get("model", "unknown")
|
|
@@ -129,6 +174,52 @@ try:
|
|
|
129
174
|
if "top_p" in kwargs:
|
|
130
175
|
set_span_attribute(span, "gen_ai.request.top_p", kwargs["top_p"])
|
|
131
176
|
|
|
177
|
+
metadata = (
|
|
178
|
+
kwargs.get("litellm_params").get(
|
|
179
|
+
"metadata", kwargs.get("metadata", {})
|
|
180
|
+
)
|
|
181
|
+
or {}
|
|
182
|
+
)
|
|
183
|
+
tags = metadata.get("tags", [])
|
|
184
|
+
if isinstance(tags, str):
|
|
185
|
+
try:
|
|
186
|
+
tags = json.loads(tags)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
if (
|
|
190
|
+
tags
|
|
191
|
+
and isinstance(tags, (list, tuple, set))
|
|
192
|
+
and all(isinstance(tag, str) for tag in tags)
|
|
193
|
+
):
|
|
194
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", tags)
|
|
195
|
+
|
|
196
|
+
user_id = metadata.get("user_id")
|
|
197
|
+
if user_id:
|
|
198
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.user_id", user_id)
|
|
199
|
+
|
|
200
|
+
session_id = metadata.get("session_id")
|
|
201
|
+
if session_id:
|
|
202
|
+
span.set_attribute(
|
|
203
|
+
f"{ASSOCIATION_PROPERTIES}.session_id", session_id
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
optional_params = kwargs.get("optional_params") or {}
|
|
207
|
+
if not optional_params:
|
|
208
|
+
hidden_params = metadata.get("hidden_params") or {}
|
|
209
|
+
optional_params = hidden_params.get("optional_params") or {}
|
|
210
|
+
response_format = optional_params.get("response_format")
|
|
211
|
+
if (
|
|
212
|
+
response_format
|
|
213
|
+
and isinstance(response_format, dict)
|
|
214
|
+
and response_format.get("type") == "json_schema"
|
|
215
|
+
):
|
|
216
|
+
schema = (response_format.get("json_schema") or {}).get("schema")
|
|
217
|
+
if schema:
|
|
218
|
+
span.set_attribute(
|
|
219
|
+
"gen_ai.request.structured_output_schema",
|
|
220
|
+
json_dumps(schema),
|
|
221
|
+
)
|
|
222
|
+
|
|
132
223
|
if is_success:
|
|
133
224
|
span.set_status(Status(StatusCode.OK))
|
|
134
225
|
if kwargs.get("complete_streaming_response"):
|
|
@@ -141,10 +232,12 @@ try:
|
|
|
141
232
|
else:
|
|
142
233
|
span.set_status(Status(StatusCode.ERROR))
|
|
143
234
|
if isinstance(response_obj, Exception):
|
|
144
|
-
|
|
235
|
+
attributes = get_event_attributes_from_context()
|
|
236
|
+
span.record_exception(response_obj, attributes=attributes)
|
|
145
237
|
|
|
146
238
|
except Exception as e:
|
|
147
|
-
|
|
239
|
+
attributes = get_event_attributes_from_context()
|
|
240
|
+
span.record_exception(e, attributes=attributes)
|
|
148
241
|
logger.error(f"Error in Laminar LiteLLM instrumentation: {e}")
|
|
149
242
|
finally:
|
|
150
243
|
span.end(int(end_time.timestamp() * 1e9))
|
|
@@ -154,35 +247,107 @@ try:
|
|
|
154
247
|
if not isinstance(messages, list):
|
|
155
248
|
return
|
|
156
249
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
250
|
+
prompt_index = 0
|
|
251
|
+
for item in messages:
|
|
252
|
+
block_dict = model_as_dict(item)
|
|
253
|
+
if block_dict.get("type", "message") == "message":
|
|
254
|
+
tool_calls = block_dict.get("tool_calls", [])
|
|
255
|
+
self._process_tool_calls(
|
|
256
|
+
span, tool_calls, prompt_index, is_response=False
|
|
257
|
+
)
|
|
258
|
+
content = block_dict.get("content")
|
|
259
|
+
if is_validator_iterator(content):
|
|
260
|
+
# Have not been able to catch this in the wild, but keeping
|
|
261
|
+
# just in case, as raw OpenAI responses do that
|
|
262
|
+
content = [self._process_content_part(part) for part in content]
|
|
263
|
+
try:
|
|
264
|
+
stringified_content = (
|
|
265
|
+
content if isinstance(content, str) else json_dumps(content)
|
|
266
|
+
)
|
|
267
|
+
except Exception:
|
|
268
|
+
stringified_content = (
|
|
269
|
+
str(content) if content is not None else ""
|
|
270
|
+
)
|
|
271
|
+
set_span_attribute(
|
|
272
|
+
span,
|
|
273
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.content",
|
|
274
|
+
stringified_content,
|
|
275
|
+
)
|
|
276
|
+
set_span_attribute(
|
|
277
|
+
span,
|
|
278
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.role",
|
|
279
|
+
block_dict.get("role"),
|
|
280
|
+
)
|
|
281
|
+
prompt_index += 1
|
|
164
282
|
|
|
165
|
-
|
|
166
|
-
if content is None:
|
|
167
|
-
continue
|
|
168
|
-
if isinstance(content, str):
|
|
169
|
-
set_span_attribute(span, f"gen_ai.prompt.{i}.content", content)
|
|
170
|
-
elif isinstance(content, list):
|
|
283
|
+
elif block_dict.get("type") == "computer_call_output":
|
|
171
284
|
set_span_attribute(
|
|
172
|
-
span,
|
|
285
|
+
span,
|
|
286
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.role",
|
|
287
|
+
"computer_call_output",
|
|
288
|
+
)
|
|
289
|
+
output_image_url = block_dict.get("output", {}).get("image_url")
|
|
290
|
+
if output_image_url:
|
|
291
|
+
set_span_attribute(
|
|
292
|
+
span,
|
|
293
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.content",
|
|
294
|
+
json.dumps(
|
|
295
|
+
[
|
|
296
|
+
{
|
|
297
|
+
"type": "image_url",
|
|
298
|
+
"image_url": {"url": output_image_url},
|
|
299
|
+
}
|
|
300
|
+
]
|
|
301
|
+
),
|
|
302
|
+
)
|
|
303
|
+
prompt_index += 1
|
|
304
|
+
elif block_dict.get("type") == "computer_call":
|
|
305
|
+
set_span_attribute(
|
|
306
|
+
span, f"{GEN_AI_PROMPT}.{prompt_index}.role", "assistant"
|
|
307
|
+
)
|
|
308
|
+
call_content = {}
|
|
309
|
+
if block_dict.get("id"):
|
|
310
|
+
call_content["id"] = block_dict.get("id")
|
|
311
|
+
if block_dict.get("action"):
|
|
312
|
+
call_content["action"] = block_dict.get("action")
|
|
313
|
+
set_span_attribute(
|
|
314
|
+
span,
|
|
315
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.arguments",
|
|
316
|
+
json.dumps(call_content),
|
|
173
317
|
)
|
|
174
|
-
else:
|
|
175
318
|
set_span_attribute(
|
|
176
319
|
span,
|
|
177
|
-
f"
|
|
178
|
-
|
|
320
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.id",
|
|
321
|
+
block_dict.get("call_id"),
|
|
179
322
|
)
|
|
180
|
-
if role == "tool":
|
|
181
323
|
set_span_attribute(
|
|
182
324
|
span,
|
|
183
|
-
f"
|
|
184
|
-
|
|
325
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.tool_calls.0.name",
|
|
326
|
+
"computer_call",
|
|
185
327
|
)
|
|
328
|
+
prompt_index += 1
|
|
329
|
+
elif block_dict.get("type") == "reasoning":
|
|
330
|
+
reasoning_summary = block_dict.get("summary")
|
|
331
|
+
if reasoning_summary and isinstance(reasoning_summary, list):
|
|
332
|
+
processed_chunks = [
|
|
333
|
+
{"type": "text", "text": chunk.get("text")}
|
|
334
|
+
for chunk in reasoning_summary
|
|
335
|
+
if isinstance(chunk, dict)
|
|
336
|
+
and chunk.get("type") == "summary_text"
|
|
337
|
+
]
|
|
338
|
+
set_span_attribute(
|
|
339
|
+
span,
|
|
340
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.reasoning",
|
|
341
|
+
json_dumps(processed_chunks),
|
|
342
|
+
)
|
|
343
|
+
set_span_attribute(
|
|
344
|
+
span,
|
|
345
|
+
f"{GEN_AI_PROMPT}.{prompt_index}.role",
|
|
346
|
+
"assistant",
|
|
347
|
+
)
|
|
348
|
+
# reasoning is followed by other content parts in the same messge,
|
|
349
|
+
# so we don't increment the prompt index
|
|
350
|
+
# TODO: handle other block types
|
|
186
351
|
|
|
187
352
|
def _process_request_tool_definitions(self, span, tools):
|
|
188
353
|
"""Process and set tool definitions attributes on the span"""
|
|
@@ -191,14 +356,10 @@ try:
|
|
|
191
356
|
|
|
192
357
|
for i, tool in enumerate(tools):
|
|
193
358
|
tool_dict = model_as_dict(tool)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
function_dict = tool_dict.get("function", {})
|
|
199
|
-
function_name = function_dict.get("name", "")
|
|
200
|
-
function_description = function_dict.get("description", "")
|
|
201
|
-
function_parameters = function_dict.get("parameters", {})
|
|
359
|
+
tool_definition = get_tool_definition(tool_dict)
|
|
360
|
+
function_name = tool_definition.get("name")
|
|
361
|
+
function_description = tool_definition.get("description")
|
|
362
|
+
function_parameters = tool_definition.get("parameters")
|
|
202
363
|
set_span_attribute(
|
|
203
364
|
span,
|
|
204
365
|
f"llm.request.functions.{i}.name",
|
|
@@ -245,7 +406,15 @@ try:
|
|
|
245
406
|
details.get("cached_tokens"),
|
|
246
407
|
)
|
|
247
408
|
# TODO: add audio/image/text token details
|
|
248
|
-
|
|
409
|
+
if usage_dict.get("completion_tokens_details"):
|
|
410
|
+
details = usage_dict.get("completion_tokens_details", {})
|
|
411
|
+
details = model_as_dict(details)
|
|
412
|
+
if details.get("reasoning_tokens"):
|
|
413
|
+
set_span_attribute(
|
|
414
|
+
span,
|
|
415
|
+
"gen_ai.usage.reasoning_tokens",
|
|
416
|
+
details.get("reasoning_tokens"),
|
|
417
|
+
)
|
|
249
418
|
|
|
250
419
|
def _process_tool_calls(self, span, tool_calls, choice_index, is_response=True):
|
|
251
420
|
"""Process and set tool call attributes on the span"""
|
|
@@ -306,19 +475,160 @@ try:
|
|
|
306
475
|
content = message.get("content", "")
|
|
307
476
|
if content is None:
|
|
308
477
|
continue
|
|
478
|
+
reasoning_content = message.get("reasoning_content")
|
|
479
|
+
if reasoning_content:
|
|
480
|
+
if isinstance(reasoning_content, str):
|
|
481
|
+
reasoning_content = [
|
|
482
|
+
{
|
|
483
|
+
"type": "text",
|
|
484
|
+
"text": reasoning_content,
|
|
485
|
+
}
|
|
486
|
+
]
|
|
487
|
+
elif not isinstance(reasoning_content, list):
|
|
488
|
+
reasoning_content = [
|
|
489
|
+
{
|
|
490
|
+
"type": "text",
|
|
491
|
+
"text": str(reasoning_content),
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
else:
|
|
495
|
+
reasoning_content = []
|
|
309
496
|
if isinstance(content, str):
|
|
310
|
-
|
|
497
|
+
if reasoning_content:
|
|
498
|
+
set_span_attribute(
|
|
499
|
+
span,
|
|
500
|
+
f"gen_ai.completion.{i}.content",
|
|
501
|
+
json.dumps(
|
|
502
|
+
reasoning_content
|
|
503
|
+
+ [
|
|
504
|
+
{
|
|
505
|
+
"type": "text",
|
|
506
|
+
"text": content,
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
set_span_attribute(
|
|
513
|
+
span,
|
|
514
|
+
f"gen_ai.completion.{i}.content",
|
|
515
|
+
content,
|
|
516
|
+
)
|
|
311
517
|
elif isinstance(content, list):
|
|
312
518
|
set_span_attribute(
|
|
313
|
-
span,
|
|
519
|
+
span,
|
|
520
|
+
f"gen_ai.completion.{i}.content",
|
|
521
|
+
json.dumps(reasoning_content + content),
|
|
314
522
|
)
|
|
315
523
|
else:
|
|
316
524
|
set_span_attribute(
|
|
317
525
|
span,
|
|
318
526
|
f"gen_ai.completion.{i}.content",
|
|
319
|
-
json.dumps(model_as_dict(content)),
|
|
527
|
+
json.dumps(reasoning_content + [model_as_dict(content)]),
|
|
320
528
|
)
|
|
321
529
|
|
|
530
|
+
def _process_content_part(self, content_part: dict) -> dict:
|
|
531
|
+
content_part_dict = model_as_dict(content_part)
|
|
532
|
+
if content_part_dict.get("type") == "output_text":
|
|
533
|
+
return {"type": "text", "text": content_part_dict.get("text")}
|
|
534
|
+
return content_part_dict
|
|
535
|
+
|
|
536
|
+
def _process_response_output(self, span, output):
|
|
537
|
+
"""Response of OpenAI Responses API"""
|
|
538
|
+
if not isinstance(output, list):
|
|
539
|
+
return
|
|
540
|
+
set_span_attribute(span, "gen_ai.completion.0.role", "assistant")
|
|
541
|
+
tool_call_index = 0
|
|
542
|
+
for block in output:
|
|
543
|
+
block_dict = model_as_dict(block)
|
|
544
|
+
if block_dict.get("type") == "message":
|
|
545
|
+
content = block_dict.get("content")
|
|
546
|
+
if content is None:
|
|
547
|
+
continue
|
|
548
|
+
if isinstance(content, str):
|
|
549
|
+
set_span_attribute(span, "gen_ai.completion.0.content", content)
|
|
550
|
+
elif isinstance(content, list):
|
|
551
|
+
set_span_attribute(
|
|
552
|
+
span,
|
|
553
|
+
"gen_ai.completion.0.content",
|
|
554
|
+
json_dumps(
|
|
555
|
+
[self._process_content_part(part) for part in content]
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
if block_dict.get("type") == "function_call":
|
|
559
|
+
set_span_attribute(
|
|
560
|
+
span,
|
|
561
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.id",
|
|
562
|
+
block_dict.get("id"),
|
|
563
|
+
)
|
|
564
|
+
set_span_attribute(
|
|
565
|
+
span,
|
|
566
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.name",
|
|
567
|
+
block_dict.get("name"),
|
|
568
|
+
)
|
|
569
|
+
set_span_attribute(
|
|
570
|
+
span,
|
|
571
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.arguments",
|
|
572
|
+
block_dict.get("arguments"),
|
|
573
|
+
)
|
|
574
|
+
tool_call_index += 1
|
|
575
|
+
elif block_dict.get("type") == "file_search_call":
|
|
576
|
+
set_span_attribute(
|
|
577
|
+
span,
|
|
578
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.id",
|
|
579
|
+
block_dict.get("id"),
|
|
580
|
+
)
|
|
581
|
+
set_span_attribute(
|
|
582
|
+
span,
|
|
583
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.name",
|
|
584
|
+
"file_search_call",
|
|
585
|
+
)
|
|
586
|
+
tool_call_index += 1
|
|
587
|
+
elif block_dict.get("type") == "web_search_call":
|
|
588
|
+
set_span_attribute(
|
|
589
|
+
span,
|
|
590
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.id",
|
|
591
|
+
block_dict.get("id"),
|
|
592
|
+
)
|
|
593
|
+
set_span_attribute(
|
|
594
|
+
span,
|
|
595
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.name",
|
|
596
|
+
"web_search_call",
|
|
597
|
+
)
|
|
598
|
+
tool_call_index += 1
|
|
599
|
+
elif block_dict.get("type") == "computer_call":
|
|
600
|
+
set_span_attribute(
|
|
601
|
+
span,
|
|
602
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.id",
|
|
603
|
+
block_dict.get("call_id"),
|
|
604
|
+
)
|
|
605
|
+
set_span_attribute(
|
|
606
|
+
span,
|
|
607
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.name",
|
|
608
|
+
"computer_call",
|
|
609
|
+
)
|
|
610
|
+
set_span_attribute(
|
|
611
|
+
span,
|
|
612
|
+
f"gen_ai.completion.0.tool_calls.{tool_call_index}.arguments",
|
|
613
|
+
json_dumps(block_dict.get("action")),
|
|
614
|
+
)
|
|
615
|
+
tool_call_index += 1
|
|
616
|
+
elif block_dict.get("type") == "reasoning":
|
|
617
|
+
reasoning_summary = block_dict.get("summary")
|
|
618
|
+
if reasoning_summary and isinstance(reasoning_summary, list):
|
|
619
|
+
processed_chunks = [
|
|
620
|
+
{"type": "text", "text": chunk.get("text")}
|
|
621
|
+
for chunk in reasoning_summary
|
|
622
|
+
if isinstance(chunk, dict)
|
|
623
|
+
and chunk.get("type") == "summary_text"
|
|
624
|
+
]
|
|
625
|
+
set_span_attribute(
|
|
626
|
+
span,
|
|
627
|
+
"gen_ai.completion.0.reasoning",
|
|
628
|
+
json_dumps(processed_chunks),
|
|
629
|
+
)
|
|
630
|
+
# TODO: handle other block types, in particular other calls
|
|
631
|
+
|
|
322
632
|
def _process_success_response(self, span, response_obj):
|
|
323
633
|
"""Process successful response attributes"""
|
|
324
634
|
response_dict = model_as_dict(response_obj)
|
|
@@ -327,7 +637,9 @@ try:
|
|
|
327
637
|
span, "gen_ai.response.model", response_dict.get("model")
|
|
328
638
|
)
|
|
329
639
|
|
|
330
|
-
if
|
|
640
|
+
if getattr(response_obj, "usage", None):
|
|
641
|
+
self._process_response_usage(span, getattr(response_obj, "usage", None))
|
|
642
|
+
elif response_dict.get("usage"):
|
|
331
643
|
self._process_response_usage(span, response_dict.get("usage"))
|
|
332
644
|
|
|
333
645
|
if response_dict.get("cache_creation_input_tokens"):
|
|
@@ -345,6 +657,8 @@ try:
|
|
|
345
657
|
|
|
346
658
|
if response_dict.get("choices"):
|
|
347
659
|
self._process_response_choices(span, response_dict.get("choices"))
|
|
660
|
+
elif response_dict.get("output"):
|
|
661
|
+
self._process_response_output(span, response_dict.get("output"))
|
|
348
662
|
|
|
349
663
|
except ImportError as e:
|
|
350
664
|
logger.debug(f"LiteLLM callback unavailable: {e}")
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from pydantic import BaseModel
|
|
2
3
|
from opentelemetry.sdk.trace import Span
|
|
3
4
|
from opentelemetry.util.types import AttributeValue
|
|
5
|
+
from typing_extensions import TypedDict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ToolDefinition(TypedDict):
|
|
9
|
+
name: str | None
|
|
10
|
+
description: str | None
|
|
11
|
+
parameters: dict | None
|
|
4
12
|
|
|
5
13
|
|
|
6
14
|
def model_as_dict(model: BaseModel | dict) -> dict:
|
|
@@ -16,3 +24,77 @@ def set_span_attribute(span: Span, key: str, value: AttributeValue | None):
|
|
|
16
24
|
if value is None or value == "":
|
|
17
25
|
return
|
|
18
26
|
span.set_attribute(key, value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_tool_definition(tool: dict) -> ToolDefinition:
|
|
30
|
+
parameters = None
|
|
31
|
+
description = None
|
|
32
|
+
name = (tool.get("function") or {}).get("name") or tool.get("name")
|
|
33
|
+
if tool.get("type") == "function":
|
|
34
|
+
function = tool.get("function") or {}
|
|
35
|
+
parameters = function.get("parameters") or tool.get("parameters")
|
|
36
|
+
description = function.get("description") or tool.get("description")
|
|
37
|
+
elif isinstance(tool.get("type"), str) and tool.get("type").startswith("computer"):
|
|
38
|
+
# Anthropic beta computer tools
|
|
39
|
+
# https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/computer-use-tool
|
|
40
|
+
|
|
41
|
+
# OpenAI computer use API
|
|
42
|
+
# https://platform.openai.com/docs/guides/tools-computer-use
|
|
43
|
+
if not name:
|
|
44
|
+
name = tool.get("type")
|
|
45
|
+
|
|
46
|
+
parameters = {}
|
|
47
|
+
tool_parameters = (tool.get("function") or {}).get("parameters") or {}
|
|
48
|
+
# Anthropic
|
|
49
|
+
display_width_px = tool_parameters.get("display_width_px") or tool.get(
|
|
50
|
+
"display_width_px"
|
|
51
|
+
)
|
|
52
|
+
display_height_px = tool_parameters.get("display_height_px") or tool.get(
|
|
53
|
+
"display_height_px"
|
|
54
|
+
)
|
|
55
|
+
display_number = tool_parameters.get("display_number") or tool.get(
|
|
56
|
+
"display_number"
|
|
57
|
+
)
|
|
58
|
+
if display_width_px:
|
|
59
|
+
parameters["display_width_px"] = display_width_px
|
|
60
|
+
if display_height_px:
|
|
61
|
+
parameters["display_height_px"] = display_height_px
|
|
62
|
+
if display_number:
|
|
63
|
+
parameters["display_number"] = display_number
|
|
64
|
+
# OpenAI
|
|
65
|
+
display_width = tool_parameters.get("display_width") or tool.get(
|
|
66
|
+
"display_width"
|
|
67
|
+
)
|
|
68
|
+
display_height = tool_parameters.get("display_height") or tool.get(
|
|
69
|
+
"display_height"
|
|
70
|
+
)
|
|
71
|
+
environment = tool_parameters.get("environment") or tool.get("environment")
|
|
72
|
+
if display_width:
|
|
73
|
+
parameters["display_width"] = display_width
|
|
74
|
+
if display_height:
|
|
75
|
+
parameters["display_height"] = tool.get("display_height")
|
|
76
|
+
if environment: # Literal['browser', 'mac', 'windows', 'ubuntu']
|
|
77
|
+
parameters["environment"] = environment
|
|
78
|
+
else:
|
|
79
|
+
# Some versions of LiteLLM (around 1.69.0) flatten the tool definition in
|
|
80
|
+
# anthropic style, not the OpenAI style as they do with other tool types.
|
|
81
|
+
function = tool.get("function") or tool
|
|
82
|
+
parameters = function.get("parameters") or function.get("input_schema")
|
|
83
|
+
description = function.get("description") or ""
|
|
84
|
+
|
|
85
|
+
return ToolDefinition(
|
|
86
|
+
name=name,
|
|
87
|
+
description=description,
|
|
88
|
+
parameters=parameters,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_validator_iterator(content):
|
|
93
|
+
"""
|
|
94
|
+
Some OpenAI objects contain fields typed as Iterable, which pydantic
|
|
95
|
+
internally converts to a ValidatorIterator, and they cannot be trivially
|
|
96
|
+
serialized without consuming the iterator to, for example, a list.
|
|
97
|
+
|
|
98
|
+
See: https://github.com/pydantic/pydantic/issues/9541#issuecomment-2189045051
|
|
99
|
+
"""
|
|
100
|
+
return re.search(r"pydantic.*ValidatorIterator'>$", str(type(content)))
|