fix(text-completion): send chat-only params in the request body instead of as SDK kwargs

OpenAITextCompletionConfig lists response_format, tools, tool_choice,
functions, function_call and top_logprobs as supported, then spreads them
as keyword arguments into the OpenAI SDK's completions.create(), which has
no such keywords. The SDK raised a TypeError and the proxy answered 500
before any request reached the backend. Pass them via extra_body so
backends that accept them (e.g. vLLM) receive them and others answer with
their own 4xx.

Fixes #47951
This commit is contained in:
Prasanna Jain 2026-10-22 20:14:06 -04:00
parent f2118fc767
commit 7836a54018
2 changed files with 48 additions and 1 deletions

View file

@ -13,6 +13,12 @@ from litellm.types.utils import Choices, Message, ModelResponse, TextCompletionR
from ..chat.gpt_transformation import OpenAIGPTConfig
from .utils import transform_prompt
# Supported params the SDK's ``completions.create()`` has no keyword for. They go in ``extra_body``
# so backends that accept them (e.g. vLLM) receive them, instead of the SDK raising a TypeError.
_BODY_ONLY_PARAMS: Final = frozenset(
{"functions", "function_call", "response_format", "tools", "tool_choice", "top_logprobs"}
)
class OpenAITextCompletionConfig(BaseTextCompletionConfig, OpenAIGPTConfig):
"""
@ -155,8 +161,12 @@ class OpenAITextCompletionConfig(BaseTextCompletionConfig, OpenAIGPTConfig):
headers: dict,
) -> dict:
prompt: Final = transform_prompt(messages)
body_only: Final = {k: v for k, v in optional_params.items() if k in _BODY_ONLY_PARAMS}
params: Final = {k: v for k, v in optional_params.items() if k not in _BODY_ONLY_PARAMS}
if body_only:
params["extra_body"] = {**(params.get("extra_body") or {}), **body_only}
return {
"model": model,
"prompt": prompt,
**optional_params,
**params,
}

View file

@ -0,0 +1,37 @@
import json
from typing import Final
import respx
from httpx import Response
import litellm
_COMPLETION: Final = {
"id": "cmpl-1",
"object": "text_completion",
"created": 1,
"model": "glm",
"choices": [{"index": 0, "text": '{"capital": "Paris"}', "finish_reason": "stop", "logprobs": None}],
"usage": {"prompt_tokens": 5, "completion_tokens": 6, "total_tokens": 11},
}
@respx.mock
def test_text_completion_sends_chat_only_params_in_the_request_body() -> None:
route: Final = respx.post("http://vllm:8000/v1/completions").mock(return_value=Response(200, json=_COMPLETION))
tools: Final = [{"type": "function", "function": {"name": "lookup", "parameters": {"type": "object"}}}]
response: Final = litellm.text_completion(
model="hosted_vllm/glm",
api_base="http://vllm:8000/v1",
prompt="The capital of France is",
response_format={"type": "json_object"},
tools=tools,
tool_choice="auto",
)
body: Final = json.loads(route.calls.last.request.content)
assert body["response_format"] == {"type": "json_object"}
assert body["tools"] == tools
assert body["tool_choice"] == "auto"
assert response.choices[0].text == '{"capital": "Paris"}'