feat(langfuse): migrate the sdk callback to langfuse v4

Replace the v2 trace()/generation()/span() calls with SDK v4 observations exported over OpenTelemetry, with one isolated tracer provider per Langfuse credential set, a discarding exporter for mock mode, and v4 trace and observation id normalization. Keeps the session-header trace provenance logic from main so each call under a session alias still gets its own trace

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-14 20:42:36 +00:00
parent 4123b4bc2b
commit c37e2488de
16 changed files with 2655 additions and 636 deletions

View file

@ -32,10 +32,10 @@ RUN uv venv --python python && \
"anthropic[vertex]==0.84.0" \
"grpcio==1.78.0" \
"prometheus-client==0.20.0" \
"langfuse==2.59.7" \
"opentelemetry-api==1.28.0" \
"opentelemetry-sdk==1.28.0" \
"opentelemetry-exporter-otlp==1.28.0" \
"langfuse==4.15.1" \
"opentelemetry-api==1.33.1" \
"opentelemetry-sdk==1.33.1" \
"opentelemetry-exporter-otlp==1.33.1" \
"ddtrace==4.11.0" \
"sentry-sdk==2.21.0" \
"mangum==0.17.0" \

View file

@ -84,7 +84,8 @@ async def _add_langfuse_trace_id_to_alert(
#########################################################
langfuse_object: Final = litellm_logging_obj._get_callback_object(service_name="langfuse")
if langfuse_object is not None:
base_url: Final = langfuse_object.Langfuse.base_url
return f"{base_url}/trace/{trace_id}"
base_url: Final = getattr(langfuse_object, "langfuse_host", None)
if base_url is not None:
return f"{base_url}/trace/{trace_id}"
return None

View file

@ -1,12 +1,12 @@
#### What this does ####
# On success, logs events to Langfuse
import inspect
import os
import re
import traceback
from collections.abc import Callable, Iterable, Mapping
from collections.abc import Callable, Iterable, Mapping, Sequence
from datetime import datetime
from functools import lru_cache
from importlib.metadata import version
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, cast
@ -45,12 +45,13 @@ from litellm.types.utils import (
)
if TYPE_CHECKING:
from langfuse.client import Langfuse, StatefulTraceClient
from langfuse import Langfuse
from opentelemetry.context import Context
from litellm.litellm_core_utils.litellm_logging import DynamicLoggingCache
else:
Context = Any
DynamicLoggingCache = Any
StatefulTraceClient = Any
Langfuse = Any
@ -158,6 +159,107 @@ def _as_steering_key_sequence(value: object) -> tuple[str, ...]:
return ()
MINIMUM_LANGFUSE_VERSION: Final = "4.7"
UNSUPPORTED_LANGFUSE_VERSION: Final = "5"
def installed_langfuse_version() -> str:
"""Only ``importlib.metadata`` reads correctly on every major.
``langfuse.version`` was removed in v4, ``langfuse.__version__`` does not
exist in v3, and in v2 it reports a different value from the distribution
that is actually installed.
"""
return version("langfuse")
def raise_if_unsupported_langfuse_version(installed_version: str) -> None:
"""Fail at logger construction rather than dropping every event at request time.
v4 moved the callback onto OpenTelemetry, so on an older SDK the import of
`propagate_attributes` raises inside the per-request handler and the broad
except there turns it into silent total data loss.
"""
installed: Final = Version(installed_version)
# compare majors, not versions: "5.0.0rc1" sorts below "5" but is just as unsupported
if Version(MINIMUM_LANGFUSE_VERSION) <= installed and installed.major < Version(UNSUPPORTED_LANGFUSE_VERSION).major:
return
raise ImportError(
f"\033[91mlitellm requires langfuse>={MINIMUM_LANGFUSE_VERSION},<{UNSUPPORTED_LANGFUSE_VERSION} for the "
f"'langfuse' callback, but {installed_version} is installed. Run "
f"'pip install \"langfuse>={MINIMUM_LANGFUSE_VERSION},<{UNSUPPORTED_LANGFUSE_VERSION}\"' to upgrade, or use "
f"the 'langfuse_otel' callback, which does not depend on the langfuse SDK\033[0m"
)
_PROPAGATED_TRACE_KEYS: Final = MappingProxyType(
{"name": "trace_name", "user_id": "user_id", "session_id": "session_id", "version": "version", "tags": "tags"}
)
_GENERATION_ONLY_KEYS: Final = frozenset(
{"id", "start_time", "end_time", "parent_observation_id", "usage", "name", "version"}
)
_PROPAGATED_VALUE_MAX_CHARS: Final = 200
def _coerce_propagated_value(value: object) -> str | Sequence[str]:
"""v4 silently drops non-string or >200-char propagated values; v2's pydantic coerced them."""
if isinstance(value, (list, tuple)):
return [str(item)[:_PROPAGATED_VALUE_MAX_CHARS] for item in value]
return str(value)[:_PROPAGATED_VALUE_MAX_CHARS]
def _trace_attributes_for_propagation(trace_params: Mapping[str, object]) -> Mapping[str, object]:
"""Trace-level fields in v4 are propagated onto the observations, not set on a trace object.
Values are coerced and capped up front: the SDK drops offenders with only a
warning, and a dropped ``version`` would vanish from the generation too,
because ``_generation_attributes`` already stripped it as propagated.
"""
return MappingProxyType(
{
propagated: _coerce_propagated_value(trace_params[key])
for key, propagated in _PROPAGATED_TRACE_KEYS.items()
if trace_params.get(key) is not None
}
)
def _optional_str(value: object) -> str | None:
"""v4 sets attribute values raw; a non-string version would be dropped by the server."""
return str(value) if value is not None else None
def _trace_public_flag(value: object) -> bool | None:
"""``trace_public`` reaches here as a bool from metadata or a string from a ``langfuse_*`` header."""
if value is None:
return None
return _as_steering_flag(value)
def _generation_attributes(
generation_params: Mapping[str, object], *, propagated: Mapping[str, object]
) -> Mapping[str, object]:
"""Drop what the v4 wrapper cannot take: ids it generates, and timings set on the span itself.
``usage`` is the v2 shape that v4 replaced with ``usage_details``, which the
caller already builds alongside it.
v4 has one ``version`` for a trace and its observations, so the trace's
propagated value covers the generation; a continued trace propagates none
and the generation keeps its own, as it did in v2.
"""
keep_version: Final = "version" not in propagated and generation_params.get("version") is not None
return MappingProxyType(
{
key: value
for key, value in generation_params.items()
if key not in _GENERATION_ONLY_KEYS or (key == "version" and keep_version)
}
)
def resolve_langfuse_credentials(
langfuse_public_key=None,
langfuse_secret=None,
@ -172,11 +274,18 @@ def resolve_langfuse_credentials(
secret_key = langfuse_secret or langfuse_secret_key or os.getenv("LANGFUSE_SECRET_KEY")
public_key = langfuse_public_key or os.getenv("LANGFUSE_PUBLIC_KEY")
resolved_host: Final = langfuse_host or os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com")
resolved_host: Final = (
langfuse_host or os.getenv("LANGFUSE_HOST") or os.getenv("LANGFUSE_BASE_URL") or "https://cloud.langfuse.com"
)
return public_key, secret_key, resolved_host
def parse_langfuse_debug(raw_value: str | None) -> bool:
"""Parse the LANGFUSE_DEBUG value into the boolean flag the langfuse client expects."""
return raw_value is not None and raw_value.strip().lower() in ("true", "1")
@lru_cache(maxsize=8)
def _warn_invalid_deployment_environment(raw_value: str, error: str) -> None:
verbose_logger.warning(
@ -199,12 +308,14 @@ class LangFuseLogger:
allow_env_credentials: bool = True,
):
try:
import langfuse
from langfuse import Langfuse
except Exception as e:
raise Exception(
f"\033[91mLangfuse not installed, try running 'pip install langfuse' to fix this error: {e}\n{traceback.format_exc()}\033[0m"
)
self.langfuse_sdk_version: str = installed_langfuse_version()
raise_if_unsupported_langfuse_version(self.langfuse_sdk_version)
self.public_key, self.secret_key, self.langfuse_host = resolve_langfuse_credentials(
langfuse_public_key=langfuse_public_key,
langfuse_secret=langfuse_secret,
@ -221,7 +332,7 @@ class LangFuseLogger:
else:
self.langfuse_environment = self.resolve_deployment_environment()
self.langfuse_release = os.getenv("LANGFUSE_RELEASE")
self.langfuse_debug = os.getenv("LANGFUSE_DEBUG")
self.langfuse_debug = parse_langfuse_debug(os.getenv("LANGFUSE_DEBUG"))
self.langfuse_flush_interval = LangFuseLogger._get_langfuse_flush_interval(flush_interval)
if should_use_langfuse_mock():
@ -235,18 +346,13 @@ class LangFuseLogger:
parameters: Final = {
"public_key": self.public_key,
"secret_key": self.secret_key,
"host": self.langfuse_host,
"base_url": self.langfuse_host,
"release": self.langfuse_release,
"debug": self.langfuse_debug,
"flush_interval": self.langfuse_flush_interval, # flush interval in seconds
"httpx_client": self.langfuse_client,
"environment": self.langfuse_environment,
}
self.langfuse_sdk_version: str = langfuse.version.__version__
if "environment" in inspect.signature(Langfuse.__init__).parameters:
parameters["environment"] = self.langfuse_environment
if Version(self.langfuse_sdk_version) >= Version("2.6.0"):
parameters["sdk_integration"] = "litellm"
self.Langfuse: Langfuse = self.safe_init_langfuse_client(parameters)
# set the current langfuse project id in the environ
@ -256,30 +362,21 @@ class LangFuseLogger:
verbose_logger.debug("Langfuse Mock: Using mock project ID")
else:
try:
project_id = self.Langfuse.client.projects.get().data[0].id
project_id: Final = self.Langfuse.api.projects.get().data[0].id
os.environ["LANGFUSE_PROJECT_ID"] = project_id
except Exception:
project_id = None
verbose_logger.debug("Langfuse project id unavailable, alerting links will omit it")
if os.getenv("UPSTREAM_LANGFUSE_SECRET_KEY") is not None:
upstream_langfuse_debug_env: Final = os.getenv("UPSTREAM_LANGFUSE_DEBUG")
upstream_langfuse_debug: Final = (
str_to_bool(upstream_langfuse_debug_env) if upstream_langfuse_debug_env is not None else None
verbose_logger.warning(
"UPSTREAM_LANGFUSE_* is no longer supported: the langfuse callback moved to SDK v4, "
"which has no second ingestion client. The values are ignored."
)
self.upstream_langfuse_secret_key = os.getenv("UPSTREAM_LANGFUSE_SECRET_KEY")
self.upstream_langfuse_public_key = os.getenv("UPSTREAM_LANGFUSE_PUBLIC_KEY")
self.upstream_langfuse_host = os.getenv("UPSTREAM_LANGFUSE_HOST")
self.upstream_langfuse_release = os.getenv("UPSTREAM_LANGFUSE_RELEASE")
self.upstream_langfuse_debug = upstream_langfuse_debug_env
self.upstream_langfuse = Langfuse(
public_key=self.upstream_langfuse_public_key,
secret_key=self.upstream_langfuse_secret_key,
host=self.upstream_langfuse_host,
release=self.upstream_langfuse_release,
debug=(upstream_langfuse_debug if upstream_langfuse_debug is not None else False),
)
else:
self.upstream_langfuse = None
self.upstream_langfuse_debug = os.getenv("UPSTREAM_LANGFUSE_DEBUG")
def safe_init_langfuse_client(self, parameters: dict) -> Langfuse:
"""
@ -289,13 +386,20 @@ class LangFuseLogger:
- Langfuse initializes 1 thread everytime a client is initialized.
- We've had an incident in the past where we reached 100% cpu utilization because Langfuse was initialized several times.
"""
from langfuse import Langfuse
if litellm.initialized_langfuse_clients >= MAX_LANGFUSE_INITIALIZED_CLIENTS:
raise Exception(
f"Max langfuse clients reached: {litellm.initialized_langfuse_clients} is greater than {MAX_LANGFUSE_INITIALIZED_CLIENTS}"
)
langfuse_client: Final = Langfuse(**parameters)
from litellm.integrations.langfuse.langfuse_sdk import acquire_langfuse_client
environment_param: Final = cast(str | None, parameters.get("environment")) # cast-ok: untyped dict
release_param: Final = cast(str | None, parameters.get("release")) # cast-ok: untyped dict
langfuse_client: Final = acquire_langfuse_client(
parameters=parameters,
environment=environment_param,
release=release_param,
mock_mode=self.is_mock_mode,
)
litellm.initialized_langfuse_clients += 1
verbose_logger.debug("Created langfuse client number %s", litellm.initialized_langfuse_clients)
return langfuse_client
@ -394,9 +498,9 @@ class LangFuseLogger:
status_message=status_message,
)
verbose_logger.debug("OUTPUT IN LANGFUSE: %s; original: %s", output, response_obj)
trace_id = None
generation_id = None
if self._is_langfuse_v2():
from litellm.integrations.langfuse.langfuse_sdk import lease_langfuse_client
with lease_langfuse_client(self.Langfuse):
trace_id, generation_id = self._log_langfuse_v2(
user_id=user_id,
metadata=metadata,
@ -411,18 +515,6 @@ class LangFuseLogger:
level=level,
litellm_call_id=litellm_call_id,
)
elif response_obj is not None:
self._log_langfuse_v1(
user_id=user_id,
metadata=metadata,
output=output,
start_time=start_time,
end_time=end_time,
kwargs=kwargs,
optional_params=optional_params,
input=input,
response_obj=response_obj,
)
verbose_logger.debug("Langfuse Layer Logging - final response object: %s", response_obj)
verbose_logger.info("Langfuse Layer Logging - logging success")
@ -518,58 +610,6 @@ class LangFuseLogger:
This approach does not impact latency and runs in the background
"""
def _is_langfuse_v2(self):
import langfuse
return Version(langfuse.version.__version__) >= Version("2.0.0")
def _log_langfuse_v1(
self,
user_id,
metadata,
output,
start_time,
end_time,
kwargs,
optional_params,
input,
response_obj,
):
from langfuse.model import CreateGeneration, CreateTrace
verbose_logger.warning(
"Please upgrade langfuse to v2.0.0 or higher: https://github.com/langfuse/langfuse-python/releases/tag/v2.0.1"
)
trace: Final = self.Langfuse.trace(
CreateTrace(
name=metadata.get("generation_name", "litellm-completion"),
input=input,
output=output,
userId=user_id,
)
)
custom_llm_provider: Final = cast(str | None, kwargs.get("custom_llm_provider"))
model_name: Final = reconstruct_model_name(kwargs.get("model", ""), custom_llm_provider, metadata)
trace.generation(
CreateGeneration(
name=metadata.get("generation_name", "litellm-completion"),
startTime=start_time,
endTime=end_time,
model=model_name,
modelParameters=optional_params,
prompt=input,
completion=output,
usage={
"prompt_tokens": response_obj.usage.prompt_tokens,
"completion_tokens": response_obj.usage.completion_tokens,
},
metadata=metadata,
)
)
def _log_langfuse_v2(
self,
user_id: str | None,
@ -592,11 +632,7 @@ class LangFuseLogger:
StandardLoggingPayload | None,
kwargs.get("standard_logging_object", None),
)
tags = (
self._get_langfuse_tags(standard_logging_object=standard_logging_object)
if self._supports_tags()
else []
)
tags = self._get_langfuse_tags(standard_logging_object=standard_logging_object)
allowlisted_metadata: Final[StandardLoggingMetadata | Mapping[str, object]] = (
standard_logging_object["metadata"] if standard_logging_object is not None else _NO_METADATA
@ -648,17 +684,17 @@ class LangFuseLogger:
# This allows continuing an existing trace while still returning the correct trace_id
if existing_trace_id is not None:
trace_id = existing_trace_id
resolved_trace_id: Final = (
call_trace_id: Final = (
litellm_call_id or trace_id
if existing_trace_id is None
and _is_session_header_trace(trace_id, session_id, litellm_params.get("proxy_server_request"))
else trace_id
)
if resolved_trace_id != trace_id:
if call_trace_id != trace_id:
verbose_logger.debug(
"Langfuse: trace_id %s came from a session header; using call id %s so each call gets its own trace",
trace_id,
resolved_trace_id,
call_trace_id,
)
requested_trace_keys: Final = _as_steering_key_sequence(clean_metadata.pop("update_trace_keys", ()))
update_trace_keys: Final = (
@ -714,7 +750,7 @@ class LangFuseLogger:
trace_params["output"] = masked_output if not mask_output else "redacted-by-litellm"
else: # don't overwrite an existing trace
trace_params = {
"id": resolved_trace_id,
"id": call_trace_id,
"name": trace_name,
"session_id": session_id,
"input": masked_input if not mask_input else "redacted-by-litellm",
@ -764,17 +800,16 @@ class LangFuseLogger:
("api_base", api_base, bool(api_base)),
("vertex_location", vertex_location, bool(vertex_location)),
("aws_region_name", aws_region_name, bool(aws_region_name)),
("cache_hit", kwargs.get("cache_hit") or False, self._supports_tags() and "cache_hit" in kwargs),
("cache_hit", kwargs.get("cache_hit") or False, "cache_hit" in kwargs),
)
enrichments: Final[Mapping[str, object]] = {
key: value for key, value, include in candidate_enrichments if include
}
if self._supports_tags():
if "cache_hit" in kwargs and kwargs["cache_hit"] is None:
kwargs["cache_hit"] = False # rebind-ok: pre-existing normalization other integrations rely on
if existing_trace_id is None:
trace_params.update({"tags": tags})
if "cache_hit" in kwargs and kwargs["cache_hit"] is None:
kwargs["cache_hit"] = False # rebind-ok: pre-existing normalization other integrations rely on
if existing_trace_id is None:
trace_params.update({"tags": tags})
proxy_server_request: Final = litellm_params.get("proxy_server_request", None)
if proxy_server_request:
@ -788,17 +823,6 @@ class LangFuseLogger:
if key.lower() not in _REDACTED_PROXY_HEADERS:
clean_headers[key] = value
trace: Final[StatefulTraceClient] = self.Langfuse.trace(**trace_params)
# Log provider specific information as a span
log_provider_specific_information_as_span(trace, enrichments)
# Log guardrail information as a span
self._log_guardrail_information_as_span(
trace=trace,
standard_logging_object=standard_logging_object,
)
generation_id = None
usage = None
usage_details = None
@ -820,7 +844,7 @@ class LangFuseLogger:
usage = {
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_cost": cost if self._supports_costs() else None,
"total_cost": cost,
}
# According to langfuse documentation: "the input value must be reduced by the number of cache_read_input_tokens"
input_tokens: Final = prompt_tokens - cache_read_input_tokens
@ -864,45 +888,87 @@ class LangFuseLogger:
"output": masked_output if not mask_output else "redacted-by-litellm",
"usage": usage,
"usage_details": usage_details,
"metadata": {
**log_requester_metadata(redact_user_api_key_info(metadata=allowlisted_metadata)),
"cost_details": {"total": cost} # mutable-ok: langfuse serializes this payload
if usage is not None and isinstance(cost, (int, float))
else None,
"metadata": { # mutable-ok: langfuse serializes this payload, a proxy is not json-encodable
**(trace_params.get("metadata") or {}),
**log_requester_metadata(redact_user_api_key_info(metadata=allowlisted_metadata)), # pyright: ignore[reportArgumentType] # TypedDict in, plain metadata dict out
**enrichments,
},
"level": level,
"version": clean_metadata.pop("version", None),
"version": _optional_str(clean_metadata.pop("version", None)),
}
parent_observation_id: Final = metadata.get("parent_observation_id", None)
if parent_observation_id is not None:
generation_params["parent_observation_id"] = parent_observation_id
if self._supports_prompt():
generation_params = _add_prompt_to_generation_params(
generation_params=generation_params,
clean_metadata=clean_metadata,
prompt_management_metadata=prompt_management_metadata,
langfuse_client=self.Langfuse,
)
generation_params = _add_prompt_to_generation_params(
generation_params=generation_params,
clean_metadata=clean_metadata,
prompt_management_metadata=prompt_management_metadata,
langfuse_client=self.Langfuse,
)
if masked_output is not None and isinstance(masked_output, str) and level == "ERROR":
generation_params["status_message"] = masked_output
if self._supports_completion_start_time():
generation_params["completion_start_time"] = kwargs.get("completion_start_time", None)
generation_params["completion_start_time"] = kwargs.get("completion_start_time", None)
generation_client: Final = trace.generation(**generation_params)
# langfuse ships in the proxy-runtime extra, so this module must import cleanly without it
from litellm.integrations.langfuse.langfuse_sdk import (
open_trace_context,
propagate_attributes,
resolve_observation_id,
resolve_trace_id,
start_generation,
to_unix_nanos,
)
# Return the trace_id we set (which should be litellm_call_id when no explicit trace_id provided)
# We explicitly set trace_id in trace_params["id"], so langfuse should use it
# Verify langfuse accepted our trace_id; if it differs, log a warning but still return our intended value
# to match expected test behavior
if hasattr(generation_client, "trace_id") and generation_client.trace_id:
if generation_client.trace_id != resolved_trace_id:
verbose_logger.warning(
"Langfuse trace_id mismatch: set %s, but langfuse returned %s. Using our intended trace_id for consistency.",
resolved_trace_id,
generation_client.trace_id,
resolved_trace_id: Final = resolve_trace_id(call_trace_id) # pyright: ignore[reportArgumentType] # metadata value, str or None at runtime
propagated_trace_attributes: Final = _trace_attributes_for_propagation(trace_params)
with propagate_attributes(**propagated_trace_attributes): # pyright: ignore[reportArgumentType] # kwargs-ok: keys fixed by _PROPAGATED_TRACE_KEYS, values are the SDK's own trace fields
trace_context, claim_trace_root = open_trace_context(
client=self.Langfuse,
trace_id=resolved_trace_id,
parent_observation_id=resolve_observation_id(parent_observation_id), # pyright: ignore[reportArgumentType] # metadata value, str or None at runtime
)
log_provider_specific_information_as_span(
client=self.Langfuse,
context=trace_context,
enrichments=enrichments,
claim_trace_root=claim_trace_root,
)
self._log_guardrail_information_as_span(
client=self.Langfuse,
context=trace_context,
standard_logging_object=standard_logging_object,
claim_trace_root=claim_trace_root,
)
generation: Final = start_generation(
client=self.Langfuse,
context=trace_context,
name=generation_params["name"], # pyright: ignore[reportArgumentType] # always the str set a few lines up
start_time=start_time,
claim_trace_root=claim_trace_root,
release=trace_params.get("release"),
public=_trace_public_flag(trace_params.get("public")),
attributes=_generation_attributes(generation_params, propagated=propagated_trace_attributes),
)
if existing_trace_id is not None and ("input" in update_trace_keys or "output" in update_trace_keys):
# with a real parent the generation is not the trace root, so trace-level
# I/O has to be stamped explicitly; v2 updated the trace object directly
generation.set_trace_io( # pyright: ignore[reportDeprecated] # the SDK keeps it exactly for this legacy trace-level contract
input=trace_params.get("input") if "input" in update_trace_keys else None,
output=trace_params.get("output") if "output" in update_trace_keys else None,
)
return resolved_trace_id, generation_id
generation.end(end_time=to_unix_nanos(end_time))
# log_event_on_langfuse tuple-unpacks this and re-wraps it in the dict callers cache.
# The wrapper's id is the exported observation id; the pre-computed generation_id would
# name nothing in langfuse, because v4 derives observation ids from the OTel span.
return resolved_trace_id, generation.id
except Exception:
verbose_logger.error("Langfuse Layer Error - %s", traceback.format_exc())
return None, None
@ -971,27 +1037,11 @@ class LangFuseLogger:
_cache_key = _hidden_params.get("cache_key", None)
if _cache_key is None and litellm.cache is not None:
# fallback to using "preset_cache_key"
_preset_cache_key: Final = litellm.cache._get_preset_cache_key_from_kwargs(**kwargs)
_preset_cache_key: Final = litellm.cache._get_preset_cache_key_from_kwargs(**kwargs) # pyright: ignore[reportPrivateUsage] # kwargs-ok: no public preset-cache-key accessor
_cache_key = _preset_cache_key
tags.append(f"cache_key:{_cache_key}")
return tags
def _supports_tags(self):
"""Check if current langfuse version supports tags"""
return Version(self.langfuse_sdk_version) >= Version("2.6.3")
def _supports_prompt(self):
"""Check if current langfuse version supports prompt"""
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
def _supports_costs(self):
"""Check if current langfuse version supports costs"""
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
def _supports_completion_start_time(self):
"""Check if current langfuse version supports completion start time"""
return Version(self.langfuse_sdk_version) >= Version("2.7.3")
@staticmethod
def _apply_masking_function(data: object, masking_function: Callable[[object], object]) -> object:
"""
@ -1056,8 +1106,10 @@ class LangFuseLogger:
def _log_guardrail_information_as_span(
self,
trace: StatefulTraceClient,
client: "Langfuse",
context: "Context",
standard_logging_object: StandardLoggingPayload | None,
claim_trace_root: bool,
):
"""
Log guardrail information as a span
@ -1078,6 +1130,8 @@ class LangFuseLogger:
)
return
from litellm.integrations.langfuse.langfuse_sdk import start_child_span, to_unix_nanos
for guardrail_entry in guardrail_information:
if not isinstance(guardrail_entry, dict):
verbose_logger.debug(
@ -1086,21 +1140,25 @@ class LangFuseLogger:
)
continue
span = trace.span(
span = start_child_span(
client=client,
context=context,
name="guardrail",
input=guardrail_entry.get("guardrail_request", None),
output=guardrail_entry.get("guardrail_response", None),
metadata={
"guardrail_name": guardrail_entry.get("guardrail_name", None),
"guardrail_mode": guardrail_entry.get("guardrail_mode", None),
"guardrail_masked_entity_count": guardrail_entry.get("masked_entity_count", None),
},
start_time=guardrail_entry.get("start_time", None),
end_time=guardrail_entry.get("end_time", None),
claim_trace_root=claim_trace_root,
attributes={ # mutable-ok: langfuse serializes this payload, a proxy is not json-encodable
"input": guardrail_entry.get("guardrail_request", None),
"output": guardrail_entry.get("guardrail_response", None),
"metadata": {
"guardrail_name": guardrail_entry.get("guardrail_name", None),
"guardrail_mode": guardrail_entry.get("guardrail_mode", None),
"guardrail_masked_entity_count": guardrail_entry.get("masked_entity_count", None),
},
},
)
verbose_logger.debug("Logged guardrail information as span: %s", span)
span.end()
span.end(end_time=to_unix_nanos(guardrail_entry.get("end_time", None)))
def _add_prompt_to_generation_params(
@ -1142,7 +1200,7 @@ def _add_prompt_to_generation_params(
if "labels" in prompt_text_params and "tags" in prompt_text_params:
_data["labels"] = user_prompt.get("labels", []) or []
_data["tags"] = user_prompt.get("tags", []) or []
_prompt_obj = Prompt_Text(**_data)
_prompt_obj = Prompt_Text(**_data) # pyright: ignore[reportArgumentType] # kwargs-ok: shape mirrors the pydantic model, values from the user's prompt dict
generation_params["prompt"] = TextPromptClient(prompt=_prompt_obj)
elif isinstance(user_prompt["prompt"], list):
@ -1157,7 +1215,7 @@ def _add_prompt_to_generation_params(
_data["labels"] = user_prompt.get("labels", []) or []
_data["tags"] = user_prompt.get("tags", []) or []
_prompt_obj = Prompt_Chat(**_data)
_prompt_obj = Prompt_Chat(**_data) # pyright: ignore[reportArgumentType] # kwargs-ok: shape mirrors the pydantic model, values from the user's prompt dict
generation_params["prompt"] = ChatPromptClient(prompt=_prompt_obj)
else:
@ -1177,21 +1235,24 @@ def _add_prompt_to_generation_params(
def log_provider_specific_information_as_span(
trace,
clean_metadata: Mapping[str, Any],
*,
client: "Langfuse",
context: "Context",
enrichments: Mapping[str, Any],
claim_trace_root: bool,
):
"""
Logs provider-specific information as spans.
Parameters:
trace: The tracing object used to log spans.
clean_metadata: A dictionary containing metadata to be logged.
enrichments: The litellm-computed fields on the emitted payload.
Returns:
None
"""
_hidden_params: Final[Mapping[str, object] | None] = clean_metadata.get("hidden_params", None)
_hidden_params: Final[Mapping[str, object] | None] = enrichments.get("hidden_params", None)
if _hidden_params is None:
return
@ -1202,22 +1263,42 @@ def log_provider_specific_information_as_span(
for elem in vertex_ai_grounding_metadata:
if isinstance(elem, dict):
for key, value in elem.items():
trace.span(
name=key,
input=value,
_end_grounding_span(
client=client, context=context, name=key, value=value, claim_trace_root=claim_trace_root
)
else:
trace.span(
_end_grounding_span(
client=client,
context=context,
name="vertex_ai_grounding_metadata",
input=elem,
value=elem,
claim_trace_root=claim_trace_root,
)
else:
trace.span(
_end_grounding_span(
client=client,
context=context,
name="vertex_ai_grounding_metadata",
input=vertex_ai_grounding_metadata,
value=vertex_ai_grounding_metadata,
claim_trace_root=claim_trace_root,
)
def _end_grounding_span(
*, client: "Langfuse", context: "Context", name: str, value: object, claim_trace_root: bool
) -> None:
from litellm.integrations.langfuse.langfuse_sdk import start_child_span
start_child_span(
client=client,
context=context,
name=name,
start_time=None,
claim_trace_root=claim_trace_root,
attributes={"input": value}, # mutable-ok: langfuse serializes this payload
).end()
def log_requester_metadata(clean_metadata: Mapping[str, Any]):
returned_metadata: Final = {}
requester_metadata: Final = clean_metadata.get("requester_metadata") or {}

View file

@ -2,13 +2,10 @@
Call Hook for LiteLLM Proxy which allows Langfuse prompt management.
"""
import inspect
import os
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Final, Literal, TypeAlias, cast
from packaging.version import Version
from litellm.integrations.custom_logger import CustomLogger
from litellm.integrations.prompt_management_base import PromptManagementClient
from litellm.litellm_core_utils.asyncify import run_async_function
@ -20,12 +17,19 @@ 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,
installed_langfuse_version,
parse_langfuse_debug,
raise_if_unsupported_langfuse_version,
resolve_langfuse_credentials,
)
from .langfuse_handler import LangFuseHandler
from .langfuse_mock_client import create_mock_langfuse_client, should_use_langfuse_mock
if TYPE_CHECKING:
from langfuse import Langfuse
from langfuse.client import ChatPromptClient, TextPromptClient
from langfuse.model import ChatPromptClient, TextPromptClient
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
@ -64,8 +68,9 @@ def langfuse_client_init(
Exception: If langfuse package is not installed
"""
try:
import langfuse
from langfuse import Langfuse
from langfuse import (
Langfuse, # noqa: F401 # the import is the install probe; construction moved to acquire_langfuse_client
)
except Exception as e:
raise Exception(
f"\033[91mLangfuse not installed, try running 'pip install langfuse' to fix this error: {e}\n\033[0m"
@ -84,36 +89,45 @@ def langfuse_client_init(
langfuse_host = "http://" + langfuse_host
langfuse_release: Final = os.getenv("LANGFUSE_RELEASE")
langfuse_debug: Final = os.getenv("LANGFUSE_DEBUG")
langfuse_debug: Final = parse_langfuse_debug(os.getenv("LANGFUSE_DEBUG"))
parameters: Final = {
"public_key": public_key,
"secret_key": secret_key,
"host": langfuse_host,
"base_url": langfuse_host,
"release": langfuse_release,
"debug": langfuse_debug,
"flush_interval": LangFuseLogger._get_langfuse_flush_interval(flush_interval), # flush interval in seconds
"flush_interval": LangFuseLogger._get_langfuse_flush_interval(flush_interval), # pyright: ignore[reportPrivateUsage] # shared env-fallback helper, not part of the logger's API
}
if Version(langfuse.version.__version__) >= Version("2.6.0"):
parameters["sdk_integration"] = "litellm"
raise_if_unsupported_langfuse_version(installed_langfuse_version())
if Version(langfuse.version.__version__) >= Version("2.7.3"):
import httpx
import httpx
import litellm
import litellm
from ...llms.custom_httpx.http_handler import get_ssl_configuration
from ...llms.custom_httpx.http_handler import get_ssl_configuration
parameters["httpx_client"] = httpx.Client(
is_mock_mode: Final = should_use_langfuse_mock()
parameters["httpx_client"] = (
create_mock_langfuse_client()
if is_mock_mode
else httpx.Client(
verify=get_ssl_configuration(),
cert=os.getenv("SSL_CERTIFICATE", litellm.ssl_certificate),
)
)
if "environment" in inspect.signature(Langfuse.__init__).parameters:
parameters["environment"] = LangFuseLogger.resolve_deployment_environment()
parameters["environment"] = LangFuseLogger.resolve_deployment_environment()
client: Final = Langfuse(**parameters)
from .langfuse_sdk import acquire_langfuse_client
client: Final = acquire_langfuse_client(
parameters=parameters,
environment=parameters["environment"],
release=langfuse_release,
mock_mode=is_mock_mode,
)
return client
@ -126,9 +140,8 @@ class LangfusePromptManagement(LangFuseLogger, PromptManagementBase, CustomLogge
langfuse_host=None,
flush_interval=1,
):
import langfuse
self.langfuse_sdk_version = langfuse.version.__version__
self.langfuse_sdk_version = installed_langfuse_version()
self.Langfuse = langfuse_client_init(
langfuse_public_key=langfuse_public_key,
langfuse_secret=langfuse_secret,

View file

@ -0,0 +1,572 @@
from __future__ import annotations
import os
import re
import threading
from base64 import b64encode
from collections.abc import Generator, Mapping
from contextlib import contextmanager
from datetime import datetime
from hashlib import sha256
from types import MappingProxyType
from typing import Final
from weakref import WeakKeyDictionary, WeakSet
import opentelemetry.trace as otel_trace
from langfuse import Langfuse, LangfuseGeneration, LangfuseSpan, propagate_attributes
from langfuse._client.resource_manager import LangfuseResourceManager
from opentelemetry.context import Context
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
__all__ = (
"AS_ROOT_ATTRIBUTE",
"PUBLIC_ATTRIBUTE",
"RELEASE_ATTRIBUTE",
"DiscardingSpanExporter",
"acquire_langfuse_client",
"build_isolated_tracer_provider",
"evict_stale_langfuse_resources",
"lease_langfuse_client",
"open_trace_context",
"propagate_attributes",
"register_langfuse_client",
"resolve_observation_id",
"resolve_trace_id",
"shutdown_langfuse_client",
"start_child_span",
"start_generation",
"to_unix_nanos",
)
AS_ROOT_ATTRIBUTE: Final = "langfuse.internal.as_root"
PUBLIC_ATTRIBUTE: Final = "langfuse.trace.public"
RELEASE_ATTRIBUTE: Final = "langfuse.release"
_TRACE_ID_PATTERN: Final = re.compile(r"^[0-9a-f]{32}$")
_OBSERVATION_ID_PATTERN: Final = re.compile(r"^[0-9a-f]{16}$")
def to_unix_nanos(value: datetime | float | None) -> int | None:
"""Langfuse v4 takes OTel timestamps, which are integer nanoseconds since the epoch.
Guardrail entries carry unix seconds as floats rather than datetimes, so both
shapes have to convert; the v2 SDK accepted either through a pydantic model.
"""
if value is None:
return None
seconds: Final = value.timestamp() if isinstance(value, datetime) else float(value)
return int(seconds * 1_000_000_000)
def resolve_trace_id(trace_id: object | None) -> str:
"""Map a caller's trace id onto the 32 lowercase hex characters v4 requires."""
serialized: Final = "" if trace_id is None else str(trace_id)
normalized: Final = serialized.lower().replace("-", "")
if _TRACE_ID_PATTERN.fullmatch(normalized):
return normalized
return Langfuse.create_trace_id(seed=serialized) if serialized else Langfuse.create_trace_id()
def resolve_observation_id(observation_id: object | None) -> str | None:
"""Map a caller's parent observation id onto v4's 16 lowercase hex characters."""
serialized: Final = "" if observation_id is None else str(observation_id)
normalized: Final = serialized.lower().replace("-", "")
if _OBSERVATION_ID_PATTERN.fullmatch(normalized):
return normalized
if not serialized:
return None
return sha256(serialized.encode("utf-8")).digest()[:8].hex()
def open_trace_context(
*,
client: Langfuse,
trace_id: str,
parent_observation_id: str | None,
) -> tuple[Context, bool]:
"""Build the OTel context that places new observations inside ``trace_id``.
Returns the context plus whether the caller must claim trace root. Langfuse
fabricates a random parent span id when no real parent is supplied, so the
observation is a child of something that will never be exported; the public
SDK path compensates by marking the span as root and this path must do the
same.
"""
remote_parent: Final = client._create_remote_parent_span( # pyright: ignore[reportPrivateUsage] # no public equivalent in v4
trace_id=trace_id, parent_span_id=parent_observation_id
)
return otel_trace.set_span_in_context(remote_parent), parent_observation_id is None
def start_generation(
*,
client: Langfuse,
context: Context,
name: str,
start_time: datetime | float | None,
claim_trace_root: bool,
release: str | None = None,
public: bool | None = None,
attributes: Mapping[str, object],
) -> LangfuseGeneration:
"""Create a generation whose start time is when the model call began.
No public v4 API accepts a historical start time, so this drives the SDK's
own OTel tracer, which does. Langfuse documents this route for backdated
ingestion.
``public`` is the v2 ``trace(public=...)`` flag; v4 reads it off the root
observation's ``langfuse.trace.public`` attribute instead.
"""
otel_span: Final = client._otel_tracer.start_span( # pyright: ignore[reportPrivateUsage] # only route to a historical start time
name=name, context=context, start_time=to_unix_nanos(start_time)
)
if claim_trace_root:
otel_span.set_attribute(AS_ROOT_ATTRIBUTE, True)
if public is not None:
otel_span.set_attribute(PUBLIC_ATTRIBUTE, public)
generation: Final = LangfuseGeneration(otel_span=otel_span, langfuse_client=client, **attributes) # pyright: ignore[reportArgumentType] # kwargs-ok: callback-built params, v2 accepted the same shapes
if release is not None:
# after the wrapper, which stamps the client-wide release and would otherwise
# overwrite the release this request asked for
otel_span.set_attribute(RELEASE_ATTRIBUTE, release)
return generation
def start_child_span(
*,
client: Langfuse,
context: Context,
name: str,
start_time: datetime | float | None,
claim_trace_root: bool,
attributes: Mapping[str, object],
) -> LangfuseSpan:
"""Create a sibling observation inside the same trace, keeping its own window.
When the shared parent is the fabricated remote span, every observation must
claim trace root itself the SDK's own remote-parent paths stamp each span —
or it exports with a parent id that is never exported.
"""
otel_span: Final = client._otel_tracer.start_span( # pyright: ignore[reportPrivateUsage] # only route to a historical start time
name=name, context=context, start_time=to_unix_nanos(start_time)
)
if claim_trace_root:
otel_span.set_attribute(AS_ROOT_ATTRIBUTE, True)
return LangfuseSpan(otel_span=otel_span, langfuse_client=client, **attributes) # pyright: ignore[reportArgumentType] # kwargs-ok: callback-built params, v2 accepted the same shapes
_ENVIRONMENT_ATTRIBUTE: Final = "langfuse.environment"
# providers litellm itself constructed; a bundle adopted from user code may hold the
# process-global provider, which litellm must never shut down.
_litellm_built_providers: Final[WeakSet] = WeakSet()
def build_isolated_tracer_provider(*, environment: str | None, release: str | None) -> TracerProvider:
"""Give the langfuse client a provider of its own instead of the process-wide one.
v4 is built on OpenTelemetry and otherwise either claims the global tracer
provider, which silently disables litellm's own exporters, or attaches its
processor to litellm's, which sends litellm spans to every langfuse project
and langfuse spans to every other litellm destination.
The resource is rebuilt here because langfuse only applies ``environment``
and ``release`` when it constructs the provider itself, and the sampler is
rebuilt for the same reason: ``LANGFUSE_SAMPLE_RATE`` is otherwise silently
ignored and every trace exports.
"""
raw_sample_rate: Final = os.environ.get("LANGFUSE_SAMPLE_RATE")
sample_rate: Final = float(raw_sample_rate) if raw_sample_rate is not None else 1.0
if not 0.0 <= sample_rate <= 1.0:
raise ValueError(f"Sample rate must be between 0.0 and 1.0, got {sample_rate}")
attributes: Final = MappingProxyType(
{
key: value
for key, value in ((_ENVIRONMENT_ATTRIBUTE, environment), (RELEASE_ATTRIBUTE, release))
if value is not None
}
)
provider: Final = TracerProvider(
resource=Resource.create(dict(attributes)),
sampler=TraceIdRatioBased(sample_rate) if sample_rate < 1 else None,
)
with _LIVE_CLIENTS_LOCK:
_litellm_built_providers.add(provider)
return provider
class DiscardingSpanExporter(SpanExporter):
"""Accept and drop every span, for mock mode.
The mock intercepts the httpx client langfuse used to take, but v4 ships
observations through its own OTLP exporter, so without this the "no network
calls" contract silently sends real traces to the configured host.
"""
def export(self, spans: object) -> SpanExportResult:
return SpanExportResult.SUCCESS
def shutdown(self) -> None:
return None
def force_flush(self, timeout_millis: int = 30_000) -> bool:
return True
_LIVE_CLIENTS_LOCK: Final = threading.Lock()
# litellm clients still using each SDK resource bundle; the bundle is torn down with the last one.
# Both sides are weak so a throwaway client (a health probe, an alerting lookup) that is simply
# garbage-collected stops holding the bundle open rather than inflating a counter forever.
_live_clients: Final[WeakKeyDictionary[LangfuseResourceManager, WeakSet]] = WeakKeyDictionary()
class _LangfuseLifecycleState:
"""How many callbacks are leasing one SDK resource bundle, and what eviction has queued behind them.
``lock`` is never held across a teardown, which takes the SDK's own registry lock.
"""
def __init__(self) -> None:
self.lock = threading.Lock()
self.active_leases = 0
self.teardown_in_progress = False
self.teardown_owner: int | None = None
self.pending_clients: set[Langfuse] = set() # mutable-ok: eviction and callback threads queue into it
def open_lease(self) -> None:
with self.lock:
self.active_leases += 1
def claim_for_teardown(self, client: Langfuse) -> bool:
"""Whether this thread owns ``client``'s teardown; a lease or another teardown in flight queues it instead."""
with self.lock:
if self.active_leases > 0 or self.teardown_in_progress:
self.pending_clients.add(client)
return False
self.teardown_in_progress = True
self.teardown_owner = threading.get_ident()
return True
def release_lease(self) -> tuple[Langfuse, ...]:
"""Drop this lease and take ownership of the teardowns it was holding up, if it was the last one."""
with self.lock:
self.active_leases -= 1
if self.active_leases > 0 or self.teardown_in_progress or not self.pending_clients:
return ()
claimed: Final = tuple(self.pending_clients)
self.pending_clients.clear()
self.teardown_in_progress = True
self.teardown_owner = threading.get_ident()
return claimed
def next_teardown_batch(self) -> tuple[Langfuse, ...]:
"""Whatever eviction queued while the last batch was draining, handing the ownership flag back when empty."""
with self.lock:
if self.active_leases == 0 and self.pending_clients:
claimed: Final = tuple(self.pending_clients)
self.pending_clients.clear()
return claimed
self.teardown_in_progress = False
self.teardown_owner = None
return ()
def requeue(self, clients: tuple[Langfuse, ...]) -> None:
with self.lock:
self.pending_clients.update(clients)
def end_teardown(self) -> None:
with self.lock:
if self.teardown_owner == threading.get_ident():
self.teardown_in_progress = False
self.teardown_owner = None
_LIFECYCLE_STATES_LOCK: Final = threading.Lock()
_LIFECYCLE_STATES: Final[WeakKeyDictionary[object, _LangfuseLifecycleState]] = WeakKeyDictionary()
def _lifecycle_state(client: Langfuse) -> _LangfuseLifecycleState:
"""One state per resource bundle, since teardown closes the provider every client on that bundle exports through."""
resources: Final = getattr(client, "_resources", None)
key: Final = client if resources is None else resources
with _LIFECYCLE_STATES_LOCK:
existing: Final = _LIFECYCLE_STATES.get(key)
if existing is not None:
return existing
created: Final = _LangfuseLifecycleState()
_LIFECYCLE_STATES[key] = created
return created
@contextmanager
def lease_langfuse_client(client: Langfuse) -> Generator[None]:
"""Hold off cache eviction's teardown of ``client`` while the export inside is in flight.
Eviction reaches a client the cache handed a callback moments earlier, so closing the SDK client
and its tracer provider there drops the spans that callback is still writing. The lease protects
exactly the window it wraps: an eviction arriving inside it is deferred to the last lease exit.
Taking a lease never blocks; a teardown already running keeps running, because the spans of a
lease taken that late were lost before the lease began, and stalling every other callback in the
process would not bring them back. A client the registry hands out during the deferral registers
as a holder, and the reference count keeps its bundle alive from there.
"""
state: Final = _lifecycle_state(client)
state.open_lease()
try:
yield
finally:
_run_teardowns(state, state.release_lease(), propagate_base_exception=False)
def _run_teardowns(
state: _LangfuseLifecycleState,
clients: tuple[Langfuse, ...],
*,
propagate_base_exception: bool = True,
) -> None:
"""Tear down ``clients``, then whatever eviction queued meanwhile, and hand the flag back.
A failing ordinary teardown is logged and skipped rather than raised: the thread here is usually a
request callback that merely held the last lease, and its request must not fail on eviction's behalf.
Interrupts requeue the unfinished batch and normally propagate, while a callback exception already
in flight takes precedence over an eviction interrupt.
"""
batch = clients # rebind-ok: drains each batch queued while the previous one was being torn down
try:
from litellm._logging import verbose_logger
while batch:
for index, client in enumerate(batch):
try:
_teardown_langfuse_client(client)
except Exception:
verbose_logger.exception("Langfuse client teardown failed during cache eviction")
except BaseException:
state.requeue(batch[index:])
if propagate_base_exception:
raise
return
batch = state.next_teardown_batch()
finally:
state.end_teardown()
def _evict_if_stale_locked(
*, public_key: object, secret_key: object, base_url: object
) -> LangfuseResourceManager | None:
"""Assumes ``LangfuseResourceManager._lock`` is held; returns the still-valid bundle, evicting a stale one."""
if not public_key:
return None
cached: Final = LangfuseResourceManager._instances.get(public_key) # pyright: ignore[reportPrivateUsage] # registry has no public accessor
if cached is None:
return None
if getattr(cached, "secret_key", None) == secret_key and getattr(cached, "base_url", None) == base_url:
return cached
LangfuseResourceManager._instances.pop(public_key, None) # pyright: ignore[reportPrivateUsage] # registry has no public accessor
return None
def _retire_orphaned_providers() -> None:
"""Shut down every provider litellm built whose bundle nothing uses any more.
A rotated-out bundle whose last client is simply garbage collected, which is how the
prompt-management LRU drops clients, never reaches ``shutdown_langfuse_client``, and the
provider's own atexit hook would keep its export thread alive for the rest of the process.
Holders are snapshotted last: a client is registered in the same registry-locked block
that builds its provider, so once the registry snapshot's lock has been acquired, the
client of any provider from the first snapshot is visible to the final one even when a
concurrent rotation already evicted its bundle again. Runs outside both locks because
provider shutdown flushes and joins the export thread.
"""
with _LIVE_CLIENTS_LOCK:
candidates: Final = tuple(_litellm_built_providers)
with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor
registered: Final = tuple(
getattr(resources, "tracer_provider", None)
for resources in LangfuseResourceManager._instances.values() # pyright: ignore[reportPrivateUsage] # registry has no public accessor
)
with _LIVE_CLIENTS_LOCK:
held: Final = tuple(
getattr(resources, "tracer_provider", None)
for resources, holders in _live_clients.items()
if len(holders) > 0
)
orphaned: Final = tuple(provider for provider in candidates if provider not in registered and provider not in held)
for provider in orphaned:
_litellm_built_providers.discard(provider)
provider.shutdown()
def evict_stale_langfuse_resources(*, public_key: str | None, secret_key: str | None, base_url: str | None) -> None:
"""Drop a cached client whose credentials no longer match the ones being requested."""
with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor
_evict_if_stale_locked(public_key=public_key, secret_key=secret_key, base_url=base_url)
_retire_orphaned_providers()
def _build_verified_span_exporter(*, public_key: object, secret_key: object, base_url: object) -> SpanExporter | None:
"""Rebuild litellm's TLS material onto the export channel.
v2 ingested through the injected httpx client, which carried litellm's CA
bundle and client certificate; v4 ships every observation through its own
OTLP exporter, so a private-CA deployment would fail TLS on every export in
a background thread while ``auth_check`` (still on the httpx client) stays
green. Only built when custom TLS material is configured; endpoint and
headers mirror ``langfuse._client.span_processor``.
"""
import litellm
ca_bundle: Final = litellm.ssl_verify if isinstance(litellm.ssl_verify, str) else None
configured_certificate: Final = os.getenv("SSL_CERTIFICATE") or litellm.ssl_certificate
client_certificate: Final = configured_certificate if isinstance(configured_certificate, str) else None
if ca_bundle is None and client_certificate is None:
return None
import langfuse as langfuse_package
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
langfuse_version: Final = getattr(langfuse_package, "__version__", "unknown")
export_path: Final = os.getenv("LANGFUSE_OTEL_TRACES_EXPORT_PATH")
endpoint: Final = f"{base_url}/{export_path}" if export_path else f"{base_url}/api/public/otel/v1/traces"
encoded_auth: Final = b64encode(f"{public_key}:{secret_key}".encode()).decode("ascii")
return OTLPSpanExporter(
endpoint=endpoint,
headers={ # mutable-ok: the exporter copies these into its session headers
"Authorization": "Basic " + encoded_auth,
"x-langfuse-sdk-name": "python",
"x-langfuse-sdk-version": langfuse_version,
"x-langfuse-public-key": str(public_key),
},
certificate_file=ca_bundle,
client_certificate_file=client_certificate,
)
def acquire_langfuse_client(
*,
parameters: Mapping[str, object],
environment: str | None,
release: str | None,
mock_mode: bool,
) -> Langfuse:
"""Evict-check, construct, and register a client as one atomic step.
The SDK registry lock is held across the whole sequence: released between
eviction and construction, two concurrent inits for the same public key
with different secrets can bind one tenant's logger to the other tenant's
exporter. The isolated provider is only built when the registry does not
already hold the key a discarded ``TracerProvider`` stays pinned forever
by its atexit hook, so building one per health probe or alerting lookup
would leak a provider each time.
"""
public_key: Final = parameters.get("public_key")
span_exporter: Final = (
DiscardingSpanExporter()
if mock_mode
else _build_verified_span_exporter(
public_key=public_key,
secret_key=parameters.get("secret_key"),
base_url=parameters.get("base_url"),
)
)
with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor
cached: Final = _evict_if_stale_locked(
public_key=public_key,
secret_key=parameters.get("secret_key"),
base_url=parameters.get("base_url"),
)
client: Final = Langfuse(
**parameters, # pyright: ignore[reportArgumentType] # kwargs-ok: dict mirrors the typed ctor, values resolved by the callers
tracer_provider=None
if cached is not None
else build_isolated_tracer_provider(environment=environment, release=release),
span_exporter=span_exporter,
)
register_langfuse_client(client)
_retire_orphaned_providers()
return client
def register_langfuse_client(client: Langfuse) -> None:
"""Track the client against the SDK resources it ended up with.
langfuse keys its resources on the public key alone, so a second client for
the same key (a per-key ``langfuse_environment`` override, a team whose
callback_vars repeat the global credentials) is handed the first client's
tracer provider and export thread rather than its own. Only the last live
client may shut those down; see ``shutdown_langfuse_client``.
"""
resources: Final = getattr(client, "_resources", None)
if resources is None:
return
with _LIVE_CLIENTS_LOCK:
holders = _live_clients.get(resources)
if holders is None:
holders = WeakSet()
_live_clients[resources] = holders
holders.add(client)
def _release_langfuse_resources(resources: LangfuseResourceManager, client: Langfuse) -> bool:
"""Drop the client's claim; True when no other live client still uses ``resources``."""
with _LIVE_CLIENTS_LOCK:
holders: Final = _live_clients.get(resources)
if holders is None:
return True
holders.discard(client)
if len(holders) > 0:
return False
_live_clients.pop(resources, None)
return True
def shutdown_langfuse_client(client: Langfuse) -> None:
"""Release everything the client owns, which the SDK's own shutdown does not.
``Langfuse.shutdown`` joins the score and media consumers but leaves the
tracer provider's export thread running and leaves the client in the
registry, so a later request for the same key gets a dead client back.
A callback holding a lease on the client's bundle postpones all of this to
the moment that lease ends, so eviction cannot close the provider out from
under an export the lease is wrapping. See ``lease_langfuse_client``.
"""
state: Final = _lifecycle_state(client)
if not state.claim_for_teardown(client):
return
_run_teardowns(state, (client,))
def _teardown_langfuse_client(client: Langfuse) -> None:
"""The blocking teardown behind ``shutdown_langfuse_client``.
A client that shares its resources with another live client only flushes:
shutting the shared provider down here would silence the other client for
the rest of its life, as it did before the reference count existed.
The registry entry is removed before the blocking shutdown so a concurrent
construct builds a fresh bundle instead of adopting a dying one, and the
provider is only shut down when litellm built it: a bundle adopted from
user code may share the process-global provider.
"""
resources: Final = getattr(client, "_resources", None)
client.flush()
if resources is None:
client.shutdown()
return
public_key: Final = getattr(resources, "public_key", None)
with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor
if not _release_langfuse_resources(resources, client):
return
if public_key is not None and LangfuseResourceManager._instances.get(public_key) is resources: # pyright: ignore[reportPrivateUsage] # registry has no public accessor
LangfuseResourceManager._instances.pop(public_key, None) # pyright: ignore[reportPrivateUsage] # registry has no public accessor
client.shutdown()
provider: Final = getattr(resources, "tracer_provider", None)
if provider is not None and provider in _litellm_built_providers:
_litellm_built_providers.discard(provider)
provider.shutdown()
_retire_orphaned_providers()

View file

@ -43,9 +43,12 @@ class LangfuseInMemoryCache(InMemoryCache):
#########################################################
# Clean up Langfuse initialized clients
#########################################################
from litellm.integrations.langfuse.langfuse_sdk import (
shutdown_langfuse_client,
)
litellm.initialized_langfuse_clients -= 1
_created_langfuse_logger.Langfuse.flush()
_created_langfuse_logger.Langfuse.shutdown()
shutdown_langfuse_client(_created_langfuse_logger.Langfuse)
# Loggers with a periodic flush task (e.g. NewRelicMetricsLogger) expose
# stop() so eviction actually ends the task instead of leaking it.

View file

@ -391,7 +391,10 @@ async def health_services_endpoint(
from litellm.integrations.langfuse.langfuse import LangFuseLogger
langfuse_logger: Final = LangFuseLogger()
langfuse_logger.Langfuse.auth_check()
if langfuse_logger.Langfuse.auth_check() is False:
raise ValueError(
"langfuse auth_check failed - verify LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY are set correctly"
)
_ = litellm.completion(
model="openai/litellm-mock-response-model",
messages=[{"role": "user", "content": "Hey, how's it going?"}],

View file

@ -155,11 +155,11 @@ proxy-runtime = [
"anthropic[vertex]>=0.84.0,<1.0",
"grpcio==1.78.0",
"prometheus-client>=0.20.0,<1.0",
"langfuse>=2.59.7,<3.0",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"langfuse>=4.7,<5.0",
"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"ddtrace>=4.8.2,<5.0",
"sentry-sdk>=2.21.0,<3.0",
"mangum>=0.17.0,<1.0",
@ -203,11 +203,11 @@ dev = [
"types-PyYAML==6.0.12.20250915",
"botocore-stubs==1.43.14",
"types-boto3[bedrock,bedrock-agent,bedrock-runtime,kms,s3,sagemaker-runtime,sts]==1.43.30",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"langfuse==2.59.7",
"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"langfuse>=4.7,<5.0",
"fastapi-offline==1.7.6",
"fakeredis==2.34.1",
"pytest-rerunfailures==15.1",
@ -229,10 +229,10 @@ proxy-dev = [
"prisma==0.11.0",
"hypercorn==0.17.3",
"prometheus-client==0.20.0",
"opentelemetry-api==1.28.0",
"opentelemetry-sdk==1.28.0",
"opentelemetry-exporter-otlp==1.28.0",
"opentelemetry-instrumentation-fastapi==0.49b0",
"opentelemetry-api==1.33.1",
"opentelemetry-sdk==1.33.1",
"opentelemetry-exporter-otlp==1.33.1",
"opentelemetry-instrumentation-fastapi==0.54b1",
"azure-identity==1.25.2",
"a2a-sdk==1.1.0",
]
@ -252,7 +252,7 @@ ci = [
"lunary==1.4.36; python_version == '3.10'",
"lunary==1.4.37; python_version >= '3.11'",
"logfire==4.6.0",
"traceloop-sdk==0.33.12",
"traceloop-sdk==0.34.0",
"detect-secrets==1.5.0",
"PyGithub==2.8.1",
"aiodynamo==24.7",

View file

@ -914,6 +914,7 @@ def test_logging_trace_id(langfuse_trace_id, langfuse_existing_trace_id):
"""
- Unit test for `_get_trace_id` function in Logging obj
"""
from litellm.integrations.langfuse.langfuse_sdk import resolve_trace_id
from litellm.litellm_core_utils.litellm_logging import Logging
litellm.success_callback = ["langfuse"]
@ -946,24 +947,18 @@ def test_logging_trace_id(langfuse_trace_id, langfuse_existing_trace_id):
time.sleep(3)
assert litellm_logging_obj._get_trace_id(service_name="langfuse") is not None
## if existing_trace_id exists
# langfuse addresses a trace by a 32-hex id, so the id litellm reports back is the
# resolved form of whichever source won; that is what the alerting deep link needs
if langfuse_existing_trace_id is not None:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== langfuse_existing_trace_id
)
## if trace_id exists
expected_source = langfuse_existing_trace_id
elif langfuse_trace_id is not None:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== langfuse_trace_id
)
## if no trace_id or existing_trace_id is provided, use litellm_trace_id
expected_source = langfuse_trace_id
else:
assert (
litellm_logging_obj._get_trace_id(service_name="langfuse")
== litellm_logging_obj.litellm_trace_id
)
expected_source = litellm_logging_obj.litellm_trace_id
assert litellm_logging_obj._get_trace_id(service_name="langfuse") == resolve_trace_id(
expected_source
)
def test_convert_model_response_object():

View file

@ -51,7 +51,13 @@ def test_resolve_langfuse_credentials_keeps_env_for_global_config(monkeypatch):
assert host == "https://admin-configured.example"
def test_upstream_langfuse_debug_env_is_passed(monkeypatch):
def test_upstream_langfuse_env_is_read_without_building_a_client(monkeypatch):
"""The UPSTREAM_LANGFUSE_* values are recorded, and nothing consumes them.
A client was built here and never referenced. On v4 that means a second
exporter, its own threads, and an entry in the SDK's per-key registry, so it
is no longer constructed.
"""
from litellm.integrations.langfuse.langfuse import LangFuseLogger
class FakeLangfuse:
@ -81,7 +87,10 @@ def test_upstream_langfuse_debug_env_is_passed(monkeypatch):
)
assert logger.upstream_langfuse_debug == "true"
assert FakeLangfuse.instances[-1].kwargs["debug"] is True
assert logger.upstream_langfuse_public_key == "upstream-public"
assert logger.upstream_langfuse_host == "https://upstream.example"
assert logger.upstream_langfuse_release == "release"
assert not hasattr(logger, "upstream_langfuse")
def test_langfuse_handler_accepts_secret_key_alias(monkeypatch):

View file

@ -306,35 +306,58 @@ def test_get_langfuse_flush_interval():
def test_langfuse_e2e_sync(monkeypatch):
from litellm import completion
import litellm
import respx
import httpx
"""A sync completion must reach langfuse over the wire, not just build a span.
v4 exports OTLP over ``requests`` rather than the v2 ingestion endpoint over
httpx, so this stands up a real receiver and asserts langfuse posted to it.
"""
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
litellm.disable_aiohttp_transport = (
True # since this uses respx, we need to set use_aiohttp_transport to False
)
import litellm
from litellm import completion
from litellm.integrations.langfuse.langfuse import LangFuseLogger
litellm._turn_on_debug()
received_paths = []
class _Receiver(BaseHTTPRequestHandler):
def do_POST(self):
received_paths.append(self.path)
self.rfile.read(int(self.headers.get("Content-Length") or 0))
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), _Receiver)
threading.Thread(target=server.serve_forever, daemon=True).start()
monkeypatch.setenv("LANGFUSE_HOST", f"http://127.0.0.1:{server.server_port}")
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-e2e-sync")
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-e2e-sync")
monkeypatch.setattr(litellm, "success_callback", ["langfuse"])
monkeypatch.setattr(litellm, "_langfuse_logger_cache", {}, raising=False)
with respx.mock:
# Mock Langfuse
# Mock any Langfuse endpoint
langfuse_mock = respx.post(
"https://*.cloud.langfuse.com/api/public/ingestion"
).mock(return_value=httpx.Response(200))
try:
completion(
model="openai/my-fake-endpoint",
messages=[{"role": "user", "content": "hello from litellm"}],
stream=False,
mock_response="Hello from litellm 2",
)
for logger in litellm.logging_callback_manager._get_all_callbacks():
if isinstance(logger, LangFuseLogger):
logger.Langfuse.flush()
deadline = time.time() + 10
while not received_paths and time.time() < deadline:
time.sleep(0.1)
finally:
server.shutdown()
time.sleep(3)
assert langfuse_mock.called
assert received_paths, "langfuse exported nothing"
assert all(path.endswith("/api/public/otel/v1/traces") for path in received_paths)
def test_get_chat_content_for_langfuse():

View file

@ -1,9 +1,13 @@
from datetime import datetime, timezone
from types import MappingProxyType
from typing import Final
from unittest.mock import MagicMock, patch
import pytest
# langfuse_client_init imports this lazily; cache it before any test mocks
# sys.modules["langfuse"], or a single-file run dies on the real import
import litellm.integrations.langfuse.langfuse_sdk # noqa: F401
from litellm.integrations.langfuse.langfuse_prompt_management import (
LangfusePromptManagement,
langfuse_client_init,
@ -87,13 +91,12 @@ class TestLangfusePromptManagement:
"litellm.integrations.langfuse.langfuse_prompt_management.LangFuseLogger._get_langfuse_flush_interval",
return_value=1,
),
patch.dict("sys.modules", {"langfuse": self._mock_langfuse}),
patch("litellm.integrations.langfuse.langfuse_sdk.Langfuse", mock_langfuse_class), # test-quality-ok: the ctor must be intercepted where acquire_langfuse_client resolves it; a real client spawns export threads
patch(
"litellm.llms.custom_httpx.http_handler.get_ssl_configuration",
return_value=False,
) as mock_get_ssl,
):
self._mock_langfuse.Langfuse = mock_langfuse_class
langfuse_client_init(
langfuse_public_key="pk-1234",
@ -124,16 +127,76 @@ class _RecordingLangfuseForEnv:
(("Production", "default"), ("production ", "production"), ("prod", "prod")),
)
def test_langfuse_client_init_resolves_deployment_environment(monkeypatch, env_value, expected):
mock_langfuse_module: Final = MagicMock()
mock_langfuse_module.version.__version__ = "2.60.0"
mock_langfuse_module.Langfuse = _RecordingLangfuseForEnv
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-test")
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-test")
monkeypatch.setenv("LANGFUSE_HOST", "https://test.langfuse.com")
monkeypatch.setenv("LANGFUSE_TRACING_ENVIRONMENT", env_value)
monkeypatch.setattr(_RecordingLangfuseForEnv, "last_environment", None)
with patch.dict("sys.modules", MappingProxyType({"langfuse": mock_langfuse_module})):
with patch("litellm.integrations.langfuse.langfuse_sdk.Langfuse", _RecordingLangfuseForEnv): # test-quality-ok: the ctor must be intercepted where acquire_langfuse_client resolves it; a real client spawns export threads
langfuse_client_init.cache_clear()
langfuse_client_init()
langfuse_client_init.cache_clear()
assert _RecordingLangfuseForEnv.last_environment == expected
def test_langfuse_client_init_mock_mode_makes_no_network_calls(monkeypatch):
"""LANGFUSE_MOCK promises full execution without egress.
The registry maps the "langfuse" callback to LangfusePromptManagement, so
this client is the one the standard proxy path emits observations through;
v4 ships them over its own OTLP exporter, which the httpx mock cannot see.
"""
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
from langfuse._client.resource_manager import LangfuseResourceManager
from litellm.integrations.langfuse.langfuse_sdk import (
open_trace_context,
start_generation,
to_unix_nanos,
)
received = []
class _Receiver(BaseHTTPRequestHandler):
def do_POST(self):
received.append(self.path)
self.rfile.read(int(self.headers.get("Content-Length") or 0))
self.send_response(200)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, *args):
pass
server = HTTPServer(("127.0.0.1", 0), _Receiver)
threading.Thread(target=server.serve_forever, daemon=True).start()
monkeypatch.setenv("LANGFUSE_MOCK", "true")
monkeypatch.setenv("LANGFUSE_HOST", f"http://127.0.0.1:{server.server_port}")
monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-pm-mock-egress")
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-pm-mock-egress")
LangfuseResourceManager._instances.pop("pk-pm-mock-egress", None)
langfuse_client_init.cache_clear()
try:
client = langfuse_client_init()
context, claim_root = open_trace_context(client=client, trace_id="a" * 32, parent_observation_id=None)
now = datetime.now(timezone.utc)
start_generation(
client=client,
context=context,
name="pm-mock-gen",
start_time=now,
claim_trace_root=claim_root,
attributes={},
).end(end_time=to_unix_nanos(now))
client.flush()
time.sleep(1)
finally:
server.shutdown()
langfuse_client_init.cache_clear()
LangfuseResourceManager._instances.pop("pk-pm-mock-egress", None)
assert received == [], f"LANGFUSE_MOCK still sent spans to the configured host: {received}"

View file

@ -0,0 +1,893 @@
"""Covers the v4 observation plumbing: historical timestamps and id normalisation.
The timestamp assertions are the regression guard for the migration: v4 has no
public API for an observation start time, so a callback running after the model
call would otherwise record its own duration instead of the call's.
"""
import json
import threading
from datetime import datetime, timedelta, timezone
import opentelemetry.trace as otel_trace
import pytest
from langfuse import Langfuse
from langfuse._client.resource_manager import LangfuseResourceManager
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
from litellm.integrations.langfuse.langfuse import (
MINIMUM_LANGFUSE_VERSION,
installed_langfuse_version,
raise_if_unsupported_langfuse_version,
)
from litellm.integrations.langfuse.langfuse_sdk import (
AS_ROOT_ATTRIBUTE,
PUBLIC_ATTRIBUTE,
RELEASE_ATTRIBUTE,
_lifecycle_state,
_litellm_built_providers,
_teardown_langfuse_client,
build_isolated_tracer_provider,
evict_stale_langfuse_resources,
lease_langfuse_client,
open_trace_context,
register_langfuse_client,
resolve_observation_id,
resolve_trace_id,
shutdown_langfuse_client,
start_child_span,
start_generation,
to_unix_nanos,
)
CALL_START = datetime(2024, 3, 1, 12, 0, 0, tzinfo=timezone.utc)
FIRST_TOKEN = CALL_START + timedelta(seconds=5)
CALL_END = CALL_START + timedelta(seconds=20)
@pytest.fixture(name="client")
def _client():
LangfuseResourceManager._instances.pop("pk-obs-test", None)
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
client = Langfuse(
public_key="pk-obs-test",
secret_key="sk-obs-test",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
yield client, exporter
LangfuseResourceManager._instances.pop("pk-obs-test", None)
def _only_span(exporter, name):
return next(s for s in exporter.get_finished_spans() if s.name == name)
def test_generation_records_the_model_call_window_not_the_callback(client):
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="a" * 32, parent_observation_id=None)
start_generation(
client=lf,
context=context,
name="gen",
start_time=CALL_START,
claim_trace_root=claim_root,
attributes={"completion_start_time": FIRST_TOKEN},
).end(end_time=to_unix_nanos(CALL_END))
lf.flush()
span = _only_span(exporter, "gen")
assert span.start_time == to_unix_nanos(CALL_START)
assert span.end_time == to_unix_nanos(CALL_END)
assert (span.end_time - span.start_time) == 20 * 1_000_000_000
assert json.loads(span.attributes["langfuse.observation.completion_start_time"]) == FIRST_TOKEN.isoformat().replace(
"+00:00", "Z"
)
@pytest.mark.parametrize(
"supplied",
[1709294400.5, datetime(2024, 3, 1, 12, 0, 0, 500000, tzinfo=timezone.utc)],
ids=["unix-seconds-float", "datetime"],
)
def test_timestamps_accept_both_shapes_guardrails_and_callbacks_use(supplied):
"""Guardrail entries carry unix seconds as floats, the callback carries datetimes."""
assert to_unix_nanos(supplied) == 1709294400500000000
def test_guardrail_span_with_float_timestamps_does_not_break_the_generation(client):
"""A guardrail entry must not take the whole event down with it."""
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="9" * 32, parent_observation_id=None)
guardrail_start = 1709294400.0
start_child_span(
client=lf,
context=context,
name="guardrail",
start_time=guardrail_start,
claim_trace_root=claim_root,
attributes={},
).end(end_time=to_unix_nanos(guardrail_start + 2))
start_generation(
client=lf, context=context, name="gen", start_time=CALL_START, claim_trace_root=claim_root, attributes={}
).end(end_time=to_unix_nanos(CALL_END))
lf.flush()
guardrail = _only_span(exporter, "guardrail")
assert (guardrail.end_time - guardrail.start_time) == 2 * 1_000_000_000
assert _only_span(exporter, "gen") is not None
def test_generation_claims_trace_root_only_without_a_real_parent(client):
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="b" * 32, parent_observation_id=None)
assert claim_root is True
start_generation(
client=lf, context=context, name="root-gen", start_time=CALL_START, claim_trace_root=claim_root, attributes={}
).end()
parented_context, parented_claim = open_trace_context(client=lf, trace_id="b" * 32, parent_observation_id="c" * 16)
assert parented_claim is False
start_generation(
client=lf,
context=parented_context,
name="child-gen",
start_time=CALL_START,
claim_trace_root=parented_claim,
attributes={},
).end()
lf.flush()
assert _only_span(exporter, "root-gen").attributes.get(AS_ROOT_ATTRIBUTE) is True
assert _only_span(exporter, "child-gen").attributes.get(AS_ROOT_ATTRIBUTE) is None
def test_child_span_keeps_its_own_window_and_stays_a_sibling(client):
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="d" * 32, parent_observation_id=None)
guardrail_start = CALL_START + timedelta(seconds=1)
start_child_span(
client=lf,
context=context,
name="guardrail",
start_time=guardrail_start,
claim_trace_root=claim_root,
attributes={},
).end(end_time=to_unix_nanos(guardrail_start + timedelta(seconds=2)))
start_generation(
client=lf, context=context, name="gen", start_time=CALL_START, claim_trace_root=claim_root, attributes={}
).end(end_time=to_unix_nanos(CALL_END))
lf.flush()
guardrail = _only_span(exporter, "guardrail")
generation = _only_span(exporter, "gen")
assert (guardrail.end_time - guardrail.start_time) == 2 * 1_000_000_000
assert guardrail.context.trace_id == generation.context.trace_id
# the shared remote parent is fabricated and never exported, so both must claim trace root
assert guardrail.attributes.get(AS_ROOT_ATTRIBUTE) is True
assert generation.attributes.get(AS_ROOT_ATTRIBUTE) is True
def test_release_is_carried_on_the_root_observation(client):
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="e" * 32, parent_observation_id=None)
start_generation(
client=lf,
context=context,
name="gen",
start_time=CALL_START,
claim_trace_root=claim_root,
release="v1.2.3",
attributes={},
).end()
lf.flush()
assert _only_span(exporter, "gen").attributes[RELEASE_ATTRIBUTE] == "v1.2.3"
@pytest.mark.parametrize("public", [True, False], ids=["public", "private"])
def test_trace_public_flag_lands_on_the_root_observation(client, public):
"""v2 took ``public`` on ``trace()``; v4 reads ``langfuse.trace.public`` off the root observation."""
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="a" * 32, parent_observation_id=None)
start_generation(
client=lf,
context=context,
name="gen",
start_time=CALL_START,
claim_trace_root=claim_root,
public=public,
attributes={},
).end()
lf.flush()
assert _only_span(exporter, "gen").attributes[PUBLIC_ATTRIBUTE] is public
def test_trace_public_flag_is_absent_when_not_requested(client):
lf, exporter = client
context, claim_root = open_trace_context(client=lf, trace_id="b" * 32, parent_observation_id=None)
start_generation(
client=lf, context=context, name="gen", start_time=CALL_START, claim_trace_root=claim_root, attributes={}
).end()
lf.flush()
assert PUBLIC_ATTRIBUTE not in _only_span(exporter, "gen").attributes
def test_request_release_beats_the_client_wide_release(monkeypatch):
"""A client configured with its own release must not overwrite trace_release."""
monkeypatch.setenv("LANGFUSE_RELEASE", "client-wide-release")
LangfuseResourceManager._instances.pop("pk-release-test", None)
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
lf = Langfuse(
public_key="pk-release-test",
secret_key="sk-release-test",
host="http://127.0.0.1:1",
release="client-wide-release",
tracer_provider=provider,
span_exporter=exporter,
)
context, claim_root = open_trace_context(client=lf, trace_id="f" * 32, parent_observation_id=None)
start_generation(
client=lf,
context=context,
name="gen",
start_time=CALL_START,
claim_trace_root=claim_root,
release="per-request-release",
attributes={},
).end()
lf.flush()
LangfuseResourceManager._instances.pop("pk-release-test", None)
assert _only_span(exporter, "gen").attributes[RELEASE_ATTRIBUTE] == "per-request-release"
@pytest.mark.parametrize(
"supplied, expected",
[
("0123456789abcdef0123456789abcdef", "0123456789abcdef0123456789abcdef"),
("0123456789ABCDEF0123456789ABCDEF", "0123456789abcdef0123456789abcdef"),
("3fe0c940-b69a-de3b-a77c-06102505349a", "3fe0c940b69ade3ba77c06102505349a"),
],
ids=["already-hex", "uppercase-hex", "uuid-with-dashes"],
)
def test_trace_id_passes_through_when_it_is_already_usable(supplied, expected):
assert resolve_trace_id(supplied) == expected
def test_arbitrary_trace_id_is_hashed_deterministically():
first = resolve_trace_id("order-4471")
assert first == resolve_trace_id("order-4471")
assert len(first) == 32 and first == first.lower()
assert first != resolve_trace_id("order-4472")
@pytest.mark.parametrize("supplied", [12345, 12.5, True, False], ids=["int", "float", "true", "false"])
def test_non_string_trace_id_is_normalized(supplied):
resolved = resolve_trace_id(supplied)
assert len(resolved) == 32
assert resolved == resolve_trace_id(supplied)
@pytest.mark.parametrize("supplied", [12345, 12.5, True, False], ids=["int", "float", "true", "false"])
def test_non_string_observation_id_is_normalized(supplied):
resolved = resolve_observation_id(supplied)
assert len(resolved) == 16
assert resolved == resolve_observation_id(supplied)
def test_hyphen_only_trace_ids_are_deterministic():
assert resolve_trace_id("---") == resolve_trace_id("---")
def test_trace_id_with_trailing_newline_is_hashed():
supplied = "a" * 32 + "\n"
resolved = resolve_trace_id(supplied)
assert resolved != supplied
assert len(resolved) == 32
def test_missing_trace_id_still_yields_a_valid_trace_id():
generated = resolve_trace_id(None)
assert len(generated) == 32
assert int(generated, 16) >= 0
@pytest.mark.parametrize(
"supplied, expected",
[
("0123456789abcdef", "0123456789abcdef"),
(None, None),
("", None),
],
ids=["already-hex", "none", "empty"],
)
def test_observation_id_normalisation(supplied, expected):
assert resolve_observation_id(supplied) == expected
def test_arbitrary_observation_id_is_hashed_to_a_span_id():
resolved = resolve_observation_id("my-parent-observation")
assert len(resolved) == 16
assert resolved == resolve_observation_id("my-parent-observation")
PUBLIC_KEY = "pk-lifecycle-test"
@pytest.fixture(autouse=True)
def _clean_registry():
LangfuseResourceManager._instances.pop(PUBLIC_KEY, None)
yield
LangfuseResourceManager._instances.pop(PUBLIC_KEY, None)
def _lifecycle_client(secret_key="sk-original", host="http://127.0.0.1:1"):
return Langfuse(
public_key=PUBLIC_KEY,
secret_key=secret_key,
host=host,
tracer_provider=build_isolated_tracer_provider(environment=None, release=None),
)
@pytest.mark.parametrize("unsupported", ["2.59.7", "3.15.0", "5.0.0"], ids=["v2", "v3", "v5"])
def test_unsupported_sdk_fails_loudly_rather_than_dropping_every_event(unsupported):
with pytest.raises(ImportError) as raised:
raise_if_unsupported_langfuse_version(unsupported)
assert unsupported in str(raised.value)
assert MINIMUM_LANGFUSE_VERSION in str(raised.value)
def test_supported_sdk_is_accepted():
assert raise_if_unsupported_langfuse_version(installed_langfuse_version()) is None
def test_isolated_provider_carries_environment_and_release():
provider = build_isolated_tracer_provider(environment="staging", release="v9")
attributes = provider.resource.attributes
assert attributes["langfuse.environment"] == "staging"
assert attributes["langfuse.release"] == "v9"
def test_langfuse_sample_rate_drops_spans_on_the_isolated_provider(monkeypatch):
"""The SDK only installs its sampler on providers it builds itself; v2 sampled via the same env var."""
monkeypatch.setenv("LANGFUSE_SAMPLE_RATE", "0")
dropped_exporter = InMemorySpanExporter()
dropping_provider = build_isolated_tracer_provider(environment=None, release=None)
dropping_provider.add_span_processor(SimpleSpanProcessor(dropped_exporter))
dropping_provider.get_tracer("test").start_span("dropped").end()
assert not dropped_exporter.get_finished_spans()
monkeypatch.delenv("LANGFUSE_SAMPLE_RATE")
kept_exporter = InMemorySpanExporter()
keeping_provider = build_isolated_tracer_provider(environment=None, release=None)
keeping_provider.add_span_processor(SimpleSpanProcessor(kept_exporter))
keeping_provider.get_tracer("test").start_span("kept").end()
assert [span.name for span in kept_exporter.get_finished_spans()] == ["kept"]
def test_invalid_sample_rate_fails_at_construction_like_the_sdk(monkeypatch):
monkeypatch.setenv("LANGFUSE_SAMPLE_RATE", "1.5")
with pytest.raises(ValueError, match=r"between 0\.0 and 1\.0"):
build_isolated_tracer_provider(environment=None, release=None)
def test_environment_override_lands_per_span_despite_shared_resources():
"""The SDK registry is keyed on public key alone, so a second client for the
same key adopts the first client's provider; the observation wrapper stamps
each span with its own client's environment, which the server prefers over
the resource-level value."""
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
first = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
environment="prod",
tracer_provider=provider,
span_exporter=exporter,
)
second = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
environment="staging",
)
assert second._resources is first._resources
for client, environment in ((first, "prod"), (second, "staging")):
context, claim_trace_root = open_trace_context(client=client, trace_id="a" * 32, parent_observation_id=None)
start_generation(
client=client,
context=context,
name=f"generation-{environment}",
start_time=CALL_START,
claim_trace_root=claim_trace_root,
attributes={},
).end()
first.flush()
spans = {span.name: span for span in exporter.get_finished_spans()}
assert spans["generation-prod"].attributes["langfuse.environment"] == "prod"
assert spans["generation-staging"].attributes["langfuse.environment"] == "staging"
def test_client_does_not_take_over_the_process_tracer_provider():
# the global provider can only be set once per process, so assert it is left
# alone rather than assuming this test is the one that installed it
provider_before = otel_trace.get_tracer_provider()
client = _lifecycle_client()
assert otel_trace.get_tracer_provider() is provider_before
assert client._resources.tracer_provider is not provider_before
active = getattr(provider_before, "_active_span_processor", None)
if active is not None:
assert not any("Langfuse" in type(processor).__name__ for processor in active._span_processors)
def test_rotated_credentials_replace_the_cached_client():
original = _lifecycle_client(secret_key="sk-original", host="http://127.0.0.1:1")
original_resources = original._resources
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-rotated", base_url="http://127.0.0.1:2")
rotated = _lifecycle_client(secret_key="sk-rotated", host="http://127.0.0.1:2")
assert rotated._resources is not original_resources
assert rotated._resources.secret_key == "sk-rotated"
assert rotated._resources.base_url == "http://127.0.0.1:2"
def test_unchanged_credentials_keep_the_cached_client():
original = _lifecycle_client()
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-original", base_url="http://127.0.0.1:1")
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is original._resources
def test_eviction_flushes_queued_observations_before_tearing_down():
"""An observation already ended when the cache evicts must still reach langfuse."""
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
InMemorySpanExporter,
)
from litellm.integrations.langfuse.langfuse_sdk import (
open_trace_context,
start_generation,
)
exporter = InMemorySpanExporter()
provider = TracerProvider()
# a long delay keeps the span queued, so only the shutdown can flush it
provider.add_span_processor(BatchSpanProcessor(exporter, schedule_delay_millis=600000))
client = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
context, claim_root = open_trace_context(client=client, trace_id="a" * 32, parent_observation_id=None)
start_generation(
client=client, context=context, name="in-flight", start_time=None, claim_trace_root=claim_root, attributes={}
).end()
assert exporter.get_finished_spans() == ()
shutdown_langfuse_client(client)
assert any(span.name == "in-flight" for span in exporter.get_finished_spans())
def test_shutdown_deregisters_so_a_later_client_is_not_a_corpse():
client = _lifecycle_client()
resources = client._resources
shutdown_langfuse_client(client)
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is not resources
def _shared_resources_pair():
"""The SDK hands a second client on the same public key the first client's resources."""
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
_litellm_built_providers.add(provider)
first = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
second = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=build_isolated_tracer_provider(environment="per-key-override", release=None),
)
assert second._resources is first._resources
register_langfuse_client(first)
register_langfuse_client(second)
return first, second, exporter
def _exports(client, exporter, name):
client.start_observation(name=name).end()
client.flush()
return any(span.name == name for span in exporter.get_finished_spans())
def test_garbage_collected_throwaway_clients_do_not_hold_shared_resources_open():
"""A health probe or alerting lookup builds a client it never shuts down.
Once such a client is garbage collected it must stop counting, or the last
managed client's shutdown would skip the teardown forever.
"""
import gc
first, second, exporter = _shared_resources_pair()
throwaway = Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1")
register_langfuse_client(throwaway)
shutdown_langfuse_client(second)
del throwaway
gc.collect()
shutdown_langfuse_client(first)
assert not _exports(first, exporter, "after-managed-teardown")
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is not first._resources
def test_evicting_a_client_that_shares_resources_keeps_the_other_exporting():
"""A per-key ``langfuse_environment`` override is a second client on the global key.
When the cache evicts it, the global logger must keep exporting.
"""
first, second, exporter = _shared_resources_pair()
shutdown_langfuse_client(second)
assert _exports(first, exporter, "after-sibling-eviction")
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is first._resources
def test_eviction_defers_teardown_until_active_callback_finishes():
"""A cached client must keep exporting while its callback lease is active."""
exporter = InMemorySpanExporter()
provider = build_isolated_tracer_provider(environment=None, release=None)
provider.add_span_processor(SimpleSpanProcessor(exporter))
client = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
register_langfuse_client(client)
def evict() -> None:
shutdown_langfuse_client(client)
with lease_langfuse_client(client):
evictor = threading.Thread(target=evict)
evictor.start()
evictor.join(timeout=5)
assert not evictor.is_alive()
assert not exporter._stopped
context, claim_root = open_trace_context(client=client, trace_id="a" * 32, parent_observation_id=None)
start_generation(
client=client,
context=context,
name="active-callback",
start_time=None,
claim_trace_root=claim_root,
attributes={},
).end()
client.flush()
assert any(span.name == "active-callback" for span in exporter.get_finished_spans())
evictor.join(timeout=5)
assert not evictor.is_alive()
assert exporter._stopped
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is not client._resources
def test_teardown_failure_does_not_strand_queued_clients(monkeypatch):
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
first = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
clients = (
first,
Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1"),
Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1"),
)
assert len({client._resources for client in clients}) == 1
for client in clients:
register_langfuse_client(client)
state = _lifecycle_state(clients[0])
original_teardown = _teardown_langfuse_client
calls = []
def teardown(client):
calls.append(client)
original_teardown(client)
if len(calls) == 1:
raise RuntimeError("teardown failed")
monkeypatch.setattr("litellm.integrations.langfuse.langfuse_sdk._teardown_langfuse_client", teardown)
with lease_langfuse_client(clients[0]):
for client in clients:
shutdown_langfuse_client(client)
assert len(calls) == 3
assert not state.pending_clients
assert not state.teardown_in_progress
def test_queued_eviction_waits_for_the_last_of_two_overlapping_leases():
exporter = InMemorySpanExporter()
provider = build_isolated_tracer_provider(environment=None, release=None)
provider.add_span_processor(SimpleSpanProcessor(exporter))
client = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
register_langfuse_client(client)
with lease_langfuse_client(client):
with lease_langfuse_client(client):
shutdown_langfuse_client(client)
assert not exporter._stopped
assert exporter._stopped
def test_a_client_adopted_during_deferred_teardown_keeps_exporting():
"""The registry hands the same bundle back out while its teardown is queued behind a lease;
the holder count must degrade that teardown to a flush."""
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
_litellm_built_providers.add(provider)
evicted = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
register_langfuse_client(evicted)
with lease_langfuse_client(evicted):
shutdown_langfuse_client(evicted)
adopter = Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1")
assert adopter._resources is evicted._resources
register_langfuse_client(adopter)
assert _exports(adopter, exporter, "after-deferred-teardown")
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is adopter._resources
def test_leases_on_one_client_do_not_serialise_callbacks():
"""Every langfuse callback in the process shares one client, so leases must overlap."""
client = _lifecycle_client()
both_inside = threading.Barrier(2, timeout=5)
def hold_lease() -> None:
with lease_langfuse_client(client):
both_inside.wait()
holders = tuple(threading.Thread(target=hold_lease) for _ in range(2))
for holder in holders:
holder.start()
for holder in holders:
holder.join(timeout=5)
assert not any(holder.is_alive() for holder in holders)
assert not both_inside.broken
def test_last_client_on_shared_resources_tears_them_down():
first, second, exporter = _shared_resources_pair()
shutdown_langfuse_client(second)
shutdown_langfuse_client(first)
assert not _exports(first, exporter, "after-last-eviction")
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is not first._resources
def test_shutdown_of_a_stale_client_does_not_deregister_the_live_one():
stale = _lifecycle_client(secret_key="sk-original", host="http://127.0.0.1:1")
stale_resources = stale._resources
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-rotated", base_url="http://127.0.0.1:2")
live = _lifecycle_client(secret_key="sk-rotated", host="http://127.0.0.1:2")
shutdown_langfuse_client(stale)
assert stale_resources is not live._resources
assert LangfuseResourceManager._instances.get(PUBLIC_KEY) is live._resources
def _rotation_provider():
"""A litellm-built provider on the lifecycle public key, exporting in memory."""
exporter = InMemorySpanExporter()
provider = build_isolated_tracer_provider(environment=None, release=None)
provider.add_span_processor(SimpleSpanProcessor(exporter))
client = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
register_langfuse_client(client)
assert _exports(client, exporter, "before-rotation")
return client, exporter
def test_rotation_retires_the_provider_no_client_is_left_on():
"""Prompt management builds throwaway clients from request credentials.
Alternating the secret for one public key evicts a bundle nobody holds any
more, and its export thread has to go with it or every rotation leaks one.
"""
import gc
client, exporter = _rotation_provider()
del client
gc.collect()
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-rotated", base_url="http://127.0.0.1:2")
assert exporter._stopped
def test_rotation_keeps_a_still_live_client_exporting():
"""The evicted bundle is only retired when nothing is on it; a live logger must survive."""
client, exporter = _rotation_provider()
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-rotated", base_url="http://127.0.0.1:2")
assert _exports(client, exporter, "after-rotation")
def test_a_client_dropped_without_shutdown_gets_its_provider_retired():
"""The prompt-management LRU drops rotated-out clients without shutting them down.
Nothing ever calls ``shutdown_langfuse_client`` on such a client, so the next
lifecycle call has to reap the bundle instead of leaking its export thread.
"""
import gc
client, exporter = _rotation_provider()
evict_stale_langfuse_resources(public_key=PUBLIC_KEY, secret_key="sk-rotated", base_url="http://127.0.0.1:2")
assert _exports(client, exporter, "still-held")
del client
gc.collect()
evict_stale_langfuse_resources(public_key="pk-unrelated", secret_key="sk", base_url="http://127.0.0.1:3")
assert exporter._stopped
def test_the_registrys_current_bundle_is_not_reaped_when_its_clients_die():
"""The registry hands its bundle to the next client on the same key, so a bundle
that is still current keeps its provider even after every client is collected."""
import gc
client, exporter = _rotation_provider()
del client
gc.collect()
evict_stale_langfuse_resources(public_key="pk-unrelated", secret_key="sk", base_url="http://127.0.0.1:3")
successor = Langfuse(public_key=PUBLIC_KEY, secret_key="sk-original", host="http://127.0.0.1:1")
assert _exports(successor, exporter, "after-collection")
def test_a_sweep_overlapping_registration_and_rotation_keeps_the_live_provider():
"""A sweep can snapshot providers before a client registers, then wait on the registry
lock while that client registers and a rotation evicts its fresh bundle. Holders are
re-read after the registry snapshot, so the stale first look must not win."""
import threading
from litellm.integrations.langfuse.langfuse_sdk import _retire_orphaned_providers
exporter = InMemorySpanExporter()
provider = build_isolated_tracer_provider(environment=None, release=None)
provider.add_span_processor(SimpleSpanProcessor(exporter))
client = Langfuse(
public_key=PUBLIC_KEY,
secret_key="sk-original",
host="http://127.0.0.1:1",
tracer_provider=provider,
span_exporter=exporter,
)
registry_lock = LangfuseResourceManager._lock
registry_lock.acquire()
try:
sweeper = threading.Thread(target=_retire_orphaned_providers)
sweeper.start()
sweeper.join(timeout=0.5) # parks on the registry lock once its provider snapshot is taken
register_langfuse_client(client)
LangfuseResourceManager._instances.pop(PUBLIC_KEY, None) # the rotation that evicts the fresh bundle
finally:
registry_lock.release()
sweeper.join(timeout=5)
assert not sweeper.is_alive()
assert _exports(client, exporter, "after-racing-sweep")
def test_ssl_exporter_is_only_built_with_custom_tls_material(monkeypatch, tmp_path):
"""v4 exports over its own OTLP channel, so litellm's CA bundle must be rebuilt onto it."""
import litellm
from litellm.integrations.langfuse.langfuse_sdk import _build_verified_span_exporter
monkeypatch.delenv("SSL_CERTIFICATE", raising=False)
monkeypatch.setattr(litellm, "ssl_verify", True)
monkeypatch.setattr(litellm, "ssl_certificate", None)
assert (
_build_verified_span_exporter(public_key="pk", secret_key="sk", base_url="https://lf.internal.example") is None
)
ca_path = tmp_path / "private-ca.pem"
ca_path.write_text("dummy")
monkeypatch.setattr(litellm, "ssl_verify", str(ca_path))
exporter = _build_verified_span_exporter(public_key="pk", secret_key="sk", base_url="https://lf.internal.example")
assert exporter is not None
assert exporter._endpoint == "https://lf.internal.example/api/public/otel/v1/traces"
assert exporter._certificate_file == str(ca_path)
assert exporter._headers["x-langfuse-public-key"] == "pk"
def test_second_client_on_the_same_key_does_not_build_another_provider():
"""A discarded TracerProvider is pinned forever by its atexit hook."""
import gc
from litellm.integrations.langfuse.langfuse_sdk import (
_retire_orphaned_providers,
acquire_langfuse_client,
)
# reap earlier tests' orphans first, so the count below only moves if a provider is built
gc.collect()
_retire_orphaned_providers()
pk = "pk-provider-reuse-test"
LangfuseResourceManager._instances.pop(pk, None)
parameters = {"public_key": pk, "secret_key": "sk-reuse", "base_url": "http://127.0.0.1:1"}
try:
first = acquire_langfuse_client(parameters=parameters, environment=None, release=None, mock_mode=True)
providers_after_first = len(_litellm_built_providers)
second = acquire_langfuse_client(parameters=parameters, environment=None, release=None, mock_mode=True)
assert second._resources is first._resources
assert len(_litellm_built_providers) == providers_after_first
finally:
LangfuseResourceManager._instances.pop(pk, None)

File diff suppressed because it is too large Load diff

View file

@ -4179,3 +4179,19 @@ async def test_health_services_endpoint_pointfive_blocks_non_admin(monkeypatch,
assert str(raised.value.code) == "403"
logger_class.assert_not_called()
@pytest.mark.asyncio
async def test_health_services_endpoint_langfuse_missing_keys_errors(monkeypatch):
"""A disabled v4 client returns False from auth_check instead of raising.
v2 raised out of ``auth_check`` on missing keys, so the endpoint errored;
the endpoint must not report success when the return value says the check
failed.
"""
for key in ("LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY", "LANGFUSE_HOST", "LANGFUSE_BASE_URL", "LANGFUSE_MOCK"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setattr(litellm, "initialized_langfuse_clients", litellm.initialized_langfuse_clients)
with pytest.raises(ProxyException, match="auth_check failed"):
await health_services_endpoint(service="langfuse")

303
uv.lock generated
View file

@ -10,7 +10,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-09-09T21:39:49.468411Z"
exclude-newer = "2026-09-11T20:34:42.422096527Z"
exclude-newer-span = "P3D"
[manifest]
@ -4214,21 +4214,22 @@ wheels = [
[[package]]
name = "langfuse"
version = "2.59.7"
version = "4.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "backoff" },
{ name = "httpx" },
{ name = "idna" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d5/0e/8390bd3a4ad92ecb1ba0462ec8b7c7d328b2e2f31ae0e734bf2f50dbdc96/langfuse-2.59.7.tar.gz", hash = "sha256:f631981705177bf53d030d191397da9b864b99729a7273448afed10d76f78e23", size = 146608, upload-time = "2025-03-03T16:30:59.926Z" }
sdist = { url = "https://files.pythonhosted.org/packages/97/30/6a64dcf84de2f2eb4d03adbfd22cc7bdc95ce67e3e56cd6288405087fb8a/langfuse-4.15.2.tar.gz", hash = "sha256:7f818f38cc22daba88fdcec62d2addcee4e18d1af4529978b6d07501e86b6946", size = 391727, upload-time = "2026-09-09T16:01:25.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/f3/420518b9003c997cdcb0a86473bf0c111181578a95565823c333cb58eb7b/langfuse-2.59.7-py3-none-any.whl", hash = "sha256:2c6890f5b842257173eb54d08f2890c7fd7617859a48b3914ef73f13a6514473", size = 260468, upload-time = "2025-03-03T16:30:57.426Z" },
{ url = "https://files.pythonhosted.org/packages/02/de/e59da18cb5ca9cb8515a254199bd96ace8cf918788d13ded66aa2693e007/langfuse-4.15.2-py3-none-any.whl", hash = "sha256:98c27a3c06e18c4497045f2d4decce2c716ef11cb215cf5b27bbea6ee0877115", size = 705824, upload-time = "2026-09-09T16:01:23.696Z" },
]
[[package]]
@ -4624,7 +4625,7 @@ requires-dist = [
{ name = "jinja2", specifier = ">=3.1.6,<4.0" },
{ name = "jsonschema", specifier = ">=4.0.0,<5.0" },
{ name = "keyring", marker = "extra == 'cli'", specifier = ">=25.6.0,<26.0" },
{ name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=2.59.7,<3.0" },
{ name = "langfuse", marker = "extra == 'proxy-runtime'", specifier = ">=4.7,<5.0" },
{ name = "litellm-enterprise", marker = "extra == 'proxy'", editable = "enterprise" },
{ name = "litellm-proxy-extras", marker = "extra == 'proxy'", editable = "litellm-proxy-extras" },
{ name = "llm-sandbox", marker = "extra == 'proxy-runtime'", specifier = ">=0.3.39,<1.0" },
@ -4636,10 +4637,10 @@ requires-dist = [
{ name = "numpydoc", marker = "extra == 'utils'", specifier = ">=1.8.0,<2.0" },
{ name = "nvidia-riva-client", marker = "extra == 'stt-nvidia-riva'", specifier = ">=2.15.0" },
{ name = "openai", specifier = ">=2.20.0,<3.0.0" },
{ name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
{ name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.49b0" },
{ name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.28.0" },
{ name = "opentelemetry-api", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
{ name = "opentelemetry-exporter-otlp", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
{ name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'proxy-runtime'", specifier = "==0.54b1" },
{ name = "opentelemetry-sdk", marker = "extra == 'proxy-runtime'", specifier = "==1.33.1" },
{ name = "orjson", marker = "extra == 'proxy'", specifier = ">=3.11.6,<4.0" },
{ name = "polars", marker = "extra == 'proxy'", specifier = ">=1.38.1,<2.0" },
{ name = "prisma", marker = "extra == 'extra-proxy'", specifier = ">=0.11.0,<1.0" },
@ -4707,7 +4708,7 @@ ci = [
{ name = "pytest-codspeed", specifier = "==4.3.0" },
{ name = "pytest-retry", specifier = "==1.7.0" },
{ name = "tenacity", specifier = "==8.5.0" },
{ name = "traceloop-sdk", specifier = "==0.33.12" },
{ name = "traceloop-sdk", specifier = "==0.34.0" },
]
dev = [
{ name = "basedpyright", specifier = "==1.39.7" },
@ -4717,12 +4718,12 @@ dev = [
{ name = "fastapi-offline", specifier = "==1.7.6" },
{ name = "hypothesis", specifier = "==6.165.10" },
{ name = "keyring", specifier = "==25.7.0" },
{ name = "langfuse", specifier = "==2.59.7" },
{ name = "langfuse", specifier = ">=4.7,<5.0" },
{ name = "openapi-core", specifier = "==0.22.0" },
{ name = "opentelemetry-api", specifier = "==1.28.0" },
{ name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = "==0.49b0" },
{ name = "opentelemetry-sdk", specifier = "==1.28.0" },
{ name = "opentelemetry-api", specifier = "==1.33.1" },
{ name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
{ name = "opentelemetry-sdk", specifier = "==1.33.1" },
{ name = "parameterized", specifier = "==0.9.0" },
{ name = "psycopg", specifier = "==3.3.3" },
{ name = "psycopg-binary", specifier = "==3.3.3" },
@ -4763,10 +4764,10 @@ proxy-dev = [
{ name = "a2a-sdk", specifier = "==1.1.0" },
{ name = "azure-identity", specifier = "==1.25.2" },
{ name = "hypercorn", specifier = "==0.17.3" },
{ name = "opentelemetry-api", specifier = "==1.28.0" },
{ name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = "==0.49b0" },
{ name = "opentelemetry-sdk", specifier = "==1.28.0" },
{ name = "opentelemetry-api", specifier = "==1.33.1" },
{ name = "opentelemetry-exporter-otlp", specifier = "==1.33.1" },
{ name = "opentelemetry-instrumentation-fastapi", specifier = "==0.54b1" },
{ name = "opentelemetry-sdk", specifier = "==1.33.1" },
{ name = "prisma", specifier = "==0.11.0" },
{ name = "prometheus-client", specifier = "==0.20.0" },
]
@ -5895,45 +5896,45 @@ wheels = [
[[package]]
name = "opentelemetry-api"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "importlib-metadata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/79/36/260eaea0f74fdd0c0d8f22ed3a3031109ea1c85531f94f4fde266c29e29a/opentelemetry_api-1.28.0.tar.gz", hash = "sha256:578610bcb8aa5cdcb11169d136cc752958548fb6ccffb0969c1036b0ee9e5353", size = 62803, upload-time = "2024-11-05T19:14:45.497Z" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/8d/1f5a45fbcb9a7d87809d460f09dc3399e3fbd31d7f3e14888345e9d29951/opentelemetry_api-1.33.1.tar.gz", hash = "sha256:1c6055fc0a2d3f23a50c7e17e16ef75ad489345fd3df1f8b8af7c0bbf8a109e8", size = 65002, upload-time = "2025-05-16T18:52:41.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/e4/3b25d8b856791c04d8a62b1257b5fc09dc41a057800db06885af8ddcdce1/opentelemetry_api-1.28.0-py3-none-any.whl", hash = "sha256:8457cd2c59ea1bd0988560f021656cecd254ad7ef6be4ba09dbefeca2409ce52", size = 64314, upload-time = "2024-11-05T19:14:21.659Z" },
{ url = "https://files.pythonhosted.org/packages/05/44/4c45a34def3506122ae61ad684139f0bbc4e00c39555d4f7e20e0e001c8a/opentelemetry_api-1.33.1-py3-none-any.whl", hash = "sha256:4db83ebcf7ea93e64637ec6ee6fabee45c5cbe4abd9cf3da95c43828ddb50b83", size = 65771, upload-time = "2025-05-16T18:52:17.419Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/16/14e3fc163930ea68f0980a4cdd4ae5796e60aeb898965990e13263d64baf/opentelemetry_exporter_otlp-1.28.0.tar.gz", hash = "sha256:31ae7495831681dd3da34ac457f6970f147465ae4b9aae3a888d7a581c7cd868", size = 6170, upload-time = "2024-11-05T19:14:47.349Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b1/3f/c8ad4f1c3aaadcea2b0f1b4d7970e7b7898c145699769a789f3435143f69/opentelemetry_exporter_otlp-1.33.1.tar.gz", hash = "sha256:4d050311ea9486e3994575aa237e32932aad58330a31fba24fdba5c0d531cf04", size = 6189, upload-time = "2025-05-16T18:52:43.176Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/82/3f521b3c1f2a411ed60a24a8c9f486c1beeaf8c6c55337c87d3ae1642151/opentelemetry_exporter_otlp-1.28.0-py3-none-any.whl", hash = "sha256:1fd02d70f2c1b7ac5579c81e78de4594b188d3317c8ceb69e8b53900fb7b40fd", size = 7024, upload-time = "2024-11-05T19:14:24.534Z" },
{ url = "https://files.pythonhosted.org/packages/4d/32/b9add70dd4e845654fc9fcd1401a705477743880be6c3e62acb1ad0d8662/opentelemetry_exporter_otlp-1.33.1-py3-none-any.whl", hash = "sha256:9bcf1def35b880b55a49e31ebd63910edac14b294fd2ab884953c4deaff5b300", size = 7045, upload-time = "2025-05-16T18:52:21.022Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/8d/5d411084ac441052f4c9bae03a1aec65ae5d16b439fea7b9c5ac3842c013/opentelemetry_exporter_otlp_proto_common-1.28.0.tar.gz", hash = "sha256:5fa0419b0c8e291180b0fc8430a20dd44a3f3236f8e0827992145914f273ec4f", size = 18505, upload-time = "2024-11-05T19:14:48.204Z" }
sdist = { url = "https://files.pythonhosted.org/packages/7a/18/a1ec9dcb6713a48b4bdd10f1c1e4d5d2489d3912b80d2bcc059a9a842836/opentelemetry_exporter_otlp_proto_common-1.33.1.tar.gz", hash = "sha256:c57b3fa2d0595a21c4ed586f74f948d259d9949b58258f11edb398f246bec131", size = 20828, upload-time = "2025-05-16T18:52:43.795Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/72/3c44aabc74db325aaba09361b6a0d80f6d601f0ff86ecea8ee655c9538fc/opentelemetry_exporter_otlp_proto_common-1.28.0-py3-none-any.whl", hash = "sha256:467e6437d24e020156dffecece8c0a4471a8a60f6a34afeda7386df31a092410", size = 18403, upload-time = "2024-11-05T19:14:25.798Z" },
{ url = "https://files.pythonhosted.org/packages/09/52/9bcb17e2c29c1194a28e521b9d3f2ced09028934c3c52a8205884c94b2df/opentelemetry_exporter_otlp_proto_common-1.33.1-py3-none-any.whl", hash = "sha256:b81c1de1ad349785e601d02715b2d29d6818aed2c809c20219f3d1f20b038c36", size = 18839, upload-time = "2025-05-16T18:52:22.447Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-grpc"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
@ -5944,14 +5945,14 @@ dependencies = [
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
]
sdist = { url = "https://files.pythonhosted.org/packages/43/4d/f215162e58041afb4bdf5dbd0d8faf0b7fc9bf7b3d3fc0e44e06f9e7e869/opentelemetry_exporter_otlp_proto_grpc-1.28.0.tar.gz", hash = "sha256:47a11c19dc7f4289e220108e113b7de90d59791cb4c37fc29f69a6a56f2c3735", size = 26237, upload-time = "2024-11-05T19:14:49.026Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/5f/75ef5a2a917bd0e6e7b83d3fb04c99236ee958f6352ba3019ea9109ae1a6/opentelemetry_exporter_otlp_proto_grpc-1.33.1.tar.gz", hash = "sha256:345696af8dc19785fac268c8063f3dc3d5e274c774b308c634f39d9c21955728", size = 22556, upload-time = "2025-05-16T18:52:44.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/b5/afabc8106abc0f9cfeecf5b3e682622b3e04bba1d9b967dbfcd91b9c4ebe/opentelemetry_exporter_otlp_proto_grpc-1.28.0-py3-none-any.whl", hash = "sha256:edbdc53e7783f88d4535db5807cb91bd7b1ec9e9b9cdbfee14cd378f29a3b328", size = 18532, upload-time = "2024-11-05T19:14:26.853Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ec/6047e230bb6d092c304511315b13893b1c9d9260044dd1228c9d48b6ae0e/opentelemetry_exporter_otlp_proto_grpc-1.33.1-py3-none-any.whl", hash = "sha256:7e8da32c7552b756e75b4f9e9c768a61eb47dee60b6550b37af541858d669ce1", size = 18591, upload-time = "2025-05-16T18:52:23.772Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
@ -5962,14 +5963,14 @@ dependencies = [
{ name = "opentelemetry-sdk" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/555f2845928086cd51aa6941c7a546470805b68ed631ec139ce7d841763d/opentelemetry_exporter_otlp_proto_http-1.28.0.tar.gz", hash = "sha256:d83a9a03a8367ead577f02a64127d827c79567de91560029688dd5cfd0152a8e", size = 15051, upload-time = "2024-11-05T19:14:49.813Z" }
sdist = { url = "https://files.pythonhosted.org/packages/60/48/e4314ac0ed2ad043c07693d08c9c4bf5633857f5b72f2fefc64fd2b114f6/opentelemetry_exporter_otlp_proto_http-1.33.1.tar.gz", hash = "sha256:46622d964a441acb46f463ebdc26929d9dec9efb2e54ef06acdc7305e8593c38", size = 15353, upload-time = "2025-05-16T18:52:45.522Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/ce/80d5adabbf7ab4a0ca7b5e0f4039b24d273be370c3ba85fc05b13794411c/opentelemetry_exporter_otlp_proto_http-1.28.0-py3-none-any.whl", hash = "sha256:e8f3f7961b747edb6b44d51de4901a61e9c01d50debd747b120a08c4996c7e7b", size = 17228, upload-time = "2024-11-05T19:14:28.613Z" },
{ url = "https://files.pythonhosted.org/packages/63/ba/5a4ad007588016fe37f8d36bf08f325fe684494cc1e88ca8fa064a4c8f57/opentelemetry_exporter_otlp_proto_http-1.33.1-py3-none-any.whl", hash = "sha256:ebd6c523b89a2ecba0549adb92537cc2bf647b4ee61afbbd5a4c6535aa3da7cf", size = 17733, upload-time = "2025-05-16T18:52:25.137Z" },
]
[[package]]
name = "opentelemetry-instrumentation"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -5977,14 +5978,14 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/6b/6c25b15063c92a011cf3f68375971e2c58a9c764690847edc97df2d94eeb/opentelemetry_instrumentation-0.49b0.tar.gz", hash = "sha256:398a93e0b9dc2d11cc8627e1761665c506fe08c6b2df252a2ab3ade53d751c46", size = 26478, upload-time = "2024-11-05T19:21:41.402Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fd/5756aea3fdc5651b572d8aef7d94d22a0a36e49c8b12fcb78cb905ba8896/opentelemetry_instrumentation-0.54b1.tar.gz", hash = "sha256:7658bf2ff914b02f246ec14779b66671508125c0e4227361e56b5ebf6cef0aec", size = 28436, upload-time = "2025-05-16T19:03:22.223Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/61/e0d21e958d6072ce25c4f5e26a1d22835fc86f80836660adf6badb6038ce/opentelemetry_instrumentation-0.49b0-py3-none-any.whl", hash = "sha256:68364d73a1ff40894574cbc6138c5f98674790cae1f3b0865e21cf702f24dcb3", size = 30694, upload-time = "2024-11-05T19:20:38.584Z" },
{ url = "https://files.pythonhosted.org/packages/f4/89/0790abc5d9c4fc74bd3e03cb87afe2c820b1d1a112a723c1163ef32453ee/opentelemetry_instrumentation-0.54b1-py3-none-any.whl", hash = "sha256:a4ae45f4a90c78d7006c51524f57cd5aa1231aef031eae905ee34d5423f5b198", size = 31019, upload-time = "2025-05-16T19:02:15.611Z" },
]
[[package]]
name = "opentelemetry-instrumentation-alephalpha"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -5992,14 +5993,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/47/32/15048d7773f6018abcd5b85f5c346b44fad8322031f6b4ea5a6c5ada304a/opentelemetry_instrumentation_alephalpha-0.33.12.tar.gz", hash = "sha256:b474ac634cd1e12b30c8863a925320a01043af8c0f46fd58288e587073d6ddec", size = 3727, upload-time = "2024-11-13T20:27:50.425Z" }
sdist = { url = "https://files.pythonhosted.org/packages/64/12/b962c7fd3d29bc4ffe70f41fab8054d0221ebfecc28a344aef6fc749be67/opentelemetry_instrumentation_alephalpha-0.34.0.tar.gz", hash = "sha256:ed6647505963d53aed63b0b2ca84c989ca94ccc215ad19355a7de33e0b10f0ac", size = 3688, upload-time = "2024-12-12T21:02:01.771Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d3/77/e483e2fa14fddc87324b242d59992cfbd2d590563352aa044d11e1d200ed/opentelemetry_instrumentation_alephalpha-0.33.12-py3-none-any.whl", hash = "sha256:b3c7e3dd99121f5c52d7c7a3a82dd2d7a9ba7360f63ac6fcbdca187f58756e16", size = 5116, upload-time = "2024-11-13T20:27:12.818Z" },
{ url = "https://files.pythonhosted.org/packages/ef/1b/d37c9af6319ad64b182f77aec1154f5fab25b9123c9e04fe1a6d19e19e7e/opentelemetry_instrumentation_alephalpha-0.34.0-py3-none-any.whl", hash = "sha256:4e05e1b12edf30597e3cb6163d2e63f938fd3b061a3251940ac12783d1103ce6", size = 5101, upload-time = "2024-12-12T21:01:12.317Z" },
]
[[package]]
name = "opentelemetry-instrumentation-anthropic"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6007,14 +6008,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/0a/cba0a6ac1832e3002158b5a9451268aebfe0150c7b8355068d1f2cea148b/opentelemetry_instrumentation_anthropic-0.33.12.tar.gz", hash = "sha256:0bc1fd9d4cf2feec4fe9f80c0bdfcbfab33ed9cf0edea850b6c198a8679b01ff", size = 8711, upload-time = "2024-11-13T20:27:52.005Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/56/57bbdb8907e14793d9831b220a6561a29033204acc60ce2ebc6387d29ad5/opentelemetry_instrumentation_anthropic-0.34.0.tar.gz", hash = "sha256:ab4336723de8cc3327aeacfab6e2fa085101f92614a402ee2822f8fb557ba7a6", size = 8693, upload-time = "2024-12-12T21:02:02.731Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/46/ba2dc8d18b04acae3d34facd8fe1e5e0cdc9fe64292d45eca9d1d4a8a298/opentelemetry_instrumentation_anthropic-0.33.12-py3-none-any.whl", hash = "sha256:b31618d12a429045db14ed982a142a25df0f0f1dbf03d756e8d597f25b9a053d", size = 11024, upload-time = "2024-11-13T20:27:14.622Z" },
{ url = "https://files.pythonhosted.org/packages/5c/8e/ef2782ecd3e2b03fb792f42ade5fea3c549ba28e6ebefdcf95a4c14412df/opentelemetry_instrumentation_anthropic-0.34.0-py3-none-any.whl", hash = "sha256:8fc397802033636eb74967ffc6a85344e575ea615b5de502386b0a004b07ba68", size = 11005, upload-time = "2024-12-12T21:01:13.846Z" },
]
[[package]]
name = "opentelemetry-instrumentation-asgi"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
@ -6023,14 +6024,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/55/693c3d0938ba5fead5c3aa4ac7022a992b4ff99a8e9979800d0feb843ff4/opentelemetry_instrumentation_asgi-0.49b0.tar.gz", hash = "sha256:959fd9b1345c92f20c6ef1d42f92ef6a76b3c3083fbc4104d59da6859b15b083", size = 24117, upload-time = "2024-11-05T19:21:46.769Z" }
sdist = { url = "https://files.pythonhosted.org/packages/20/f7/a3377f9771947f4d3d59c96841d3909274f446c030dbe8e4af871695ddee/opentelemetry_instrumentation_asgi-0.54b1.tar.gz", hash = "sha256:ab4df9776b5f6d56a78413c2e8bbe44c90694c67c844a1297865dc1bd926ed3c", size = 24230, upload-time = "2025-05-16T19:03:30.234Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/0b/7900c782a1dfaa584588d724bc3bbdf8405a32497537dd96b3fcbf8461b9/opentelemetry_instrumentation_asgi-0.49b0-py3-none-any.whl", hash = "sha256:722a90856457c81956c88f35a6db606cc7db3231046b708aae2ddde065723dbe", size = 16326, upload-time = "2024-11-05T19:20:46.176Z" },
{ url = "https://files.pythonhosted.org/packages/20/24/7a6f0ae79cae49927f528ecee2db55a5bddd87b550e310ce03451eae7491/opentelemetry_instrumentation_asgi-0.54b1-py3-none-any.whl", hash = "sha256:84674e822b89af563b283a5283c2ebb9ed585d1b80a1c27fb3ac20b562e9f9fc", size = 16338, upload-time = "2025-05-16T19:02:22.808Z" },
]
[[package]]
name = "opentelemetry-instrumentation-bedrock"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anthropic" },
@ -6039,14 +6040,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/5a/346c17fca4dd929ce6be8cf402cd3580bb6e4da42ca8eadd2b6f2b4907e4/opentelemetry_instrumentation_bedrock-0.33.12.tar.gz", hash = "sha256:6f5a3f7044edff020d62b3e94f0ea543da4e5c23b7cdb72642692952843b0003", size = 7690, upload-time = "2024-11-13T20:27:53.497Z" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/79/c384051d3e234ffb5f995ecb2245aef54083dc4919258601d9449c8c47bd/opentelemetry_instrumentation_bedrock-0.34.0.tar.gz", hash = "sha256:07f0ed84fa6d9e93c8cefee48ce171c59961c44708fcc11ec21fc1fbcdfb314d", size = 7695, upload-time = "2024-12-12T21:02:04.602Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/04/d93857519edd693e72e6d9ba08a6f0feda2ca21a08e3bc02cbfe242495f6/opentelemetry_instrumentation_bedrock-0.33.12-py3-none-any.whl", hash = "sha256:f9749898c52643d5027b45ac92bf4d3fd39b83adfaf68705a0ed9b4f04b8afae", size = 8982, upload-time = "2024-11-13T20:27:15.935Z" },
{ url = "https://files.pythonhosted.org/packages/56/2c/6d3e353d69407b308a254713728a613651bbe34138956f4f6b0104a5cc0a/opentelemetry_instrumentation_bedrock-0.34.0-py3-none-any.whl", hash = "sha256:1e521e33721e0fbcde2c2cb7cf788e2b8926063846800db777678be583bf1420", size = 8966, upload-time = "2024-12-12T21:01:16.457Z" },
]
[[package]]
name = "opentelemetry-instrumentation-chromadb"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6054,14 +6055,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/05/ae78dd08c30203009815b35bce9b524458d73174e7dc4924a431e7b0b65b/opentelemetry_instrumentation_chromadb-0.33.12.tar.gz", hash = "sha256:eb4c591d398963504f82c20879030ea3694f10065ee62450da761c9b6e1792e7", size = 4598, upload-time = "2024-11-13T20:27:54.38Z" }
sdist = { url = "https://files.pythonhosted.org/packages/29/8e/0846e9c8846eee6f782767a1ee2f760ed5ca53cc95035189706c63027d58/opentelemetry_instrumentation_chromadb-0.34.0.tar.gz", hash = "sha256:ed0b4842db9bd35a0cff138d88d84d63a1529038ac11cf37eeba1dd294d4a2e8", size = 4596, upload-time = "2024-12-12T21:02:06.825Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/2f/3fabec28e538fc0671c5c149e979e6f36f823078aa8c43ab0f69392185e1/opentelemetry_instrumentation_chromadb-0.33.12-py3-none-any.whl", hash = "sha256:2413426c3bf1f3714a95318e934f090fa778ab7b3d7bdd2cc8ee068cda216a06", size = 6322, upload-time = "2024-11-13T20:27:18.789Z" },
{ url = "https://files.pythonhosted.org/packages/9a/b6/132c1cdd8dea4f0e4e1cab910dabdacd9802fb3a8e802e0c825bf6e9691f/opentelemetry_instrumentation_chromadb-0.34.0-py3-none-any.whl", hash = "sha256:d95df8285405a23b82c3b6d0c1b7c439ec86793d21b3a23e51965853d3e9c4a6", size = 6303, upload-time = "2024-12-12T21:01:17.711Z" },
]
[[package]]
name = "opentelemetry-instrumentation-cohere"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6069,14 +6070,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/96/f9cfc4f27c20deabfca237cb26734f060525b43e6993f753fad4ee0eded1/opentelemetry_instrumentation_cohere-0.33.12.tar.gz", hash = "sha256:4ea626d096fdf4c64e04a63b437e36f72a4341f818034ee6dc73ba1dba9ab341", size = 4235, upload-time = "2024-11-13T20:27:55.358Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/bc/d38c64d0e0f92fb8b8bcde241024dd5d4810c2fbe379fdfbbbb32dee957f/opentelemetry_instrumentation_cohere-0.34.0.tar.gz", hash = "sha256:80e27c6f86a73a2c0e89aa3c9ca1a37ff58a01b4c0eb7f249d7ae66568730477", size = 4227, upload-time = "2024-12-12T21:02:08.177Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/08/dce2b7926ace0204ce7946563348e1ff755873e387833484791e4ed391c8/opentelemetry_instrumentation_cohere-0.33.12-py3-none-any.whl", hash = "sha256:3bee3f7f7105259c85145be8c3b68612421860c95ad170f4d03144a3b8c07418", size = 5589, upload-time = "2024-11-13T20:27:21.317Z" },
{ url = "https://files.pythonhosted.org/packages/e3/bb/5efa301486ad236777d15b515158224cb17ca4e1f138e1480ce8a9d5c369/opentelemetry_instrumentation_cohere-0.34.0-py3-none-any.whl", hash = "sha256:6238c84948d809ea5feb1ce603de2c8f9d72d7b8286d9f9115edf33b91202011", size = 5576, upload-time = "2024-12-12T21:01:20.273Z" },
]
[[package]]
name = "opentelemetry-instrumentation-fastapi"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6085,14 +6086,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/bf/8e6d2a4807360f2203192017eb4845f5628dbeaf0597adf3d141cc5c24e1/opentelemetry_instrumentation_fastapi-0.49b0.tar.gz", hash = "sha256:6d14935c41fd3e49328188b6a59dd4c37bd17a66b01c15b0c64afa9714a1f905", size = 19230, upload-time = "2024-11-05T19:21:59.361Z" }
sdist = { url = "https://files.pythonhosted.org/packages/98/3b/9a262cdc1a4defef0e52afebdde3e8add658cc6f922e39e9dcee0da98349/opentelemetry_instrumentation_fastapi-0.54b1.tar.gz", hash = "sha256:1fcad19cef0db7092339b571a59e6f3045c9b58b7fd4670183f7addc459d78df", size = 19325, upload-time = "2025-05-16T19:03:45.359Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/f4/0895b9410c10abf987c90dee1b7688a8f2214a284fe15e575648f6a1473a/opentelemetry_instrumentation_fastapi-0.49b0-py3-none-any.whl", hash = "sha256:646e1b18523cbe6860ae9711eb2c7b9c85466c3c7697cd6b8fb5180d85d3fe6e", size = 12101, upload-time = "2024-11-05T19:21:01.805Z" },
{ url = "https://files.pythonhosted.org/packages/df/9c/6b2b0f9d6c5dea7528ae0bf4e461dd765b0ae35f13919cd452970bb0d0b3/opentelemetry_instrumentation_fastapi-0.54b1-py3-none-any.whl", hash = "sha256:fb247781cfa75fd09d3d8713c65e4a02bd1e869b00e2c322cc516d4b5429860c", size = 12125, upload-time = "2025-05-16T19:02:41.172Z" },
]
[[package]]
name = "opentelemetry-instrumentation-google-generativeai"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6100,14 +6101,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/39/d33585303893fec6d4e828b794b8b26188658e3bd905098e568031eb0698/opentelemetry_instrumentation_google_generativeai-0.33.12.tar.gz", hash = "sha256:9d09cd39afecf70063733b3f2f15200b7dc28addfa6384947a9514557f18d64b", size = 4302, upload-time = "2024-11-13T20:27:56.179Z" }
sdist = { url = "https://files.pythonhosted.org/packages/30/c8/4620090d09b3d450ac7069ad84b366b34c2488df290c7fc0af6582178812/opentelemetry_instrumentation_google_generativeai-0.34.0.tar.gz", hash = "sha256:b0ecc9cb840277d4040277158c4d77a48c171a64ca556c54ddc1c5ce5105ebd8", size = 4288, upload-time = "2024-12-12T21:02:10.891Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/9a/622ca1552d05b5b948c1f0e78d8456464697118c63a58d4f5aa01c105d45/opentelemetry_instrumentation_google_generativeai-0.33.12-py3-none-any.whl", hash = "sha256:0dcd71c38331c47663d7ba6237ddfe02c14e3d1e3a47524c1437e3ee56cd0036", size = 5889, upload-time = "2024-11-13T20:27:22.37Z" },
{ url = "https://files.pythonhosted.org/packages/37/6e/e20b5fce0020a1f3de78227610a7d018764e9b26e8766c4f948493c2485e/opentelemetry_instrumentation_google_generativeai-0.34.0-py3-none-any.whl", hash = "sha256:eb42d8d48e3d13e03363932b69f424d27e8d9a53c8cbd23f190c4a294a881edc", size = 5879, upload-time = "2024-12-12T21:01:22.735Z" },
]
[[package]]
name = "opentelemetry-instrumentation-groq"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6115,14 +6116,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fa/39/71c87d595a312e2cfef83b006070e5d56c73895c59b596336a20aa43e79c/opentelemetry_instrumentation_groq-0.33.12.tar.gz", hash = "sha256:1460901e66c87b47198d639fb22ec25552281cdf7cafe13ae9605447661d6871", size = 5687, upload-time = "2024-11-13T20:28:00.703Z" }
sdist = { url = "https://files.pythonhosted.org/packages/af/1d/443944e52fc37a5e564525134dd86bee6a3f2db7be1c08f6459f056965ad/opentelemetry_instrumentation_groq-0.34.0.tar.gz", hash = "sha256:0c9162ce1a7b5b5a613dbf50f5f2ee8d5e6e175e0cc1758d53d71cb22c7aac1b", size = 5670, upload-time = "2024-12-12T21:02:12.256Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/24/631269741eabb0b028a313f15063871b28e700ba27feece154a3dd71f62d/opentelemetry_instrumentation_groq-0.33.12-py3-none-any.whl", hash = "sha256:4d239c73d689c046ab2c90a25b78d6c7406cef1e26f04633bc148464b66cc74c", size = 7270, upload-time = "2024-11-13T20:27:23.508Z" },
{ url = "https://files.pythonhosted.org/packages/a3/81/beb464fdd0d3f568b589b45629f74e0fb1a1e518a9df2f575bb68ea2096a/opentelemetry_instrumentation_groq-0.34.0-py3-none-any.whl", hash = "sha256:0f74c8b0df2984b27aadabebf3bed4443c0db45fbd851f956299207be12bb207", size = 7252, upload-time = "2024-12-12T21:01:24.069Z" },
]
[[package]]
name = "opentelemetry-instrumentation-haystack"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6130,14 +6131,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9c/06/067e4b2db2bc29d0a7e3a6cc8676d5f1971b0ecbaf7e5fa0c1e478e092af/opentelemetry_instrumentation_haystack-0.33.12.tar.gz", hash = "sha256:3d45df14aff1f2321066e55ecce632653d67c36249d3eaccbefa189f0daaba05", size = 4663, upload-time = "2024-11-13T20:28:01.551Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/61/2aa5d850c1891fd99636a4ad724489ed792ac4aa560be75ab34af0ee26eb/opentelemetry_instrumentation_haystack-0.34.0.tar.gz", hash = "sha256:29739e9429a1a327dc72f743a0b37a3b7f26a742ac762791a75b1bc2f3ba43ff", size = 4645, upload-time = "2024-12-12T21:02:13.193Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/ba/b8872dce7eb67bd6589d4cccfc97554851dad719067b24c7697a5ffef69a/opentelemetry_instrumentation_haystack-0.33.12-py3-none-any.whl", hash = "sha256:d2a3041a58e1027d8728e1a24430f7b01e8c27e9c04c22fa4204c590b12a95f6", size = 7513, upload-time = "2024-11-13T20:27:24.671Z" },
{ url = "https://files.pythonhosted.org/packages/58/15/682dfc4717e4ddbb668fdcb5a12a8b22a2f6c9402d78c26690528722e8e5/opentelemetry_instrumentation_haystack-0.34.0-py3-none-any.whl", hash = "sha256:2ae56f4abc7a2bafad7b2b3ec8e218edf2aa0daaa6570c692076c24682ee78ce", size = 7495, upload-time = "2024-12-12T21:01:25.723Z" },
]
[[package]]
name = "opentelemetry-instrumentation-lancedb"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6145,14 +6146,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/52/16eef8e5c92627a82904f0112715ee95b2a6ee74ec958ec69f7db803a8d5/opentelemetry_instrumentation_lancedb-0.33.12.tar.gz", hash = "sha256:0aa9f6319374f532e2087949c15674f4d8036591ba70f91f9ce6996ea34508c3", size = 3198, upload-time = "2024-11-13T20:28:02.455Z" }
sdist = { url = "https://files.pythonhosted.org/packages/05/00/ad6383e2981308146e282da4d977ed61dd63321c6ac72751aa1c9eb26d74/opentelemetry_instrumentation_lancedb-0.34.0.tar.gz", hash = "sha256:5d081f36335d7b5dd3a8ae3b0fac0b895f4284941e3521f32332d3393b3b1178", size = 3185, upload-time = "2024-12-12T21:02:14.193Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/1d/218b74341471aa370999f7dbee09de44e32d8f822d69b6ca6d95f400be36/opentelemetry_instrumentation_lancedb-0.33.12-py3-none-any.whl", hash = "sha256:e1cdd55ef38d939d8af924478486e66c0cf65a7e5ac19c82f20ad5d06e682b9d", size = 4794, upload-time = "2024-11-13T20:27:25.825Z" },
{ url = "https://files.pythonhosted.org/packages/5b/65/db706f845a5ab861ee59feb6eb394843e27bf0f025bc1438e44a7af19f19/opentelemetry_instrumentation_lancedb-0.34.0-py3-none-any.whl", hash = "sha256:b8284453cb3d98fbe83bd286448eca4edbb779fc79ffc58bdb3a344137d82719", size = 4780, upload-time = "2024-12-12T21:01:26.96Z" },
]
[[package]]
name = "opentelemetry-instrumentation-langchain"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6160,14 +6161,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/60/4fb638bc69cc63bbf7aad81a08650c99bd343a67f49c532261190e7ee7e4/opentelemetry_instrumentation_langchain-0.33.12.tar.gz", hash = "sha256:ff607742c76a1844211648415fa35da9eac22a42da2a9732c673bd13f2973994", size = 8518, upload-time = "2024-11-13T20:28:03.247Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ce/b4/a8bafbc727a874eb26e788b1fe667db85c7dbcb6d685a6b9da07f6ba231b/opentelemetry_instrumentation_langchain-0.34.0.tar.gz", hash = "sha256:2a25bc07ff8719d30b9a01acf29305c7de5418683c14334ad7ddef4608222911", size = 8508, upload-time = "2024-12-12T21:02:15.138Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/fe/215a5b5b52360c94b2223f4dfb339665a60838d8f53edd37dd1c40897de4/opentelemetry_instrumentation_langchain-0.33.12-py3-none-any.whl", hash = "sha256:7406ab7116fa43343f53602f7b530f9bb1552e20ad750dbfc5aa1761c027c2d3", size = 9749, upload-time = "2024-11-13T20:27:27.623Z" },
{ url = "https://files.pythonhosted.org/packages/a1/3f/01f5d6e5fc3e34e068b6ad650bde73facb9748df74de305a33702ad06820/opentelemetry_instrumentation_langchain-0.34.0-py3-none-any.whl", hash = "sha256:373c69adcf18e9d37cd47d96fad78c57959c3f8af7034aff50553103fdbf0ba8", size = 9734, upload-time = "2024-12-12T21:01:28.244Z" },
]
[[package]]
name = "opentelemetry-instrumentation-llamaindex"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "inflection" },
@ -6176,27 +6177,27 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/e7/9b9d43c7b78eea5ecf95b378ed4c12850f8745ff5b46ac5fa9042c89c941/opentelemetry_instrumentation_llamaindex-0.33.12.tar.gz", hash = "sha256:7a278dfe21fbba7dd1b8fe824c9baee0bfb3b4f7ccd71aae5f677412be45587e", size = 9285, upload-time = "2024-11-13T20:28:04.082Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/fe/b73490ee120672c81f78209a787feb1a5fbf19f2ec0657cf9b85277597ae/opentelemetry_instrumentation_llamaindex-0.34.0.tar.gz", hash = "sha256:f84eaa198873e856401fd8382f86d4f099e8edd712369579f4b74c24e0404933", size = 9274, upload-time = "2024-12-12T21:02:17.055Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/6a/aef813dff690cf06c62a86bf3f723ab1331b55f7c3dfd56690e93ac993df/opentelemetry_instrumentation_llamaindex-0.33.12-py3-none-any.whl", hash = "sha256:7f0d0700015f1e1576cf2de211a4062ae2d0ea899c298f4d7d44ce2a37226135", size = 16372, upload-time = "2024-11-13T20:27:28.724Z" },
{ url = "https://files.pythonhosted.org/packages/71/38/01d81a1bae3965031d612afe162a4fdee40d181b46d0f2aab7d2ac49d015/opentelemetry_instrumentation_llamaindex-0.34.0-py3-none-any.whl", hash = "sha256:0058a44a584ccb9046bed3d5da7bb64160c51d46f0a9946d2bb6517ffcd29fd0", size = 16354, upload-time = "2024-12-12T21:01:32.806Z" },
]
[[package]]
name = "opentelemetry-instrumentation-logging"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c8/80/1d15f8afebc2b67ed47bfe45ee97c042808441586617d5aea8df1f1cbd96/opentelemetry_instrumentation_logging-0.49b0.tar.gz", hash = "sha256:d8058216b06c029785113a71428c6edbb3f0e3b9f69ee917050cb98cd8137fb2", size = 9731, upload-time = "2024-11-05T19:22:05.252Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d9/5b/88ed39f22e8c6eb4f6192ab9a62adaa115579fcbcadb3f0241ee645eea56/opentelemetry_instrumentation_logging-0.54b1.tar.gz", hash = "sha256:893a3cbfda893b64ff71b81991894e2fd6a9267ba85bb6c251f51c0419fbe8fa", size = 9976, upload-time = "2025-05-16T19:03:49.976Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c4/0eedcf9ccce07a64baa002fae7001d84f2c032cf5b2ff1a9438bff0479dd/opentelemetry_instrumentation_logging-0.49b0-py3-none-any.whl", hash = "sha256:9f9405d2f8e6fd756d49da979710f7b5ba1b95bd534467f176aae756102eed58", size = 12150, upload-time = "2024-11-05T19:21:07.826Z" },
{ url = "https://files.pythonhosted.org/packages/96/0c/b441fb30d860f25040eaed61e89d68f4d9ee31873159ed18cbc1b92eba56/opentelemetry_instrumentation_logging-0.54b1-py3-none-any.whl", hash = "sha256:01a4cec54348f13941707d857b850b0febf9d49f45d0fcf0673866e079d7357b", size = 12579, upload-time = "2025-05-16T19:02:49.039Z" },
]
[[package]]
name = "opentelemetry-instrumentation-marqo"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6204,14 +6205,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/fb/775a1a5f9b9f641b3c7aa7ea8a7d83cbb97a4ddf86e1c5a4dd2a3c42af8d/opentelemetry_instrumentation_marqo-0.33.12.tar.gz", hash = "sha256:802def00b35033055618dc137f81895496bb449ff405580ff9414eeda139b89f", size = 3479, upload-time = "2024-11-13T20:28:04.892Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/50/2585b0d337a15b7fe31ec0e7245c09154b7d3d7e7e172c3973d40ad313ee/opentelemetry_instrumentation_marqo-0.34.0.tar.gz", hash = "sha256:7bcc091b89717ac7b04c224dfc1429f200ba2b3e930d7a4de80bf9bc054fc0db", size = 3471, upload-time = "2024-12-12T21:02:17.979Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/b1/153592356d8cc6faab61f7e5c727ab3c382984cede3fbdc228872ba5ac7c/opentelemetry_instrumentation_marqo-0.33.12-py3-none-any.whl", hash = "sha256:6f939532f1f953a22eb2811dfdb439bb8bd813182d59d3444dcc4eca46d0805f", size = 5091, upload-time = "2024-11-13T20:27:31.133Z" },
{ url = "https://files.pythonhosted.org/packages/d4/51/9be4f5df62db6ff6e786933136541e3534656bb49a7d35324d96a5c07818/opentelemetry_instrumentation_marqo-0.34.0-py3-none-any.whl", hash = "sha256:dd342cfd4b70d4f65830708bc253397734d3da51d3773b677693e9007217e3ed", size = 5077, upload-time = "2024-12-12T21:01:35.643Z" },
]
[[package]]
name = "opentelemetry-instrumentation-milvus"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6219,14 +6220,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/58/be/d86538d7b09c6ed77f137229ba418e402659e4aa9268ffff696e059ec8fb/opentelemetry_instrumentation_milvus-0.33.12.tar.gz", hash = "sha256:8720e8fd29ea3009dd0e5b8849b1d24657a5750d81f6c1af605e8aabe827f742", size = 3666, upload-time = "2024-11-13T20:28:06.004Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3b/a8/18725e95cb5cf0d01c001698aa4b01198b5f205da4bb98acc8e0b0da1b6c/opentelemetry_instrumentation_milvus-0.34.0.tar.gz", hash = "sha256:6c19aa93c392f5c736390320b27e035761f36ead904277943d62ac6662d77f83", size = 3657, upload-time = "2024-12-12T21:02:18.849Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/22/0972d94433358624e8f956228763650a2366921d4659035b260aad9775aa/opentelemetry_instrumentation_milvus-0.33.12-py3-none-any.whl", hash = "sha256:608783fa555aded64606cfda64f4aa6ace5c2f9790b2b920e114ca229ab00915", size = 5311, upload-time = "2024-11-13T20:27:32.219Z" },
{ url = "https://files.pythonhosted.org/packages/7b/fb/685282b0e0339d629d4fdb03af7d2904461f20c128ae88fa148847e8664a/opentelemetry_instrumentation_milvus-0.34.0-py3-none-any.whl", hash = "sha256:4c587c6031bc82d78189b31f6acd4f36a62ce3ae1f2b18bc7fabb667912cc2d7", size = 5294, upload-time = "2024-12-12T21:01:38.115Z" },
]
[[package]]
name = "opentelemetry-instrumentation-mistralai"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6234,14 +6235,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/78/5cab3468d3885cc391f67ef0a4a845abb085aa8d786aaa565b9cd9c6612f/opentelemetry_instrumentation_mistralai-0.33.12.tar.gz", hash = "sha256:c22f7006a56180ab6384e47b4e49bde8597833f73955e48ac323cdbe107f06ae", size = 4383, upload-time = "2024-11-13T20:28:09.179Z" }
sdist = { url = "https://files.pythonhosted.org/packages/49/0e/3d86aa6b5a31a20ecadbd4423e83255fd09d9648f431ccc786665e0f98be/opentelemetry_instrumentation_mistralai-0.34.0.tar.gz", hash = "sha256:7c81d8602a16b37d698002a7b06233095fe5c17ddf2f0b9d973b78255cdf7547", size = 4387, upload-time = "2024-12-12T21:02:19.71Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/c8/f47d404273e7130f4e4360f33a093d73f8fbbf232ef536a761baeb91027c/opentelemetry_instrumentation_mistralai-0.33.12-py3-none-any.whl", hash = "sha256:66c5961a33492aaf4420ed1ab8c63e533162a06366108c3398c461d05b2a1154", size = 5858, upload-time = "2024-11-13T20:27:33.251Z" },
{ url = "https://files.pythonhosted.org/packages/16/c8/5644b1a821b60a34bebc58f96367571bcdcdf5ab1522137e13ae3a936360/opentelemetry_instrumentation_mistralai-0.34.0-py3-none-any.whl", hash = "sha256:f682b8d4011124fa326308e8fc4ce9e9fdbacfc72fc77431682d2ef950e636d8", size = 5842, upload-time = "2024-12-12T21:01:39.204Z" },
]
[[package]]
name = "opentelemetry-instrumentation-ollama"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6249,14 +6250,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d9/aa/e9c0f903b8ae750794688f82a00ac0b7ab00a42e57d7c279738b5a80a0ce/opentelemetry_instrumentation_ollama-0.33.12.tar.gz", hash = "sha256:4cd012503f8d692453645353231e216c756fc926bdd3142e8c97fc8e87cbe06f", size = 4512, upload-time = "2024-11-13T20:28:10.969Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ff/f2/4c8e16bb5b13a85d86f6a3c515bee05051dcc08f8023dd201a69c9c0580f/opentelemetry_instrumentation_ollama-0.34.0.tar.gz", hash = "sha256:c9cabfac35945eb9b167f174a9fcafe82ec7c70ae1ba04d462486d2ef4c20f70", size = 4491, upload-time = "2024-12-12T21:02:20.656Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/58/5f11976bc5fde11390e709d96c757e1b21ff73faf88d5b0fd97ace56b061/opentelemetry_instrumentation_ollama-0.33.12-py3-none-any.whl", hash = "sha256:ed5313f45f5d46e17d93096eba91ed6338abf535cf2d2eca648d0e2d621e9d6b", size = 5847, upload-time = "2024-11-13T20:27:35.323Z" },
{ url = "https://files.pythonhosted.org/packages/16/3b/1547e92c76b9dd3097a98a67a84b0641ecbba3348f07d5825ecfa3433c7d/opentelemetry_instrumentation_ollama-0.34.0-py3-none-any.whl", hash = "sha256:17beea413c78be8510409aa4b5a5f909ba9e9d14799fd6372b16d84cefb21120", size = 5832, upload-time = "2024-12-12T21:01:40.792Z" },
]
[[package]]
name = "opentelemetry-instrumentation-openai"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6265,14 +6266,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions-ai" },
{ name = "tiktoken" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/ec/2f9bb0a22ba916c10b2ef63ccde48f17348c49c2a651b8590a94076308e8/opentelemetry_instrumentation_openai-0.33.12.tar.gz", hash = "sha256:2c6dfd74d9d56ca393f9dbfc92883c7397d63408ff18b3d9a774ea1611a48ed9", size = 14631, upload-time = "2024-11-13T20:28:11.767Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/9a/04bb865c14d44111ccde056ffa994d5c29ee604c286d6248b2365f53d676/opentelemetry_instrumentation_openai-0.34.0.tar.gz", hash = "sha256:67fabd6b178837c3d115296654a0daaebeeec763789e3f7ffd9a3db6117b354e", size = 14967, upload-time = "2024-12-12T21:02:22.935Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/21/c4a2b70e9f3487ba7123fde8c55090ff4a3f227477261fac1d0b73d6d349/opentelemetry_instrumentation_openai-0.33.12-py3-none-any.whl", hash = "sha256:d5d0c83a469dbf7ab97d1c482ce78f7ba23c00015b01bc8be43cdc0e5d7c497f", size = 22089, upload-time = "2024-11-13T20:27:36.375Z" },
{ url = "https://files.pythonhosted.org/packages/fe/08/6b3c0404d53a2ca913a98fecb4228be9238965cf2f9092acf5c3e960cba0/opentelemetry_instrumentation_openai-0.34.0-py3-none-any.whl", hash = "sha256:22e902b1b830ca53a0a94ec523880a4d39a210e4ec34d0ce76605b726eed1aab", size = 22597, upload-time = "2024-12-12T21:01:43.669Z" },
]
[[package]]
name = "opentelemetry-instrumentation-pinecone"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6280,14 +6281,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9a/2f/f9e0d1d5eb6f3eee791dbeac13185f4b77bbdce774a01011e03a5d8e2a71/opentelemetry_instrumentation_pinecone-0.33.12.tar.gz", hash = "sha256:92ed3221bddb061ebe7f50cd4804c76c9f5d019e2afb967858c1be62c1a3ebf2", size = 4651, upload-time = "2024-11-13T20:28:14.402Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0e/a0/35791b83b78157f4bfb2f7d9c356a1f42a6ffc30f17f2e96210dabe090f7/opentelemetry_instrumentation_pinecone-0.34.0.tar.gz", hash = "sha256:573483686da9fd2be48c6de870b87515e479d6ee489ebc471d7c90e0de4106e1", size = 4649, upload-time = "2024-12-12T21:02:23.857Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/36/8458e916a2cca0378ac28e6d70347d5263160600fcb47b131f029bdd390a/opentelemetry_instrumentation_pinecone-0.33.12-py3-none-any.whl", hash = "sha256:8d6185bd2f5bf34f3983cad48bbaa86fc72925c02a07ab4a79eb805401556b19", size = 6377, upload-time = "2024-11-13T20:27:38.177Z" },
{ url = "https://files.pythonhosted.org/packages/9b/39/0f09e3de4fa72f17438a1ab7f2a8693a9c983a1e1ad6bf6e72c590616e4a/opentelemetry_instrumentation_pinecone-0.34.0-py3-none-any.whl", hash = "sha256:d81387e703bfd59ff03ed46acf0bf6a0c11cf4cdec16a440acbdea18987fda71", size = 6363, upload-time = "2024-12-12T21:01:46.926Z" },
]
[[package]]
name = "opentelemetry-instrumentation-qdrant"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6295,14 +6296,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/0a/216d28c48dc8b9e37094c54cd1e3ef9a43609fc25b2eeb8bd939f0026047/opentelemetry_instrumentation_qdrant-0.33.12.tar.gz", hash = "sha256:ba34c6863c652f27ae28b9922b25d77617ece4a2233ad0c9c1ebd257605853a5", size = 3988, upload-time = "2024-11-13T20:28:18.929Z" }
sdist = { url = "https://files.pythonhosted.org/packages/da/03/bbf02439ba6c6077ac814957695846bc44edc4e631fce8f0cbb792aa5572/opentelemetry_instrumentation_qdrant-0.34.0.tar.gz", hash = "sha256:8d569b2d7ac70bbf7e75abe5f572ff9576fa175660d1ecdc98f60c6ea1d7010b", size = 3977, upload-time = "2024-12-12T21:02:24.795Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/dc/caa6f4951c84ecb11594ab8545a8692c8a166d618a67687e7f07694dcd97/opentelemetry_instrumentation_qdrant-0.33.12-py3-none-any.whl", hash = "sha256:e759fe49c67092197eaa547570d67e15f30675bfdc772839d0456d535297d4c0", size = 6317, upload-time = "2024-11-13T20:27:39.638Z" },
{ url = "https://files.pythonhosted.org/packages/2f/fe/1797190a4a6b81b50a5c83615590929a21775bd4cd8a855102703243fd51/opentelemetry_instrumentation_qdrant-0.34.0-py3-none-any.whl", hash = "sha256:34ef85e62f3039a2b61a68c6decdb9e5d05ab9f7303d08fc010d6d5ec8f144e0", size = 6302, upload-time = "2024-12-12T21:01:48.042Z" },
]
[[package]]
name = "opentelemetry-instrumentation-replicate"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6310,14 +6311,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/89/1e/b4260513c9526b2113772e4bf10bee714555abfb6e7cf69f86326e5607d5/opentelemetry_instrumentation_replicate-0.33.12.tar.gz", hash = "sha256:5dafad1a7a20ba762f689f30c4f76bcb3817b617adb7da3288ac545d15a14565", size = 3767, upload-time = "2024-11-13T20:28:19.766Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0e/98/a36a16df6876396962071d8fbc6f0dc1c81bc1fdcb324e72871683b83e51/opentelemetry_instrumentation_replicate-0.34.0.tar.gz", hash = "sha256:124796ff8593cd211bfa05773f70e8f087a8c0522a544be39bed212a95c8dec3", size = 3767, upload-time = "2024-12-12T21:02:25.678Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/6d/20dab686dce1ce491c97ea4a0feec86b9a2207891a2627b1f4a670d109b5/opentelemetry_instrumentation_replicate-0.33.12-py3-none-any.whl", hash = "sha256:dc527f470080248a57b738b63ed29eae2d82ef68a100fe6e3548f5de7677f2ed", size = 5189, upload-time = "2024-11-13T20:27:40.792Z" },
{ url = "https://files.pythonhosted.org/packages/69/4b/cb70ab819ec045c2e494deea99a542e4819c9d1c5f09ec99d6dffacb00ad/opentelemetry_instrumentation_replicate-0.34.0-py3-none-any.whl", hash = "sha256:c5f3d712702f3cbcfde619d08e83b1c2fd70e4ad36190d68575d576e27370c4d", size = 5175, upload-time = "2024-12-12T21:01:49.409Z" },
]
[[package]]
name = "opentelemetry-instrumentation-requests"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6325,14 +6326,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-util-http" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/16/c71196d8f4cac30b6936c77567ae769f44ac97227255627f5277d825277d/opentelemetry_instrumentation_requests-0.49b0.tar.gz", hash = "sha256:b75a282b3641547272dc7d2fdc0dd68269d0c1e685e4d17579b7fbd34c19b6bb", size = 14123, upload-time = "2024-11-05T19:22:14.128Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/45/116da84930d3dc2f5cdd876283ca96e9b96547bccee7eaa0bd01ce6bf046/opentelemetry_instrumentation_requests-0.54b1.tar.gz", hash = "sha256:3eca5d697c5564af04c6a1dd23b6a3ffbaf11e64887c6051655cee03998f4654", size = 15148, upload-time = "2025-05-16T19:04:00.488Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/33/4b8a4a839401290c44c65a8ca926a60a86c5ee3ecdcf54de4575c288b5ac/opentelemetry_instrumentation_requests-0.49b0-py3-none-any.whl", hash = "sha256:bb39803359e226b8eb0d4c8aaba6fd8a883a7f869fc331ff861743173b33d26d", size = 12368, upload-time = "2024-11-05T19:21:22.387Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b1/6e33d2c3d3cc9e3ae20a9a77625ec81a509a0e5d7fa87e09e7f879468990/opentelemetry_instrumentation_requests-0.54b1-py3-none-any.whl", hash = "sha256:a0c4cd5d946224f336d6bd73cdabdecc6f80d5c39208f84eb96eb15f16cd41a0", size = 12968, upload-time = "2025-05-16T19:03:03.131Z" },
]
[[package]]
name = "opentelemetry-instrumentation-sagemaker"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6340,14 +6341,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/97/2ddbceba0f95f9b28e7ed75ee2d38214ea1e7d071585d9afea35d0e71619/opentelemetry_instrumentation_sagemaker-0.33.12.tar.gz", hash = "sha256:286bb0e7765967212e111274ca523084d8105a3f18d1dfc90873bca60f6ad766", size = 4508, upload-time = "2024-11-13T20:28:21.829Z" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/98/fc4a33b8800a4a774c50a4b3da83a95359b89b3744c259b2ebbafc3b2f3a/opentelemetry_instrumentation_sagemaker-0.34.0.tar.gz", hash = "sha256:b7c2be5ba9ea4f4b9705705cddc1c3474cf4cb4e6db9fdf6968ad97ec8e6f1df", size = 4506, upload-time = "2024-12-12T21:02:26.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/f3/21896275fb1b4954082c4b95277d8ce66d6e947f1c153b0f01182e9a852f/opentelemetry_instrumentation_sagemaker-0.33.12-py3-none-any.whl", hash = "sha256:da72e78a094106c3ce48e2410665016f161211976c577e92b4624dfbbc54e47e", size = 6296, upload-time = "2024-11-13T20:27:41.815Z" },
{ url = "https://files.pythonhosted.org/packages/89/c2/b60f211e51b3c8346073dde33e7053ba1027b943da50d34ec6f00afe7d78/opentelemetry_instrumentation_sagemaker-0.34.0-py3-none-any.whl", hash = "sha256:ed7a50a5a863bfc81bc792fd3bc7b33bbf0af9e279b6e527c79e93034deda1a0", size = 6282, upload-time = "2024-12-12T21:01:50.527Z" },
]
[[package]]
name = "opentelemetry-instrumentation-sqlalchemy"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6356,28 +6357,28 @@ dependencies = [
{ name = "packaging" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a0/a7/24f6cce3808ae1802dd1b60d752fbab877db5655198929cf4ee8ea416923/opentelemetry_instrumentation_sqlalchemy-0.49b0.tar.gz", hash = "sha256:32658e520fc8b35823c722f5d8831d3a410b76dd2724adb2887befc041ddef04", size = 13194, upload-time = "2024-11-05T19:22:14.92Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ac/33/78a25ae4233d42058bb0b363ba4fea7d7210e53c24e5e31f16d5cf6cf957/opentelemetry_instrumentation_sqlalchemy-0.54b1.tar.gz", hash = "sha256:97839acf1c9b96ded857fca57a09b86a56cf8d9eb6d706b7ceaee9352a460e03", size = 14620, upload-time = "2025-05-16T19:04:01.215Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/6b/a1a3685fed593282999cdc374ece15efbd56f8d774bd368bf7ff2cf5923c/opentelemetry_instrumentation_sqlalchemy-0.49b0-py3-none-any.whl", hash = "sha256:d854052d2b02cd0562e5628a514c8153fceada7f585137e173165dfd0a46ef6a", size = 13358, upload-time = "2024-11-05T19:21:23.654Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2b/1c954885815614ef5c1e8c7bbf57a5275e64cd6fb5946b65e17162a34037/opentelemetry_instrumentation_sqlalchemy-0.54b1-py3-none-any.whl", hash = "sha256:d2ca5edb4c7ecef120d51aad6793b7da1cc80207ccfd31c437ee18f098e7c4c4", size = 14169, upload-time = "2025-05-16T19:03:04.119Z" },
]
[[package]]
name = "opentelemetry-instrumentation-threading"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-instrumentation" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/80/88/b19f064ebf1650a7291cb7fcb623129997a7d8af603ffe7cd1907fe469ba/opentelemetry_instrumentation_threading-0.49b0.tar.gz", hash = "sha256:b65ec668a3ee73fccb1432edf52556f374cb9d9e5b160a6da3a6f67890adf444", size = 8283, upload-time = "2024-11-05T19:22:18.778Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a0/bd/561245292e7cc78ac7a0a75537873aea87440cb9493d41371421b3308c2b/opentelemetry_instrumentation_threading-0.54b1.tar.gz", hash = "sha256:3a081085b59675baf7bd93126a681903e6304a5f283df5eaecdd44bcb66df578", size = 8774, upload-time = "2025-05-16T19:04:04.482Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/77/cf262caae1a8903bbe9c379dc6908fddc9f7bbd5c51866d7c7fbae2edb70/opentelemetry_instrumentation_threading-0.49b0-py3-none-any.whl", hash = "sha256:47a49931a2244c2b17db985c512e6c922328b891ff2b64d37b0cd3bd00fd00a9", size = 9072, upload-time = "2024-11-05T19:21:29.564Z" },
{ url = "https://files.pythonhosted.org/packages/81/10/d87ec07d69546adaad525ba5d40d27324a45cba29097d9854a53d9af5047/opentelemetry_instrumentation_threading-0.54b1-py3-none-any.whl", hash = "sha256:bc229e6cd3f2b29fafe0a8dd3141f452e16fcb4906bca4fbf52609f99fb1eb42", size = 9314, upload-time = "2025-05-16T19:03:09.527Z" },
]
[[package]]
name = "opentelemetry-instrumentation-together"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6385,14 +6386,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7f/5f/63ee7efc3de97e12eadc927ac4079caef454ee41f8e608bf7a83734024a9/opentelemetry_instrumentation_together-0.33.12.tar.gz", hash = "sha256:4ac8676560e93492bdd0540d67672424e26f1eb9a41a266d5248eb09b00dc4d2", size = 3907, upload-time = "2024-11-13T20:28:22.971Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/f306f884fd775aff4195ca1d8c7a1426829cd7060ff50b293be23e1ad869/opentelemetry_instrumentation_together-0.34.0.tar.gz", hash = "sha256:f8968d2aaae123e556e9bd7ce9213f40888a180a8014382bb738cff0bc8de8a1", size = 3907, upload-time = "2024-12-12T21:02:28.939Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/6a/56f3a5abea0a3086d24a32398231e0a578c01b7fad174cd69cdd644fb87d/opentelemetry_instrumentation_together-0.33.12-py3-none-any.whl", hash = "sha256:6a1941e3d02b1505bd79a1ef3540d1fe15bf4f61c72cc162445d25d8715386b3", size = 5284, upload-time = "2024-11-13T20:27:42.798Z" },
{ url = "https://files.pythonhosted.org/packages/78/82/32bc20923c9ecd4495a01bc2dcabd377e4fd82c9cf334ed3ad3a81afaf02/opentelemetry_instrumentation_together-0.34.0-py3-none-any.whl", hash = "sha256:9b5069c3a294c161d8ad638a6d234484a2c600f77260902fb8e15afdd8dfdd33", size = 5267, upload-time = "2024-12-12T21:01:51.576Z" },
]
[[package]]
name = "opentelemetry-instrumentation-transformers"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6400,14 +6401,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3f/25/f73bfae73a466b0ee206168a0ec2212db694132f0af75ff7bf8d7da74488/opentelemetry_instrumentation_transformers-0.33.12.tar.gz", hash = "sha256:d7b9c0d4bd71b834a79c2522455799feb7e76148e1dd371408e9907e847e8d6a", size = 3714, upload-time = "2024-11-13T20:28:24.399Z" }
sdist = { url = "https://files.pythonhosted.org/packages/02/54/1ab4fb5409cf6c48f7b0c0a48b39cbea70b4083d994719ba0975ba9a9580/opentelemetry_instrumentation_transformers-0.34.0.tar.gz", hash = "sha256:586b146509a90900486039850f5f3d63256c7f1546e1a897912ba454aa14e5af", size = 3714, upload-time = "2024-12-12T21:02:29.913Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/ef/4305bdf6af7161c2b38d5341b25bb2a0817f8ba40bf370bb6eeb131a223c/opentelemetry_instrumentation_transformers-0.33.12-py3-none-any.whl", hash = "sha256:14c3f3831a892ae38f8bb85240c195ed95e8fa996f60930e2e4f00bb73073036", size = 5255, upload-time = "2024-11-13T20:27:43.888Z" },
{ url = "https://files.pythonhosted.org/packages/25/e9/081aeb69bf4170a5d88de48db11837cce136649b35304ab0ac7164fcc06a/opentelemetry_instrumentation_transformers-0.34.0-py3-none-any.whl", hash = "sha256:984cf5e0f4ef31662382019e3a18edf821f8ce3c20d53aeea68cee5718aad752", size = 5241, upload-time = "2024-12-12T21:01:53.001Z" },
]
[[package]]
name = "opentelemetry-instrumentation-urllib3"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6416,14 +6417,14 @@ dependencies = [
{ name = "opentelemetry-util-http" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/fd/79fa96e997a9ba9f90dd6fd9bd20c67db8b965dea035e54b864665a2508d/opentelemetry_instrumentation_urllib3-0.49b0.tar.gz", hash = "sha256:33db59eafc80877c225467bf71dfe098874dd7f4463a4f12c61fb7dbcd3b4e31", size = 15432, upload-time = "2024-11-05T19:22:23.261Z" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/6f/76a46806cd21002cac1bfd087f5e4674b195ab31ab44c773ca534b6bb546/opentelemetry_instrumentation_urllib3-0.54b1.tar.gz", hash = "sha256:0d30ba3b230e4100cfadaad29174bf7bceac70e812e4f5204e681e4b55a74cd9", size = 15697, upload-time = "2025-05-16T19:04:07.709Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/56/6339b51038142ffacc33821d1cf9a3cf91d9c9166088a5c7d862d40000bb/opentelemetry_instrumentation_urllib3-0.49b0-py3-none-any.whl", hash = "sha256:672855f033e608c857353b6e098551f70088664fbec227f4ea5d90463d602adc", size = 12847, upload-time = "2024-11-05T19:21:34.14Z" },
{ url = "https://files.pythonhosted.org/packages/ff/7a/d75bec41edb6deaf1d2859bab66a84c8ba03e822e7eafdb245da205e53f6/opentelemetry_instrumentation_urllib3-0.54b1-py3-none-any.whl", hash = "sha256:e87958c297ddd36d30e1c9069f34a9690e845e4ccc2662dd80e99ed976d4c03e", size = 13123, upload-time = "2025-05-16T19:03:14.053Z" },
]
[[package]]
name = "opentelemetry-instrumentation-vertexai"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6431,14 +6432,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/99/355c73ba6fb1679f32caa5579d9956dd3e0d40fa2205b41932694bd54696/opentelemetry_instrumentation_vertexai-0.33.12.tar.gz", hash = "sha256:a4ff534f24d4e1caecc621bea1ad19905bafc8ebf2fd1506e9eb1ae8f2a7831a", size = 4356, upload-time = "2024-11-13T20:28:25.514Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/63/37f55389efdffcb167ec6e5cdb6b01cfeaaab01becac80d300aff547c2f5/opentelemetry_instrumentation_vertexai-0.34.0.tar.gz", hash = "sha256:4db963d487a4c26875c50dfeddfb589d998cc46b3cb89dc9a3f1083352b9e607", size = 4343, upload-time = "2024-12-12T21:02:32.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e6/04cb7853674e4412d929d7c3172b073b402b3ee8f049e57dce042bdd5a2d/opentelemetry_instrumentation_vertexai-0.33.12-py3-none-any.whl", hash = "sha256:cf61cdc08bb6cb4dcbb0b59d1d0432cc1b0b7bee8fa25c69ae4886cf048204c4", size = 5789, upload-time = "2024-11-13T20:27:45.036Z" },
{ url = "https://files.pythonhosted.org/packages/4a/11/6dbf0defdfeeeaf4bb2037732bedbe020e048157642519cd022b33af84e6/opentelemetry_instrumentation_vertexai-0.34.0-py3-none-any.whl", hash = "sha256:d9206a65a416159597676ac60d1331abdc3844e98982126c155e1cacd939d395", size = 5773, upload-time = "2024-12-12T21:01:55.753Z" },
]
[[package]]
name = "opentelemetry-instrumentation-watsonx"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6446,14 +6447,14 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/f4/13359f1ef849d87010e494f503ce0d90c0b37f25b3cdf1ce58f0eaf0aa0b/opentelemetry_instrumentation_watsonx-0.33.12.tar.gz", hash = "sha256:98d537e3e9a919eab87f1f5f487679dd642d0742032635001b974c2154cedc0b", size = 6552, upload-time = "2024-11-13T20:28:26.346Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/ed/78fafee5b64f728c5d8958f0fa558148674fb878b5585873e7d76708fa18/opentelemetry_instrumentation_watsonx-0.34.0.tar.gz", hash = "sha256:149a2ec1c6aa476c6258d7f00fc7951220ea8cc23be9a7a1273009377b9df0a4", size = 6552, upload-time = "2024-12-12T21:02:32.962Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/f6/74c9e14dc3324a9e5fb9a31c1ae29f92d25afe7930d1430dc55923867547/opentelemetry_instrumentation_watsonx-0.33.12-py3-none-any.whl", hash = "sha256:76bde9b15ca9be9fa6124b7e09606203103b77e7fa05227b8c9145fd2a782102", size = 7457, upload-time = "2024-11-13T20:27:46.861Z" },
{ url = "https://files.pythonhosted.org/packages/9c/ef/4b2189eda9ed49f4ea69e6b102351944d7e17ab90bfa6ff451ee20c1c97d/opentelemetry_instrumentation_watsonx-0.34.0-py3-none-any.whl", hash = "sha256:85d352880c8abccba92c728cbea7cab455a4acb454d43ed0037b6afecdb3a90c", size = 7442, upload-time = "2024-12-12T21:01:58.071Z" },
]
[[package]]
name = "opentelemetry-instrumentation-weaviate"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
@ -6461,48 +6462,48 @@ dependencies = [
{ name = "opentelemetry-semantic-conventions" },
{ name = "opentelemetry-semantic-conventions-ai" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1d/50/38b46f295c4f28301d6aea15aeddcbc9550bb51a005da95045310549191f/opentelemetry_instrumentation_weaviate-0.33.12.tar.gz", hash = "sha256:1d14949e2123e5a2bd0eb149d8281713b33623d3f09f7aa587d4fca130d11b70", size = 4635, upload-time = "2024-11-13T20:28:27.189Z" }
sdist = { url = "https://files.pythonhosted.org/packages/68/47/9f0fc2310ef155edd22ae8ee3444e76d91a100a1579b40d034d85d2b0806/opentelemetry_instrumentation_weaviate-0.34.0.tar.gz", hash = "sha256:b69294e0b6b2fc5b90cd389c1a2bc75d18ed09f075ab589a61a0bcbe049ef9db", size = 4654, upload-time = "2024-12-12T21:02:34.344Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/e7/16d9a936d716af84546045fa62e593079069e14c667d049602b6e19d6e31/opentelemetry_instrumentation_weaviate-0.33.12-py3-none-any.whl", hash = "sha256:afa500e59bd7059495c6190decb1dd57dc620e17181c9543bd91e26afce74dcd", size = 6428, upload-time = "2024-11-13T20:27:48.71Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/f55c020a3619d31dd39d32e376d8d5f8f6322f82b1c27acd5666503d6643/opentelemetry_instrumentation_weaviate-0.34.0-py3-none-any.whl", hash = "sha256:79eaa9be4393702d7b3cc938f3d01d82371d4a236326b01819002bac3f118194", size = 6410, upload-time = "2024-12-12T21:02:00.604Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/63/ac4cef4d30ea0ca1d2153ad2fc62d91d1cf3b89b0e4e5cbd61a8c567885f/opentelemetry_proto-1.28.0.tar.gz", hash = "sha256:4a45728dfefa33f7908b828b9b7c9f2c6de42a05d5ec7b285662ddae71c4c870", size = 34331, upload-time = "2024-11-05T19:14:59.503Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/dc/791f3d60a1ad8235930de23eea735ae1084be1c6f96fdadf38710662a7e5/opentelemetry_proto-1.33.1.tar.gz", hash = "sha256:9627b0a5c90753bf3920c398908307063e4458b287bb890e5c1d6fa11ad50b68", size = 34363, upload-time = "2025-05-16T18:52:52.141Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/94/c0b43d16e1d96ee1e699373aa59f14a3aa2e7126af3f11d6adc5dcc531cd/opentelemetry_proto-1.28.0-py3-none-any.whl", hash = "sha256:d5ad31b997846543b8e15504657d9a8cf1ad3c71dcbbb6c4799b1ab29e38f7f9", size = 55832, upload-time = "2024-11-05T19:14:40.446Z" },
{ url = "https://files.pythonhosted.org/packages/c4/29/48609f4c875c2b6c80930073c82dd1cafd36b6782244c01394007b528960/opentelemetry_proto-1.33.1-py3-none-any.whl", hash = "sha256:243d285d9f29663fc7ea91a7171fcc1ccbbfff43b48df0774fd64a37d98eda70", size = 55854, upload-time = "2025-05-16T18:52:36.269Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.28.0"
version = "1.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0c/5b/a509ccab93eacc6044591d5ec437d8266e76f893d0389bbf7e5592c7da32/opentelemetry_sdk-1.28.0.tar.gz", hash = "sha256:41d5420b2e3fb7716ff4981b510d551eff1fc60eb5a95cf7335b31166812a893", size = 156155, upload-time = "2024-11-05T19:15:00.451Z" }
sdist = { url = "https://files.pythonhosted.org/packages/67/12/909b98a7d9b110cce4b28d49b2e311797cffdce180371f35eba13a72dd00/opentelemetry_sdk-1.33.1.tar.gz", hash = "sha256:85b9fcf7c3d23506fbc9692fd210b8b025a1920535feec50bd54ce203d57a531", size = 161885, upload-time = "2025-05-16T18:52:52.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/fe/c8decbebb5660529f1d6ba65e50a45b1294022dfcba2968fc9c8697c42b2/opentelemetry_sdk-1.28.0-py3-none-any.whl", hash = "sha256:4b37da81d7fad67f6683c4420288c97f4ed0d988845d5886435f428ec4b8429a", size = 118692, upload-time = "2024-11-05T19:14:41.669Z" },
{ url = "https://files.pythonhosted.org/packages/df/8e/ae2d0742041e0bd7fe0d2dcc5e7cce51dcf7d3961a26072d5b43cc8fa2a7/opentelemetry_sdk-1.33.1-py3-none-any.whl", hash = "sha256:19ea73d9a01be29cacaa5d6c8ce0adc0b7f7b4d58cc52f923e4413609f670112", size = 118950, upload-time = "2025-05-16T18:52:37.297Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecated" },
{ name = "opentelemetry-api" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ee/c8/433b0e54143f8c9369f5c4a7a83e73eec7eb2ee7d0b7e81a9243e78c8e80/opentelemetry_semantic_conventions-0.49b0.tar.gz", hash = "sha256:dbc7b28339e5390b6b28e022835f9bac4e134a80ebf640848306d3c5192557e8", size = 95227, upload-time = "2024-11-05T19:15:01.443Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5b/2c/d7990fc1ffc82889d466e7cd680788ace44a26789809924813b164344393/opentelemetry_semantic_conventions-0.54b1.tar.gz", hash = "sha256:d1cecedae15d19bdaafca1e56b29a66aa286f50b5d08f036a145c7f3e9ef9cee", size = 118642, upload-time = "2025-05-16T18:52:53.962Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/25/05/20104df4ef07d3bf5c3fd6bcc796ef70ab4ea4309378a9ba57bc4b4d01fa/opentelemetry_semantic_conventions-0.49b0-py3-none-any.whl", hash = "sha256:0458117f6ead0b12e3221813e3e511d85698c31901cac84682052adb9c17c7cd", size = 159214, upload-time = "2024-11-05T19:14:43.047Z" },
{ url = "https://files.pythonhosted.org/packages/0a/80/08b1698c52ff76d96ba440bf15edc2f4bc0a279868778928e947c1004bdd/opentelemetry_semantic_conventions-0.54b1-py3-none-any.whl", hash = "sha256:29dab644a7e435b58d3a3918b58c333c92686236b30f7891d5e51f02933ca60d", size = 194938, upload-time = "2025-05-16T18:52:38.796Z" },
]
[[package]]
@ -6516,11 +6517,11 @@ wheels = [
[[package]]
name = "opentelemetry-util-http"
version = "0.49b0"
version = "0.54b1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/99/377ef446928808211b127b9ab31c348bc465c8da4514ebeec6e4a3de3d21/opentelemetry_util_http-0.49b0.tar.gz", hash = "sha256:02928496afcffd58a7c15baf99d2cedae9b8325a8ac52b0d0877b2e8f936dd1b", size = 7863, upload-time = "2024-11-05T19:22:26.973Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a8/9f/1d8a1d1f34b9f62f2b940b388bf07b8167a8067e70870055bd05db354e5c/opentelemetry_util_http-0.54b1.tar.gz", hash = "sha256:f0b66868c19fbaf9c9d4e11f4a7599fa15d5ea50b884967a26ccd9d72c7c9d15", size = 8044, upload-time = "2025-05-16T19:04:10.79Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/0e/ab0a89b315d0bacdd355a345bb69b20c50fc1f0804b52b56fe1c35a60e68/opentelemetry_util_http-0.49b0-py3-none-any.whl", hash = "sha256:8661bbd6aea1839badc44de067ec9c15c05eab05f729f496c856c50a1203caf1", size = 6945, upload-time = "2024-11-05T19:21:37.81Z" },
{ url = "https://files.pythonhosted.org/packages/a4/ef/c5aa08abca6894792beed4c0405e85205b35b8e73d653571c9ff13a8e34e/opentelemetry_util_http-0.54b1-py3-none-any.whl", hash = "sha256:b1c91883f980344a1c3c486cffd47ae5c9c1dd7323f9cbe9fdb7cadb401c87c9", size = 7301, upload-time = "2025-05-16T19:03:18.18Z" },
]
[[package]]
@ -9587,7 +9588,7 @@ wheels = [
[[package]]
name = "traceloop-sdk"
version = "0.33.12"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@ -9633,9 +9634,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "tenacity" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/0d/d7d413e9fe907a8abc33e6f93044484d158722b5ca0bfe22e1ef9ad4e729/traceloop_sdk-0.33.12.tar.gz", hash = "sha256:999ae50b1e5773b2802a8b3e8585c3826b7867bba032a88b6f30ec2727225dda", size = 19768, upload-time = "2024-11-13T20:29:26.67Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/b1/fd7360d97c651098da505e95600e067a7eedb1b78635b2f1d23545ee4a46/traceloop_sdk-0.34.0.tar.gz", hash = "sha256:4aa26003dfa2e417f73728bd847284a12d6da43a946dd588603a0966e753b3e6", size = 19808, upload-time = "2024-12-12T21:03:41.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/13/53c2ab6ac27804769314554a062e0651a44db2360be47e21cf0a29d202ee/traceloop_sdk-0.33.12-py3-none-any.whl", hash = "sha256:d47a474afbf4a68ff38a702dbaca7b17d2d4f0b0e14dc2f1560b6bdd3859ac75", size = 25932, upload-time = "2024-11-13T20:29:25.174Z" },
{ url = "https://files.pythonhosted.org/packages/c5/e8/c89cc77c272312930cc263c45fbd2a648536e93358611bf03dba6f176a0b/traceloop_sdk-0.34.0-py3-none-any.whl", hash = "sha256:1cc3e5be9dd2765212feaa5655e1f43ddc66739585d78d9c81134428a2a7d927", size = 25944, upload-time = "2024-12-12T21:03:39.565Z" },
]
[[package]]