| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import dataclasses |
| 4 | import functools |
| 5 | import json |
| 6 | from concurrent.futures import Future |
| 7 | from typing import TYPE_CHECKING, Any, cast |
| 8 | |
| 9 | from vllm.sampling_params import SamplingParams, StructuredOutputsParams |
| 10 | from vllm.v1.structured_output.backend_types import ( |
| 11 | StructuredOutputGrammar, |
| 12 | StructuredOutputKey, |
| 13 | StructuredOutputOptions, |
| 14 | ) |
| 15 | |
| 16 | if TYPE_CHECKING: |
| 17 | from vllm.reasoning import ReasoningParser |
| 18 | |
| 19 | |
| 20 | @dataclasses.dataclass |
| 21 | class StructuredOutputRequest: |
| 22 | params: StructuredOutputsParams |
| 23 | _grammar: ( |
| 24 | Future[StructuredOutputGrammar] | StructuredOutputGrammar | Exception | None |
| 25 | ) = None |
| 26 | reasoning_ended: bool | None = None |
| 27 | reasoning_parser_kwargs: dict[str, Any] | None = None |
| 28 | # Cached per request; do not share reasoning parsers across requests because |
| 29 | # their behavior can depend on reasoning_parser_kwargs. |
| 30 | reasoner: "ReasoningParser | None" = None |
| 31 | |
| 32 | @staticmethod |
| 33 | def from_sampling_params( |
| 34 | sampling_params: SamplingParams | None, |
| 35 | ) -> "StructuredOutputRequest | None": |
| 36 | if sampling_params is None: |
| 37 | return None |
| 38 | params = sampling_params.structured_outputs |
| 39 | if not params or params.all_constraints_none(): |
| 40 | return None |
| 41 | return StructuredOutputRequest(params=params) |
| 42 | |
| 43 | def _check_grammar_completion(self) -> bool: |
| 44 | if isinstance(self._grammar, Future): |
| 45 | if not self._grammar.done(): |
| 46 | return False |
| 47 | try: |
| 48 | self._grammar = self._grammar.result() |
| 49 | except Exception as e: |
| 50 | self._grammar = e |
| 51 | return True |
| 52 | |
| 53 | @property |
| 54 | def is_grammar_ready(self) -> bool: |
| 55 | return self._check_grammar_completion() |
| 56 | |
| 57 | @property |
| 58 | def grammar(self) -> StructuredOutputGrammar | Exception | None: |
| 59 | if not self._check_grammar_completion(): |
| 60 | return None |
| 61 | return cast(StructuredOutputGrammar | Exception | None, self._grammar) |
| 62 | |
| 63 | @grammar.setter |
| 64 | def grammar( |
| 65 | self, grammar: StructuredOutputGrammar | Future[StructuredOutputGrammar] |
| 66 | ) -> None: |
| 67 | self._grammar = grammar |
| 68 | |
| 69 | @functools.cached_property |
| 70 | def structured_output_key(self) -> StructuredOutputKey: |
| 71 | return get_structured_output_key(self.params) |
| 72 | |
| 73 | |
| 74 | def get_structured_output_key(params: StructuredOutputsParams) -> StructuredOutputKey: |
| 75 | if params.json is not None: |
| 76 | if not isinstance(params.json, str): |
| 77 | json_str = json.dumps(params.json) |
| 78 | else: |
| 79 | json_str = params.json |
| 80 | return StructuredOutputOptions.JSON, json_str |
| 81 | if params.json_object: |
| 82 | return StructuredOutputOptions.JSON_OBJECT, "" |
| 83 | if params.regex is not None: |
| 84 | return StructuredOutputOptions.REGEX, params.regex |
| 85 | if params.choice is not None: |
| 86 | if not isinstance(params.choice, str): |
| 87 | json_str = json.dumps(params.choice) |
| 88 | else: |
| 89 | json_str = params.choice |
| 90 | return StructuredOutputOptions.CHOICE, json_str |
| 91 | if params.grammar is not None: |
| 92 | return StructuredOutputOptions.GRAMMAR, params.grammar |
| 93 | if params.structural_tag is not None: |
| 94 | return StructuredOutputOptions.STRUCTURAL_TAG, params.structural_tag |
| 95 | raise ValueError("No valid structured output parameter found") |