main
py 135 lines 4.53 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from __future__ import annotations
5
6 from collections.abc import Sequence
7 from typing import TYPE_CHECKING
8
9 from vllm.entrypoints.generate.base.protocol import (
10 DeltaMessage,
11 FunctionCall,
12 )
13 from vllm.parser.abstract_parser import DelegatingParser
14 from vllm.reasoning.kimi_k3_reasoning_parser import KimiK3ReasoningParser
15
16 if TYPE_CHECKING:
17 from vllm.entrypoints.openai.chat_completion.protocol import (
18 ChatCompletionRequest,
19 )
20 from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
21
22
23 class KimiK3Parser(DelegatingParser):
24 """Compose the Kimi K3 reasoning and tool parsers for XTML output."""
25
26 # TODO: Switch Kimi K3 to the parser engine once its XTML reasoning/tool
27 # path is covered there.
28 def _extract_tool_calls(
29 self,
30 content: str | None,
31 request: ChatCompletionRequest | ResponsesRequest,
32 enable_auto_tools: bool = False,
33 ) -> tuple[list[FunctionCall] | None, str | None]:
34 if self._tool_parser is None or not enable_auto_tools:
35 return super()._extract_tool_calls(content, request, enable_auto_tools)
36
37 tool_call_info = self.extract_tool_calls(content or "", request=request)
38 if request.tool_choice == "none":
39 return [], tool_call_info.content
40 if not tool_call_info.tools_called:
41 return None, tool_call_info.content
42
43 tool_calls = [
44 FunctionCall(
45 id=tool_call.id,
46 name=tool_call.function.name,
47 arguments=tool_call.function.arguments,
48 )
49 for tool_call in tool_call_info.tool_calls
50 ]
51 parsed_content = tool_call_info.content
52 if parsed_content and parsed_content.strip() == "":
53 parsed_content = None
54 return tool_calls, parsed_content
55
56 def _extract_tool_calls_streaming(
57 self,
58 previous_text: str,
59 current_text: str,
60 delta_text: str,
61 previous_token_ids: Sequence[int],
62 current_token_ids: Sequence[int],
63 delta_token_ids: Sequence[int],
64 request: ChatCompletionRequest | ResponsesRequest,
65 tool_call_idx: int | None = None,
66 tool_call_id_type: str = "random",
67 function_name_returned: bool = False,
68 ) -> tuple[DeltaMessage | None, bool]:
69 if request.tool_choice != "none":
70 return super()._extract_tool_calls_streaming(
71 previous_text,
72 current_text,
73 delta_text,
74 previous_token_ids,
75 current_token_ids,
76 delta_token_ids,
77 request,
78 tool_call_idx=tool_call_idx,
79 tool_call_id_type=tool_call_id_type,
80 function_name_returned=function_name_returned,
81 )
82
83 delta_message = self.extract_tool_calls_streaming(
84 previous_text,
85 current_text,
86 delta_text,
87 previous_token_ids,
88 current_token_ids,
89 delta_token_ids,
90 request,
91 )
92 if delta_message is not None:
93 delta_message.tool_calls = []
94 return delta_message, False
95
96 def parse_delta(
97 self,
98 delta_text: str,
99 delta_token_ids: list[int],
100 request: ChatCompletionRequest | ResponsesRequest,
101 prompt_token_ids: list[int] | None = None,
102 *,
103 finished: bool,
104 ) -> DeltaMessage | None:
105 state = self._stream_state
106 previous_content = state.previous_text if state.reasoning_ended else ""
107 delta_message = super().parse_delta(
108 delta_text,
109 delta_token_ids,
110 request,
111 prompt_token_ids,
112 finished=finished,
113 )
114
115 if (
116 self._tool_parser is not None
117 or not isinstance(self._reasoning_parser, KimiK3ReasoningParser)
118 or not state.reasoning_ended
119 or delta_message is None
120 ):
121 return delta_message
122
123 stripped = self._reasoning_parser.strip_content_streaming(
124 previous_text=previous_content,
125 current_text=state.previous_text,
126 )
127 delta_message.content = stripped.content if stripped is not None else None
128 if (
129 delta_message.role is None
130 and delta_message.content is None
131 and delta_message.reasoning is None
132 and not delta_message.tool_calls
133 ):
134 return None
135 return delta_message