fix(langfuse): emit litellm_call_id and response_id as generation metadata

v2 put the provider response id inside the generation id. v4 observation ids are 16 hex chars derived from that string, so the ids move to generation metadata to keep generations searchable by response id

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-15 08:06:43 +00:00
parent 765667444c
commit 79ec58ad33
2 changed files with 50 additions and 0 deletions

View file

@ -143,6 +143,17 @@ def _logging_id(start_time: datetime | None, response_obj: object) -> str | None
return litellm.utils.get_logging_id(start_time, response_obj)
def _lookup_ids(litellm_call_id: str | None, response_obj: object) -> Mapping[str, str]:
"""v2 carried the response id inside the generation id; v4 hashes ids to 16 hex chars, so they ride in metadata."""
response_id: Final[object] = (
cast(Mapping[str, object], response_obj).get("id")
if isinstance(response_obj, Mapping)
else getattr(response_obj, "id", None)
)
ids: Final[tuple[tuple[str, object], ...]] = (("litellm_call_id", litellm_call_id), ("response_id", response_id))
return MappingProxyType({key: str(value) for key, value in ids if value is not None})
def _as_steering_flag(value: object) -> bool:
"""A string ``str_to_bool`` does not recognise falls back to its truthiness."""
if isinstance(value, str):
@ -923,6 +934,7 @@ class LangFuseLogger:
**(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,
**_lookup_ids(litellm_call_id, response_obj),
},
"level": level,
"version": _optional_str(clean_metadata.pop("version", None)),

View file

@ -687,6 +687,44 @@ class TestLangfuseUsageDetails(unittest.TestCase):
assert generation_metadata["litellm_response_cost"] == 0.25
assert generation_metadata["api_base"] == "https://real-api-base"
def test_generation_metadata_carries_the_call_id_and_response_id(self):
"""
v2's generation id was ``time-<hh-mm-ss-us>_<response id>``, so a generation could
be found from the provider response id. v4 hashes that string onto 16 hex chars,
which leaves nothing searchable unless both ids are emitted as metadata.
"""
payload = self._build_standard_logging_payload(trace_id="canary-trace-id")
kwargs = {**self._build_langfuse_kwargs(payload), "response_cost": 0.25}
metadata = self._canary_request_metadata()
self.use_real_langfuse_client()
with patch(
"litellm.integrations.langfuse.langfuse._add_prompt_to_generation_params",
side_effect=lambda generation_params, **kw: generation_params,
create=True,
):
self.logger._log_langfuse_v2(
user_id="user-1",
metadata=metadata,
litellm_params={"metadata": metadata},
output=None,
start_time=datetime.datetime(2024, 1, 1, 12, 0, 0),
end_time=datetime.datetime(2024, 1, 1, 12, 0, 1),
kwargs=kwargs,
optional_params={},
input=None,
response_obj=litellm.ModelResponse(
id="chatcmpl-canary-response", choices=[{"message": {"role": "assistant", "content": "OK"}}]
),
level="DEFAULT",
litellm_call_id="canary-call-id",
)
generation_metadata = self.exported_generation_metadata()
assert generation_metadata["litellm_call_id"] == "canary-call-id"
assert generation_metadata["response_id"] == "chatcmpl-canary-response"
assert "chatcmpl-canary-response" in self._emitted_payload_text()
def test_denied_steering_keys_and_enrichments(self):
"""
endpoint is a plain string, so without the deny-list it would ride the