mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
fix(langfuse): do not claim trace root when continuing an existing trace
Langfuse derives a trace's name and I/O from any observation flagged langfuse.internal.as_root, so a request carrying existing_trace_id renamed the trace to the generation name and replaced the trace input and output on every continuation. v2 only updated the keys listed in update_trace_keys. Continuations now export as plain children of the remote parent and keep the explicit langfuse.trace.* attributes for the fields they do want changed. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
4cfed77d9a
commit
35e5a08f57
3 changed files with 30 additions and 1 deletions
|
|
@ -976,6 +976,7 @@ class LangFuseLogger:
|
|||
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
|
||||
existing_trace=existing_trace_id is not None,
|
||||
)
|
||||
log_provider_specific_information_as_span(
|
||||
client=self.Langfuse,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ def open_trace_context(
|
|||
client: Langfuse,
|
||||
trace_id: str,
|
||||
parent_observation_id: str | None,
|
||||
existing_trace: bool = False,
|
||||
) -> tuple[Context, bool]:
|
||||
"""Build the OTel context that places new observations inside ``trace_id``.
|
||||
|
||||
|
|
@ -96,11 +97,16 @@ def open_trace_context(
|
|||
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.
|
||||
|
||||
``existing_trace`` is the v2 ``existing_trace_id`` contract: the trace is
|
||||
appended to, never rewritten. The server takes a root observation's name and
|
||||
I/O as the trace's, so a continuation must not claim root; trace fields it
|
||||
does want changed travel as explicit ``langfuse.trace.*`` attributes.
|
||||
"""
|
||||
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
|
||||
return otel_trace.set_span_in_context(remote_parent), parent_observation_id is None and not existing_trace
|
||||
|
||||
|
||||
def start_generation(
|
||||
|
|
|
|||
|
|
@ -1923,6 +1923,28 @@ def test_update_trace_keys_input_output_reach_the_trace_even_under_a_parent():
|
|||
assert "the-output" in str(span.attributes["langfuse.trace.output"])
|
||||
|
||||
|
||||
def test_existing_trace_id_appends_without_claiming_trace_root():
|
||||
"""Langfuse copies a root observation's name and I/O onto the trace, so a
|
||||
continuation that claimed root would rename the trace after every request;
|
||||
v2 only ever touched the keys in ``update_trace_keys``."""
|
||||
rig = _steering_logger()
|
||||
|
||||
_, _, span = _emit(rig, metadata={"existing_trace_id": "trace-1", "trace_name": "second-call"})
|
||||
|
||||
assert span.parent is not None
|
||||
assert span.attributes.get("langfuse.internal.as_root") is None
|
||||
assert "langfuse.trace.name" not in (span.attributes or {})
|
||||
|
||||
|
||||
def test_a_fresh_trace_still_claims_root_so_its_generation_names_it():
|
||||
rig = _steering_logger()
|
||||
|
||||
_, _, span = _emit(rig, metadata={"trace_id": "a" * 32, "trace_name": "first-call"})
|
||||
|
||||
assert span.attributes.get("langfuse.internal.as_root") is True
|
||||
assert span.attributes["langfuse.trace.name"] == "first-call"
|
||||
|
||||
|
||||
def test_trace_io_is_not_stamped_when_update_trace_keys_does_not_ask():
|
||||
rig = _steering_logger()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue