| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | # Adapted from |
| 5 | # https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py |
| 6 | import time |
| 7 | from typing import ClassVar |
| 8 | |
| 9 | from pydantic import ( |
| 10 | BaseModel, |
| 11 | ConfigDict, |
| 12 | Field, |
| 13 | model_validator, |
| 14 | ) |
| 15 | |
| 16 | from vllm.logger import init_logger |
| 17 | from vllm.utils import random_uuid |
| 18 | |
| 19 | logger = init_logger(__name__) |
| 20 | |
| 21 | # Legacy guided-decoding fields are treated like any other extra field since |
| 22 | # their removal, so requests still using them get HTTP 200 with unconstrained |
| 23 | # output and no visible signal (see #53975). Warn instead of burying the |
| 24 | # only breadcrumb at DEBUG level. |
| 25 | _REMOVED_GUIDED_FIELDS = frozenset( |
| 26 | { |
| 27 | "guided_json", |
| 28 | "guided_regex", |
| 29 | "guided_choice", |
| 30 | "guided_grammar", |
| 31 | "guided_decoding_backend", |
| 32 | "guided_whitespace_pattern", |
| 33 | } |
| 34 | ) |
| 35 | |
| 36 | |
| 37 | class OpenAIBaseModel(BaseModel): |
| 38 | # OpenAI API does allow extra fields |
| 39 | model_config = ConfigDict(extra="allow") |
| 40 | |
| 41 | # Cache class field names |
| 42 | field_names: ClassVar[set[str] | None] = None |
| 43 | |
| 44 | @model_validator(mode="wrap") |
| 45 | @classmethod |
| 46 | def __log_extra_fields__(cls, data, handler): |
| 47 | result = handler(data) |
| 48 | if not isinstance(data, dict): |
| 49 | return result |
| 50 | field_names = cls.field_names |
| 51 | if field_names is None: |
| 52 | # Get all class field names and their potential aliases |
| 53 | field_names = set() |
| 54 | for field_name, field in cls.model_fields.items(): |
| 55 | field_names.add(field_name) |
| 56 | if alias := getattr(field, "alias", None): |
| 57 | field_names.add(alias) |
| 58 | cls.field_names = field_names |
| 59 | |
| 60 | # Compare against both field names and aliases |
| 61 | extra_keys = data.keys() - field_names |
| 62 | if extra_keys: |
| 63 | removed = extra_keys & _REMOVED_GUIDED_FIELDS |
| 64 | if removed: |
| 65 | logger.warning_once( |
| 66 | "Request contains the removed guided-decoding field(s) " |
| 67 | "%s, which are ignored; output will NOT be constrained. " |
| 68 | "Use `structured_outputs` (or `response_format`) " |
| 69 | "instead; see docs/features/structured_outputs.md.", |
| 70 | str(sorted(removed)), |
| 71 | ) |
| 72 | logger.debug( |
| 73 | "The following fields were present in the request but ignored: %s", |
| 74 | extra_keys, |
| 75 | ) |
| 76 | return result |
| 77 | |
| 78 | |
| 79 | class ErrorInfo(OpenAIBaseModel): |
| 80 | message: str |
| 81 | type: str |
| 82 | param: str | None = None |
| 83 | code: int |
| 84 | |
| 85 | |
| 86 | class ErrorResponse(OpenAIBaseModel): |
| 87 | error: ErrorInfo |
| 88 | |
| 89 | |
| 90 | class ModelPermission(OpenAIBaseModel): |
| 91 | id: str = Field(default_factory=lambda: f"modelperm-{random_uuid()}") |
| 92 | object: str = "model_permission" |
| 93 | created: int = Field(default_factory=lambda: int(time.time())) |
| 94 | allow_create_engine: bool = False |
| 95 | allow_sampling: bool = True |
| 96 | allow_logprobs: bool = True |
| 97 | allow_search_indices: bool = False |
| 98 | allow_view: bool = True |
| 99 | allow_fine_tuning: bool = False |
| 100 | organization: str = "*" |
| 101 | group: str | None = None |
| 102 | is_blocking: bool = False |
| 103 | |
| 104 | |
| 105 | class ModelCard(OpenAIBaseModel): |
| 106 | id: str |
| 107 | object: str = "model" |
| 108 | created: int = Field(default_factory=lambda: int(time.time())) |
| 109 | owned_by: str = "vllm" |
| 110 | root: str | None = None |
| 111 | parent: str | None = None |
| 112 | max_model_len: int | None = None |
| 113 | permission: list[ModelPermission] = Field(default_factory=list) |
| 114 | |
| 115 | |
| 116 | class ModelList(OpenAIBaseModel): |
| 117 | object: str = "list" |
| 118 | data: list[ModelCard] = Field(default_factory=list) |
| 119 | |
| 120 | |
| 121 | class PromptTokenUsageInfo(OpenAIBaseModel): |
| 122 | cached_tokens: int | None = None |
| 123 | created_cache_tokens: int | None = None |
| 124 | multimodal_tokens: dict[str, int] | None = None |
| 125 | """Prompt tokens contributed by each input modality, keyed by modality name |
| 126 | (e.g. `image`, `audio`, `video`). A breakdown of the multimodal |
| 127 | placeholder tokens already counted in `prompt_tokens`; `None` when the |
| 128 | request has no multimodal input.""" |
| 129 | |
| 130 | |
| 131 | class CompletionTokenUsageInfo(OpenAIBaseModel): |
| 132 | reasoning_tokens: int = 0 |
| 133 | |
| 134 | |
| 135 | class UsageInfo(OpenAIBaseModel): |
| 136 | prompt_tokens: int = 0 |
| 137 | total_tokens: int = 0 |
| 138 | completion_tokens: int | None = 0 |
| 139 | prompt_tokens_details: PromptTokenUsageInfo | None = None |
| 140 | completion_tokens_details: CompletionTokenUsageInfo | None = None |