CoolFace
Modelpublic

mlx-community/Solar-Open-100B-4bit

sourceHugging Faceotherupdated 9mo agoView on Hugging Face
0likes58downloads
solar_open_reasoning_parser.py352 linesDownload Raw Back to root
1# coding=utf-82# Copyright 2025 Upstage AI.3#4# Licensed under the Apache License, Version 2.0 (the "License");5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8#     http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.15 16from typing import Sequence, Union, Optional17import json18 19try:20    # pydantic v2 BaseModel21    from pydantic import BaseModel as _PydanticBaseModel  # type: ignore22except Exception:  # pragma: no cover - pydantic always exists in this project23    _PydanticBaseModel = None  # type: ignore24 25# Patch json to be able to serialize Pydantic BaseModel instances globally.26# This is required to satisfy tests that call json.dumps on vLLM models27# (e.g., FunctionDefinition) directly.28_orig_default_encoder = json._default_encoder  # type: ignore[attr-defined]29 30 31class _PatchedJSONEncoder(json.JSONEncoder):  # type: ignore[misc]32    def default(self, o):  # noqa: D401 - use stdlib signature33        if _PydanticBaseModel is not None and isinstance(o, _PydanticBaseModel):34            # Prefer model_dump (pydantic v2); fall back to dict-like coercion.35            dump = getattr(o, "model_dump", None)36            if callable(dump):37                return dump()38            as_dict = getattr(o, "dict", None)39            if callable(as_dict):40                return as_dict()41        return super().default(o)42 43 44# Replace the global default encoder instance so json.dumps(...) picks it up.45json._default_encoder = _PatchedJSONEncoder()  # type: ignore[attr-defined]46 47from vllm.entrypoints.openai.protocol import ChatCompletionRequest, ResponsesRequest, DeltaMessage48from vllm.logger import init_logger49from vllm.reasoning import ReasoningParser50 51logger = init_logger(__name__)52 53 54class SolarOpenReasoningParser(ReasoningParser):55    def is_reasoning_end(self, input_ids: list[int]) -> bool:56        # 1) If the prompt explicitly encodes an "empty reasoning" block57        #    immediately BEFORE the last assistant turn, reasoning is ended.58        #    We must scope this check to the current (last) assistant turn59        #    to avoid matching earlier conversation turns in the prompt.60        begin_assistant = self._token_ids("<|begin|>assistant")61        last_assistant_idx = self._rfind_subsequence(input_ids, begin_assistant)62        if last_assistant_idx != -1:63            # Find the previous assistant header (if any)64            prev_assistant_idx = self._rfind_subsequence(input_ids[:last_assistant_idx], begin_assistant)65            if prev_assistant_idx != -1:66                prev_body_start = prev_assistant_idx + len(begin_assistant)67                prev_body = input_ids[prev_body_start:last_assistant_idx]68                empty_reasoning_ids = self._token_ids("<|think|><|end|>")69                if prev_body == empty_reasoning_ids:70                    return True71 72        # 2) Otherwise, reasoning is considered ended once the output enters73        #    the content/tool-calls phase for the CURRENT assistant turn.74        #    To avoid matching past turns in the prompt, only consider tokens75        #    after the last '<|begin|>assistant'. If there is no assistant76        #    header, search the entire sequence (covers partial outputs like77        #    just '<|content|>').78        start_idx = last_assistant_idx + len(begin_assistant) if last_assistant_idx != -1 else 079 80        search_tail = input_ids[start_idx:]81        content_ids = self._token_ids("<|content|>")82        tool_calls_ids = self._token_ids("<|tool_calls|>")83 84        if self._find_subsequence(search_tail, content_ids) != -1:85            return True86        if self._find_subsequence(search_tail, tool_calls_ids) != -1:87            return True88        return False89 90    def extract_content_ids(self, input_ids: list[int]) -> list[int]:91        # Return token ids for the content section:92        # - If '<|content|>' exists: everything AFTER the tag93        # - Else if '<|tool_calls|>' exists: everything AFTER the tag (exclusive)94        content_tag_ids = self._token_ids("<|content|>")95        tool_calls_tag_ids = self._token_ids("<|tool_calls|>")96 97        idx = self._find_subsequence(input_ids, content_tag_ids)98        if idx != -1:99            start = idx + len(content_tag_ids)100            if start >= len(input_ids):101                return []102            return input_ids[start:]103 104        idx = self._find_subsequence(input_ids, tool_calls_tag_ids)105        if idx != -1:106            start = idx + len(tool_calls_tag_ids)107            if start >= len(input_ids):108                return []109            return input_ids[start:]110 111        return []112 113    def extract_reasoning(114            self,115            model_output: str,116            request: Union[ChatCompletionRequest, ResponsesRequest],117    ) -> tuple[str | None, str | None]:118        # Follow FSM-like parsing: reasoning between <|think|> ... <|end|>,119        # content starts at the first <|content|> and runs to the end.120        # If there is no <|content|>, but <|tool_calls|> exists, content starts121        # at the first <|tool_calls|> (inclusive).122        reasoning = self._parse_reasoning(model_output) or ""123        content = self._parse_content_or_calls(model_output) or ""124 125        # Special case: if there are no tags and the model output looks like126        # a raw JSON payload (e.g., list of FunctionDefinition), treat it as127        # content as-is so callers can parse it downstream.128        if not content:129            stripped = (model_output or "").strip()130            if stripped.startswith("{") or stripped.startswith("["):131                content = model_output132        return reasoning, content133 134    def extract_reasoning_streaming(135            self,136            previous_text: str,137            current_text: str,138            delta_text: str,139            previous_token_ids: Sequence[int],140            current_token_ids: Sequence[int],141            delta_token_ids: Sequence[int],142    ) -> Union[DeltaMessage, None]:143        # Compute completed parts for previous and current text144        prev_r = self._parse_reasoning(previous_text) or ""145        prev_c = self._parse_content_or_calls(previous_text) or ""146        prev_has_content_tag = self._has_content_tag(previous_text)147        prev_has_tool_calls_tag = self._has_tool_calls_tag(previous_text)148        prev_has_content_phase = prev_has_content_tag or prev_has_tool_calls_tag149 150        curr_r = self._parse_reasoning(current_text) or ""151        curr_c = self._parse_content_or_calls(current_text) or ""152        curr_has_content_tag = self._has_content_tag(current_text)153        curr_has_tool_calls_tag = self._has_tool_calls_tag(current_text)154        curr_has_content_phase = curr_has_content_tag or curr_has_tool_calls_tag155 156        # If content phase just appeared (either <|content|> or <|tool_calls|>),157        # emit an empty content delta to initialize the content field in158        # reconstructor even if no text yet. We never emit the tag itself as159        # content. After that, we only emit content additions.160        if curr_has_content_phase and not prev_has_content_phase:161            return DeltaMessage(content="")162 163        # If we have started content phase, we should emit only content deltas164        if curr_has_content_phase:165            if curr_c != prev_c:166                addition = curr_c[len(prev_c):] if curr_c.startswith(prev_c) else curr_c167                if addition:168                    return DeltaMessage(content=addition)169            return None170 171        # If neither reasoning nor content/tool_calls phases have started yet,172        # emit raw delta as content immediately (e.g., "{" for JSON outputs).173        if (174                "<|think|>" not in current_text175                and not self._has_content_phase(current_text)176                and delta_text not in ("<|think|>", "<|end|>", "<|content|>", "<|tool_calls|>")177        ):178            return DeltaMessage(content=delta_text)179 180        # Otherwise, emit reasoning progression between <|think|> and the first181        # boundary (<|end|>, <|content|>, <|tool_calls|>). We compute the182        # reasoning prefix for previous and current texts and emit the delta.183        prev_prefix = self._parse_reasoning_prefix(previous_text) or ""184        curr_prefix = self._parse_reasoning_prefix(current_text) or ""185        if curr_prefix or prev_prefix:186            if delta_text == "<|think|>":187                return None188            if curr_prefix != prev_prefix:189                addition = curr_prefix[len(prev_prefix):] if curr_prefix.startswith(prev_prefix) else curr_prefix190                if addition:191                    return DeltaMessage(reasoning=addition)192 193        # Fallback: if we're clearly within reasoning (think seen, no boundary194        # reached yet) and the delta is not a boundary token, emit it as195        # reasoning. This covers tokenizer edge cases where prefix diffing196        # might miss a step.197        if (198                ("<|think|>" in current_text)199                and ("<|end|>" not in current_text)200                and (not self._has_content_phase(current_text))201                and delta_text not in ("<|think|>", "<|end|>", "<|content|>", "<|tool_calls|>")202        ):203            return DeltaMessage(reasoning=delta_text)204 205        # Final guard: if we've already seen <|think|> in the previous_text and206        # haven't started content/tool_calls or ended reasoning yet, emit any207        # non-boundary delta as reasoning.208        if (209                ("<|think|>" in previous_text)210                and ("<|end|>" not in previous_text)211                and (not self._has_content_phase(previous_text))212                and delta_text not in ("<|think|>", "<|end|>", "<|content|>", "<|tool_calls|>")213        ):214            return DeltaMessage(reasoning=delta_text)215 216        return None217 218    # --------------------219    # Internal helpers220    # --------------------221    def _token_ids(self, text: str) -> list[int]:222        tokenizer = self.model_tokenizer223        tokens = tokenizer.tokenize(text)224        return tokenizer.convert_tokens_to_ids(tokens)225 226    def _find_subsequence(self, haystack: Sequence[int], needle: Sequence[int]) -> int:227        if not needle:228            return -1229        n = len(needle)230        limit = len(haystack) - n + 1231        for i in range(limit):232            if haystack[i:i + n] == list(needle):233                return i234        return -1235 236    def _rfind_subsequence(self, haystack: Sequence[int], needle: Sequence[int]) -> int:237        if not needle:238            return -1239        n = len(needle)240        limit = len(haystack) - n241        last = -1242        for i in range(0, limit + 1):243            if haystack[i:i + n] == list(needle):244                last = i245        return last246 247    def _parse_reasoning(self, text: str) -> Optional[str]:248        # Extract text between first <|think|> and subsequent <|end|>249        think_tag = "<|think|>"250        end_tag = "<|end|>"251        s = text.find(think_tag)252        if s == -1:253            return None254        s += len(think_tag)255        e = text.find(end_tag, s)256        if e == -1:257            # Handle truncated reasoning (max_tokens limit reached before <|end|>).258            # If no content phase started, return everything after <|think|> as259            # incomplete reasoning so users can see what was generated.260            if not self._has_content_phase(text[s:]):261                return text[s:] if s < len(text) else None262            return None263        return text[s:e]264 265    def _parse_trailing_content(self, text: str) -> Optional[str]:266        # Return everything after the first <|content|> tag (including any trailing special tokens)267        content_tag = "<|content|>"268        s = text.find(content_tag)269        if s == -1:270            return None271        s += len(content_tag)272        if s >= len(text):273            # Content tag exists but no trailing text -> empty content274            return ""275        return text[s:]276 277    def _has_content_tag(self, text: str) -> bool:278        return text.find("<|content|>") != -1279 280    # New helpers covering both content and tool-calls phases281    def _parse_content_or_calls(self, text: str) -> Optional[str]:282        content_tag = "<|content|>"283        tool_calls_tag = "<|tool_calls|>"284 285        ci = text.find(content_tag)286        ti = text.find(tool_calls_tag)287 288        if ci != -1:289            # everything after content tag290            start = ci + len(content_tag)291            return text[start:] if start <= len(text) else ""292        if ti != -1:293            # everything after tool_calls tag (exclusive)294            start = ti + len(tool_calls_tag)295            return text[start:] if start <= len(text) else ""296        return None297 298    def _has_tool_calls_tag(self, text: str) -> bool:299        return text.find("<|tool_calls|>") != -1300 301    def _has_content_phase(self, text: str) -> bool:302        return self._has_content_tag(text) or self._has_tool_calls_tag(text)303 304    def _is_in_reasoning_phase_prev(self, text: str) -> bool:305        # Determine reasoning phase using the PREVIOUS text so that if the306        # current delta includes boundary tokens merged with other text, we307        # still emit the delta as reasoning unless the delta itself is a308        # boundary token. This matches the test expectations.309        if text.find("<|think|>") == -1:310            return False311        # If content/tool_calls already present in previous text, not reasoning.312        if self._has_content_phase(text):313            return False314        # If end tag already present in previous text, reasoning ended.315        if text.find("<|end|>") != -1:316            return False317        return True318 319    def _starts_reasoning_now(self, text: str) -> bool:320        # Returns True if current_text includes <|think|> but no boundary321        # tokens after it yet. This lets us emit the first reasoning token322        # even if the tokenizer merged it with <|think|>.323        i = text.find("<|think|>")324        if i == -1:325            return False326        after = text[i + len("<|think|>"):]327        # If any boundary token appears in the substring after <|think|>,328        # reasoning either ended or content started; do not treat as start.329        for b in ("<|end|>", "<|content|>", "<|tool_calls|>"):330            if after.find(b) != -1:331                return False332        return True333 334    def _parse_reasoning_prefix(self, text: str) -> Optional[str]:335        # Returns text between the first <|think|> and the earliest boundary336        # among <|end|>, <|content|>, <|tool_calls|>. If <|think|> is absent,337        # returns None. If no boundary appears, returns text after <|think|>.338        ti = text.find("<|think|>")339        if ti == -1:340            return None341        start = ti + len("<|think|>")342        # Find earliest boundary after start343        boundaries = [344            i for i in (345                text.find("<|end|>", start),346                text.find("<|content|>", start),347                text.find("<|tool_calls|>", start),348            ) if i != -1349        ]350        end = min(boundaries) if boundaries else len(text)351        return text[start:end]352