diff --git a/docker/build_from_pip/Dockerfile.build_from_pip b/docker/build_from_pip/Dockerfile.build_from_pip index e27e8f26cc2..a7758b79fe3 100644 --- a/docker/build_from_pip/Dockerfile.build_from_pip +++ b/docker/build_from_pip/Dockerfile.build_from_pip @@ -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.7,<5.0" \ + "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" \ diff --git a/litellm/integrations/SlackAlerting/utils.py b/litellm/integrations/SlackAlerting/utils.py index 297d069a868..4a2e116d4a9 100644 --- a/litellm/integrations/SlackAlerting/utils.py +++ b/litellm/integrations/SlackAlerting/utils.py @@ -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 diff --git a/litellm/integrations/langfuse/langfuse.py b/litellm/integrations/langfuse/langfuse.py index 9576eabaa34..507f6c0fb92 100644 --- a/litellm/integrations/langfuse/langfuse.py +++ b/litellm/integrations/langfuse/langfuse.py @@ -1,11 +1,11 @@ #### What this does #### # On success, logs events to Langfuse -import inspect import os import traceback from collections.abc import Callable, Iterable, Mapping 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 @@ -44,12 +44,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 @@ -119,6 +120,85 @@ 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. + """ + if Version(MINIMUM_LANGFUSE_VERSION) <= Version(installed_version) < Version(UNSUPPORTED_LANGFUSE_VERSION): + 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"} +) + + +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.""" + return MappingProxyType( + { + propagated: trace_params[key] + for key, propagated in _PROPAGATED_TRACE_KEYS.items() + if trace_params.get(key) is not 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, @@ -133,11 +213,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( @@ -160,12 +247,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, @@ -182,7 +271,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(): @@ -196,18 +285,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, } - 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" + parameters["environment"] = self.langfuse_environment self.Langfuse: Langfuse = self.safe_init_langfuse_client(parameters) # set the current langfuse project id in the environ @@ -217,30 +301,17 @@ 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 - ) 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: """ @@ -256,7 +327,27 @@ class LangFuseLogger: 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 ( + DiscardingSpanExporter, + build_isolated_tracer_provider, + evict_stale_langfuse_resources, + register_langfuse_client, + ) + + evict_stale_langfuse_resources( + public_key=parameters.get("public_key"), + secret_key=parameters.get("secret_key"), + base_url=parameters.get("base_url"), + ) + langfuse_client: Final = Langfuse( + **parameters, + tracer_provider=build_isolated_tracer_provider( + environment=parameters.get("environment"), + release=parameters.get("release"), + ), + span_exporter=DiscardingSpanExporter() if self.is_mock_mode else None, + ) + register_langfuse_client(langfuse_client) litellm.initialized_langfuse_clients += 1 verbose_logger.debug("Created langfuse client number %s", litellm.initialized_langfuse_clients) return langfuse_client @@ -357,33 +448,20 @@ class LangFuseLogger: verbose_logger.debug("OUTPUT IN LANGFUSE: %s; original: %s", output, response_obj) trace_id = None generation_id = None - if self._is_langfuse_v2(): - trace_id, generation_id = self._log_langfuse_v2( - user_id=user_id, - metadata=metadata, - litellm_params=litellm_params, - output=output, - start_time=start_time, - end_time=end_time, - kwargs=kwargs, - optional_params=optional_params, - input=input, - response_obj=response_obj, - 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, - ) + trace_id, generation_id = self._log_langfuse_v2( + user_id=user_id, + metadata=metadata, + litellm_params=litellm_params, + output=output, + start_time=start_time, + end_time=end_time, + kwargs=kwargs, + optional_params=optional_params, + input=input, + response_obj=response_obj, + level=level, + litellm_call_id=litellm_call_id, + ) verbose_logger.debug("Langfuse Layer Logging - final response object: %s", response_obj) verbose_logger.info("Langfuse Layer Logging - logging success") @@ -479,58 +557,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, @@ -737,17 +763,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 @@ -769,7 +784,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 @@ -813,8 +828,12 @@ 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, @@ -835,23 +854,48 @@ class LangFuseLogger: 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 != trace_id: - verbose_logger.warning( - "Langfuse trace_id mismatch: set %s, but langfuse returned %s. Using our intended trace_id for consistency.", - trace_id, - generation_client.trace_id, - ) - return trace_id, generation_id + resolved_trace_id: Final = resolve_trace_id(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 + ) + self._log_guardrail_information_as_span( + client=self.Langfuse, + context=trace_context, + standard_logging_object=standard_logging_object, + ) + 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), + ).end(end_time=to_unix_nanos(end_time)) + + # log_event_on_langfuse tuple-unpacks this and re-wraps it in the dict callers cache + return resolved_trace_id, generation_id except Exception: verbose_logger.error("Langfuse Layer Error - %s", traceback.format_exc()) return None, None @@ -920,7 +964,7 @@ 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 @@ -933,14 +977,6 @@ class LangFuseLogger: """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: """ @@ -1005,7 +1041,8 @@ class LangFuseLogger: def _log_guardrail_information_as_span( self, - trace: StatefulTraceClient, + client: "Langfuse", + context: "Context", standard_logging_object: StandardLoggingPayload | None, ): """ @@ -1027,6 +1064,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( @@ -1035,21 +1074,24 @@ 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), + 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( @@ -1091,7 +1133,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): @@ -1106,7 +1148,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: @@ -1126,21 +1168,23 @@ 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], ): """ 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 @@ -1151,22 +1195,30 @@ 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) else: - trace.span( - name="vertex_ai_grounding_metadata", - input=elem, - ) + _end_grounding_span(client=client, context=context, name="vertex_ai_grounding_metadata", value=elem) 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, ) +def _end_grounding_span(*, client: "Langfuse", context: "Context", name: str, value: object) -> None: + from litellm.integrations.langfuse.langfuse_sdk import start_child_span + + start_child_span( + client=client, + context=context, + name=name, + start_time=None, + 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 {} diff --git a/litellm/integrations/langfuse/langfuse_prompt_management.py b/litellm/integrations/langfuse/langfuse_prompt_management.py index 90db0626e23..7dadffa353d 100644 --- a/litellm/integrations/langfuse/langfuse_prompt_management.py +++ b/litellm/integrations/langfuse/langfuse_prompt_management.py @@ -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,7 +68,6 @@ def langfuse_client_init( Exception: If langfuse package is not installed """ try: - import langfuse from langfuse import Langfuse except Exception as e: raise Exception( @@ -84,36 +87,51 @@ 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 ( + DiscardingSpanExporter, + build_isolated_tracer_provider, + evict_stale_langfuse_resources, + register_langfuse_client, + ) + + evict_stale_langfuse_resources(public_key=public_key, secret_key=secret_key, base_url=langfuse_host) + client: Final = Langfuse( + **parameters, # pyright: ignore[reportArgumentType] # kwargs-ok: dict mirrors the typed ctor, values resolved above + tracer_provider=build_isolated_tracer_provider(environment=parameters["environment"], release=langfuse_release), + span_exporter=DiscardingSpanExporter() if is_mock_mode else None, + ) + register_langfuse_client(client) return client @@ -126,9 +144,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, diff --git a/litellm/integrations/langfuse/langfuse_sdk.py b/litellm/integrations/langfuse/langfuse_sdk.py new file mode 100644 index 00000000000..f3ad9ae57a6 --- /dev/null +++ b/litellm/integrations/langfuse/langfuse_sdk.py @@ -0,0 +1,279 @@ +from __future__ import annotations + +import re +import threading +from collections.abc import Mapping +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 + +__all__ = ( + "AS_ROOT_ATTRIBUTE", + "PUBLIC_ATTRIBUTE", + "RELEASE_ATTRIBUTE", + "DiscardingSpanExporter", + "build_isolated_tracer_provider", + "evict_stale_langfuse_resources", + "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: str | None) -> str: + """Map litellm's trace id onto the 32 lowercase hex characters v4 requires. + + Anything else raises inside the SDK rather than being ignored, so a plain + uuid is dash-stripped and any other identifier is hashed deterministically, + which keeps repeat calls with the same id on the same trace. + """ + normalized: Final = trace_id.lower().replace("-", "") if trace_id else "" + if _TRACE_ID_PATTERN.match(normalized): + return normalized + return Langfuse.create_trace_id(seed=trace_id) if trace_id else Langfuse.create_trace_id() + + +def resolve_observation_id(observation_id: str | None) -> str | None: + """Same for a caller-supplied parent, which v4 requires to be 16 hex characters.""" + normalized: Final = observation_id.lower().replace("-", "") if observation_id else "" + if not normalized: + return None + if _OBSERVATION_ID_PATTERN.match(normalized): + return normalized + return sha256(normalized.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, + attributes: Mapping[str, object], +) -> LangfuseSpan: + """Create a sibling observation inside the same trace, keeping its own window.""" + 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) + ) + 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" +_RELEASE_ATTRIBUTE: Final = "langfuse.release" + + +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. + """ + attributes: Final = MappingProxyType( + { + key: value + for key, value in ((_ENVIRONMENT_ATTRIBUTE, environment), (_RELEASE_ATTRIBUTE, release)) + if value is not None + } + ) + return TracerProvider(resource=Resource.create(dict(attributes))) + + +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 + + +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. + + langfuse keys its client registry on the public key alone, so a rotated + secret or a moved host silently keeps exporting with the original values. + Only the one stale entry is removed; the SDK's own reset would shut down + every other tenant in the process. + """ + if not public_key: + return + with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor + cached: Final = LangfuseResourceManager._instances.get(public_key) # pyright: ignore[reportPrivateUsage] # registry has no public accessor + if cached is None: + return + if getattr(cached, "secret_key", None) == secret_key and getattr(cached, "base_url", None) == base_url: + return + LangfuseResourceManager._instances.pop(public_key, None) # pyright: ignore[reportPrivateUsage] # registry has no public accessor + + +_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() + + +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 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. + """ + resources: Final = getattr(client, "_resources", None) + client.flush() + if resources is None: + client.shutdown() + return + if not _release_langfuse_resources(resources, client): + return + client.shutdown() + provider: Final = getattr(resources, "tracer_provider", None) + if provider is not None and not isinstance(provider, otel_trace.ProxyTracerProvider): + provider.shutdown() + public_key: Final = getattr(resources, "public_key", None) + if public_key is None: + return + with LangfuseResourceManager._lock: # pyright: ignore[reportPrivateUsage] # registry has no public accessor + if 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 diff --git a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py index da3ac366bfd..88d959ee0db 100644 --- a/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py +++ b/litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py @@ -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. diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 8b57bdca2fe..9e04437f514 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -355,7 +355,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?"}], diff --git a/pyproject.toml b/pyproject.toml index 2866e27e84c..0496487f2a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -151,11 +151,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", @@ -196,11 +196,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", @@ -221,10 +221,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", ] @@ -244,7 +244,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", diff --git a/tests/litellm_utils_tests/test_utils.py b/tests/litellm_utils_tests/test_utils.py index 0ccfae55290..4341a7b3442 100644 --- a/tests/litellm_utils_tests/test_utils.py +++ b/tests/litellm_utils_tests/test_utils.py @@ -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(): diff --git a/tests/logging_callback_tests/test_langfuse_dynamic_credentials.py b/tests/logging_callback_tests/test_langfuse_dynamic_credentials.py index 2346a5ee047..9c3fc79a4a4 100644 --- a/tests/logging_callback_tests/test_langfuse_dynamic_credentials.py +++ b/tests/logging_callback_tests/test_langfuse_dynamic_credentials.py @@ -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): diff --git a/tests/logging_callback_tests/test_langfuse_unit_tests.py b/tests/logging_callback_tests/test_langfuse_unit_tests.py index 405b6e9e48e..6e44840a535 100644 --- a/tests/logging_callback_tests/test_langfuse_unit_tests.py +++ b/tests/logging_callback_tests/test_langfuse_unit_tests.py @@ -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(): diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py b/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py index 7dea4e67cdd..f9009737ca5 100644 --- a/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_prompt_management.py @@ -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, @@ -137,3 +141,66 @@ def test_langfuse_client_init_resolves_deployment_environment(monkeypatch, env_v 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}" diff --git a/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py new file mode 100644 index 00000000000..39cfeb2fff8 --- /dev/null +++ b/tests/test_litellm/integrations/langfuse/test_langfuse_sdk.py @@ -0,0 +1,475 @@ +"""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 +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, + raise_if_unsupported_langfuse_version, + installed_langfuse_version, +) +from litellm.integrations.langfuse.langfuse_sdk import ( + AS_ROOT_ATTRIBUTE, + PUBLIC_ATTRIBUTE, + RELEASE_ATTRIBUTE, + build_isolated_tracer_provider, + evict_stale_langfuse_resources, + 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, 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, 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.parent.span_id == generation.parent.span_id + assert guardrail.context.trace_id == generation.context.trace_id + + +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") + + +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_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)) + 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_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 diff --git a/tests/test_litellm/integrations/test_langfuse.py b/tests/test_litellm/integrations/test_langfuse.py index d36878e455f..424e847d044 100644 --- a/tests/test_litellm/integrations/test_langfuse.py +++ b/tests/test_litellm/integrations/test_langfuse.py @@ -1,6 +1,5 @@ import datetime import json -import sys import types import unittest from typing import Final, Optional @@ -11,6 +10,7 @@ import pytest import litellm from litellm.integrations.langfuse import langfuse as langfuse_module from litellm.integrations.langfuse.langfuse import LangFuseLogger +from litellm.integrations.langfuse.langfuse_sdk import resolve_trace_id # Import LangfuseUsageDetails directly from the module where it's defined @@ -56,28 +56,23 @@ class TestLangfuseUsageDetails(unittest.TestCase): self.mock_langfuse_client.trace.side_effect = _trace_side_effect - # Mock the langfuse module that's imported locally in methods - self.langfuse_module_patcher = patch.dict( - "sys.modules", {"langfuse": MagicMock()} - ) - self.mock_langfuse_module = self.langfuse_module_patcher.start() - - # Create a mock for the langfuse module with version - self.mock_langfuse = MagicMock() - self.mock_langfuse.version = MagicMock() - self.mock_langfuse.version.__version__ = ( - "3.0.0" # Set a version that supports all features + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export import SimpleSpanProcessor + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, ) - # Mock the Langfuse class - self.mock_langfuse_class = MagicMock() - self.mock_langfuse_class.return_value = self.mock_langfuse_client + self.span_exporter = InMemorySpanExporter() + self.real_provider = TracerProvider() + self.real_provider.add_span_processor(SimpleSpanProcessor(self.span_exporter)) - # Set up the sys.modules['langfuse'] mock - sys.modules["langfuse"] = self.mock_langfuse - sys.modules["langfuse"].Langfuse = self.mock_langfuse_class + # the real SDK is installed; inject the client instead of replacing the module, + # so the v4 imports under test resolve normally + import langfuse as _langfuse_module - # Create a fresh logger instance for each test + self.real_langfuse_class = _langfuse_module.Langfuse + # no patching: the host above is unreachable, so a real client is cheap to build + # and each test swaps in the client it wants self.logger = LangFuseLogger() # Explicitly set the Langfuse client to our mock @@ -113,9 +108,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): ) # Bind the method to the instance - self.logger.log_event_on_langfuse = types.MethodType( - log_event_on_langfuse, self.logger - ) + self.logger.log_event_on_langfuse = types.MethodType(log_event_on_langfuse, self.logger) # Make sure _is_langfuse_v2 returns True def mock_is_langfuse_v2(self): @@ -135,7 +128,37 @@ class TestLangfuseUsageDetails(unittest.TestCase): litellm.initialized_langfuse_clients = self._original_langfuse_clients_count self.env_patcher.stop() - self.langfuse_module_patcher.stop() # patch.dict automatically restores sys.modules + + def use_real_langfuse_client(self): + """Point the logger at a real v4 client whose spans land in memory.""" + 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, + ) + + self.span_exporter = InMemorySpanExporter() + self.real_provider = TracerProvider() + LangfuseResourceManager._instances.pop("pk-unit-test", None) + self.logger.Langfuse = self.real_langfuse_class( + public_key="pk-unit-test", + secret_key="sk-unit-test", + host="http://127.0.0.1:1", + tracer_provider=self.real_provider, + span_exporter=self.span_exporter, + ) + return self.logger.Langfuse + + def exported_generation(self): + self.logger.Langfuse.flush() + spans = [s for s in self.span_exporter.get_finished_spans()] + assert spans, "no spans were exported" + return spans[-1] + + @staticmethod + def span_trace_id(span): + return format(span.context.trace_id, "032x") def test_langfuse_usage_details_type(self): """Test that LangfuseUsageDetails TypedDict is properly defined with the correct fields""" @@ -278,9 +301,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): self.mock_langfuse_trace.span.return_value = mock_span self.mock_langfuse_trace.generation.return_value = self.mock_langfuse_generation - # Ensure trace returns our mock - self.mock_langfuse_client.trace.return_value = self.mock_langfuse_trace - self.logger.Langfuse = self.mock_langfuse_client + self.use_real_langfuse_client() with ( patch( @@ -338,29 +359,12 @@ class TestLangfuseUsageDetails(unittest.TestCase): except Exception as e: self.fail(f"_log_langfuse_v2 raised an exception: {e}") - # Verify that trace was called first - self.mock_langfuse_client.trace.assert_called() - - # Check the arguments passed to the mocked langfuse generation call - self.mock_langfuse_trace.generation.assert_called_once() - call_args, call_kwargs = self.mock_langfuse_trace.generation.call_args - - # Inspect the usage and usage_details dictionaries - usage_arg = call_kwargs.get("usage") - usage_details_arg = call_kwargs.get("usage_details") - - self.assertIsNotNone(usage_arg) - self.assertIsNotNone(usage_details_arg) - - # Verify that None values were converted to 0 - self.assertEqual(usage_arg["prompt_tokens"], 0) - self.assertEqual(usage_arg["completion_tokens"], 0) - - self.assertEqual(usage_details_arg["input"], 0) - self.assertEqual(usage_details_arg["output"], 0) - self.assertEqual(usage_details_arg["total"], 0) - self.assertEqual(usage_details_arg["cache_creation_input_tokens"], 0) - self.assertEqual(usage_details_arg["cache_read_input_tokens"], 0) + usage_details = json.loads(self.exported_generation().attributes["langfuse.observation.usage_details"]) + assert usage_details["input"] == 0 + assert usage_details["output"] == 0 + assert usage_details["total"] == 0 + assert usage_details["cache_creation_input_tokens"] == 0 + assert usage_details["cache_read_input_tokens"] == 0 mock_add_prompt_params.assert_called_once() @@ -413,7 +417,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): def test_log_langfuse_v2_uses_standard_trace_id_when_available(self): payload = self._build_standard_logging_payload(trace_id="std-trace-id") kwargs = self._build_langfuse_kwargs(payload) - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -435,12 +439,12 @@ class TestLangfuseUsageDetails(unittest.TestCase): litellm_call_id="call-id-xyz", ) - assert self.last_trace_kwargs.get("id") == "std-trace-id" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("std-trace-id") def test_log_langfuse_v2_defaults_to_call_id_without_standard_trace_id(self): payload = self._build_standard_logging_payload() kwargs = self._build_langfuse_kwargs(payload) - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -462,7 +466,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): litellm_call_id="call-id-xyz", ) - assert self.last_trace_kwargs.get("id") == "call-id-xyz" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("call-id-xyz") def test_log_langfuse_v2_uses_litellm_trace_id_fallback_over_call_id(self): """ @@ -474,7 +478,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): payload = self._build_standard_logging_payload() # no trace_id kwargs = self._build_langfuse_kwargs(payload) kwargs["litellm_trace_id"] = "trace-id-from-kwargs" - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -497,7 +501,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): ) # litellm_trace_id should be preferred over litellm_call_id - assert self.last_trace_kwargs.get("id") == "trace-id-from-kwargs" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("trace-id-from-kwargs") CANARY = "sk-lf-canary-SECRET-d4e5f6" @@ -527,14 +531,41 @@ class TestLangfuseUsageDetails(unittest.TestCase): } def _emitted_payload_text(self): - """Every blob this logger handed to the langfuse SDK, as one searchable string.""" + """Every attribute this logger exported to langfuse, as one searchable string.""" import json - blobs = [self.last_trace_kwargs] - if self.mock_langfuse_trace.generation.call_args is not None: - blobs.append(self.mock_langfuse_trace.generation.call_args.kwargs) - blobs.extend(call.kwargs for call in self.mock_langfuse_trace.span.call_args_list) - return json.dumps(blobs, default=repr) + self.logger.Langfuse.flush() + return json.dumps( + [dict(span.attributes or {}) for span in self.span_exporter.get_finished_spans()], + default=repr, + ) + + def exported_generation_metadata(self): + """The generation's metadata as langfuse receives it, one attribute per key. + + v4 serializes each value onto the span, so they are decoded back here to + keep these assertions about what litellm emitted rather than about the + SDK's wire encoding. + """ + import json + + prefix = "langfuse.observation.metadata." + + def decoded(raw): + try: + return json.loads(raw) + except (TypeError, ValueError): + return raw + + return { + key[len(prefix) :]: decoded(value) + for key, value in (self.exported_generation().attributes or {}).items() + if key.startswith(prefix) + } + + def exported_spans_named(self, name): + self.logger.Langfuse.flush() + return [span for span in self.span_exporter.get_finished_spans() if span.name == name] def _drive_with_canary(self, extra_metadata=None, hidden_params=None): metadata = {**self._canary_request_metadata(), **(extra_metadata or {})} @@ -542,9 +573,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): if hidden_params is not None: payload["hidden_params"] = hidden_params kwargs = {**self._build_langfuse_kwargs(payload), "response_cost": 0.25} - self.last_trace_kwargs = {} - self.mock_langfuse_trace.generation.reset_mock() - self.mock_langfuse_trace.span.reset_mock() + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -565,7 +594,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): level="INFO", litellm_call_id="canary-call-id", ) - return self.mock_langfuse_trace.generation.call_args.kwargs["metadata"] + return self.exported_generation_metadata() def test_team_callback_credentials_never_reach_langfuse(self): """ @@ -589,9 +618,8 @@ class TestLangfuseUsageDetails(unittest.TestCase): debug_langfuse dumps request metadata into the trace as a second emit site. It must be sourced from the allowlisted payload too. """ - self._drive_with_canary(extra_metadata={"debug_langfuse": True}) + dumped = self._drive_with_canary(extra_metadata={"debug_langfuse": True})["metadata_passed_to_litellm"] - dumped = self.last_trace_kwargs["metadata"]["metadata_passed_to_litellm"] assert "user_api_key_auth" not in dumped assert self.CANARY not in self._emitted_payload_text() @@ -616,7 +644,10 @@ class TestLangfuseUsageDetails(unittest.TestCase): """ self._drive_with_canary(hidden_params={"vertex_ai_grounding_metadata": ["ground-a", "ground-b"]}) - span_inputs = [call.kwargs.get("input") for call in self.mock_langfuse_trace.span.call_args_list] + span_inputs = [ + span.attributes.get("langfuse.observation.input") + for span in self.exported_spans_named("vertex_ai_grounding_metadata") + ] assert span_inputs == ["ground-a", "ground-b"] assert self.CANARY not in self._emitted_payload_text() @@ -625,9 +656,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): Request metadata never reaches the blob, so a caller naming user_api_key_alias cannot have their value emitted in place of the proxy-resolved one. """ - generation_metadata = self._drive_with_canary( - extra_metadata={"user_api_key_alias": "spoofed-by-caller"} - ) + generation_metadata = self._drive_with_canary(extra_metadata={"user_api_key_alias": "spoofed-by-caller"}) assert generation_metadata["user_api_key_alias"] == "canary-alias" @@ -642,7 +671,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): payload["metadata"]["requester_metadata"] = {"litellm_response_cost": "caller-value", "api_base": "caller"} kwargs = {**self._build_langfuse_kwargs(payload), "response_cost": 0.25} metadata = self._canary_request_metadata() - self.mock_langfuse_trace.generation.reset_mock() + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -664,7 +693,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): litellm_call_id="canary-call-id", ) - generation_metadata = self.mock_langfuse_trace.generation.call_args.kwargs["metadata"] + generation_metadata = self.exported_generation_metadata() assert generation_metadata["litellm_response_cost"] == 0.25 assert generation_metadata["api_base"] == "https://real-api-base" @@ -732,8 +761,9 @@ class TestLangfuseUsageDetails(unittest.TestCase): """ self._drive_with_canary() - assert self.last_trace_kwargs.get("session_id") == "canary-session" - assert self.last_trace_kwargs.get("name") == "canary-trace" + generation = self.exported_generation() + assert generation.attributes["session.id"] == "canary-session" + assert generation.attributes["langfuse.trace.name"] == "canary-trace" def test_failure_trace_survives_a_missing_standard_logging_object(self): """ @@ -752,8 +782,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): "messages": [], "litellm_trace_id": "trace-id-failure", } - self.last_trace_kwargs = {} - self.mock_langfuse_trace.generation.reset_mock() + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -777,9 +806,12 @@ class TestLangfuseUsageDetails(unittest.TestCase): import json - assert trace_id == "trace-id-failure" - assert self.last_trace_kwargs.get("id") == "trace-id-failure" - generation_metadata = self.mock_langfuse_trace.generation.call_args.kwargs["metadata"] + # Must use litellm_trace_id, not litellm_call_id. v4 addresses a trace by a + # 32-hex id, so the callback returns the resolved form, which is what makes + # the alerting deep link point at a trace langfuse can actually open + assert trace_id == resolve_trace_id("trace-id-failure") + assert self.span_trace_id(self.exported_generation()) == trace_id + generation_metadata = self.exported_generation_metadata() assert "user_api_key_auth" not in generation_metadata assert self.CANARY not in self._emitted_payload_text() assert "first_custom" not in generation_metadata @@ -796,7 +828,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): """ payload = self._build_standard_logging_payload(trace_id="std-trace-123") kwargs = self._build_langfuse_kwargs(payload) - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -819,9 +851,9 @@ class TestLangfuseUsageDetails(unittest.TestCase): ) # session_id should be set for Langfuse session grouping - assert self.last_trace_kwargs.get("session_id") == "my-session-abc" + assert self.exported_generation().attributes["session.id"] == "my-session-abc" # trace_id should remain the standard trace_id, NOT the session_id - assert self.last_trace_kwargs.get("id") == "std-trace-123" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("std-trace-123") def test_log_langfuse_v2_session_id_preserved_for_error_level(self): """ @@ -831,7 +863,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): """ payload = self._build_standard_logging_payload(trace_id="std-trace-err") kwargs = self._build_langfuse_kwargs(payload) - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -854,11 +886,11 @@ class TestLangfuseUsageDetails(unittest.TestCase): ) # session_id must be preserved even for ERROR level logs - assert self.last_trace_kwargs.get("session_id") == "error-session-xyz" + assert self.exported_generation().attributes["session.id"] == "error-session-xyz" # trace_id should be the standard trace_id, not the session_id - assert self.last_trace_kwargs.get("id") == "std-trace-err" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("std-trace-err") # status_message should be set for error traces - assert self.last_trace_kwargs.get("status_message") is not None + assert self.exported_generation().attributes["langfuse.observation.level"] == "ERROR" def test_log_langfuse_v2_explicit_trace_id_takes_priority_over_session_id(self): """ @@ -867,7 +899,7 @@ class TestLangfuseUsageDetails(unittest.TestCase): """ payload = self._build_standard_logging_payload() kwargs = self._build_langfuse_kwargs(payload) - self.last_trace_kwargs = {} + self.use_real_langfuse_client() with patch( "litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params", @@ -898,9 +930,9 @@ class TestLangfuseUsageDetails(unittest.TestCase): ) # Explicit trace_id must take priority - assert self.last_trace_kwargs.get("id") == "explicit-trace-id-777" + assert self.span_trace_id(self.exported_generation()) == resolve_trace_id("explicit-trace-id-777") # session_id must still be set for session grouping - assert self.last_trace_kwargs.get("session_id") == "session-999" + assert self.exported_generation().attributes["session.id"] == "session-999" def test_failure_handler_langfuse_kwargs_excludes_original_response(): @@ -948,12 +980,8 @@ def test_failure_handler_langfuse_kwargs_excludes_original_response(): try: # Mock LangFuseHandler to return our capturing mock logger - with patch( - "litellm.litellm_core_utils.litellm_logging.LangFuseHandler" - ) as mock_handler_class: - mock_handler_class.get_langfuse_logger_for_request.return_value = ( - mock_langfuse_logger - ) + with patch("litellm.litellm_core_utils.litellm_logging.LangFuseHandler") as mock_handler_class: + mock_handler_class.get_langfuse_logger_for_request.return_value = mock_langfuse_logger # Call the actual failure_handler test_exception = Exception("TestError: model not found") @@ -965,23 +993,19 @@ def test_failure_handler_langfuse_kwargs_excludes_original_response(): ) # Verify log_event_on_langfuse was actually called - assert ( - mock_langfuse_logger.log_event_on_langfuse.called - ), "log_event_on_langfuse was not called" + assert mock_langfuse_logger.log_event_on_langfuse.called, "log_event_on_langfuse was not called" # Verify original_response is NOT in the kwargs passed to Langfuse langfuse_kwargs = captured_kwargs.get("kwargs", {}) - assert ( - "original_response" not in langfuse_kwargs - ), "original_response should be excluded from kwargs passed to Langfuse" + assert "original_response" not in langfuse_kwargs, ( + "original_response should be excluded from kwargs passed to Langfuse" + ) # Verify session_id metadata is preserved in the kwargs - langfuse_metadata = langfuse_kwargs.get("litellm_params", {}).get( - "metadata", {} + langfuse_metadata = langfuse_kwargs.get("litellm_params", {}).get("metadata", {}) + assert langfuse_metadata.get("session_id") == "test-session-failure", ( + "session_id should be preserved in kwargs passed to Langfuse" ) - assert ( - langfuse_metadata.get("session_id") == "test-session-failure" - ), "session_id should be preserved in kwargs passed to Langfuse" # Verify level is ERROR assert captured_kwargs.get("level") == "ERROR" @@ -1023,9 +1047,7 @@ async def test_async_log_failure_event_logs_to_langfuse(): "generation_id": "mock-gen", } - with patch( - "litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler" - ) as mock_handler: + with patch("litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler") as mock_handler: mock_handler.get_langfuse_logger_for_request.return_value = mock_logger kwargs = { @@ -1050,9 +1072,7 @@ async def test_async_log_failure_event_logs_to_langfuse(): ) # Verify log_event_on_langfuse was called - assert ( - mock_logger.log_event_on_langfuse.called - ), "log_event_on_langfuse was not called for failure event" + assert mock_logger.log_event_on_langfuse.called, "log_event_on_langfuse was not called for failure event" call_kwargs = mock_logger.log_event_on_langfuse.call_args[1] assert call_kwargs["level"] == "ERROR" assert call_kwargs["status_message"] == "API error: model not found" @@ -1092,9 +1112,7 @@ async def test_async_log_failure_event_works_without_standard_logging_object(): "generation_id": "mock-gen", } - with patch( - "litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler" - ) as mock_handler: + with patch("litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler") as mock_handler: mock_handler.get_langfuse_logger_for_request.return_value = mock_logger kwargs = { @@ -1125,6 +1143,64 @@ async def test_async_log_failure_event_works_without_standard_logging_object(): assert "InternalServerError" in call_kwargs["status_message"] +def test_mock_mode_makes_no_network_calls(monkeypatch): + """LANGFUSE_MOCK promises full execution without egress. + + The mock intercepts httpx, but v4 ships observations over its own OTLP + exporter, so nothing stops a real request to the configured host without an + exporter that drops them. + """ + import threading + import time + from http.server import BaseHTTPRequestHandler, HTTPServer + + from langfuse._client.resource_manager import LangfuseResourceManager + + 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-mock-egress") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-mock-egress") + LangfuseResourceManager._instances.pop("pk-mock-egress", None) + + try: + logger = LangFuseLogger() + assert logger.is_mock_mode is True + now = datetime.datetime.now() + logger.log_event_on_langfuse( + kwargs={ + "call_type": "completion", + "litellm_params": {"metadata": {}, "proxy_server_request": {"headers": {}}}, + "messages": [{"role": "user", "content": "hi"}], + "optional_params": {}, + }, + response_obj=litellm.ModelResponse(choices=[{"message": {"role": "assistant", "content": "yo"}}]), + start_time=now, + end_time=now, + ) + logger.Langfuse.flush() + time.sleep(1) + finally: + server.shutdown() + LangfuseResourceManager._instances.pop("pk-mock-egress", None) + + assert received == [], f"mock mode sent real requests: {received}" + + def test_max_langfuse_clients_limit(): """ Test that the max langfuse clients limit is respected when initializing multiple clients @@ -1184,14 +1260,6 @@ class _RecordingLangfuse: self.client = MagicMock() -class _RecordingLangfuseWithoutEnvironment: - last_parameters: Optional[dict] = None - - def __init__(self, **parameters): - type(self).last_parameters = parameters - self.client = MagicMock() - - def _build_langfuse_logger(monkeypatch) -> LangFuseLogger: monkeypatch.setenv("LANGFUSE_MOCK", "false") monkeypatch.setattr(litellm, "initialized_langfuse_clients", 0) @@ -1232,19 +1300,6 @@ def test_langfuse_environment_falls_back_to_deployment_env_var(monkeypatch): assert _RecordingLangfuse.last_parameters["environment"] == "deployment-wide" -def test_langfuse_environment_omitted_for_old_sdk_versions(monkeypatch): - monkeypatch.setenv("LANGFUSE_MOCK", "false") - monkeypatch.setattr(litellm, "initialized_langfuse_clients", 0) - with patch("langfuse.Langfuse", _RecordingLangfuseWithoutEnvironment): - LangFuseLogger( - langfuse_public_key="pk-env", - langfuse_secret="sk-env", - langfuse_host="https://test.langfuse.com", - langfuse_environment="staging", - ) - assert "environment" not in _RecordingLangfuseWithoutEnvironment.last_parameters - - def test_dynamic_langfuse_environment_triggers_dynamic_logger(): from litellm.integrations.langfuse.langfuse_handler import LangFuseHandler from litellm.types.utils import StandardCallbackDynamicParams @@ -1307,71 +1362,110 @@ def test_langfuse_logger_reuses_the_shared_cached_client(monkeypatch): _LANGFUSE_REDACTED = "redacted-by-litellm" -def _steering_logger() -> LangFuseLogger: - """``__new__`` skips the SDK and network setup in ``__init__``.""" +def _steering_logger(): + """``__new__`` skips the network setup in ``__init__``; spans land in memory.""" + from langfuse import Langfuse + from langfuse._client.resource_manager import LangfuseResourceManager + from opentelemetry.sdk.trace import TracerProvider + from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( + InMemorySpanExporter, + ) + + from litellm.integrations.langfuse.langfuse import installed_langfuse_version + + exporter = InMemorySpanExporter() + LangfuseResourceManager._instances.pop("pk-steering-test", None) logger = LangFuseLogger.__new__(LangFuseLogger) - logger.Langfuse = MagicMock() - logger.langfuse_sdk_version = "2.60.0" - return logger + logger.Langfuse = Langfuse( + public_key="pk-steering-test", + secret_key="sk-steering-test", + host="http://127.0.0.1:1", + tracer_provider=TracerProvider(), + span_exporter=exporter, + ) + logger.langfuse_sdk_version = installed_langfuse_version() + return logger, exporter -def _emit(logger: LangFuseLogger, *, metadata=None, headers=None): - """``log_event_on_langfuse`` is the entry point that folds ``langfuse_*`` headers into metadata.""" +def _emit(rig, *, metadata=None, headers=None): + """``log_event_on_langfuse`` is the entry point that folds ``langfuse_*`` headers into metadata. + + v4 has no trace object, so the trace-level fields are captured where the + callback hands them to propagation, and the observation fields are read back + off the span langfuse actually exported. + """ + from litellm.integrations.langfuse import langfuse as langfuse_module + + logger, exporter = rig + exporter.clear() + captured_trace_params = {} + propagate_for_real = langfuse_module._trace_attributes_for_propagation + + def capture(trace_params): + captured_trace_params.update(trace_params) + return propagate_for_real(trace_params) + now = datetime.datetime.now() - response_obj = litellm.ModelResponse( - choices=[{"message": {"role": "assistant", "content": "the-output"}}] - ) - logger.log_event_on_langfuse( - kwargs={ - "call_type": "completion", - "litellm_params": { - "metadata": dict(metadata or {}), - "proxy_server_request": {"headers": dict(headers or {})}, + response_obj = litellm.ModelResponse(choices=[{"message": {"role": "assistant", "content": "the-output"}}]) + with patch.object( # test-quality-ok: v4 has no trace object to read back; the propagation call is the only observable trace-level boundary + langfuse_module, "_trace_attributes_for_propagation", capture + ): + logger.log_event_on_langfuse( + kwargs={ + "call_type": "completion", + "litellm_params": { + "metadata": dict(metadata or {}), + "proxy_server_request": {"headers": dict(headers or {})}, + }, + "messages": [{"role": "user", "content": "the-input"}], + "optional_params": {}, }, - "messages": [{"role": "user", "content": "the-input"}], - "optional_params": {}, - }, - response_obj=response_obj, - start_time=now, - end_time=now, - ) - return ( - logger.Langfuse.trace.call_args.kwargs, - logger.Langfuse.trace.return_value.generation.call_args.kwargs, - ) + response_obj=response_obj, + start_time=now, + end_time=now, + ) + logger.Langfuse.flush() + prefix = "langfuse.observation." + span = exporter.get_finished_spans()[-1] + generation_params = { + key[len(prefix) :]: value + for key, value in (span.attributes or {}).items() + if key.startswith(prefix) and not key.startswith(prefix + "metadata.") + } + return captured_trace_params, generation_params, span def test_mask_input_header_false_keeps_the_prompt(): - logger = _steering_logger() + rig = _steering_logger() - trace_params, generation_params = _emit(logger, headers={"langfuse_mask_input": "false"}) + trace_params, generation_params, _ = _emit(rig, headers={"langfuse_mask_input": "false"}) assert trace_params["input"] == {"messages": [{"role": "user", "content": "the-input"}]} - assert generation_params["input"] == {"messages": [{"role": "user", "content": "the-input"}]} + assert json.loads(generation_params["input"]) == {"messages": [{"role": "user", "content": "the-input"}]} def test_mask_input_header_true_redacts_the_prompt(): - logger = _steering_logger() + rig = _steering_logger() - trace_params, generation_params = _emit(logger, headers={"langfuse_mask_input": "true"}) + trace_params, generation_params, _ = _emit(rig, headers={"langfuse_mask_input": "true"}) assert trace_params["input"] == _LANGFUSE_REDACTED assert generation_params["input"] == _LANGFUSE_REDACTED def test_mask_output_header_false_keeps_the_completion(): - logger = _steering_logger() + rig = _steering_logger() - trace_params, generation_params = _emit(logger, headers={"langfuse_mask_output": "false"}) + trace_params, generation_params, _ = _emit(rig, headers={"langfuse_mask_output": "false"}) assert trace_params["output"] != _LANGFUSE_REDACTED assert generation_params["output"] != _LANGFUSE_REDACTED def test_mask_output_header_true_redacts_the_completion(): - logger = _steering_logger() + rig = _steering_logger() - trace_params, generation_params = _emit(logger, headers={"langfuse_mask_output": "true"}) + trace_params, generation_params, _ = _emit(rig, headers={"langfuse_mask_output": "true"}) assert trace_params["output"] == _LANGFUSE_REDACTED assert generation_params["output"] == _LANGFUSE_REDACTED @@ -1387,20 +1481,20 @@ def test_mask_output_header_true_redacts_the_completion(): ], ) def test_mask_input_from_the_request_body_is_unchanged(mask_input, expect_redacted): - logger = _steering_logger() + rig = _steering_logger() - trace_params, _ = _emit(logger, metadata={"mask_input": mask_input}) + trace_params, _, _ = _emit(rig, metadata={"mask_input": mask_input}) assert (trace_params["input"] == _LANGFUSE_REDACTED) is expect_redacted @pytest.mark.parametrize("flag", [True, "true"]) def test_update_trace_keys_header_applies_every_key_when_enabled(flag): - logger = _steering_logger() + rig = _steering_logger() with patch.object(litellm, "langfuse_enable_update_trace_keys", flag): - trace_params, _ = _emit( - logger, + trace_params, _, span = _emit( + rig, headers={ "langfuse_existing_trace_id": "trace-1", "langfuse_update_trace_keys": "trace_release, trace_tail", @@ -1411,6 +1505,9 @@ def test_update_trace_keys_header_applies_every_key_when_enabled(flag): assert trace_params["release"] == "v1.2.3" assert trace_params["tail"] == "last" + # v4 models release, so it reaches langfuse; a key it does not model cannot + assert span.attributes["langfuse.release"] == "v1.2.3" + assert not [key for key in span.attributes if key.endswith("tail")] def test_update_trace_keys_is_off_by_default(): @@ -1419,10 +1516,10 @@ def test_update_trace_keys_is_off_by_default(): user_api_key_auth and have the resolved auth object, including team callback credentials, serialized onto the trace. It stays inert until an operator opts in. """ - logger = _steering_logger() + rig = _steering_logger() - trace_params, _ = _emit( - logger, + trace_params, _, span = _emit( + rig, metadata={ "existing_trace_id": "trace-1", "update_trace_keys": ["user_api_key_auth", "trace_release"], @@ -1434,25 +1531,26 @@ def test_update_trace_keys_is_off_by_default(): assert "user_api_key_auth" not in trace_params assert "release" not in trace_params assert "sk-canary" not in json.dumps(trace_params, default=repr) + assert "sk-canary" not in json.dumps(dict(span.attributes or {}), default=repr) def test_update_trace_keys_input_and_output_are_gated_too(): - logger = _steering_logger() + rig = _steering_logger() - off, _ = _emit(logger, metadata={"existing_trace_id": "trace-1", "update_trace_keys": ["input", "output"]}) + off, _, _ = _emit(rig, metadata={"existing_trace_id": "trace-1", "update_trace_keys": ["input", "output"]}) with patch.object(litellm, "langfuse_enable_update_trace_keys", True): - on, _ = _emit(logger, metadata={"existing_trace_id": "trace-1", "update_trace_keys": ["input", "output"]}) + on, _, _ = _emit(rig, metadata={"existing_trace_id": "trace-1", "update_trace_keys": ["input", "output"]}) assert "input" not in off and "output" not in off assert "input" in on and "output" in on def test_update_trace_keys_from_the_request_body_list_applies_when_enabled(): - logger = _steering_logger() + rig = _steering_logger() with patch.object(litellm, "langfuse_enable_update_trace_keys", True): - trace_params, _ = _emit( - logger, + trace_params, _, span = _emit( + rig, metadata={ "existing_trace_id": "trace-1", "update_trace_keys": ["trace_release"], @@ -1461,13 +1559,14 @@ def test_update_trace_keys_from_the_request_body_list_applies_when_enabled(): ) assert trace_params["release"] == "v1.2.3" + assert span.attributes["langfuse.release"] == "v1.2.3" def test_update_trace_keys_matches_whole_keys_not_substrings(): - logger = _steering_logger() + rig = _steering_logger() - trace_params, _ = _emit( - logger, + trace_params, _, _ = _emit( + rig, headers={"langfuse_existing_trace_id": "trace-1", "langfuse_update_trace_keys": "my_input"}, ) @@ -1554,3 +1653,119 @@ def test_langfuse_deployment_environment_fallback_never_raises(monkeypatch, env_ langfuse_host="https://test.langfuse.com", ) assert logger.langfuse_environment == expected + + +def test_continued_trace_keeps_the_generation_version(): + """v2 set ``version`` on the generation even when the trace was not being updated.""" + rig = _steering_logger() + + captured_trace_params, _, span = _emit(rig, metadata={"existing_trace_id": "b" * 32, "version": "gen-7"}) + + assert "version" not in captured_trace_params + assert span.attributes["langfuse.version"] == "gen-7" + + +def test_new_trace_version_takes_precedence_over_the_generation_version(): + """v4 has one ``version`` for the trace and its root observation; ``trace_version`` wins as in v2.""" + rig = _steering_logger() + + captured_trace_params, _, span = _emit(rig, metadata={"trace_version": "trace-1", "version": "gen-7"}) + + assert captured_trace_params["version"] == "trace-1" + assert span.attributes["langfuse.version"] == "trace-1" + + +def test_log_event_returns_the_v2_dict_shape_for_the_alerting_trace_id_cache(): + """litellm_logging only caches the langfuse trace id off a dict with a ``trace_id`` key. + + Slack alerting builds its trace URL from that cache, so a different return + shape silently breaks alert links. + """ + rig = _steering_logger() + logger, _ = rig + + returned = logger.log_event_on_langfuse( + kwargs={ + "call_type": "completion", + "litellm_params": {"metadata": {"trace_id": "c" * 32}}, + "messages": [{"role": "user", "content": "the-input"}], + "optional_params": {}, + }, + response_obj=litellm.ModelResponse(choices=[{"message": {"role": "assistant", "content": "the-output"}}]), + start_time=datetime.datetime.now(), + end_time=datetime.datetime.now(), + ) + + assert isinstance(returned, dict) + assert returned["trace_id"] == "c" * 32 + assert returned["generation_id"] + + +def test_parse_langfuse_debug_only_enables_on_true_strings(): + """v4 treats any truthy value as debug=on, so the raw env string "false" would enable debug.""" + assert langfuse_module.parse_langfuse_debug("true") is True + assert langfuse_module.parse_langfuse_debug("True") is True + assert langfuse_module.parse_langfuse_debug("1") is True + assert langfuse_module.parse_langfuse_debug("false") is False + assert langfuse_module.parse_langfuse_debug("False") is False + assert langfuse_module.parse_langfuse_debug("") is False + assert langfuse_module.parse_langfuse_debug(None) is False + + +def test_langfuse_debug_env_string_false_stays_off(monkeypatch): + """LANGFUSE_DEBUG=false must not reach the v4 client as a truthy string. + + The v4 client does ``if debug:`` and then mutates root logging via + ``logging.basicConfig``, so the unparsed string "false" turns debug ON. + """ + from langfuse._client.resource_manager import LangfuseResourceManager + + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-debug-parse-test") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-debug-parse-test") + monkeypatch.setenv("LANGFUSE_MOCK", "true") + monkeypatch.setenv("LANGFUSE_DEBUG", "false") + monkeypatch.setattr(litellm, "initialized_langfuse_clients", litellm.initialized_langfuse_clients) + + logger = LangFuseLogger() + try: + assert logger.langfuse_debug is False + finally: + LangfuseResourceManager._instances.pop("pk-debug-parse-test", None) + + +def test_explicit_langfuse_host_beats_the_v4_base_url_env(monkeypatch): + """Per-key/per-team ``langfuse_host`` must win over LANGFUSE_BASE_URL. + + v4 resolves ``base_url or $LANGFUSE_BASE_URL or host``, so passing the + resolved host as ``host=`` lets a stray env var silently redirect every + tenant's traces to one server. + """ + from langfuse._client.resource_manager import LangfuseResourceManager + + monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "pk-base-url-test") + monkeypatch.setenv("LANGFUSE_SECRET_KEY", "sk-base-url-test") + monkeypatch.setenv("LANGFUSE_MOCK", "true") + monkeypatch.setenv("LANGFUSE_BASE_URL", "https://elsewhere.example") + monkeypatch.setattr(litellm, "initialized_langfuse_clients", litellm.initialized_langfuse_clients) + + logger = LangFuseLogger(langfuse_host="https://good.example") + try: + assert logger.Langfuse._base_url == "https://good.example" + finally: + LangfuseResourceManager._instances.pop("pk-base-url-test", None) + + +def test_resolve_credentials_falls_back_to_langfuse_base_url(monkeypatch): + """v4's canonical env var works when LANGFUSE_HOST is unset, but never beats it.""" + monkeypatch.setenv("LANGFUSE_BASE_URL", "https://from-base-url.example") + monkeypatch.delenv("LANGFUSE_HOST", raising=False) + + _, _, host = langfuse_module.resolve_langfuse_credentials() + assert host == "https://from-base-url.example" + + monkeypatch.setenv("LANGFUSE_HOST", "https://from-host.example") + _, _, host = langfuse_module.resolve_langfuse_credentials() + assert host == "https://from-host.example" + + _, _, host = langfuse_module.resolve_langfuse_credentials(langfuse_host="https://explicit.example") + assert host == "https://explicit.example" diff --git a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py index e3f71692c78..d40e6bf6ef7 100644 --- a/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py +++ b/tests/test_litellm/proxy/health_endpoints/test_health_endpoints.py @@ -2928,3 +2928,19 @@ def test_test_model_connection_accepts_image_edit_mode(monkeypatch): assert response.status_code == 200, response.text assert response.json()["status"] == "success" + + +@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") diff --git a/uv.lock b/uv.lock index 27be919eea1..5597903bed6 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-08-29T17:58:57.633306Z" +exclude-newer = "2026-08-29T07:41:48.685791Z" exclude-newer-span = "P3D" [manifest] @@ -4122,21 +4122,22 @@ wheels = [ [[package]] name = "langfuse" -version = "2.59.7" +version = "4.15.1" 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/c2/51/ed5569bc2dcc8fe767e3b315207a7511ad23d00d811f02bbf0d6f80bf906/langfuse-4.15.1.tar.gz", hash = "sha256:70cb47529a6ba78383c4f2a197eb3d3ab9d39529c9e6b568d392150c1f40dbb9", size = 432353, upload-time = "2026-08-28T07:55:15.894Z" } 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/2a/f9/7160bafcfe9797575359a9e77f810cf26f57f55a7c5ceda602fd9e353ddf/langfuse-4.15.1-py3-none-any.whl", hash = "sha256:795760693a62895157b6f5d6c80f1ad524fd12380995f06f82fc3a627b88c8d4", size = 789929, upload-time = "2026-08-28T07:55:14.034Z" }, ] [[package]] @@ -4266,7 +4267,7 @@ wheels = [ [[package]] name = "litellm" -version = "1.101.0" +version = "1.100.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -4526,7 +4527,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" }, @@ -4538,10 +4539,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" }, @@ -4607,7 +4608,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" }, @@ -4616,12 +4617,12 @@ dev = [ { name = "fakeredis", specifier = "==2.34.1" }, { name = "fastapi-offline", specifier = "==1.7.6" }, { 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" }, @@ -4659,22 +4660,22 @@ 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" }, ] [[package]] name = "litellm-enterprise" -version = "0.1.63" +version = "0.1.62" source = { editable = "enterprise" } [[package]] name = "litellm-proxy-extras" -version = "0.4.92" +version = "0.4.91" source = { editable = "litellm-proxy-extras" } [[package]] @@ -5791,45 +5792,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" }, @@ -5840,14 +5841,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" }, @@ -5858,14 +5859,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" }, @@ -5873,14 +5874,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" }, @@ -5888,14 +5889,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" }, @@ -5903,14 +5904,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" }, @@ -5919,14 +5920,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" }, @@ -5935,14 +5936,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" }, @@ -5950,14 +5951,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" }, @@ -5965,14 +5966,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" }, @@ -5981,14 +5982,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" }, @@ -5996,14 +5997,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" }, @@ -6011,14 +6012,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" }, @@ -6026,14 +6027,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" }, @@ -6041,14 +6042,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" }, @@ -6056,14 +6057,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" }, @@ -6072,27 +6073,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" }, @@ -6100,14 +6101,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" }, @@ -6115,14 +6116,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" }, @@ -6130,14 +6131,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" }, @@ -6145,14 +6146,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" }, @@ -6161,14 +6162,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" }, @@ -6176,14 +6177,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" }, @@ -6191,14 +6192,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" }, @@ -6206,14 +6207,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" }, @@ -6221,14 +6222,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" }, @@ -6236,14 +6237,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" }, @@ -6252,28 +6253,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" }, @@ -6281,14 +6282,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" }, @@ -6296,14 +6297,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" }, @@ -6312,14 +6313,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" }, @@ -6327,14 +6328,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" }, @@ -6342,14 +6343,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" }, @@ -6357,48 +6358,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]] @@ -6412,11 +6413,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]] @@ -9470,7 +9471,7 @@ wheels = [ [[package]] name = "traceloop-sdk" -version = "0.33.12" +version = "0.34.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -9516,9 +9517,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]]