mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(langfuse): introspect Langfuse.__init__ signature for sdk_integration kwarg
This commit is contained in:
parent
b83d11351f
commit
70dd2755d0
3 changed files with 165 additions and 5 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#### What this does ####
|
||||
# On success, logs events to Langfuse
|
||||
import inspect
|
||||
import os
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
|
@ -113,6 +114,26 @@ def resolve_langfuse_credentials(
|
|||
return public_key, secret_key, resolved_host
|
||||
|
||||
|
||||
def _langfuse_init_supports_sdk_integration() -> bool:
|
||||
"""
|
||||
Whether the installed Langfuse SDK's ``Langfuse.__init__`` accepts the
|
||||
``sdk_integration`` kwarg.
|
||||
|
||||
The kwarg was added in Langfuse 2.6.0 and removed in Langfuse 3.x. We
|
||||
detect by signature introspection rather than version-string comparison
|
||||
so the gate stays correct across past and future SDK releases.
|
||||
|
||||
See: https://github.com/BerriAI/litellm/issues/13137
|
||||
"""
|
||||
from langfuse import Langfuse
|
||||
|
||||
try:
|
||||
params = inspect.signature(Langfuse.__init__).parameters
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
return "sdk_integration" in params
|
||||
|
||||
|
||||
class LangFuseLogger:
|
||||
# Class variables or attributes
|
||||
def __init__(
|
||||
|
|
@ -169,7 +190,7 @@ class LangFuseLogger:
|
|||
}
|
||||
self.langfuse_sdk_version: str = langfuse.version.__version__
|
||||
|
||||
if Version(self.langfuse_sdk_version) >= Version("2.6.0"):
|
||||
if _langfuse_init_supports_sdk_integration():
|
||||
parameters["sdk_integration"] = "litellm"
|
||||
self.Langfuse: Langfuse = self.safe_init_langfuse_client(parameters)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import os
|
|||
from functools import lru_cache
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union, cast
|
||||
|
||||
from packaging.version import Version
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from litellm.integrations.custom_logger import CustomLogger
|
||||
|
|
@ -20,7 +19,11 @@ from ...litellm_core_utils.specialty_caches.dynamic_logging_cache import (
|
|||
DynamicLoggingCache,
|
||||
)
|
||||
from ..prompt_management_base import PromptManagementBase
|
||||
from .langfuse import LangFuseLogger, resolve_langfuse_credentials
|
||||
from .langfuse import (
|
||||
LangFuseLogger,
|
||||
_langfuse_init_supports_sdk_integration,
|
||||
resolve_langfuse_credentials,
|
||||
)
|
||||
from .langfuse_handler import LangFuseHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -64,7 +67,6 @@ def langfuse_client_init(
|
|||
Exception: If langfuse package is not installed
|
||||
"""
|
||||
try:
|
||||
import langfuse
|
||||
from langfuse import Langfuse
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
|
|
@ -99,7 +101,7 @@ def langfuse_client_init(
|
|||
), # flush interval in seconds
|
||||
}
|
||||
|
||||
if Version(langfuse.version.__version__) >= Version("2.6.0"):
|
||||
if _langfuse_init_supports_sdk_integration():
|
||||
parameters["sdk_integration"] = "litellm"
|
||||
|
||||
client = Langfuse(**parameters)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,143 @@ from litellm.integrations.langfuse.langfuse import LangFuseLogger
|
|||
from litellm.types.integrations.langfuse import *
|
||||
|
||||
|
||||
def _make_fake_langfuse_module(langfuse_class):
|
||||
"""Build a stand-in for the langfuse package — only what LangFuseLogger / langfuse_client_init touch at init time."""
|
||||
module = MagicMock()
|
||||
module.Langfuse = langfuse_class
|
||||
module.version = MagicMock()
|
||||
module.version.__version__ = "3.14.0"
|
||||
return module
|
||||
|
||||
|
||||
class _FakeLangfuseV3:
|
||||
"""Mimics Langfuse SDK v3.x: no ``sdk_integration`` kwarg, no ``**kwargs`` catchall."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
public_key=None,
|
||||
secret_key=None,
|
||||
host=None,
|
||||
release=None,
|
||||
debug=None,
|
||||
flush_interval=None,
|
||||
httpx_client=None,
|
||||
environment=None,
|
||||
):
|
||||
self.public_key = public_key
|
||||
self.client = MagicMock()
|
||||
self.client.projects.get.side_effect = Exception("offline test")
|
||||
|
||||
|
||||
class _FakeLangfuseV2:
|
||||
"""Mimics Langfuse SDK v2.6+: ``sdk_integration`` is in the signature."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
public_key=None,
|
||||
secret_key=None,
|
||||
host=None,
|
||||
release=None,
|
||||
debug=None,
|
||||
flush_interval=None,
|
||||
httpx_client=None,
|
||||
sdk_integration=None,
|
||||
):
|
||||
self.public_key = public_key
|
||||
self.sdk_integration = sdk_integration
|
||||
self.client = MagicMock()
|
||||
self.client.projects.get.side_effect = Exception("offline test")
|
||||
|
||||
|
||||
def test_langfuse_init_supports_sdk_integration_true_on_v2_signature():
|
||||
"""Helper returns True when ``Langfuse.__init__`` accepts ``sdk_integration`` (v2.6+)."""
|
||||
fake_module = _make_fake_langfuse_module(_FakeLangfuseV2)
|
||||
with patch.dict(sys.modules, {"langfuse": fake_module}):
|
||||
from litellm.integrations.langfuse.langfuse import (
|
||||
_langfuse_init_supports_sdk_integration,
|
||||
)
|
||||
|
||||
assert _langfuse_init_supports_sdk_integration() is True
|
||||
|
||||
|
||||
def test_langfuse_init_supports_sdk_integration_false_on_v3_signature():
|
||||
"""Helper returns False when ``Langfuse.__init__`` lacks ``sdk_integration`` (v3+).
|
||||
|
||||
Regression for #13137 / #11703 — passing the kwarg to v3 raises TypeError.
|
||||
"""
|
||||
fake_module = _make_fake_langfuse_module(_FakeLangfuseV3)
|
||||
with patch.dict(sys.modules, {"langfuse": fake_module}):
|
||||
from litellm.integrations.langfuse.langfuse import (
|
||||
_langfuse_init_supports_sdk_integration,
|
||||
)
|
||||
|
||||
assert _langfuse_init_supports_sdk_integration() is False
|
||||
|
||||
|
||||
def test_lang_fuse_logger_does_not_pass_sdk_integration_on_v3():
|
||||
"""``LangFuseLogger`` constructs successfully against a v3-style ``Langfuse.__init__`` (no TypeError).
|
||||
|
||||
This is the customer-reported regression: with ``langfuse>=3.0.0`` installed,
|
||||
LangFuseLogger init blew up because the kwarg was unconditionally injected.
|
||||
"""
|
||||
fake_module = _make_fake_langfuse_module(_FakeLangfuseV3)
|
||||
with patch.dict(sys.modules, {"langfuse": fake_module}):
|
||||
from litellm.integrations.langfuse.langfuse import LangFuseLogger
|
||||
|
||||
logger = LangFuseLogger(
|
||||
langfuse_public_key="pk-test",
|
||||
langfuse_secret="sk-test",
|
||||
langfuse_host="https://example.com",
|
||||
)
|
||||
|
||||
assert isinstance(logger.Langfuse, _FakeLangfuseV3)
|
||||
assert logger.Langfuse.public_key == "pk-test"
|
||||
|
||||
|
||||
def test_lang_fuse_logger_passes_sdk_integration_on_v2():
|
||||
"""``LangFuseLogger`` still tags ``sdk_integration="litellm"`` on a v2-style SDK.
|
||||
|
||||
Guards against silently dropping the litellm-origin tag for users on v2.6+.
|
||||
"""
|
||||
fake_module = _make_fake_langfuse_module(_FakeLangfuseV2)
|
||||
fake_module.version.__version__ = "2.60.0"
|
||||
with patch.dict(sys.modules, {"langfuse": fake_module}):
|
||||
from litellm.integrations.langfuse.langfuse import LangFuseLogger
|
||||
|
||||
logger = LangFuseLogger(
|
||||
langfuse_public_key="pk-test",
|
||||
langfuse_secret="sk-test",
|
||||
langfuse_host="https://example.com",
|
||||
)
|
||||
|
||||
assert isinstance(logger.Langfuse, _FakeLangfuseV2)
|
||||
assert logger.Langfuse.sdk_integration == "litellm"
|
||||
|
||||
|
||||
def test_langfuse_client_init_does_not_pass_sdk_integration_on_v3():
|
||||
"""``langfuse_client_init`` (prompt management path) constructs against a v3-style SDK without TypeError.
|
||||
|
||||
Mirrors the LangFuseLogger test for the second crash site.
|
||||
"""
|
||||
fake_module = _make_fake_langfuse_module(_FakeLangfuseV3)
|
||||
with patch.dict(sys.modules, {"langfuse": fake_module}):
|
||||
from litellm.integrations.langfuse.langfuse_prompt_management import (
|
||||
langfuse_client_init,
|
||||
)
|
||||
|
||||
# lru_cache on langfuse_client_init means the first call to a previously-cached
|
||||
# arg tuple wins — clear to make this test independent of order.
|
||||
langfuse_client_init.cache_clear()
|
||||
client = langfuse_client_init(
|
||||
langfuse_public_key="pk-test",
|
||||
langfuse_secret="sk-test",
|
||||
langfuse_host="https://example.com",
|
||||
)
|
||||
|
||||
assert isinstance(client, _FakeLangfuseV3)
|
||||
assert client.public_key == "pk-test"
|
||||
|
||||
|
||||
class TestLangfuseUsageDetails(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Save global Langfuse client counter to restore after test
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue