From a4d59a92e9f86303b8f7b6ea75a7bb889477243d Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 23 Oct 2025 02:44:33 +0800 Subject: [PATCH 1/4] Fix #1559: Handle empty choices array in LiteLLM model Add defensive checks before accessing response.choices[0] to prevent IndexError when Gemini or other providers return an empty choices array. This follows the same pattern as PR #935 which fixed the identical issue in openai_chatcompletions.py. Changes: - Add null checks for response.choices before array access - Return empty output when choices array is empty - Preserve usage information even when choices is empty - Add appropriate type annotations for litellm types --- src/agents/extensions/models/litellm_model.py | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 7d02e2194..849f88da2 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -110,18 +110,27 @@ async def get_response( prompt=prompt, ) - assert isinstance(response.choices[0], litellm.types.utils.Choices) + message: litellm.types.utils.Message | None = None + first_choice: litellm.types.utils.Choices | litellm.types.utils.StreamingChoices | None = ( + None + ) + if response.choices and len(response.choices) > 0: + first_choice = response.choices[0] + assert isinstance(first_choice, litellm.types.utils.Choices) + message = first_choice.message if _debug.DONT_LOG_MODEL_DATA: logger.debug("Received model response") else: - logger.debug( - f"""LLM resp:\n{ - json.dumps( - response.choices[0].message.model_dump(), indent=2, ensure_ascii=False - ) - }\n""" - ) + if message is not None: + logger.debug( + f"""LLM resp:\n{ + json.dumps(message.model_dump(), indent=2, ensure_ascii=False) + }\n""" + ) + else: + finish_reason = first_choice.finish_reason if first_choice else "-" + logger.debug(f"LLM resp had no message. finish_reason: {finish_reason}") if hasattr(response, "usage"): response_usage = response.usage @@ -151,16 +160,18 @@ async def get_response( usage = Usage() logger.warning("No usage information returned from Litellm") - if tracing.include_data(): - span_generation.span_data.output = [response.choices[0].message.model_dump()] + if tracing.include_data() and message is not None: + span_generation.span_data.output = [message.model_dump()] span_generation.span_data.usage = { "input_tokens": usage.input_tokens, "output_tokens": usage.output_tokens, } - items = Converter.message_to_output_items( - LitellmConverter.convert_message_to_openai(response.choices[0].message) - ) + items = [] + if message is not None: + items = Converter.message_to_output_items( + LitellmConverter.convert_message_to_openai(message) + ) return ModelResponse( output=items, From 6aca0aadf409a20a67fb94a2a283696aefc6c5b1 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 23 Oct 2025 02:48:14 +0800 Subject: [PATCH 2/4] Fix linting error: line too long --- src/agents/extensions/models/litellm_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 849f88da2..d23a6784c 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -111,9 +111,9 @@ async def get_response( ) message: litellm.types.utils.Message | None = None - first_choice: litellm.types.utils.Choices | litellm.types.utils.StreamingChoices | None = ( - None - ) + first_choice: ( + litellm.types.utils.Choices | litellm.types.utils.StreamingChoices | None + ) = None if response.choices and len(response.choices) > 0: first_choice = response.choices[0] assert isinstance(first_choice, litellm.types.utils.Choices) From a8e2c785b293ca2c20665188a2fa3911e4af2df0 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Thu, 23 Oct 2025 02:50:47 +0800 Subject: [PATCH 3/4] Refactor: simplify items initialization with ternary expression Address Copilot suggestion to remove redundant initialization by using a ternary expression instead of if-else block. --- src/agents/extensions/models/litellm_model.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index d23a6784c..7d5c87455 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -167,11 +167,13 @@ async def get_response( "output_tokens": usage.output_tokens, } - items = [] - if message is not None: - items = Converter.message_to_output_items( + items = ( + Converter.message_to_output_items( LitellmConverter.convert_message_to_openai(message) ) + if message is not None + else [] + ) return ModelResponse( output=items, From a8c8b0339923b00172e25da222e12329d84475b5 Mon Sep 17 00:00:00 2001 From: Lucas Wang Date: Fri, 24 Oct 2025 09:39:00 +0800 Subject: [PATCH 4/4] Refine defensive checks based on code review feedback - Remove StreamingChoices from type annotation since this is non-streaming - Remove redundant type assertion as LiteLLM validates types - Simplify tracing output with ternary expression --- src/agents/extensions/models/litellm_model.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index 7d5c87455..6389b38b2 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -111,13 +111,12 @@ async def get_response( ) message: litellm.types.utils.Message | None = None - first_choice: ( - litellm.types.utils.Choices | litellm.types.utils.StreamingChoices | None - ) = None + first_choice: litellm.types.utils.Choices | None = None if response.choices and len(response.choices) > 0: - first_choice = response.choices[0] - assert isinstance(first_choice, litellm.types.utils.Choices) - message = first_choice.message + choice = response.choices[0] + if isinstance(choice, litellm.types.utils.Choices): + first_choice = choice + message = first_choice.message if _debug.DONT_LOG_MODEL_DATA: logger.debug("Received model response") @@ -160,8 +159,10 @@ async def get_response( usage = Usage() logger.warning("No usage information returned from Litellm") - if tracing.include_data() and message is not None: - span_generation.span_data.output = [message.model_dump()] + if tracing.include_data(): + span_generation.span_data.output = ( + [message.model_dump()] if message is not None else [] + ) span_generation.span_data.usage = { "input_tokens": usage.input_tokens, "output_tokens": usage.output_tokens,