mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(langfuse): emit otel trace version and release on the keys langfuse v4 reads (#36702)
* fix(langfuse): emit otel trace version and release on the keys langfuse v4 reads The langfuse_otel exporter wrote version to langfuse.generation.version and langfuse.trace.version, and release to langfuse.trace.release. Langfuse v4 recognizes neither, so both landed in the generic span attribute bag and every trace reported version and release as null. v4 has a single langfuse.version key, lifted to the trace when it sits on the root span, plus langfuse.release. Also routes the otel v2 preset's per-request headers through the shared builder so key-scoped and team-scoped exports carry x-langfuse-ingestion-version like the other three exporter paths already do. * fix(langfuse): give trace_version precedence over version on the shared v4 key Matches the documented contract in docs/observability/langfuse_integration.md and the legacy langfuse SDK callback, which both treat trace_version as the authoritative trace version with version as its fallback.
This commit is contained in:
parent
c7f5527870
commit
d86336a7c6
5 changed files with 72 additions and 14 deletions
|
|
@ -90,7 +90,6 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
"generation_name": LangfuseSpanAttributes.GENERATION_NAME,
|
||||
"generation_id": LangfuseSpanAttributes.GENERATION_ID,
|
||||
"parent_observation_id": LangfuseSpanAttributes.PARENT_OBSERVATION_ID,
|
||||
"version": LangfuseSpanAttributes.GENERATION_VERSION,
|
||||
"mask_input": LangfuseSpanAttributes.MASK_INPUT,
|
||||
"mask_output": LangfuseSpanAttributes.MASK_OUTPUT,
|
||||
"trace_user_id": LangfuseSpanAttributes.TRACE_USER_ID,
|
||||
|
|
@ -99,13 +98,18 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
"trace_name": LangfuseSpanAttributes.TRACE_NAME,
|
||||
"trace_id": LangfuseSpanAttributes.TRACE_ID,
|
||||
"trace_metadata": LangfuseSpanAttributes.TRACE_METADATA,
|
||||
"trace_version": LangfuseSpanAttributes.TRACE_VERSION,
|
||||
"trace_release": LangfuseSpanAttributes.TRACE_RELEASE,
|
||||
"trace_release": LangfuseSpanAttributes.RELEASE,
|
||||
"existing_trace_id": LangfuseSpanAttributes.EXISTING_TRACE_ID,
|
||||
"update_trace_keys": LangfuseSpanAttributes.UPDATE_TRACE_KEYS,
|
||||
"debug_langfuse": LangfuseSpanAttributes.DEBUG_LANGFUSE,
|
||||
}
|
||||
|
||||
version: Final = (
|
||||
metadata.get("trace_version") if metadata.get("trace_version") is not None else metadata.get("version")
|
||||
)
|
||||
if version is not None:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.VERSION.value, version)
|
||||
|
||||
for key, enum_attr in mapping.items():
|
||||
if key in metadata and metadata[key] is not None:
|
||||
value = metadata[key]
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ def langfuse_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str,
|
|||
public_key: Final = params.get("langfuse_public_key")
|
||||
secret_key: Final = params.get("langfuse_secret_key")
|
||||
if public_key and secret_key:
|
||||
return {
|
||||
"Authorization": _V1Langfuse._get_langfuse_authorization_header(
|
||||
public_key=public_key, secret_key=secret_key
|
||||
)
|
||||
}
|
||||
return _V1Langfuse._build_langfuse_otel_headers(
|
||||
_V1Langfuse._get_langfuse_authorization_header(public_key=public_key, secret_key=secret_key)
|
||||
)
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ class LangfuseOtelConfig(BaseModel):
|
|||
|
||||
class LangfuseSpanAttributes(str, Enum):
|
||||
LANGFUSE_ENVIRONMENT = "langfuse.environment"
|
||||
VERSION = "langfuse.version"
|
||||
RELEASE = "langfuse.release"
|
||||
|
||||
# ---- Generation-level metadata ----
|
||||
GENERATION_NAME = "langfuse.generation.name"
|
||||
GENERATION_ID = "langfuse.generation.id"
|
||||
PARENT_OBSERVATION_ID = "langfuse.generation.parent_observation_id"
|
||||
GENERATION_VERSION = "langfuse.generation.version"
|
||||
MASK_INPUT = "langfuse.generation.mask_input"
|
||||
MASK_OUTPUT = "langfuse.generation.mask_output"
|
||||
|
||||
|
|
@ -36,8 +37,6 @@ class LangfuseSpanAttributes(str, Enum):
|
|||
TRACE_NAME = "langfuse.trace.name"
|
||||
TRACE_ID = "langfuse.trace.id"
|
||||
TRACE_METADATA = "langfuse.trace.metadata"
|
||||
TRACE_VERSION = "langfuse.trace.version"
|
||||
TRACE_RELEASE = "langfuse.trace.release"
|
||||
EXISTING_TRACE_ID = "langfuse.trace.existing_id"
|
||||
UPDATE_TRACE_KEYS = "langfuse.trace.update_keys"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Per-request multi-tenant credential routing (V1 parity)."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
@ -42,6 +43,17 @@ def test_langfuse_dynamic_headers_need_both_keys():
|
|||
assert headers is not None and "Authorization" in headers
|
||||
|
||||
|
||||
def test_langfuse_dynamic_headers_carry_v4_ingestion_version():
|
||||
headers = dynamic_otlp_headers(
|
||||
"langfuse_otel", {"langfuse_public_key": "pk", "langfuse_secret_key": "sk"}
|
||||
)
|
||||
expected_auth = "Basic " + base64.b64encode(b"pk:sk").decode()
|
||||
assert headers == {
|
||||
"Authorization": expected_auth,
|
||||
"x-langfuse-ingestion-version": "4",
|
||||
}
|
||||
|
||||
|
||||
def test_weave_dynamic_headers():
|
||||
headers = dynamic_otlp_headers(
|
||||
"weave_otel", {"wandb_api_key": "w", "weave_project_id": "p"}
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ class TestLangfuseOtelIntegration:
|
|||
LangfuseSpanAttributes.GENERATION_NAME.value: "gen-name",
|
||||
LangfuseSpanAttributes.GENERATION_ID.value: "gen-id",
|
||||
LangfuseSpanAttributes.PARENT_OBSERVATION_ID.value: "parent-id",
|
||||
LangfuseSpanAttributes.GENERATION_VERSION.value: "v1",
|
||||
LangfuseSpanAttributes.VERSION.value: "t-ver",
|
||||
LangfuseSpanAttributes.MASK_INPUT.value: True,
|
||||
LangfuseSpanAttributes.MASK_OUTPUT.value: False,
|
||||
LangfuseSpanAttributes.TRACE_USER_ID.value: "user-123",
|
||||
|
|
@ -221,8 +221,7 @@ class TestLangfuseOtelIntegration:
|
|||
LangfuseSpanAttributes.TRACE_NAME.value: "trace-name",
|
||||
LangfuseSpanAttributes.TRACE_ID.value: "traceid", # stripped dashes
|
||||
LangfuseSpanAttributes.TRACE_METADATA.value: json.dumps({"k": "v"}),
|
||||
LangfuseSpanAttributes.TRACE_VERSION.value: "t-ver",
|
||||
LangfuseSpanAttributes.TRACE_RELEASE.value: "rel-1",
|
||||
LangfuseSpanAttributes.RELEASE.value: "rel-1",
|
||||
LangfuseSpanAttributes.EXISTING_TRACE_ID.value: "existing-id",
|
||||
LangfuseSpanAttributes.UPDATE_TRACE_KEYS.value: json.dumps(
|
||||
["key1", "key2"]
|
||||
|
|
@ -240,6 +239,52 @@ class TestLangfuseOtelIntegration:
|
|||
actual == expected
|
||||
), "Mismatch between expected and actual OTEL attribute mapping."
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"metadata, expected_version",
|
||||
[
|
||||
(
|
||||
{"version": "v-observation", "trace_version": "v-trace"},
|
||||
"v-trace",
|
||||
),
|
||||
({"trace_version": "v-trace"}, "v-trace"),
|
||||
({"version": "v-observation"}, "v-observation"),
|
||||
({"version": "v-observation", "trace_version": ""}, ""),
|
||||
({}, None),
|
||||
],
|
||||
ids=[
|
||||
"trace-version-wins-as-documented",
|
||||
"trace-only",
|
||||
"observation-version-is-the-fallback",
|
||||
"empty-trace-version-is-not-absent",
|
||||
"neither-key-emits-nothing",
|
||||
],
|
||||
)
|
||||
def test_version_emitted_on_langfuse_v4_key(self, metadata, expected_version):
|
||||
kwargs = {"litellm_params": {"metadata": {"trace_release": "rel-9", **metadata}}}
|
||||
|
||||
with patch(
|
||||
"litellm.integrations.arize._utils.safe_set_attribute"
|
||||
) as mock_safe_set_attribute:
|
||||
LangfuseOtelLogger._set_langfuse_specific_attributes(
|
||||
MagicMock(), kwargs, None
|
||||
)
|
||||
|
||||
emitted = {
|
||||
call.args[1]: call.args[2] for call in mock_safe_set_attribute.call_args_list
|
||||
}
|
||||
|
||||
if expected_version is None:
|
||||
assert "langfuse.version" not in emitted
|
||||
else:
|
||||
assert emitted["langfuse.version"] == expected_version
|
||||
assert emitted["langfuse.release"] == "rel-9"
|
||||
for retired_key in (
|
||||
"langfuse.generation.version",
|
||||
"langfuse.trace.version",
|
||||
"langfuse.trace.release",
|
||||
):
|
||||
assert retired_key not in emitted
|
||||
|
||||
def test_set_langfuse_specific_attributes_with_content(self):
|
||||
"""Test that _set_langfuse_specific_attributes correctly sets observation.output with regular content response."""
|
||||
from litellm.types.integrations.langfuse_otel import LangfuseSpanAttributes
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue