| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | """Native Inkling chat renderer for the Python frontend. |
| 4 | |
| 5 | Mirrors the Rust frontend's native Inkling renderer |
| 6 | (``rust/src/chat/src/renderer/inkling/mod.rs``): chat messages are rendered |
| 7 | directly to token ids — Inkling has no Jinja chat template and no faithful |
| 8 | text form. |
| 9 | |
| 10 | The encoding logic lives in ``inkling_encoding.py`` behind a narrow |
| 11 | "OpenAI messages + tools -> token ids" call; see the swap-point comment |
| 12 | in :meth:`InklingRenderer._render` for adopting a standalone Inkling |
| 13 | input-processing library (mistral-common style) later. |
| 14 | """ |
| 15 | |
| 16 | from vllm.config import VllmConfig |
| 17 | from vllm.entrypoints.chat_utils import ( |
| 18 | ChatCompletionMessageParam, |
| 19 | ConversationMessage, |
| 20 | parse_chat_messages, |
| 21 | parse_chat_messages_async, |
| 22 | ) |
| 23 | from vllm.logger import init_logger |
| 24 | from vllm.tokenizers.hf import HfTokenizer |
| 25 | from vllm.utils.async_utils import make_async |
| 26 | |
| 27 | from .base import BaseRenderer |
| 28 | from .inkling_encoding import SPECIAL_TOKEN_SPELLINGS, render_inkling_messages |
| 29 | from .inputs import DictPrompt |
| 30 | from .inputs.preprocess import parse_dec_only_prompt |
| 31 | from .params import ChatParams |
| 32 | |
| 33 | logger = init_logger(__name__) |
| 34 | |
| 35 | _NAMED_REASONING_EFFORT = { |
| 36 | "none": 0.0, |
| 37 | "minimal": 0.1, |
| 38 | "low": 0.2, |
| 39 | "medium": 0.7, |
| 40 | "high": 0.9, |
| 41 | "xhigh": 0.99, |
| 42 | "max": 0.99, |
| 43 | } |
| 44 | |
| 45 | _DEFAULT_REASONING_EFFORT = 0.9 |
| 46 | |
| 47 | |
| 48 | def _resolve_reasoning_effort(value: object) -> float | int | None: |
| 49 | if value is None: |
| 50 | return _DEFAULT_REASONING_EFFORT |
| 51 | if isinstance(value, str): |
| 52 | return _NAMED_REASONING_EFFORT.get(value) |
| 53 | if isinstance(value, bool) or not isinstance(value, (int, float)): |
| 54 | return None |
| 55 | return value |
| 56 | |
| 57 | |
| 58 | class _HfBackedTmlTokenizer: |
| 59 | """Adapts an HF tokenizer to the encoding core's tokenizer protocol. |
| 60 | |
| 61 | Special-token ids are resolved from the tokenizer vocab at |
| 62 | construction (never hardcoded), trying each known spelling — the Inkling |
| 63 | HF vocab exposes some semantic slots as ``<|unused_NNNNNN|>`` tokens. |
| 64 | """ |
| 65 | |
| 66 | def __init__(self, tokenizer: HfTokenizer) -> None: |
| 67 | self._tokenizer = tokenizer |
| 68 | |
| 69 | vocab = tokenizer.get_vocab() |
| 70 | special_ids: dict[str, int] = {} |
| 71 | missing: list[str] = [] |
| 72 | for token, spellings in SPECIAL_TOKEN_SPELLINGS.items(): |
| 73 | for spelling in spellings: |
| 74 | token_id = vocab.get(spelling) |
| 75 | if token_id is not None: |
| 76 | special_ids[token] = token_id |
| 77 | break |
| 78 | else: |
| 79 | missing.append(token) |
| 80 | if missing: |
| 81 | raise ValueError(f"Inkling tokenizer is missing special tokens: {missing}") |
| 82 | self._special_ids = special_ids |
| 83 | |
| 84 | def encode_text(self, text: str) -> list[int]: |
| 85 | return self._tokenizer.encode(text, add_special_tokens=False) |
| 86 | |
| 87 | def encode_special(self, token: str) -> int: |
| 88 | return self._special_ids[token] |
| 89 | |
| 90 | |
| 91 | class InklingRenderer(BaseRenderer[HfTokenizer]): |
| 92 | def __init__( |
| 93 | self, |
| 94 | config: VllmConfig, |
| 95 | tokenizer: HfTokenizer | None, |
| 96 | ) -> None: |
| 97 | super().__init__(config, tokenizer) |
| 98 | |
| 99 | self._inkling_tokenizer = _HfBackedTmlTokenizer(self.get_tokenizer()) |
| 100 | self._render_async = make_async(self._render, executor=self._executor) |
| 101 | |
| 102 | def _render( |
| 103 | self, |
| 104 | messages: list[ChatCompletionMessageParam], |
| 105 | params: ChatParams, |
| 106 | ) -> list[int]: |
| 107 | kwargs = params.chat_template_kwargs or {} |
| 108 | |
| 109 | if kwargs.get("continue_final_message"): |
| 110 | raise ValueError("Inkling renderer does not support continue_final_message") |
| 111 | |
| 112 | reasoning_effort = _resolve_reasoning_effort(kwargs.get("reasoning_effort")) |
| 113 | |
| 114 | try: |
| 115 | # Swap point: to adopt a standalone Inkling input-processing |
| 116 | # library, replace this call (and the _HfBackedTmlTokenizer |
| 117 | # adapter above) with the library's renderer. |
| 118 | return render_inkling_messages( |
| 119 | messages, |
| 120 | self._inkling_tokenizer, |
| 121 | add_generation_prompt=kwargs.get("add_generation_prompt", True), |
| 122 | tools=kwargs.get("tools"), |
| 123 | reasoning_effort=reasoning_effort, |
| 124 | ) |
| 125 | except ValueError: |
| 126 | raise |
| 127 | except (TypeError, KeyError) as e: |
| 128 | # Malformed request content; surface as a request error. |
| 129 | raise ValueError(str(e)) from e |
| 130 | except Exception as e: |
| 131 | logger.exception("Error while rendering Inkling chat messages") |
| 132 | raise ValueError(str(e)) from e |
| 133 | |
| 134 | def render_messages( |
| 135 | self, |
| 136 | messages: list[ChatCompletionMessageParam], |
| 137 | params: ChatParams, |
| 138 | ) -> tuple[list[ConversationMessage], DictPrompt]: |
| 139 | conversation, mm_data, mm_uuids = parse_chat_messages( |
| 140 | messages, |
| 141 | self.model_config, |
| 142 | content_format="string", |
| 143 | media_io_kwargs=params.media_io_kwargs, |
| 144 | mm_processor_kwargs=params.mm_processor_kwargs, |
| 145 | ) |
| 146 | |
| 147 | token_ids = self._render(messages, params) |
| 148 | |
| 149 | prompt = parse_dec_only_prompt(token_ids) |
| 150 | if mm_data is not None: |
| 151 | prompt["multi_modal_data"] = mm_data |
| 152 | if mm_uuids is not None: |
| 153 | prompt["multi_modal_uuids"] = mm_uuids |
| 154 | |
| 155 | return conversation, prompt |
| 156 | |
| 157 | async def render_messages_async( |
| 158 | self, |
| 159 | messages: list[ChatCompletionMessageParam], |
| 160 | params: ChatParams, |
| 161 | ) -> tuple[list[ConversationMessage], DictPrompt]: |
| 162 | conversation, mm_data, mm_uuids = await parse_chat_messages_async( |
| 163 | messages, |
| 164 | self.model_config, |
| 165 | content_format="string", |
| 166 | media_io_kwargs=params.media_io_kwargs, |
| 167 | mm_processor_kwargs=params.mm_processor_kwargs, |
| 168 | ) |
| 169 | |
| 170 | token_ids = await self._render_async(messages, params) |
| 171 | |
| 172 | prompt = parse_dec_only_prompt(token_ids) |
| 173 | if mm_data is not None: |
| 174 | prompt["multi_modal_data"] = mm_data |
| 175 | if mm_uuids is not None: |
| 176 | prompt["multi_modal_uuids"] = mm_uuids |
| 177 | |
| 178 | return conversation, prompt |