Skip to content

Commit 53a3ea3

Browse files
authored
(Refactor) Langfuse - remove prepare_metadata, langfuse python SDK now handles non-json serializable objects (#7925)
* test_langfuse_logging_completion_with_langfuse_metadata * fix litellm - remove prepare metadata * test_langfuse_logging_with_non_serializable_metadata * detailed e2e langfuse metadata tests * clean up langfuse logging * fix langfuse * remove unused imports * fix code qa checks * fix _prepare_metadata
1 parent 27560bd commit 53a3ea3

16 files changed

+1231
-133
lines changed

litellm/integrations/langfuse/langfuse.py

Lines changed: 24 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33
import copy
44
import os
55
import traceback
6-
from collections.abc import MutableMapping, MutableSequence, MutableSet
76
from typing import TYPE_CHECKING, Any, Dict, List, Optional, cast
87

98
from packaging.version import Version
10-
from pydantic import BaseModel
119

1210
import litellm
1311
from litellm._logging import verbose_logger
@@ -71,8 +69,9 @@ def __init__(
7169
"flush_interval": self.langfuse_flush_interval, # flush interval in seconds
7270
"httpx_client": self.langfuse_client,
7371
}
72+
self.langfuse_sdk_version: str = langfuse.version.__version__
7473

75-
if Version(langfuse.version.__version__) >= Version("2.6.0"):
74+
if Version(self.langfuse_sdk_version) >= Version("2.6.0"):
7675
parameters["sdk_integration"] = "litellm"
7776

7877
self.Langfuse = Langfuse(**parameters)
@@ -360,73 +359,6 @@ def _log_langfuse_v1(
360359
)
361360
)
362361

363-
def is_base_type(self, value: Any) -> bool:
364-
# Check if the value is of a base type
365-
base_types = (int, float, str, bool, list, dict, tuple)
366-
return isinstance(value, base_types)
367-
368-
def _prepare_metadata(self, metadata: Optional[dict]) -> Any:
369-
try:
370-
if metadata is None:
371-
return None
372-
373-
# Filter out function types from the metadata
374-
sanitized_metadata = {k: v for k, v in metadata.items() if not callable(v)}
375-
376-
return copy.deepcopy(sanitized_metadata)
377-
except Exception as e:
378-
verbose_logger.debug(f"Langfuse Layer Error - {e}, metadata: {metadata}")
379-
380-
new_metadata: Dict[str, Any] = {}
381-
382-
# if metadata is not a MutableMapping, return an empty dict since we can't call items() on it
383-
if not isinstance(metadata, MutableMapping):
384-
verbose_logger.debug(
385-
"Langfuse Layer Logging - metadata is not a MutableMapping, returning empty dict"
386-
)
387-
return new_metadata
388-
389-
for key, value in metadata.items():
390-
try:
391-
if isinstance(value, MutableMapping):
392-
new_metadata[key] = self._prepare_metadata(cast(dict, value))
393-
elif isinstance(value, MutableSequence):
394-
# For lists or other mutable sequences
395-
new_metadata[key] = list(
396-
(
397-
self._prepare_metadata(cast(dict, v))
398-
if isinstance(v, MutableMapping)
399-
else copy.deepcopy(v)
400-
)
401-
for v in value
402-
)
403-
elif isinstance(value, MutableSet):
404-
# For sets specifically, create a new set by passing an iterable
405-
new_metadata[key] = set(
406-
(
407-
self._prepare_metadata(cast(dict, v))
408-
if isinstance(v, MutableMapping)
409-
else copy.deepcopy(v)
410-
)
411-
for v in value
412-
)
413-
elif isinstance(value, BaseModel):
414-
new_metadata[key] = value.model_dump()
415-
elif self.is_base_type(value):
416-
new_metadata[key] = value
417-
else:
418-
verbose_logger.debug(
419-
f"Langfuse Layer Error - Unsupported metadata type: {type(value)} for key: {key}"
420-
)
421-
continue
422-
423-
except (TypeError, copy.Error):
424-
verbose_logger.debug(
425-
f"Langfuse Layer Error - Couldn't copy metadata key: {key}, type of key: {type(key)}, type of value: {type(value)} - {traceback.format_exc()}"
426-
)
427-
428-
return new_metadata
429-
430362
def _log_langfuse_v2( # noqa: PLR0915
431363
self,
432364
user_id,
@@ -443,27 +375,17 @@ def _log_langfuse_v2( # noqa: PLR0915
443375
print_verbose,
444376
litellm_call_id,
445377
) -> tuple:
446-
import langfuse
447-
448378
verbose_logger.debug("Langfuse Layer Logging - logging to langfuse v2")
449379

450380
try:
451-
metadata = self._prepare_metadata(metadata)
452-
453-
langfuse_version = Version(langfuse.version.__version__)
454-
455-
supports_tags = langfuse_version >= Version("2.6.3")
456-
supports_prompt = langfuse_version >= Version("2.7.3")
457-
supports_costs = langfuse_version >= Version("2.7.3")
458-
supports_completion_start_time = langfuse_version >= Version("2.7.3")
459-
381+
metadata = metadata or {}
460382
standard_logging_object: Optional[StandardLoggingPayload] = cast(
461383
Optional[StandardLoggingPayload],
462384
kwargs.get("standard_logging_object", None),
463385
)
464386
tags = (
465387
self._get_langfuse_tags(standard_logging_object=standard_logging_object)
466-
if supports_tags
388+
if self._supports_tags()
467389
else []
468390
)
469391

@@ -624,7 +546,7 @@ def _log_langfuse_v2( # noqa: PLR0915
624546
if aws_region_name:
625547
clean_metadata["aws_region_name"] = aws_region_name
626548

627-
if supports_tags:
549+
if self._supports_tags():
628550
if "cache_hit" in kwargs:
629551
if kwargs["cache_hit"] is None:
630552
kwargs["cache_hit"] = False
@@ -670,7 +592,7 @@ def _log_langfuse_v2( # noqa: PLR0915
670592
usage = {
671593
"prompt_tokens": _usage_obj.prompt_tokens,
672594
"completion_tokens": _usage_obj.completion_tokens,
673-
"total_cost": cost if supports_costs else None,
595+
"total_cost": cost if self._supports_costs() else None,
674596
}
675597
generation_name = clean_metadata.pop("generation_name", None)
676598
if generation_name is None:
@@ -713,7 +635,7 @@ def _log_langfuse_v2( # noqa: PLR0915
713635
if parent_observation_id is not None:
714636
generation_params["parent_observation_id"] = parent_observation_id
715637

716-
if supports_prompt:
638+
if self._supports_prompt():
717639
generation_params = _add_prompt_to_generation_params(
718640
generation_params=generation_params,
719641
clean_metadata=clean_metadata,
@@ -723,7 +645,7 @@ def _log_langfuse_v2( # noqa: PLR0915
723645
if output is not None and isinstance(output, str) and level == "ERROR":
724646
generation_params["status_message"] = output
725647

726-
if supports_completion_start_time:
648+
if self._supports_completion_start_time():
727649
generation_params["completion_start_time"] = kwargs.get(
728650
"completion_start_time", None
729651
)
@@ -770,6 +692,22 @@ def add_default_langfuse_tags(self, tags, kwargs, metadata):
770692
tags.append(f"cache_key:{_cache_key}")
771693
return tags
772694

695+
def _supports_tags(self):
696+
"""Check if current langfuse version supports tags"""
697+
return Version(self.langfuse_sdk_version) >= Version("2.6.3")
698+
699+
def _supports_prompt(self):
700+
"""Check if current langfuse version supports prompt"""
701+
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
702+
703+
def _supports_costs(self):
704+
"""Check if current langfuse version supports costs"""
705+
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
706+
707+
def _supports_completion_start_time(self):
708+
"""Check if current langfuse version supports completion start time"""
709+
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
710+
773711

774712
def _add_prompt_to_generation_params(
775713
generation_params: dict,

litellm/integrations/langfuse/langfuse_prompt_management.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ def __init__(
107107
langfuse_host=None,
108108
flush_interval=1,
109109
):
110+
import langfuse
111+
112+
self.langfuse_sdk_version = langfuse.version.__version__
110113
self.Langfuse = langfuse_client_init(
111114
langfuse_public_key=langfuse_public_key,
112115
langfuse_secret=langfuse_secret,

tests/code_coverage_tests/recursive_detector.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
"text_completion",
99
"_check_for_os_environ_vars",
1010
"clean_message",
11-
"_prepare_metadata",
1211
"unpack_defs",
1312
"convert_to_nullable",
1413
"add_object_type",
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"batch": [
3+
{
4+
"id": "9ee9100b-c4aa-4e40-a10d-bc189f8b4242",
5+
"type": "trace-create",
6+
"body": {
7+
"id": "litellm-test-c414db10-dd68-406e-9d9e-03839bc2f346",
8+
"timestamp": "2025-01-22T17:27:51.702596Z",
9+
"name": "litellm-acompletion",
10+
"input": {
11+
"messages": [
12+
{
13+
"role": "user",
14+
"content": "Hello!"
15+
}
16+
]
17+
},
18+
"output": {
19+
"content": "Hello! How can I assist you today?",
20+
"role": "assistant",
21+
"tool_calls": null,
22+
"function_call": null
23+
},
24+
"tags": []
25+
},
26+
"timestamp": "2025-01-22T17:27:51.702716Z"
27+
},
28+
{
29+
"id": "f8d20489-ed58-429f-b609-87380e223746",
30+
"type": "generation-create",
31+
"body": {
32+
"traceId": "litellm-test-c414db10-dd68-406e-9d9e-03839bc2f346",
33+
"name": "litellm-acompletion",
34+
"startTime": "2025-01-22T09:27:51.150898-08:00",
35+
"metadata": {
36+
"string_value": "hello",
37+
"int_value": 42,
38+
"float_value": 3.14,
39+
"bool_value": true,
40+
"nested_dict": {
41+
"key1": "value1",
42+
"key2": {
43+
"inner_key": "inner_value"
44+
}
45+
},
46+
"list_value": [
47+
1,
48+
2,
49+
3
50+
],
51+
"set_value": [
52+
1,
53+
2,
54+
3
55+
],
56+
"complex_list": [
57+
{
58+
"dict_in_list": "value"
59+
},
60+
"simple_string",
61+
[
62+
1,
63+
2,
64+
3
65+
]
66+
],
67+
"user": {
68+
"name": "John",
69+
"age": 30,
70+
"tags": [
71+
"customer",
72+
"active"
73+
]
74+
},
75+
"hidden_params": {
76+
"model_id": null,
77+
"cache_key": null,
78+
"api_base": "https://api.openai.com",
79+
"response_cost": 5.4999999999999995e-05,
80+
"additional_headers": {},
81+
"litellm_overhead_time_ms": null
82+
},
83+
"litellm_response_cost": 5.4999999999999995e-05,
84+
"cache_hit": false,
85+
"requester_metadata": {}
86+
},
87+
"input": {
88+
"messages": [
89+
{
90+
"role": "user",
91+
"content": "Hello!"
92+
}
93+
]
94+
},
95+
"output": {
96+
"content": "Hello! How can I assist you today?",
97+
"role": "assistant",
98+
"tool_calls": null,
99+
"function_call": null
100+
},
101+
"level": "DEFAULT",
102+
"id": "time-09-27-51-150898_chatcmpl-b783291c-dc76-4660-bfef-b79be9d54e57",
103+
"endTime": "2025-01-22T09:27:51.702048-08:00",
104+
"completionStartTime": "2025-01-22T09:27:51.702048-08:00",
105+
"model": "gpt-3.5-turbo",
106+
"modelParameters": {
107+
"extra_body": "{}"
108+
},
109+
"usage": {
110+
"input": 10,
111+
"output": 20,
112+
"unit": "TOKENS",
113+
"totalCost": 5.4999999999999995e-05
114+
}
115+
},
116+
"timestamp": "2025-01-22T17:27:51.703046Z"
117+
}
118+
],
119+
"metadata": {
120+
"batch_size": 2,
121+
"sdk_integration": "litellm",
122+
"sdk_name": "python",
123+
"sdk_version": "2.44.1",
124+
"public_key": "pk-lf-e02aaea3-8668-4c9f-8c69-771a4ea1f5c9"
125+
}
126+
}

0 commit comments

Comments
 (0)