mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-06 07:48:06 +00:00
feat(hosted_vllm): keep vLLM's stop_reason and token_ids on chat choices
Non-streamed hosted_vllm chat completions moved vLLM's choice-level stop_reason and token_ids into provider_specific_fields, so clients written against vLLM could not find them where vLLM puts them. They are now also set on the choice itself, and the copy in provider_specific_fields stays for existing readers. Fixes #47945
This commit is contained in:
parent
72f1ac4a27
commit
4e6d1840fe
2 changed files with 112 additions and 4 deletions
|
|
@ -3,8 +3,11 @@ Translate from OpenAI's `/v1/chat/completions` to VLLM's `/v1/chat/completions`
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Coroutine
|
||||
from typing import Final, Literal, cast, overload
|
||||
from collections.abc import Coroutine, Mapping
|
||||
from typing import TYPE_CHECKING, Final, Literal, cast, overload
|
||||
|
||||
import httpx
|
||||
from pydantic import JsonValue
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
get_image_mime_type_from_url,
|
||||
|
|
@ -22,10 +25,24 @@ from litellm.types.llms.openai import (
|
|||
ChatCompletionVideoObject,
|
||||
ChatCompletionVideoUrlObject,
|
||||
)
|
||||
from litellm.types.utils import Choices, ModelResponse
|
||||
|
||||
from ....utils import remove_additional_properties, remove_strict_from_schema
|
||||
from ...openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.litellm_core_utils.tokenizer import Encoding as Tokenizer
|
||||
|
||||
_VLLM_CHOICE_FIELDS: Final = ("stop_reason", "token_ids")
|
||||
|
||||
|
||||
def _vllm_choice_fields(choice: Choices) -> Mapping[str, object]:
|
||||
provider_fields: Final[Mapping[str, object]] = (
|
||||
choice.provider_specific_fields or {} if "provider_specific_fields" in choice else {}
|
||||
)
|
||||
return {field: provider_fields[field] for field in _VLLM_CHOICE_FIELDS if field in provider_fields}
|
||||
|
||||
|
||||
class HostedVLLMChatConfig(OpenAIGPTConfig):
|
||||
def _convert_custom_tools_to_function_tools(self, tools: list[dict[str, object]]) -> list[dict[str, object]]:
|
||||
|
|
@ -106,6 +123,38 @@ class HostedVLLMChatConfig(OpenAIGPTConfig):
|
|||
|
||||
return super().map_openai_params(non_default_params, optional_params, model, drop_params)
|
||||
|
||||
def transform_response(
|
||||
self,
|
||||
model: str,
|
||||
raw_response: httpx.Response,
|
||||
model_response: ModelResponse,
|
||||
logging_obj: "LiteLLMLoggingObj",
|
||||
request_data: dict[str, JsonValue], # mutable-ok: BaseConfig signature
|
||||
messages: list[AllMessageValues], # mutable-ok: BaseConfig signature
|
||||
optional_params: dict[str, JsonValue], # mutable-ok: BaseConfig signature
|
||||
litellm_params: dict[str, JsonValue], # mutable-ok: BaseConfig signature
|
||||
encoding: "Tokenizer | None",
|
||||
api_key: str | None = None,
|
||||
json_mode: bool | None = None,
|
||||
) -> ModelResponse:
|
||||
response: Final = super().transform_response( # pyright: ignore[reportUnknownMemberType] # OpenAIGPTConfig.transform_response takes bare dicts
|
||||
model=model,
|
||||
raw_response=raw_response,
|
||||
model_response=model_response,
|
||||
logging_obj=logging_obj,
|
||||
request_data=request_data,
|
||||
messages=messages,
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
encoding=encoding,
|
||||
api_key=api_key,
|
||||
json_mode=json_mode,
|
||||
)
|
||||
for choice in response.choices:
|
||||
for field, value in _vllm_choice_fields(choice).items():
|
||||
choice[field] = value
|
||||
return response
|
||||
|
||||
def _get_openai_compatible_provider_info(
|
||||
self, api_base: str | None, api_key: str | None
|
||||
) -> tuple[str | None, str | None]:
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
|
||||
from litellm.constants import (
|
||||
DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET,
|
||||
DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET,
|
||||
)
|
||||
from litellm.llms.hosted_vllm.chat.transformation import HostedVLLMChatConfig
|
||||
from litellm.types.utils import ModelResponse
|
||||
|
||||
|
||||
def test_hosted_vllm_chat_transformation_file_url():
|
||||
|
|
@ -397,3 +398,61 @@ def test_hosted_vllm_custom_tools_use_top_level_input_schema():
|
|||
assert tools[0]["function"]["name"] == "search"
|
||||
assert tools[0]["function"]["description"] == "Search docs"
|
||||
assert tools[0]["function"]["parameters"] == input_schema
|
||||
|
||||
|
||||
def _transform_vllm_chat_response(choice_extras: dict[str, object]) -> ModelResponse:
|
||||
payload = {
|
||||
"id": "chatcmpl-1",
|
||||
"object": "chat.completion",
|
||||
"created": 1,
|
||||
"model": "glm",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": "1, 2, 3, 4"},
|
||||
"logprobs": None,
|
||||
"finish_reason": "stop",
|
||||
**choice_extras,
|
||||
}
|
||||
],
|
||||
"usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
|
||||
}
|
||||
raw_response = httpx.Response(
|
||||
200, json=payload, request=httpx.Request("POST", "http://vllm.local/v1/chat/completions")
|
||||
)
|
||||
return HostedVLLMChatConfig().transform_response(
|
||||
model="hosted_vllm/glm",
|
||||
raw_response=raw_response,
|
||||
model_response=ModelResponse(),
|
||||
logging_obj=MagicMock(),
|
||||
request_data={},
|
||||
messages=[],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"choice_extras",
|
||||
[
|
||||
{"stop_reason": ", 5", "token_ids": [16, 11, 220]},
|
||||
{"stop_reason": 128009},
|
||||
{"stop_reason": None},
|
||||
],
|
||||
)
|
||||
def test_hosted_vllm_keeps_vllm_choice_fields_on_the_choice(choice_extras: dict[str, object]):
|
||||
response = _transform_vllm_chat_response(choice_extras)
|
||||
|
||||
dumped_choice: dict[str, dict[str, object]] = json.loads(response.model_dump_json())["choices"][0]
|
||||
for field, value in choice_extras.items():
|
||||
assert dumped_choice[field] == value
|
||||
assert dumped_choice["provider_specific_fields"][field] == value
|
||||
|
||||
|
||||
def test_hosted_vllm_does_not_invent_vllm_choice_fields():
|
||||
response = _transform_vllm_chat_response({})
|
||||
|
||||
dumped_choice: dict[str, object] = json.loads(response.model_dump_json())["choices"][0]
|
||||
assert "stop_reason" not in dumped_choice
|
||||
assert "token_ids" not in dumped_choice
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue