main
py 150 lines 4.78 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Iterable, Sequence
5
6 from openai.types.responses import ResponseFunctionToolCall
7
8 from vllm.entrypoints.chat_utils import ChatCompletionMessageParam
9 from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionRequest
10 from vllm.entrypoints.openai.responses.protocol import (
11 ResponseInputOutputItem,
12 ResponsesRequest,
13 )
14
15
16 def count_tool_calls(tool_calls: object) -> int:
17 if tool_calls is None:
18 return 0
19 if isinstance(tool_calls, (str, bytes, dict)):
20 return 1
21 if isinstance(tool_calls, Iterable):
22 return sum(1 for _ in tool_calls)
23 return 1
24
25
26 def count_chat_history_tool_calls(
27 messages: Sequence[ChatCompletionMessageParam],
28 ) -> int:
29 return sum(
30 count_tool_calls(msg.get("tool_calls"))
31 for msg in messages
32 if isinstance(msg, dict) and msg.get("role") == "assistant"
33 )
34
35
36 def count_response_history_tool_calls(
37 response_items: Sequence[ResponseInputOutputItem],
38 ) -> int:
39 count = 0
40 for item in response_items:
41 if isinstance(item, ResponseFunctionToolCall):
42 count += 1
43 continue
44
45 if isinstance(item, dict):
46 item_type = item.get("type")
47 if item_type == "function_call":
48 count += 1
49 elif item.get("role") == "assistant":
50 count += count_tool_calls(item.get("tool_calls"))
51
52 return count
53
54
55 def count_history_tool_calls(
56 request: ChatCompletionRequest | ResponsesRequest,
57 ) -> int:
58 if isinstance(request, ChatCompletionRequest):
59 return count_chat_history_tool_calls(request.messages)
60
61 request_input = request.input
62 if isinstance(request_input, str):
63 return 0
64
65 return count_response_history_tool_calls(request_input)
66
67
68 _JSON_WS = " \t\r\n"
69
70
71 def scan_json_object_end(raw: str, start: int) -> int | None:
72 """Return the end index (exclusive) of the JSON object starting at
73 ``raw[start]`` (which must be ``{``), or ``None`` when it is still
74 unterminated. String- and escape-aware so braces inside strings do not
75 mislead the depth count."""
76 depth = 0
77 in_string = False
78 escape = False
79 for i in range(start, len(raw)):
80 ch = raw[i]
81 if escape:
82 escape = False
83 continue
84 if in_string:
85 if ch == "\\":
86 escape = True
87 elif ch == '"':
88 in_string = False
89 continue
90 if ch == '"':
91 in_string = True
92 elif ch == "{":
93 depth += 1
94 elif ch == "}":
95 depth -= 1
96 if depth == 0:
97 return i + 1
98 return None
99
100
101 def extract_json_member_object(raw: str, key: str) -> str | None:
102 """Extract the raw text span of the top-level object-valued member ``key``
103 from a (possibly incomplete) ``{...}`` JSON wrapper.
104
105 Returns the verbatim substring (prefix-stable as the input grows, which
106 the engine's argument-delta diffing relies on), possibly an unterminated
107 object prefix; ``None`` when the value has not started. Raises
108 ``ValueError`` when the member is present but its value is not a JSON
109 object. A verbatim substring (not re-serialized JSON) is required because
110 the engine diffs successive converter outputs and expects each to extend
111 the previous one; the scanner is string/escape-aware so a matching literal
112 inside another value cannot mislead it, and it recovers a partial span when
113 the wrapper is truncated (where ``json.loads`` would fail)."""
114 depth = 0
115 in_string = False
116 escape = False
117 string_start = -1
118 last_string: str | None = None
119 for i, ch in enumerate(raw):
120 if escape:
121 escape = False
122 continue
123 if in_string:
124 if ch == "\\":
125 escape = True
126 elif ch == '"':
127 in_string = False
128 if depth == 1:
129 last_string = raw[string_start + 1 : i]
130 continue
131 if ch == '"':
132 in_string = True
133 string_start = i
134 elif ch == ":" and depth == 1 and last_string == key:
135 value_start = i + 1
136 while value_start < len(raw) and raw[value_start] in _JSON_WS:
137 value_start += 1
138 if value_start >= len(raw):
139 return None
140 if raw[value_start] != "{":
141 raise ValueError(f"member {key!r} must be a JSON object")
142 value_end = scan_json_object_end(raw, value_start)
143 if value_end is None:
144 return raw[value_start:]
145 return raw[value_start:value_end]
146 elif ch in "{[":
147 depth += 1
148 elif ch in "}]":
149 depth -= 1
150 return None