mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(langfuse_otel): leave joined traces untouched by derived trace fields
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
98964dca87
commit
0f26ca9984
2 changed files with 60 additions and 17 deletions
|
|
@ -132,7 +132,7 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
return _extract_output_items(response_obj) or _extract_choices_output(response_obj)
|
||||
|
||||
@staticmethod
|
||||
def _trace_tags(kwargs: dict, metadata: dict) -> tuple[str, ...]:
|
||||
def _trace_tags(kwargs: dict, metadata: dict, derive_defaults: bool = True) -> tuple[str, ...]:
|
||||
"""Order-preserving dedupe of caller tags, request tags and langfuse_default_tags expansions."""
|
||||
import litellm
|
||||
|
||||
|
|
@ -155,15 +155,16 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
|
||||
expanded: Final = tuple(_default_tag(key) for key in default_tags) if isinstance(default_tags, list) else ()
|
||||
candidates: Final = (
|
||||
(
|
||||
(caller_tags,)
|
||||
if isinstance(caller_tags, str)
|
||||
else tuple(tag for tag in caller_tags if isinstance(tag, str))
|
||||
if isinstance(caller_tags, list)
|
||||
else ()
|
||||
)
|
||||
+ (tuple(tag for tag in request_tags if isinstance(tag, str)) if isinstance(request_tags, list) else ())
|
||||
(caller_tags,)
|
||||
if isinstance(caller_tags, str)
|
||||
else tuple(tag for tag in caller_tags if isinstance(tag, str))
|
||||
if isinstance(caller_tags, list)
|
||||
else ()
|
||||
) + (
|
||||
(tuple(tag for tag in request_tags if isinstance(tag, str)) if isinstance(request_tags, list) else ())
|
||||
+ tuple(tag for tag in expanded if tag is not None)
|
||||
if derive_defaults
|
||||
else ()
|
||||
)
|
||||
return tuple(dict.fromkeys(candidates))
|
||||
|
||||
|
|
@ -194,26 +195,29 @@ class LangfuseOtelLogger(OpenTelemetry):
|
|||
metadata: Final = LangfuseOtelLogger._extract_langfuse_metadata(kwargs)
|
||||
LangfuseOtelLogger._set_metadata_attributes(span=span, metadata=metadata)
|
||||
|
||||
if metadata.get("trace_name") is None and metadata.get("existing_trace_id") is None:
|
||||
joins_existing_trace: Final = metadata.get("existing_trace_id") is not None
|
||||
if metadata.get("trace_name") is None and not joins_existing_trace:
|
||||
safe_set_attribute(
|
||||
span,
|
||||
LangfuseSpanAttributes.TRACE_NAME.value,
|
||||
f"litellm-{kwargs.get('call_type') or 'completion'}",
|
||||
)
|
||||
|
||||
tags: Final = LangfuseOtelLogger._trace_tags(kwargs, metadata)
|
||||
tags: Final = LangfuseOtelLogger._trace_tags(kwargs, metadata, derive_defaults=not joins_existing_trace)
|
||||
if tags:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.TAGS.value, json.dumps(list(tags)))
|
||||
|
||||
input_json: Final = safe_dumps(kwargs.get("messages")) if kwargs.get("messages") else None
|
||||
if input_json is not None:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_INPUT.value, input_json)
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.TRACE_INPUT.value, input_json)
|
||||
if not joins_existing_trace:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.TRACE_INPUT.value, input_json)
|
||||
|
||||
output_json: Final = LangfuseOtelLogger._observation_output(response_obj)
|
||||
if output_json is not None:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.OBSERVATION_OUTPUT.value, output_json)
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.TRACE_OUTPUT.value, output_json)
|
||||
if not joins_existing_trace:
|
||||
safe_set_attribute(span, LangfuseSpanAttributes.TRACE_OUTPUT.value, output_json)
|
||||
|
||||
@staticmethod
|
||||
def _get_langfuse_otel_host() -> str | None:
|
||||
|
|
@ -430,7 +434,7 @@ def _extract_choices_output(response_obj) -> str | None:
|
|||
output_data: Final = {
|
||||
key: value
|
||||
for key, value in (
|
||||
("role", message.get("role")),
|
||||
("role", message.get("role") or None),
|
||||
("content", message.get("content")),
|
||||
)
|
||||
if value is not None
|
||||
|
|
|
|||
|
|
@ -1012,15 +1012,40 @@ class TestDerivedTraceFields:
|
|||
)
|
||||
assert attributes["langfuse.trace.name"] == "caller-trace"
|
||||
|
||||
def test_existing_trace_id_without_trace_name_emits_no_name(self):
|
||||
def test_existing_trace_id_leaves_derived_trace_fields_off(self, monkeypatch):
|
||||
import litellm
|
||||
from litellm.types.utils import Choices, ModelResponse
|
||||
|
||||
monkeypatch.setattr(litellm, "langfuse_default_tags", ["cache_hit"])
|
||||
response_obj = ModelResponse(
|
||||
id="chatcmpl-joined",
|
||||
model="gpt-4o",
|
||||
choices=[
|
||||
Choices(
|
||||
finish_reason="stop",
|
||||
message={"role": "assistant", "content": "hi"},
|
||||
)
|
||||
],
|
||||
)
|
||||
attributes = _emitted(
|
||||
{
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {"metadata": {"existing_trace_id": "abc123"}},
|
||||
}
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
"cache_hit": True,
|
||||
"litellm_params": {
|
||||
"metadata": {"existing_trace_id": "abc123", "tags": ["mine"]},
|
||||
},
|
||||
"standard_logging_object": {"request_tags": ["injected"]},
|
||||
},
|
||||
response_obj,
|
||||
)
|
||||
assert "langfuse.trace.name" not in attributes
|
||||
assert attributes["langfuse.trace.existing_id"] == "abc123"
|
||||
assert "langfuse.trace.input" not in attributes
|
||||
assert "langfuse.trace.output" not in attributes
|
||||
assert "langfuse.observation.input" in attributes
|
||||
assert "langfuse.observation.output" in attributes
|
||||
assert json.loads(attributes["langfuse.trace.tags"]) == ["mine"]
|
||||
|
||||
def test_responses_api_output_mirrors_observation_output_items(self):
|
||||
from openai.types.responses import ResponseFunctionToolCall
|
||||
|
|
@ -1098,6 +1123,20 @@ class TestDerivedTraceFields:
|
|||
)
|
||||
assert "langfuse.trace.tags" not in attributes
|
||||
|
||||
def test_empty_string_role_is_omitted_from_output(self):
|
||||
response_obj = {
|
||||
"id": "chatcmpl-norole",
|
||||
"choices": [{"message": {"role": "", "content": "hi"}}],
|
||||
}
|
||||
attributes = _emitted(
|
||||
{
|
||||
"call_type": "acompletion",
|
||||
"litellm_params": {"metadata": {}},
|
||||
},
|
||||
response_obj,
|
||||
)
|
||||
assert json.loads(attributes["langfuse.observation.output"]) == {"content": "hi"}
|
||||
|
||||
def test_no_messages_leaves_trace_input_unset_but_emits_output(self):
|
||||
from litellm.types.utils import Choices, ModelResponse
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue