lmnr 0.6.19__py3-none-any.whl → 0.6.21__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/opentelemetry_lib/decorators/__init__.py +188 -138
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +674 -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 +256 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +295 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +179 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +485 -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/openai/utils.py +3 -3
- lmnr/opentelemetry_lib/tracing/__init__.py +1 -1
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +12 -7
- lmnr/opentelemetry_lib/tracing/processor.py +1 -1
- lmnr/opentelemetry_lib/utils/package_check.py +9 -0
- lmnr/sdk/browser/browser_use_otel.py +4 -2
- lmnr/sdk/browser/patchright_otel.py +0 -26
- lmnr/sdk/browser/playwright_otel.py +51 -78
- lmnr/sdk/browser/pw_utils.py +359 -114
- lmnr/sdk/client/asynchronous/async_client.py +13 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +2 -0
- 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 -1
- 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 +14 -0
- lmnr/sdk/decorators.py +39 -4
- lmnr/sdk/evaluations.py +23 -9
- lmnr/sdk/laminar.py +75 -48
- lmnr/sdk/utils.py +23 -0
- lmnr/version.py +1 -1
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/METADATA +8 -7
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/RECORD +42 -25
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/WHEEL +1 -1
- {lmnr-0.6.19.dist-info → lmnr-0.6.21.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,295 @@
|
|
1
|
+
import logging
|
2
|
+
import time
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from opentelemetry._events import EventLogger
|
6
|
+
from .config import Config
|
7
|
+
from .event_emitter import (
|
8
|
+
emit_streaming_response_events,
|
9
|
+
)
|
10
|
+
from .span_utils import (
|
11
|
+
set_streaming_response_attributes,
|
12
|
+
)
|
13
|
+
from .utils import (
|
14
|
+
count_prompt_tokens_from_request,
|
15
|
+
dont_throw,
|
16
|
+
error_metrics_attributes,
|
17
|
+
set_span_attribute,
|
18
|
+
shared_metrics_attributes,
|
19
|
+
should_emit_events,
|
20
|
+
)
|
21
|
+
from opentelemetry.metrics import Counter, Histogram
|
22
|
+
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import (
|
23
|
+
GEN_AI_RESPONSE_ID,
|
24
|
+
)
|
25
|
+
from opentelemetry.semconv_ai import SpanAttributes
|
26
|
+
from opentelemetry.trace.status import Status, StatusCode
|
27
|
+
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
@dont_throw
|
32
|
+
def _process_response_item(item, complete_response):
|
33
|
+
if item.type == "message_start":
|
34
|
+
complete_response["model"] = item.message.model
|
35
|
+
complete_response["usage"] = dict(item.message.usage)
|
36
|
+
complete_response["id"] = item.message.id
|
37
|
+
elif item.type == "content_block_start":
|
38
|
+
index = item.index
|
39
|
+
if len(complete_response.get("events")) <= index:
|
40
|
+
complete_response["events"].append(
|
41
|
+
{"index": index, "text": "", "type": item.content_block.type}
|
42
|
+
)
|
43
|
+
elif item.type == "content_block_delta" and item.delta.type in [
|
44
|
+
"thinking_delta",
|
45
|
+
"text_delta",
|
46
|
+
]:
|
47
|
+
index = item.index
|
48
|
+
if item.delta.type == "thinking_delta":
|
49
|
+
complete_response["events"][index]["text"] += item.delta.thinking
|
50
|
+
elif item.delta.type == "text_delta":
|
51
|
+
complete_response["events"][index]["text"] += item.delta.text
|
52
|
+
elif item.type == "message_delta":
|
53
|
+
for event in complete_response.get("events", []):
|
54
|
+
event["finish_reason"] = item.delta.stop_reason
|
55
|
+
if item.usage:
|
56
|
+
if "usage" in complete_response:
|
57
|
+
item_output_tokens = dict(item.usage).get("output_tokens", 0)
|
58
|
+
existing_output_tokens = complete_response["usage"].get(
|
59
|
+
"output_tokens", 0
|
60
|
+
)
|
61
|
+
complete_response["usage"]["output_tokens"] = (
|
62
|
+
item_output_tokens + existing_output_tokens
|
63
|
+
)
|
64
|
+
else:
|
65
|
+
complete_response["usage"] = dict(item.usage)
|
66
|
+
|
67
|
+
|
68
|
+
def _set_token_usage(
|
69
|
+
span,
|
70
|
+
complete_response,
|
71
|
+
prompt_tokens,
|
72
|
+
completion_tokens,
|
73
|
+
metric_attributes: dict = {},
|
74
|
+
token_histogram: Histogram = None,
|
75
|
+
choice_counter: Counter = None,
|
76
|
+
):
|
77
|
+
cache_read_tokens = (
|
78
|
+
complete_response.get("usage", {}).get("cache_read_input_tokens", 0) or 0
|
79
|
+
)
|
80
|
+
cache_creation_tokens = (
|
81
|
+
complete_response.get("usage", {}).get("cache_creation_input_tokens", 0) or 0
|
82
|
+
)
|
83
|
+
|
84
|
+
input_tokens = prompt_tokens + cache_read_tokens + cache_creation_tokens
|
85
|
+
total_tokens = input_tokens + completion_tokens
|
86
|
+
|
87
|
+
set_span_attribute(span, SpanAttributes.LLM_USAGE_PROMPT_TOKENS, input_tokens)
|
88
|
+
set_span_attribute(
|
89
|
+
span, SpanAttributes.LLM_USAGE_COMPLETION_TOKENS, completion_tokens
|
90
|
+
)
|
91
|
+
set_span_attribute(span, SpanAttributes.LLM_USAGE_TOTAL_TOKENS, total_tokens)
|
92
|
+
|
93
|
+
set_span_attribute(
|
94
|
+
span, SpanAttributes.LLM_RESPONSE_MODEL, complete_response.get("model")
|
95
|
+
)
|
96
|
+
set_span_attribute(
|
97
|
+
span, SpanAttributes.LLM_USAGE_CACHE_READ_INPUT_TOKENS, cache_read_tokens
|
98
|
+
)
|
99
|
+
set_span_attribute(
|
100
|
+
span,
|
101
|
+
SpanAttributes.LLM_USAGE_CACHE_CREATION_INPUT_TOKENS,
|
102
|
+
cache_creation_tokens,
|
103
|
+
)
|
104
|
+
|
105
|
+
if token_histogram and type(input_tokens) is int and input_tokens >= 0:
|
106
|
+
token_histogram.record(
|
107
|
+
input_tokens,
|
108
|
+
attributes={
|
109
|
+
**metric_attributes,
|
110
|
+
SpanAttributes.LLM_TOKEN_TYPE: "input",
|
111
|
+
},
|
112
|
+
)
|
113
|
+
|
114
|
+
if token_histogram and type(completion_tokens) is int and completion_tokens >= 0:
|
115
|
+
token_histogram.record(
|
116
|
+
completion_tokens,
|
117
|
+
attributes={
|
118
|
+
**metric_attributes,
|
119
|
+
SpanAttributes.LLM_TOKEN_TYPE: "output",
|
120
|
+
},
|
121
|
+
)
|
122
|
+
|
123
|
+
if type(complete_response.get("events")) is list and choice_counter:
|
124
|
+
for event in complete_response.get("events"):
|
125
|
+
choice_counter.add(
|
126
|
+
1,
|
127
|
+
attributes={
|
128
|
+
**metric_attributes,
|
129
|
+
SpanAttributes.LLM_RESPONSE_FINISH_REASON: event.get(
|
130
|
+
"finish_reason"
|
131
|
+
),
|
132
|
+
},
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
def _handle_streaming_response(span, event_logger, complete_response):
|
137
|
+
if should_emit_events() and event_logger:
|
138
|
+
emit_streaming_response_events(event_logger, complete_response)
|
139
|
+
else:
|
140
|
+
if not span.is_recording():
|
141
|
+
return
|
142
|
+
set_streaming_response_attributes(span, complete_response.get("events"))
|
143
|
+
|
144
|
+
|
145
|
+
@dont_throw
|
146
|
+
def build_from_streaming_response(
|
147
|
+
span,
|
148
|
+
response,
|
149
|
+
instance,
|
150
|
+
start_time,
|
151
|
+
token_histogram: Histogram = None,
|
152
|
+
choice_counter: Counter = None,
|
153
|
+
duration_histogram: Histogram = None,
|
154
|
+
exception_counter: Counter = None,
|
155
|
+
event_logger: Optional[EventLogger] = None,
|
156
|
+
kwargs: dict = {},
|
157
|
+
):
|
158
|
+
complete_response = {"events": [], "model": "", "usage": {}, "id": ""}
|
159
|
+
for item in response:
|
160
|
+
try:
|
161
|
+
yield item
|
162
|
+
except Exception as e:
|
163
|
+
attributes = error_metrics_attributes(e)
|
164
|
+
if exception_counter:
|
165
|
+
exception_counter.add(1, attributes=attributes)
|
166
|
+
raise e
|
167
|
+
_process_response_item(item, complete_response)
|
168
|
+
|
169
|
+
metric_attributes = shared_metrics_attributes(complete_response)
|
170
|
+
set_span_attribute(span, GEN_AI_RESPONSE_ID, complete_response.get("id"))
|
171
|
+
if duration_histogram:
|
172
|
+
duration = time.time() - start_time
|
173
|
+
duration_histogram.record(
|
174
|
+
duration,
|
175
|
+
attributes=metric_attributes,
|
176
|
+
)
|
177
|
+
|
178
|
+
# calculate token usage
|
179
|
+
if Config.enrich_token_usage:
|
180
|
+
try:
|
181
|
+
completion_tokens = -1
|
182
|
+
# prompt_usage
|
183
|
+
if usage := complete_response.get("usage"):
|
184
|
+
prompt_tokens = usage.get("input_tokens", 0) or 0
|
185
|
+
else:
|
186
|
+
prompt_tokens = count_prompt_tokens_from_request(instance, kwargs)
|
187
|
+
|
188
|
+
# completion_usage
|
189
|
+
if usage := complete_response.get("usage"):
|
190
|
+
completion_tokens = usage.get("output_tokens", 0) or 0
|
191
|
+
else:
|
192
|
+
completion_content = ""
|
193
|
+
if complete_response.get("events"):
|
194
|
+
model_name = complete_response.get("model") or None
|
195
|
+
for event in complete_response.get("events"):
|
196
|
+
if event.get("text"):
|
197
|
+
completion_content += event.get("text")
|
198
|
+
|
199
|
+
if model_name and hasattr(instance, "count_tokens"):
|
200
|
+
completion_tokens = instance.count_tokens(completion_content)
|
201
|
+
|
202
|
+
_set_token_usage(
|
203
|
+
span,
|
204
|
+
complete_response,
|
205
|
+
prompt_tokens,
|
206
|
+
completion_tokens,
|
207
|
+
metric_attributes,
|
208
|
+
token_histogram,
|
209
|
+
choice_counter,
|
210
|
+
)
|
211
|
+
except Exception as e:
|
212
|
+
logger.warning("Failed to set token usage, error: %s", e)
|
213
|
+
|
214
|
+
_handle_streaming_response(span, event_logger, complete_response)
|
215
|
+
|
216
|
+
if span.is_recording():
|
217
|
+
span.set_status(Status(StatusCode.OK))
|
218
|
+
span.end()
|
219
|
+
|
220
|
+
|
221
|
+
@dont_throw
|
222
|
+
async def abuild_from_streaming_response(
|
223
|
+
span,
|
224
|
+
response,
|
225
|
+
instance,
|
226
|
+
start_time,
|
227
|
+
token_histogram: Histogram = None,
|
228
|
+
choice_counter: Counter = None,
|
229
|
+
duration_histogram: Histogram = None,
|
230
|
+
exception_counter: Counter = None,
|
231
|
+
event_logger: Optional[EventLogger] = None,
|
232
|
+
kwargs: dict = {},
|
233
|
+
):
|
234
|
+
complete_response = {"events": [], "model": "", "usage": {}, "id": ""}
|
235
|
+
async for item in response:
|
236
|
+
try:
|
237
|
+
yield item
|
238
|
+
except Exception as e:
|
239
|
+
attributes = error_metrics_attributes(e)
|
240
|
+
if exception_counter:
|
241
|
+
exception_counter.add(1, attributes=attributes)
|
242
|
+
raise e
|
243
|
+
_process_response_item(item, complete_response)
|
244
|
+
|
245
|
+
set_span_attribute(span, GEN_AI_RESPONSE_ID, complete_response.get("id"))
|
246
|
+
|
247
|
+
metric_attributes = shared_metrics_attributes(complete_response)
|
248
|
+
|
249
|
+
if duration_histogram:
|
250
|
+
duration = time.time() - start_time
|
251
|
+
duration_histogram.record(
|
252
|
+
duration,
|
253
|
+
attributes=metric_attributes,
|
254
|
+
)
|
255
|
+
|
256
|
+
# calculate token usage
|
257
|
+
if Config.enrich_token_usage:
|
258
|
+
try:
|
259
|
+
# prompt_usage
|
260
|
+
if usage := complete_response.get("usage"):
|
261
|
+
prompt_tokens = usage.get("input_tokens", 0)
|
262
|
+
else:
|
263
|
+
prompt_tokens = count_prompt_tokens_from_request(instance, kwargs)
|
264
|
+
|
265
|
+
# completion_usage
|
266
|
+
if usage := complete_response.get("usage"):
|
267
|
+
completion_tokens = usage.get("output_tokens", 0)
|
268
|
+
else:
|
269
|
+
completion_content = ""
|
270
|
+
if complete_response.get("events"):
|
271
|
+
model_name = complete_response.get("model") or None
|
272
|
+
for event in complete_response.get("events"):
|
273
|
+
if event.get("text"):
|
274
|
+
completion_content += event.get("text")
|
275
|
+
|
276
|
+
if model_name and hasattr(instance, "count_tokens"):
|
277
|
+
completion_tokens = instance.count_tokens(completion_content)
|
278
|
+
|
279
|
+
_set_token_usage(
|
280
|
+
span,
|
281
|
+
complete_response,
|
282
|
+
prompt_tokens,
|
283
|
+
completion_tokens,
|
284
|
+
metric_attributes,
|
285
|
+
token_histogram,
|
286
|
+
choice_counter,
|
287
|
+
)
|
288
|
+
except Exception as e:
|
289
|
+
logger.warning("Failed to set token usage, error: %s", str(e))
|
290
|
+
|
291
|
+
_handle_streaming_response(span, event_logger, complete_response)
|
292
|
+
|
293
|
+
if span.is_recording():
|
294
|
+
span.set_status(Status(StatusCode.OK))
|
295
|
+
span.end()
|
@@ -0,0 +1,179 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import logging
|
4
|
+
import os
|
5
|
+
import threading
|
6
|
+
import traceback
|
7
|
+
from importlib.metadata import version
|
8
|
+
|
9
|
+
from opentelemetry import context as context_api
|
10
|
+
from .config import Config
|
11
|
+
from opentelemetry.semconv_ai import SpanAttributes
|
12
|
+
|
13
|
+
GEN_AI_SYSTEM = "gen_ai.system"
|
14
|
+
GEN_AI_SYSTEM_ANTHROPIC = "anthropic"
|
15
|
+
_PYDANTIC_VERSION = version("pydantic")
|
16
|
+
|
17
|
+
LMNR_TRACE_CONTENT = "LMNR_TRACE_CONTENT"
|
18
|
+
|
19
|
+
|
20
|
+
def set_span_attribute(span, name, value):
|
21
|
+
if value is not None:
|
22
|
+
if value != "":
|
23
|
+
span.set_attribute(name, value)
|
24
|
+
return
|
25
|
+
|
26
|
+
|
27
|
+
def should_send_prompts():
|
28
|
+
return (
|
29
|
+
os.getenv(LMNR_TRACE_CONTENT) or "true"
|
30
|
+
).lower() == "true" or context_api.get_value("override_enable_content_tracing")
|
31
|
+
|
32
|
+
|
33
|
+
def dont_throw(func):
|
34
|
+
"""
|
35
|
+
A decorator that wraps the passed in function and logs exceptions instead of throwing them.
|
36
|
+
Works for both synchronous and asynchronous functions.
|
37
|
+
"""
|
38
|
+
logger = logging.getLogger(func.__module__)
|
39
|
+
|
40
|
+
async def async_wrapper(*args, **kwargs):
|
41
|
+
try:
|
42
|
+
return await func(*args, **kwargs)
|
43
|
+
except Exception as e:
|
44
|
+
_handle_exception(e, func, logger)
|
45
|
+
|
46
|
+
def sync_wrapper(*args, **kwargs):
|
47
|
+
try:
|
48
|
+
return func(*args, **kwargs)
|
49
|
+
except Exception as e:
|
50
|
+
_handle_exception(e, func, logger)
|
51
|
+
|
52
|
+
def _handle_exception(e, func, logger):
|
53
|
+
logger.debug(
|
54
|
+
"OpenLLMetry failed to trace in %s, error: %s",
|
55
|
+
func.__name__,
|
56
|
+
traceback.format_exc(),
|
57
|
+
)
|
58
|
+
if Config.exception_logger:
|
59
|
+
Config.exception_logger(e)
|
60
|
+
|
61
|
+
return async_wrapper if asyncio.iscoroutinefunction(func) else sync_wrapper
|
62
|
+
|
63
|
+
|
64
|
+
@dont_throw
|
65
|
+
def shared_metrics_attributes(response):
|
66
|
+
if not isinstance(response, dict):
|
67
|
+
response = response.__dict__
|
68
|
+
|
69
|
+
common_attributes = Config.get_common_metrics_attributes()
|
70
|
+
|
71
|
+
return {
|
72
|
+
**common_attributes,
|
73
|
+
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
|
74
|
+
SpanAttributes.LLM_RESPONSE_MODEL: response.get("model"),
|
75
|
+
}
|
76
|
+
|
77
|
+
|
78
|
+
@dont_throw
|
79
|
+
def error_metrics_attributes(exception):
|
80
|
+
return {
|
81
|
+
GEN_AI_SYSTEM: GEN_AI_SYSTEM_ANTHROPIC,
|
82
|
+
"error.type": exception.__class__.__name__,
|
83
|
+
}
|
84
|
+
|
85
|
+
|
86
|
+
@dont_throw
|
87
|
+
def count_prompt_tokens_from_request(anthropic, request):
|
88
|
+
prompt_tokens = 0
|
89
|
+
if hasattr(anthropic, "count_tokens"):
|
90
|
+
if request.get("prompt"):
|
91
|
+
prompt_tokens = anthropic.count_tokens(request.get("prompt"))
|
92
|
+
elif messages := request.get("messages"):
|
93
|
+
prompt_tokens = 0
|
94
|
+
for m in messages:
|
95
|
+
content = m.get("content")
|
96
|
+
if isinstance(content, str):
|
97
|
+
prompt_tokens += anthropic.count_tokens(content)
|
98
|
+
elif isinstance(content, list):
|
99
|
+
for item in content:
|
100
|
+
# TODO: handle image and tool tokens
|
101
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
102
|
+
prompt_tokens += anthropic.count_tokens(
|
103
|
+
item.get("text", "")
|
104
|
+
)
|
105
|
+
return prompt_tokens
|
106
|
+
|
107
|
+
|
108
|
+
@dont_throw
|
109
|
+
async def acount_prompt_tokens_from_request(anthropic, request):
|
110
|
+
prompt_tokens = 0
|
111
|
+
if hasattr(anthropic, "count_tokens"):
|
112
|
+
if request.get("prompt"):
|
113
|
+
prompt_tokens = await anthropic.count_tokens(request.get("prompt"))
|
114
|
+
elif messages := request.get("messages"):
|
115
|
+
prompt_tokens = 0
|
116
|
+
for m in messages:
|
117
|
+
content = m.get("content")
|
118
|
+
if isinstance(content, str):
|
119
|
+
prompt_tokens += await anthropic.count_tokens(content)
|
120
|
+
elif isinstance(content, list):
|
121
|
+
for item in content:
|
122
|
+
# TODO: handle image and tool tokens
|
123
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
124
|
+
prompt_tokens += await anthropic.count_tokens(
|
125
|
+
item.get("text", "")
|
126
|
+
)
|
127
|
+
return prompt_tokens
|
128
|
+
|
129
|
+
|
130
|
+
def run_async(method):
|
131
|
+
try:
|
132
|
+
loop = asyncio.get_running_loop()
|
133
|
+
except RuntimeError:
|
134
|
+
loop = None
|
135
|
+
|
136
|
+
if loop and loop.is_running():
|
137
|
+
thread = threading.Thread(target=lambda: asyncio.run(method))
|
138
|
+
thread.start()
|
139
|
+
thread.join()
|
140
|
+
else:
|
141
|
+
asyncio.run(method)
|
142
|
+
|
143
|
+
|
144
|
+
def should_emit_events() -> bool:
|
145
|
+
"""
|
146
|
+
Checks if the instrumentation isn't using the legacy attributes
|
147
|
+
and if the event logger is not None.
|
148
|
+
"""
|
149
|
+
return not Config.use_legacy_attributes
|
150
|
+
|
151
|
+
|
152
|
+
class JSONEncoder(json.JSONEncoder):
|
153
|
+
def default(self, o):
|
154
|
+
if hasattr(o, "to_json"):
|
155
|
+
return o.to_json()
|
156
|
+
|
157
|
+
if hasattr(o, "model_dump_json"):
|
158
|
+
return o.model_dump_json()
|
159
|
+
|
160
|
+
try:
|
161
|
+
return str(o)
|
162
|
+
except Exception:
|
163
|
+
logger = logging.getLogger(__name__)
|
164
|
+
logger.debug("Failed to serialize object of type: %s", type(o).__name__)
|
165
|
+
return ""
|
166
|
+
|
167
|
+
|
168
|
+
def model_as_dict(model):
|
169
|
+
if isinstance(model, dict):
|
170
|
+
return model
|
171
|
+
if _PYDANTIC_VERSION < "2.0.0" and hasattr(model, "dict"):
|
172
|
+
return model.dict()
|
173
|
+
if hasattr(model, "model_dump"):
|
174
|
+
return model.model_dump()
|
175
|
+
else:
|
176
|
+
try:
|
177
|
+
return dict(model)
|
178
|
+
except Exception:
|
179
|
+
return model
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__ = "0.41.0"
|