This commit is contained in:
VeeraBabu Jakkula 2026-10-30 20:15:09 +05:30 • committed by GitHub
commit 01aaf55168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 170 additions and 2 deletions

View file

@ -50,6 +50,15 @@ def _identity_field(name: str) -> Callable[[LLMCallSpanData], AttrValue | None]:
return lambda d: d.request_metadata.get(name)
# Per-request trace/release metadata Langfuse v4 reads off the span. ``langfuse.environment``
# is the documented observation- and trace-level environment key (takes precedence over
# ``deployment.environment``); ``langfuse.release`` / ``langfuse.version`` map to the trace
# release and observation version respectively.
LANGFUSE_ENVIRONMENT: Final = "langfuse.environment"
LANGFUSE_RELEASE: Final = "langfuse.release"
LANGFUSE_VERSION: Final = "langfuse.version"
class LangfuseMapper:
_LLM_CALL_ATTRS: dict[str, Callable[[LLMCallSpanData], AttrValue | None]] = {
"langfuse.observation.type": lambda _: "generation",
@ -130,6 +139,9 @@ class LangfuseMapper:
(LANGFUSE_TRACE_USER_ID, trace.user_id or None),
(LANGFUSE_TRACE_SESSION_ID, trace.session_id or None),
(LANGFUSE_TRACE_TAGS, trace.tags or None),
(LANGFUSE_ENVIRONMENT, trace.environment or None),
(LANGFUSE_RELEASE, trace.release or None),
(LANGFUSE_VERSION, trace.version or None),
)
)

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import os
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Final
@ -13,18 +14,30 @@ from litellm.integrations.otel.model.utils import as_str, as_str_mapping
LANGFUSE_HEADER_PREFIX: Final = "langfuse_"
_ITEMS: Final = TypeAdapter(tuple[object, ...])
# Deployment-wide tracing environment fallback, mirroring the legacy
# ``langfuse_otel`` logger and the ``LANGFUSE_TRACING_ENVIRONMENT`` env var the
# proxy sets per-stage. Read at call time so per-request overrides win.
LANGFUSE_TRACING_ENVIRONMENT: Final = "LANGFUSE_TRACING_ENVIRONMENT"
@dataclass(frozen=True, slots=True)
class TraceControls:
"""The caller's trace-level Langfuse controls: ``metadata.trace_name`` / ``trace_user_id`` / ``session_id`` /
``tags`` on the request (SDK or proxy body), with the proxy's ``langfuse_<control>`` headers winning over the
body for the scalar ones. Mutation controls (``trace_id``, ``existing_trace_id``, ``update_trace_keys``) are
deliberately not carried."""
deliberately not carried.
``environment`` / ``release`` / ``version`` let a caller tag each trace with its own deployment
stage and release identifiers. ``environment`` falls back to the gateway's ``LANGFUSE_TRACING_ENVIRONMENT``
when the request does not set one, preserving the per-stage behaviour the legacy logger provided."""
name: str | None = None
user_id: str | None = None
session_id: str | None = None
tags: tuple[str, ...] = ()
environment: str | None = None
release: str | None = None
version: str | None = None
def caller_trace_controls(kwargs: Mapping[str, object]) -> TraceControls:
@ -45,11 +58,36 @@ def caller_trace_controls(kwargs: Mapping[str, object]) -> TraceControls:
return from_header
return next((value for body in bodies if (value := as_str(body.get(control)))), None)
def body_scalar(*controls: str) -> str | None:
"""First non-empty value across the request metadata bodies for any of ``controls``."""
for control in controls:
value = next((v for body in bodies if (v := as_str(body.get(control)))), None)
if value:
return value
return None
def header_scalar(*controls: str) -> str | None:
"""First non-empty ``langfuse_<control>`` request header for any of ``controls``."""
if headers is None:
return None
for control in controls:
value = as_str(headers.get(f"{LANGFUSE_HEADER_PREFIX}{control}"))
if value:
return value
return None
return TraceControls(
name=scalar("trace_name"),
user_id=scalar("trace_user_id"),
session_id=scalar("session_id"),
tags=next((tags for body in bodies if (tags := _str_items(body.get("tags")))), ()),
environment=(
header_scalar("environment")
or body_scalar("langfuse_environment")
or os.environ.get(LANGFUSE_TRACING_ENVIRONMENT)
),
release=header_scalar("release") or body_scalar("trace_release", "release"),
version=header_scalar("version") or body_scalar("trace_version", "version"),
)

View file

@ -508,6 +508,21 @@ def _check_banned_params(
# Proxy-wide opt-in: every banned param is permitted, exit
# entirely so the rest of the loop doesn't waste work.
return
if (
_is_param_allowed(
param=param,
request_body_value=body[param],
configurable_clientside_auth_params=general_settings.get("configurable_clientside_auth_params"),
)
is True
):
# Proxy-wide SELECTIVE opt-in: only the params the admin named in
# ``general_settings.configurable_clientside_auth_params`` are
# permitted, for EVERY model. Unlike ``allow_client_side_credentials``
# (which lifts the whole blocklist), this admits only the listed
# params, so a client that may set ``langfuse_environment`` still
# cannot smuggle ``api_base``/``aws_web_identity_token``/etc.
continue
if (
_allow_model_level_clientside_configurable_parameters(
model=model,

View file

@ -1359,6 +1359,56 @@ def test_caller_trace_controls_carry_user_session_and_tags(request_data, expecte
assert LLMCallEvent.from_dict({"litellm_params": request_data}).trace == expected
def test_caller_trace_controls_carry_environment_release_and_version(monkeypatch):
"""Caller-supplied environment, release, and version are mapped from headers or request metadata,
falling back to LANGFUSE_TRACING_ENVIRONMENT env var."""
# 1. Header precedence over metadata
from_headers: Final = caller_trace_controls(
{
"litellm_params": {
"proxy_server_request": {
"headers": {
"langfuse_environment": "prod-env",
"langfuse_release": "rel-v2",
"langfuse_version": "ver-3",
}
},
"metadata": {
"langfuse_environment": "staging-env",
"trace_release": "rel-v1",
"trace_version": "ver-2",
},
}
}
)
assert from_headers.environment == "prod-env"
assert from_headers.release == "rel-v2"
assert from_headers.version == "ver-3"
# 2. Metadata body precedence over env var fallback
monkeypatch.setenv("LANGFUSE_TRACING_ENVIRONMENT", "env-fallback")
from_body: Final = caller_trace_controls(
{
"litellm_params": {
"metadata": {
"langfuse_environment": "meta-env",
"trace_release": "rel-v1",
"trace_version": "ver-1",
}
}
}
)
assert from_body.environment == "meta-env"
assert from_body.release == "rel-v1"
assert from_body.version == "ver-1"
# 3. Environment variable fallback
from_env: Final = caller_trace_controls({"litellm_params": {}})
assert from_env.environment == "env-fallback"
assert from_env.release is None
assert from_env.version is None
def test_llm_span_data_carries_the_caller_trace_controls():
controls: Final = TraceControls(name="nightly-eval", user_id="u1", session_id="s1", tags=("a", "b"))
data: Final = LLMCallSpanData.from_standard_logging_payload(_sample_payload(), trace=controls)

View file

@ -628,7 +628,32 @@ def test_langfuse_mapper_carries_the_caller_user_session_and_tags():
def test_langfuse_mapper_omits_unset_trace_controls():
attrs = LangfuseMapper().map(_llm_call(trace=TraceControls(user_id="", session_id=None, tags=())))
assert {"user.id", "session.id", "langfuse.trace.tags", "langfuse.trace.name"}.isdisjoint(attrs)
assert {
"user.id",
"session.id",
"langfuse.trace.tags",
"langfuse.trace.name",
"langfuse.environment",
"langfuse.release",
"langfuse.version",
}.isdisjoint(attrs)
def test_langfuse_mapper_carries_environment_release_and_version():
controls: Final = TraceControls(
environment="staging",
release="commit-abc123",
version="sha256-deadbeef",
)
attrs: Final = LangfuseMapper().map(_llm_call(trace=controls))
assert attrs["langfuse.environment"] == "staging"
assert attrs["langfuse.release"] == "commit-abc123"
assert attrs["langfuse.version"] == "sha256-deadbeef"
root: Final = LangfuseMapper.trace_attributes(controls)
assert root["langfuse.environment"] == "staging"
assert root["langfuse.release"] == "commit-abc123"
assert root["langfuse.version"] == "sha256-deadbeef"
def test_langfuse_trace_attributes_match_between_root_and_generation():

View file

@ -4471,6 +4471,34 @@ def test_model_level_allow_does_not_skip_subsequent_banned_params(monkeypatch):
assert "langfuse_host" in str(exc.value)
def test_proxy_wide_selective_configurable_clientside_auth_params():
"""Proxy-wide selective opt-in via general_settings.configurable_clientside_auth_params admits
named params for every model without opening unlisted banned params."""
# When langfuse_environment is admitted proxy-wide:
is_request_body_safe(
request_body={
"model": "gpt-4",
"langfuse_environment": "staging",
},
general_settings={"configurable_clientside_auth_params": ["langfuse_environment"]},
llm_router=None,
model="gpt-4",
)
# But api_base remains banned:
with pytest.raises(ValueError, match="Rejected Request: api_base is not allowed in request body"):
is_request_body_safe(
request_body={
"model": "gpt-4",
"langfuse_environment": "staging",
"api_base": "https://attacker.example",
},
general_settings={"configurable_clientside_auth_params": ["langfuse_environment"]},
llm_router=None,
model="gpt-4",
)
def test_observability_ban_covers_canonical_supported_callback_params():
"""Guard test: every entry in the canonical
``_supported_callback_params`` allow-list must end up either banned by