Skip to content

Commit 34d2d05

Browse files
authored
[BugFix][API] Handle wrapped reasoning parsers in usage accounting (vllm-project#10645)
### What this PR does / why we need it? Fixes vllm-project#10628. `OpenAIServingChat` usage accounting is a common API-server path, but the previous backport lived entirely in `patch_minimax_usage_accounting.py`. When other parser combinations failed, the stack trace pointed at the MiniMax patch even though the failing object was a generic vLLM parser-manager wrapper. This PR splits that ownership: - `patch_chat_usage_accounting.py` owns the generic OpenAI chat usage wrapper and `UsageInfo` extension. - `patch_minimax_usage_accounting.py` only patches MiniMax-M2 reasoning parsers to provide MiniMax-specific reasoning-token counts. The generic usage wrapper now unwraps parser-manager objects through `reasoning_parser` before calling `count_reasoning_tokens()`. If the resulting parser still has no token counter, usage accounting skips reasoning-token details instead of raising a 500. MiniMax concrete and unified parser paths continue to count reasoning tokens. - vLLM version: v0.22.1 - vLLM main: vllm-project/vllm@967c5c3 Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com> Co-authored-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>
1 parent 9076e99 commit 34d2d05

5 files changed

Lines changed: 475 additions & 376 deletions

File tree

tests/ut/patch/platform/test_patch_minimax_usage_accounting.py

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@
44
from types import SimpleNamespace
55

66
import pytest
7+
from vllm.parser.parser_manager import ParserManager
78
from vllm.reasoning.minimax_m2_reasoning_parser import (
89
MiniMaxM2AppendThinkReasoningParser,
910
MiniMaxM2ReasoningParser,
1011
)
1112

1213
from vllm_ascend.patch.platform import (
13-
patch_minimax_usage_accounting as minimax_usage_patch,
14+
patch_chat_usage_accounting as usage_patch,
1415
)
16+
from vllm_ascend.patch.platform import patch_minimax_usage_accounting # noqa: F401
1517

1618

1719
class FakeTokenizer:
1820
def get_vocab(self):
1921
return {
2022
"<think>": 1,
2123
"</think>": 2,
24+
"<minimax:tool_call>": 3,
25+
"</minimax:tool_call>": 4,
2226
}
2327

2428

@@ -74,7 +78,7 @@ def test_count_reasoning_tokens(
7478

7579

7680
def test_update_usage_tracking_state_tracks_prompt_and_completion_tokens():
77-
state = minimax_usage_patch._create_usage_tracking_state(
81+
state = usage_patch._create_usage_tracking_state(
7882
num_choices=2,
7983
reasoning_parser=None,
8084
)
@@ -89,7 +93,7 @@ def test_update_usage_tracking_state_tracks_prompt_and_completion_tokens():
8993
],
9094
)
9195

92-
minimax_usage_patch._update_usage_tracking_state(state, res)
96+
usage_patch._update_usage_tracking_state(state, res)
9397

9498
assert state.num_prompt_tokens == 3
9599
assert state.num_cached_tokens == 4
@@ -99,7 +103,7 @@ def test_update_usage_tracking_state_tracks_prompt_and_completion_tokens():
99103

100104
def test_make_usage_info_injects_reasoning_token_details():
101105
fake_serving = SimpleNamespace(enable_prompt_tokens_details=True)
102-
usage = minimax_usage_patch._make_usage_info(
106+
usage = usage_patch._make_usage_info(
103107
fake_serving,
104108
prompt_tokens=3,
105109
completion_tokens=4,
@@ -115,7 +119,7 @@ def test_make_usage_info_injects_reasoning_token_details():
115119

116120
def test_make_usage_info_injects_zero_cached_tokens():
117121
fake_serving = SimpleNamespace(enable_prompt_tokens_details=True)
118-
usage = minimax_usage_patch._make_usage_info(
122+
usage = usage_patch._make_usage_info(
119123
fake_serving,
120124
prompt_tokens=3,
121125
completion_tokens=4,
@@ -132,13 +136,13 @@ class FakeServing:
132136
enable_prompt_tokens_details = False
133137

134138
def _make_usage_info(self, **kwargs):
135-
return minimax_usage_patch._make_usage_info(self, **kwargs)
139+
return usage_patch._make_usage_info(self, **kwargs)
136140

137141
class FakeReasoningParser:
138142
def count_reasoning_tokens(self, token_ids):
139143
return 2 if 2 in token_ids else 0
140144

141-
state = minimax_usage_patch._create_usage_tracking_state(
145+
state = usage_patch._create_usage_tracking_state(
142146
num_choices=2,
143147
reasoning_parser=FakeReasoningParser(),
144148
)
@@ -148,7 +152,7 @@ def count_reasoning_tokens(self, token_ids):
148152
state.completion_tokens = [4, 2]
149153
state.raw_output_token_ids = [[10, 11, 2, 20], [30, 31]]
150154

151-
usage = minimax_usage_patch._make_full_response_usage(FakeServing(), state)
155+
usage = usage_patch._make_full_response_usage(FakeServing(), state)
152156

153157
assert usage.prompt_tokens == 3
154158
assert usage.completion_tokens == 6
@@ -157,12 +161,63 @@ def count_reasoning_tokens(self, token_ids):
157161
assert usage.prompt_tokens_details is None
158162

159163

164+
def test_make_full_response_usage_accepts_wrapped_reasoning_parser():
165+
class FakeServing:
166+
enable_prompt_tokens_details = False
167+
168+
def _make_usage_info(self, **kwargs):
169+
return usage_patch._make_usage_info(self, **kwargs)
170+
171+
class FakeReasoningParser:
172+
def count_reasoning_tokens(self, token_ids):
173+
return token_ids.index(2) if 2 in token_ids else len(token_ids)
174+
175+
state = usage_patch._create_usage_tracking_state(
176+
num_choices=1,
177+
reasoning_parser=SimpleNamespace(reasoning_parser=FakeReasoningParser()),
178+
)
179+
state.num_prompt_tokens = 3
180+
state.final_res = SimpleNamespace(num_cached_tokens=None)
181+
state.completion_tokens = [4]
182+
state.raw_output_token_ids = [[10, 11, 2, 20]]
183+
184+
usage = usage_patch._make_full_response_usage(FakeServing(), state)
185+
186+
assert usage.completion_tokens_details.reasoning_tokens == 2
187+
188+
189+
def test_count_reasoning_tokens_accepts_minimax_unified_parser():
190+
parser_cls = ParserManager.get_parser(
191+
tool_parser_name="minimax_m2",
192+
reasoning_parser_name="minimax_m2",
193+
enable_auto_tools=True,
194+
model_name="MiniMax-M2",
195+
)
196+
parser = parser_cls(FakeTokenizer(), tools=[])
197+
198+
assert not hasattr(parser, "count_reasoning_tokens")
199+
assert usage_patch._count_reasoning_tokens_for_usage([10, 11, 2, 20], parser) == 2
200+
201+
202+
def test_count_reasoning_tokens_accepts_deepseek_parser_manager_wrapper():
203+
parser_cls = ParserManager.get_parser(
204+
tool_parser_name="deepseek_v4",
205+
reasoning_parser_name="deepseek_v4",
206+
enable_auto_tools=True,
207+
model_name="DeepSeek-V4",
208+
)
209+
parser = parser_cls(FakeTokenizer(), tools=[])
210+
211+
assert not hasattr(parser, "count_reasoning_tokens")
212+
assert usage_patch._count_reasoning_tokens_for_usage([10, 11], parser) == 0
213+
214+
160215
def test_stream_usage_details_are_injected_without_replacing_source():
161216
class FakeReasoningParser:
162217
def count_reasoning_tokens(self, token_ids):
163218
return token_ids.index(2) if 2 in token_ids else len(token_ids)
164219

165-
state = minimax_usage_patch._create_usage_tracking_state(
220+
state = usage_patch._create_usage_tracking_state(
166221
num_choices=1,
167222
reasoning_parser=FakeReasoningParser(),
168223
enable_prompt_tokens_details=True,
@@ -181,7 +236,7 @@ def count_reasoning_tokens(self, token_ids):
181236
},
182237
}
183238

184-
data = minimax_usage_patch._inject_stream_usage_details(
239+
data = usage_patch._inject_stream_usage_details(
185240
f"data: {json.dumps(chunk)}\n\n",
186241
state,
187242
)
@@ -193,12 +248,12 @@ def count_reasoning_tokens(self, token_ids):
193248
assert payload["usage"]["prompt_tokens_details"] == {
194249
"cached_tokens": 0,
195250
}
196-
assert not hasattr(minimax_usage_patch, "_extract_class_method_source")
197-
assert not hasattr(minimax_usage_patch, "_patch_chat_completion_stream_generator")
251+
assert not hasattr(usage_patch, "_extract_class_method_source")
252+
assert not hasattr(usage_patch, "_patch_chat_completion_stream_generator")
198253

199254

200255
def test_stream_usage_details_inject_prompt_details_without_reasoning():
201-
state = minimax_usage_patch._create_usage_tracking_state(
256+
state = usage_patch._create_usage_tracking_state(
202257
num_choices=1,
203258
reasoning_parser=None,
204259
enable_prompt_tokens_details=True,
@@ -216,7 +271,7 @@ def test_stream_usage_details_inject_prompt_details_without_reasoning():
216271
},
217272
}
218273

219-
data = minimax_usage_patch._inject_stream_usage_details(
274+
data = usage_patch._inject_stream_usage_details(
220275
f"data: {json.dumps(chunk)}\n\n",
221276
state,
222277
)

vllm_ascend/patch/__init__.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -144,25 +144,39 @@
144144
# Drop the alias once upstream registry includes it or the checkpoint
145145
# standardizes architecture strings.
146146
#
147-
# ** 7. File: platform/patch_minimax_usage_accounting.py**
147+
# ** 7. File: platform/patch_chat_usage_accounting.py**
148148
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
149149
# 1. `vllm.entrypoints.openai.chat_completion.serving.OpenAIServingChat`
150-
# `vllm.reasoning.minimax_m2_reasoning_parser`
151150
# Why:
152-
# MiniMax-M2 reasoning usage accounting needs to report
151+
# Chat usage accounting needs to report
153152
# `completion_tokens_details.reasoning_tokens` for both streaming and
154153
# non-streaming chat completions.
155154
# How:
156-
# Monkey-patch MiniMax reasoning token counters, extend `UsageInfo`, and
157-
# update chat usage construction to count reasoning tokens from raw output
158-
# token ids.
155+
# Extend `UsageInfo` and update chat usage construction to count reasoning
156+
# tokens from raw output token ids when the active reasoning parser
157+
# supports token counting.
159158
# Related PR (if no, explain why):
160159
# https://github.com/vllm-project/vllm/pull/37955
161160
# Future Plan:
162161
# Remove this patch once the runtime vLLM version contains the upstream
163-
# MiniMax usage-accounting fix.
162+
# reasoning usage-accounting support.
164163
#
165-
# ** 7a. File: platform/patch_glm_tool_call_streaming.py**
164+
# ** 7a. File: platform/patch_minimax_usage_accounting.py**
165+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
166+
# 1. `vllm.reasoning.minimax_m2_reasoning_parser`
167+
# Why:
168+
# MiniMax-M2 reasoning parsers need token-count support for the generic
169+
# chat usage-accounting wrapper.
170+
# How:
171+
# Monkey-patch MiniMax reasoning token counters so usage accounting can
172+
# derive reasoning-token counts from raw output token ids.
173+
# Related PR (if no, explain why):
174+
# https://github.com/vllm-project/vllm/pull/37955
175+
# Future Plan:
176+
# Remove this patch once the runtime vLLM version contains the upstream
177+
# MiniMax reasoning-token counting fix.
178+
#
179+
# ** 7b. File: platform/patch_glm_tool_call_streaming.py**
166180
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
167181
# 1. `vllm.entrypoints.openai.chat_completion.serving.OpenAIServingChat`
168182
# Why:
@@ -182,7 +196,7 @@
182196
# Remove this patch once the supported vLLM version contains the upstream
183197
# GLM tool-call final chunk fixes.
184198
#
185-
# ** 7b. File: platform/patch_glm47_tool_call_parser.py**
199+
# ** 7c. File: platform/patch_glm47_tool_call_parser.py**
186200
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
187201
# 1. `vllm.tool_parsers.glm47_moe_tool_parser.Glm47MoeModelToolParser`
188202
# Why:
@@ -200,7 +214,7 @@
200214
# Remove this patch once the supported vLLM version contains the upstream
201215
# GLM47 inline zero-argument streaming parser fix.
202216
#
203-
# ** 7c. File: platform/patch_anthropic_system_message.py**
217+
# ** 7d. File: platform/patch_anthropic_system_message.py**
204218
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
205219
# 1. `vllm.entrypoints.anthropic.protocol.AnthropicMessage`
206220
# `vllm.entrypoints.anthropic.serving.AnthropicServingMessages`

vllm_ascend/patch/platform/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
else:
3030
import vllm_ascend.patch.platform.patch_mamba_config_310 # noqa
3131
import vllm_ascend.patch.platform.patch_minimax_m2_config # noqa
32+
import vllm_ascend.patch.platform.patch_chat_usage_accounting # noqa
3233
import vllm_ascend.patch.platform.patch_minimax_usage_accounting # noqa
3334
import vllm_ascend.patch.platform.patch_glm_tool_call_streaming # noqa
3435
import vllm_ascend.patch.platform.patch_glm47_tool_call_parser # noqa

0 commit comments

Comments
 (0)