litellm - ✅(Solved) Fix Pydantic validation crash on Gemini finish_reason: "error" (MALFORMED_FUNCTION_CALL) [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#23273Fetched 2026-04-08 00:37:48
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
cross-referenced ×2commented ×1labeled ×1referenced ×1

When Google Gemini 3 models return finish_reason: "error" with native_finish_reason: "MALFORMED_FUNCTION_CALL", LiteLLM crashes with a Pydantic validation error instead of handling it gracefully.

Error Message

pydantic_core._pydantic_core.ValidationError: 1 validation error for Choices
finish_reason
  Input should be 'stop', 'content_filter', 'function_call', 'tool_calls', 'length',
  'guardrail_intervened', 'eos', 'finish_reason_unspecified' or 'malformed_function_call'
  [type=literal_error, input_value='error', input_type=str]

The Choices Pydantic model's finish_reason literal doesn't include "error", so the response object can't even be constructed. This bubbles up as:

litellm.APIConnectionError: OpenrouterException - Invalid response object

Root Cause

When Google Gemini 3 models return finish_reason: "error" with native_finish_reason: "MALFORMED_FUNCTION_CALL", LiteLLM crashes with a Pydantic validation error instead of handling it gracefully.

Fix Action

Fixed

PR fix notes

PR #23366: fix(gemini): map finish_reason 'error' to 'malformed_function_call' to prevent Pydantic crash

Description (problem / solution / changelog)

Problem

When Gemini (via OpenRouter or directly) returns finish_reason: "error" with native_finish_reason: "MALFORMED_FUNCTION_CALL", map_finish_reason() passes the string through unchanged. The Choices Pydantic model's OpenAIChatCompletionFinishReason Literal does not include "error", so construction raises (fixes #23273):

pydantic_core.ValidationError: Input should be 'stop', 'content_filter', ..., or 'malformed_function_call'
  [type=literal_error, input_value='error', ...]

This bubbles up as litellm.APIConnectionError: Invalid response object.

Fix

Map both 'ERROR' (already handled) and lowercase 'error' (Gemini via OpenRouter) to 'malformed_function_call', which is already in the Literal:

elif finish_reason in ('ERROR', 'error'):
    return 'malformed_function_call'

Fixes #23273

Changed files

  • litellm/litellm_core_utils/core_helpers.py (modified, +5/-4)
  • litellm/model_prices_and_context_window_backup.json (modified, +30/-0)
  • model_prices_and_context_window.json (modified, +30/-0)

Code Example

pydantic_core._pydantic_core.ValidationError: 1 validation error for Choices
finish_reason
  Input should be 'stop', 'content_filter', 'function_call', 'tool_calls', 'length',
  'guardrail_intervened', 'eos', 'finish_reason_unspecified' or 'malformed_function_call'
  [type=literal_error, input_value='error', input_type=str]

---

litellm.APIConnectionError: OpenrouterException - Invalid response object

---

{
  "choices": [{
    "index": 0,
    "finish_reason": "error",
    "native_finish_reason": "MALFORMED_FUNCTION_CALL",
    "message": {"role": "assistant", "content": null, "refusal": null, "reasoning": null}
  }],
  "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}
RAW_BUFFERClick to expand / collapse

Description

When Google Gemini 3 models return finish_reason: "error" with native_finish_reason: "MALFORMED_FUNCTION_CALL", LiteLLM crashes with a Pydantic validation error instead of handling it gracefully.

Error

pydantic_core._pydantic_core.ValidationError: 1 validation error for Choices
finish_reason
  Input should be 'stop', 'content_filter', 'function_call', 'tool_calls', 'length',
  'guardrail_intervened', 'eos', 'finish_reason_unspecified' or 'malformed_function_call'
  [type=literal_error, input_value='error', input_type=str]

The Choices Pydantic model's finish_reason literal doesn't include "error", so the response object can't even be constructed. This bubbles up as:

litellm.APIConnectionError: OpenrouterException - Invalid response object

Raw API Response

This is what OpenRouter returns from Gemini 3:

{
  "choices": [{
    "index": 0,
    "finish_reason": "error",
    "native_finish_reason": "MALFORMED_FUNCTION_CALL",
    "message": {"role": "assistant", "content": null, "refusal": null, "reasoning": null}
  }],
  "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
}

Expected Behavior

LiteLLM should:

  1. Accept "error" as a valid finish_reason in the Choices model (or map it to an existing value)
  2. Raise a clear, catchable exception (e.g., litellm.BadRequestError) with the native_finish_reason details, rather than crashing in Pydantic validation

Steps to Reproduce

  1. Use completion(model="openrouter/google/gemini-3-flash-preview", ...) with tool definitions
  2. Gemini returns MALFORMED_FUNCTION_CALL (common with Gemini 3 models via OpenRouter due to missing thought_signature)
  3. LiteLLM crashes in convert_dict_to_response.py:609

Environment

  • LiteLLM: 1.81.13
  • Provider: OpenRouter
  • Model: google/gemini-3-flash-preview (also affects google/gemini-3.1-pro-preview)

Impact

This affects any framework using LiteLLM with Gemini 3 models via OpenRouter (CrewAI, LangChain, etc.). The unhandled crash prevents frameworks from implementing retry logic or graceful fallbacks.

extent analysis

Fix Plan

To resolve the issue, we need to update the Choices Pydantic model to accept "error" as a valid finish_reason and handle the native_finish_reason details.

Step 1: Update the Choices Model

Update the finish_reason field in the Choices model to include "error" as a valid choice:

from pydantic import validator
from enum import Enum

class FinishReason(str, Enum):
    stop = "stop"
    content_filter = "content_filter"
    function_call = "function_call"
    tool_calls = "tool_calls"
    length = "length"
    guardrail_intervened = "guardrail_intervened"
    eos = "eos"
    finish_reason_unspecified = "finish_reason_unspecified"
    malformed_function_call = "malformed_function_call"
    error = "error"  # Add this line

class Choices(BaseModel):
    index: int
    finish_reason: FinishReason
    native_finish_reason: str
    message: Message

    @validator("finish_reason")
    def validate_finish_reason(cls, v):
        if v == "error":
            # Map "error" to an existing value or raise a custom exception
            raise litellm.BadRequestError("Invalid finish reason: error")
        return v

Step 2: Raise a Custom Exception

Create a custom exception litellm.BadRequestError to handle the native_finish_reason details:

class BadRequestError(Exception):
    def __init__(self, message: str):
        self.message = message
        super().__init__(message)

Step 3: Update the convert_dict_to_response.py File

Update the convert_dict_to_response.py file to catch the BadRequestError exception and handle it accordingly:

try:
    response = Choices(**response_dict)
except litellm.BadRequestError as e:
    # Handle the exception and provide a clear error message
    raise litellm.APIConnectionError(f"OpenRouterException - {e.message}")

Verification

To verify the fix, test the completion function with the Gemini 3 model and tool definitions. The BadRequestError exception should be raised with the native_finish_reason details, and the error message should be clear and catchable.

Extra Tips

  • Make sure to update the Choices model and the convert_dict_to_response.py file accordingly.
  • Test the fix thoroughly to ensure it handles the native_finish_reason details correctly.
  • Consider adding retry logic or graceful fallbacks to handle the BadRequestError exception in your framework.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING