Merge pull request #22080 from BerriAI/litellm_vllm_response_fix

Fix None (TypeError: 'NoneType' object is not a mapping)
This commit is contained in:
Sameer Kankute
2026-02-25 18:45:54 +05:30
committed by GitHub
2 changed files with 106 additions and 2 deletions
+3 -2
View File
@@ -4644,11 +4644,12 @@ def add_provider_specific_params_to_optional_params(
)
is False
):
extra_body = passed_params.pop("extra_body", {})
extra_body = passed_params.pop("extra_body", None) or {}
for k in passed_params.keys():
if k not in openai_params and passed_params[k] is not None:
extra_body[k] = passed_params[k]
optional_params.setdefault("extra_body", {})
if not isinstance(optional_params.get("extra_body"), dict):
optional_params["extra_body"] = {}
initial_extra_body = {
**optional_params["extra_body"],
**extra_body,
@@ -0,0 +1,103 @@
"""
Tests for hosted_vllm responses API support.
Regression test for: https://github.com/BerriAI/litellm/issues
Bug: client.responses.create() raised TypeError: 'NoneType' object is not a mapping
when extra_body=None was passed through the responses→completion pipeline for
hosted_vllm (and any OpenAI-compatible provider using add_provider_specific_params_to_optional_params).
"""
import json
import os
import sys
from unittest.mock import MagicMock, patch
sys.path.insert(
0, os.path.abspath("../../../../..")
) # Adds the parent directory to the system path
import litellm
def _make_mock_chat_completion_response(content: str = "Hello! I'm doing well.") -> dict:
return {
"id": "chatcmpl-test123",
"object": "chat.completion",
"created": 1234567890,
"model": "Qwen/Qwen3-8B",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": content},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30},
}
def _make_mock_http_client(response_body: dict) -> MagicMock:
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.json.return_value = response_body
mock_response.text = json.dumps(response_body)
mock_client.post.return_value = mock_response
return mock_client
def test_hosted_vllm_responses_create_with_string_input():
"""
Regression test: responses.create() with string input must not raise
TypeError: 'NoneType' object is not a mapping.
Root cause: extra_body=None was passed explicitly through the
responses→completion pipeline. In add_provider_specific_params_to_optional_params(),
passed_params.pop("extra_body", {}) returned None (key existed with value None),
and **None raised TypeError at dict unpacking.
Fix: normalize None to {} for both extra_body and optional_params["extra_body"].
"""
mock_client = _make_mock_http_client(
_make_mock_chat_completion_response("I'm doing well, thanks!")
)
with patch(
"litellm.llms.custom_httpx.llm_http_handler._get_httpx_client",
return_value=mock_client,
):
response = litellm.responses(
model="hosted_vllm/Qwen/Qwen3-8B",
input="Hello, how are you?",
api_base="https://test-vllm.example.com/v1",
api_key="test-key",
)
from litellm.types.llms.openai import ResponsesAPIResponse
assert response is not None
assert isinstance(response, ResponsesAPIResponse)
assert len(response.output) > 0
output_message = response.output[0]
assert output_message.role == "assistant" # type: ignore[union-attr]
assert len(output_message.content) > 0 # type: ignore[union-attr]
assert "well" in output_message.content[0].text # type: ignore[union-attr]
def test_hosted_vllm_responses_create_with_explicit_none_extra_body():
"""
Directly verify the fix in add_provider_specific_params_to_optional_params:
extra_body=None must not crash when building optional_params.
"""
from litellm.utils import get_optional_params
# This should not raise TypeError: 'NoneType' object is not a mapping
optional_params = get_optional_params(
model="Qwen/Qwen3-8B",
custom_llm_provider="hosted_vllm",
extra_body=None,
)
# extra_body=None should be normalized to an empty dict (or absent)
assert optional_params.get("extra_body") is not None or "extra_body" not in optional_params