From 3b5fb79ef03b17633e356e5ba37dd7b65dd0f7d7 Mon Sep 17 00:00:00 2001 From: arpan sahu <28574248+arpansahu@users.noreply.github.com> Date: Thu, 27 Aug 2026 06:46:06 +0530 Subject: [PATCH] fix(langchain): handle empty llm generations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: arpan sahu <28574248+arpansahu@users.noreply.github.com> --- langfuse/langchain/CallbackHandler.py | 21 ++++++++++----- tests/unit/test_langchain.py | 38 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/langfuse/langchain/CallbackHandler.py b/langfuse/langchain/CallbackHandler.py index 43e93b385..1c019142c 100644 --- a/langfuse/langchain/CallbackHandler.py +++ b/langfuse/langchain/CallbackHandler.py @@ -1327,12 +1327,7 @@ def on_llm_end( self._log_debug_event( "on_llm_end", run_id, parent_run_id, response=response, kwargs=kwargs ) - response_generation = response.generations[-1][-1] - extracted_response = ( - self._convert_message_to_dict(response_generation.message) - if isinstance(response_generation, ChatGeneration) - else _extract_raw_response(response_generation) - ) + extracted_response = self._extract_llm_result_response(response) llm_usage = _parse_usage(response) @@ -1501,6 +1496,20 @@ def _log_debug_event( f"Event: {event_name}, run_id: {run_id}, parent_run_id: {parent_run_id}" ) + def _extract_llm_result_response(self, response: LLMResult) -> Any: + for generation in reversed(response.generations): + if not generation: + continue + + response_generation = generation[-1] + return ( + self._convert_message_to_dict(response_generation.message) + if isinstance(response_generation, ChatGeneration) + else _extract_raw_response(response_generation) + ) + + return None + def _extract_raw_response(last_response: Any) -> Any: """Extract the response from the last response of the LLM call.""" diff --git a/tests/unit/test_langchain.py b/tests/unit/test_langchain.py index 0e63fb6b1..ceb694b12 100644 --- a/tests/unit/test_langchain.py +++ b/tests/unit/test_langchain.py @@ -132,6 +132,44 @@ def test_llm_callback_exports_generation_span(langfuse_memory_client, get_span): ) +def test_llm_callback_ends_generation_with_empty_generations( + langfuse_memory_client, get_span, json_attr +): + handler = CallbackHandler() + run_id = uuid4() + response = LLMResult( + generations=[[]], + llm_output={ + "token_usage": { + "prompt_tokens": 2, + "completion_tokens": 0, + "total_tokens": 2, + }, + "model_name": "empty-model", + }, + ) + + handler.on_llm_start( + {"name": "EmptyLLM"}, + ["hello"], + run_id=run_id, + invocation_params={"model_name": "empty-model"}, + ) + handler.on_llm_end(response, run_id=run_id) + + langfuse_memory_client.flush() + span = get_span("EmptyLLM") + + assert ( + span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_MODEL] == "empty-model" + ) + assert json_attr(span, LangfuseOtelSpanAttributes.OBSERVATION_USAGE_DETAILS) == { + "prompt_tokens": 2, + "completion_tokens": 0, + "total_tokens": 2, + } + + def test_lcel_chain_exports_intermediate_chain_spans( langfuse_memory_client, get_span, find_spans ):