[Bugfix] Fix thinking_token_budget not enforced after natural </think> re-entry (#45984)

Signed-off-by: Ashwin Giridharan <girida@amazon.com>

Ashwin Giridharan committed Jul 10, 2026 at 15:47 UTC ed908cf0a08af20839b82dc0638dae91b1ad630b
3 files changed +453 -47
tests/v1/logits_processors/test_correctness.py
+407 -3
@@ -145,6 +145,7 @@ def _generate_fake_sampling_metadata(
145 vllm_config.scheduler_config.max_num_seqs,
146 num_spec,
147 device,
148 + is_pin_memory=False,
149 )
150 fake_sampling_metadata = SamplingMetadata(
151 temperature=torch.full((batch_size,), 0.0),
@@ -879,6 +880,7 @@ def test_maybe_create_thinking_budget_holder_without_reasoning():
880 cfg.scheduler_config.max_num_seqs,
881 0,
882 torch.device("cpu"),
883 + is_pin_memory=False,
884 )
885 is None
886 )
@@ -1251,12 +1253,20 @@ def test_thinking_budget_long_thinking_section_end_marker_found_at_correct_index
1253 out.append(tok)
1254 h.update_state([out], None, None)
1255 assert h._state[0]["end_thinking"] == -1 # not present yet
1254 - expected_end_idx = len(out) # marker appended next
1256 out.extend(end)
1257 h.update_state([out], None, None)
1258
1258 - assert h._state[0]["start_thinking"] == 0
1259 - assert h._state[0]["end_thinking"] == expected_end_idx
1259 + # After a natural exit, start_thinking and end_thinking are reset to -1
1260 + # (state machine prepared for next block). Verify the exit happened at the
1261 + # correct position by checking scan_offset (set to len(output) after exit).
1262 + assert not h._state[0]["in_think"], "Should have exited think mode after </think>"
1263 + assert h._state[0]["start_thinking"] == -1, (
1264 + "start_thinking should be reset after natural exit"
1265 + )
1266 + assert h._state[0]["scan_offset"] == len(out), (
1267 + f"scan_offset should point past the end marker; "
1268 + f"expected {len(out)}, got {h._state[0]['scan_offset']}"
1269 + )
1270
1271
1272 # --- Thinking budget re-entry tests (issue #43708) ---
@@ -1381,3 +1391,397 @@ class TestThinkingBudgetReentry:
1391
1392 assert not holder._state[0]["in_end"]
1393 assert not holder._state[0]["in_think"]
1394 +
1395 +
1396 +# --- Thinking budget natural-end re-entry tests (issue #45974) ---
1397 +# After a model naturally emits </think> before exhausting its budget,
1398 +# subsequent <think> blocks must still be tracked and budget-enforced.
1399 +
1400 +
1401 +class TestThinkingBudgetNaturalEndReentry:
1402 + """Tests for thinking budget enforcement across multiple think blocks.
1403 +
1404 + Covers both natural-end re-entry (issue #45974) and forced-end re-entry
1405 + (issue #43708).
1406 + """
1407 +
1408 + THINK_START = 100
1409 + THINK_END_SINGLE = [200]
1410 + THINK_END_MULTI = [200, 201, 202]
1411 + BUDGET = 10
1412 + CONTENT_TOKEN = 50
1413 + THINK_TOKEN = 60
1414 +
1415 + @staticmethod
1416 + def _make_holder(end_token_ids):
1417 + from dataclasses import dataclass
1418 +
1419 + from vllm.v1.sample.thinking_budget_state import (
1420 + ThinkingBudgetStateHolder,
1421 + )
1422 +
1423 + @dataclass
1424 + class FakeReasoningConfig:
1425 + reasoning_start_token_ids: list[int]
1426 + reasoning_end_token_ids: list[int]
1427 + enabled: bool = True
1428 +
1429 + cfg = FakeReasoningConfig(
1430 + reasoning_start_token_ids=[TestThinkingBudgetNaturalEndReentry.THINK_START],
1431 + reasoning_end_token_ids=end_token_ids,
1432 + )
1433 + return ThinkingBudgetStateHolder(
1434 + reasoning_config=cfg,
1435 + max_num_seqs=8,
1436 + num_spec_tokens=0,
1437 + device=torch.device("cpu"),
1438 + is_pin_memory=False,
1439 + )
1440 +
1441 + @staticmethod
1442 + def _sync_batch(holder, budget):
1443 + from unittest.mock import MagicMock
1444 +
1445 + params = MagicMock()
1446 + params.thinking_token_budget = budget
1447 + batch_update = MagicMock(
1448 + removed=[],
1449 + added=[(0, params, None, [])],
1450 + moved=[],
1451 + )
1452 + holder.sync_batch(batch_update)
1453 +
1454 + @staticmethod
1455 + def _step(holder, output_tok_ids):
1456 + holder.update_state(
1457 + output_token_ids=[output_tok_ids],
1458 + spec_token_ids=None,
1459 + repeat_indices=None,
1460 + )
1461 +
1462 + # --- Natural-end re-entry tests ---
1463 +
1464 + def test_natural_end_reentry_single_token(self):
1465 + """After natural </think>, a new <think> block must be enforced."""
1466 + holder = self._make_holder(self.THINK_END_SINGLE)
1467 + self._sync_batch(holder, self.BUDGET)
1468 +
1469 + output = []
1470 +
1471 + # Block 1: 6 tokens (under budget) + natural </think>
1472 + output.append(self.THINK_START)
1473 + self._step(holder, list(output))
1474 + for _ in range(6):
1475 + output.append(self.THINK_TOKEN)
1476 + self._step(holder, list(output))
1477 + output.append(self.THINK_END_SINGLE[0])
1478 + self._step(holder, list(output))
1479 +
1480 + assert not holder._state[0]["in_end"]
1481 + assert not holder._state[0]["in_think"]
1482 +
1483 + # Some content
1484 + for _ in range(3):
1485 + output.append(self.CONTENT_TOKEN)
1486 + self._step(holder, list(output))
1487 +
1488 + # Block 2: re-entry — should be budget-tracked
1489 + output.append(self.THINK_START)
1490 + self._step(holder, list(output))
1491 + for _ in range(self.BUDGET + 1):
1492 + output.append(self.THINK_TOKEN)
1493 + self._step(holder, list(output))
1494 +
1495 + assert holder._state[0]["in_end"], (
1496 + "Second thinking block after natural end must be budget-enforced"
1497 + )
1498 +
1499 + def test_natural_end_reentry_multi_token(self):
1500 + """Multi-token </think> natural end still allows Block 2 enforcement."""
1501 + holder = self._make_holder(self.THINK_END_MULTI)
1502 + self._sync_batch(holder, self.BUDGET)
1503 +
1504 + output = []
1505 +
1506 + # Block 1: 4 tokens + natural multi-token </think>
1507 + output.append(self.THINK_START)
1508 + self._step(holder, list(output))
1509 + for _ in range(4):
1510 + output.append(self.THINK_TOKEN)
1511 + self._step(holder, list(output))
1512 + for tok in self.THINK_END_MULTI:
1513 + output.append(tok)
1514 + self._step(holder, list(output))
1515 +
1516 + assert not holder._state[0]["in_think"]
1517 +
1518 + # Content
1519 + for _ in range(5):
1520 + output.append(self.CONTENT_TOKEN)
1521 + self._step(holder, list(output))
1522 +
1523 + # Block 2: re-entry
1524 + output.append(self.THINK_START)
1525 + self._step(holder, list(output))
1526 + for _ in range(self.BUDGET + 1):
1527 + output.append(self.THINK_TOKEN)
1528 + self._step(holder, list(output))
1529 +
1530 + assert holder._state[0]["in_end"], (
1531 + "Second block after multi-token natural end must be enforced"
1532 + )
1533 +
1534 + def test_natural_end_immediate_reentry(self):
1535 + """Natural </think> followed immediately by <think> with no content."""
1536 + holder = self._make_holder(self.THINK_END_SINGLE)
1537 + self._sync_batch(holder, self.BUDGET)
1538 +
1539 + output = []
1540 +
1541 + # Block 1: 5 tokens + natural end
1542 + output.append(self.THINK_START)
1543 + self._step(holder, list(output))
1544 + for _ in range(5):
1545 + output.append(self.THINK_TOKEN)
1546 + self._step(holder, list(output))
1547 + output.append(self.THINK_END_SINGLE[0])
1548 + self._step(holder, list(output))
1549 +
1550 + # Immediate re-entry — NO content between blocks
1551 + output.append(self.THINK_START)
1552 + self._step(holder, list(output))
1553 + for _ in range(self.BUDGET + 1):
1554 + output.append(self.THINK_TOKEN)
1555 + self._step(holder, list(output))
1556 +
1557 + assert holder._state[0]["in_end"], (
1558 + "Immediate re-entry after natural end must be budget-enforced"
1559 + )
1560 +
1561 + def test_multiple_natural_reentries(self):
1562 + """Block 1 natural -> Block 2 natural -> Block 3 must be enforced."""
1563 + holder = self._make_holder(self.THINK_END_SINGLE)
1564 + self._sync_batch(holder, self.BUDGET)
1565 +
1566 + output = []
1567 +
1568 + # Block 1: 4 tokens + natural end
1569 + output.append(self.THINK_START)
1570 + self._step(holder, list(output))
1571 + for _ in range(4):
1572 + output.append(self.THINK_TOKEN)
1573 + self._step(holder, list(output))
1574 + output.append(self.THINK_END_SINGLE[0])
1575 + self._step(holder, list(output))
1576 +
1577 + # Content
1578 + for _ in range(2):
1579 + output.append(self.CONTENT_TOKEN)
1580 + self._step(holder, list(output))
1581 +
1582 + # Block 2: 3 tokens + natural end
1583 + output.append(self.THINK_START)
1584 + self._step(holder, list(output))
1585 + for _ in range(3):
1586 + output.append(self.THINK_TOKEN)
1587 + self._step(holder, list(output))
1588 + output.append(self.THINK_END_SINGLE[0])
1589 + self._step(holder, list(output))
1590 +
1591 + # Content
1592 + for _ in range(2):
1593 + output.append(self.CONTENT_TOKEN)
1594 + self._step(holder, list(output))
1595 +
1596 + # Block 3: exceed budget
1597 + output.append(self.THINK_START)
1598 + self._step(holder, list(output))
1599 + for _ in range(self.BUDGET + 1):
1600 + output.append(self.THINK_TOKEN)
1601 + self._step(holder, list(output))
1602 +
1603 + assert holder._state[0]["in_end"], (
1604 + "Third block after two natural ends must be budget-enforced"
1605 + )
1606 +
1607 + def test_natural_then_forced_in_block_2(self):
1608 + """Block 1 ends naturally, Block 2 exceeds budget -> forced end."""
1609 + budget = 5
1610 + holder = self._make_holder(self.THINK_END_SINGLE)
1611 + self._sync_batch(holder, budget)
1612 +
1613 + output = []
1614 +
1615 + # Block 1: 3 tokens + natural end (under budget)
1616 + output.append(self.THINK_START)
1617 + self._step(holder, list(output))
1618 + for _ in range(3):
1619 + output.append(self.THINK_TOKEN)
1620 + self._step(holder, list(output))
1621 + output.append(self.THINK_END_SINGLE[0])
1622 + self._step(holder, list(output))
1623 +
1624 + assert not holder._state[0]["in_end"]
1625 +
1626 + # Content
1627 + for _ in range(3):
1628 + output.append(self.CONTENT_TOKEN)
1629 + self._step(holder, list(output))
1630 +
1631 + # Block 2: exceed budget
1632 + output.append(self.THINK_START)
1633 + self._step(holder, list(output))
1634 + for _ in range(budget + 1):
1635 + output.append(self.THINK_TOKEN)
1636 + self._step(holder, list(output))
1637 +
1638 + assert holder._state[0]["in_end"], (
1639 + "Block 2 must trigger forced end after natural end in Block 1"
1640 + )
1641 +
1642 + def test_budget_one_natural_end_reentry(self):
1643 + """Budget = 1: natural end with 0 think tokens, re-entry enforced."""
1644 + budget = 1
1645 + holder = self._make_holder(self.THINK_END_SINGLE)
1646 + self._sync_batch(holder, budget)
1647 +
1648 + output = []
1649 +
1650 + # Block 1: immediate natural end (0 think tokens)
1651 + output.append(self.THINK_START)
1652 + self._step(holder, list(output))
1653 + output.append(self.THINK_END_SINGLE[0])
1654 + self._step(holder, list(output))
1655 +
1656 + assert not holder._state[0]["in_end"]
1657 +
1658 + # Content
1659 + for _ in range(3):
1660 + output.append(self.CONTENT_TOKEN)
1661 + self._step(holder, list(output))
1662 +
1663 + # Block 2: 2 think tokens (should be enforced at budget=1)
1664 + output.append(self.THINK_START)
1665 + self._step(holder, list(output))
1666 + output.append(self.THINK_TOKEN)
1667 + self._step(holder, list(output))
1668 + output.append(self.THINK_TOKEN)
1669 + self._step(holder, list(output))
1670 +
1671 + assert holder._state[0]["in_end"], (
1672 + "Budget=1 must enforce after 1 token in re-entry block"
1673 + )
1674 +
1675 + def test_partial_end_sequence_in_content(self):
1676 + """Partial end sequence in content shouldn't trigger false exit."""
1677 + holder = self._make_holder(self.THINK_END_MULTI)
1678 + self._sync_batch(holder, self.BUDGET)
1679 +
1680 + output = []
1681 +
1682 + # Block 1: natural end with full sequence [200, 201, 202]
1683 + output.append(self.THINK_START)
1684 + self._step(holder, list(output))
1685 + for _ in range(3):
1686 + output.append(self.THINK_TOKEN)
1687 + self._step(holder, list(output))
1688 + for tok in self.THINK_END_MULTI:
1689 + output.append(tok)
1690 + self._step(holder, list(output))
1691 +
1692 + # Content with partial end sequence [200, 201] but not [200, 201, 202]
1693 + output.append(self.CONTENT_TOKEN)
1694 + self._step(holder, list(output))
1695 + output.append(200)
1696 + self._step(holder, list(output))
1697 + output.append(201)
1698 + self._step(holder, list(output))
1699 + output.append(self.CONTENT_TOKEN)
1700 + self._step(holder, list(output))
1701 +
1702 + # Block 2: re-entry must still be enforced
1703 + output.append(self.THINK_START)
1704 + self._step(holder, list(output))
1705 + for _ in range(self.BUDGET + 1):
1706 + output.append(self.THINK_TOKEN)
1707 + self._step(holder, list(output))
1708 +
1709 + assert holder._state[0]["in_end"], (
1710 + "Partial end tokens in content must not prevent Block 2 enforcement"
1711 + )
1712 +
1713 + def test_continue_thinking_natural_end_reentry(self):
1714 + """Prompt-prefill inside <think>: after natural end, Block 2 must be enforced.
1715 +
1716 + When a request arrives with the prompt already inside a <think> block,
1717 + continue_thinking is set to True. This test verifies that after a
1718 + natural </think> exit, continue_thinking is cleared and Block 2
1719 + enforcement works correctly.
1720 + """
1721 + from dataclasses import dataclass
1722 + from unittest.mock import MagicMock
1723 +
1724 + from vllm.v1.sample.thinking_budget_state import ThinkingBudgetStateHolder
1725 +
1726 + @dataclass
1727 + class FakeReasoningConfig:
1728 + reasoning_start_token_ids: list[int]
1729 + reasoning_end_token_ids: list[int]
1730 + enabled: bool = True
1731 +
1732 + cfg = FakeReasoningConfig(
1733 + reasoning_start_token_ids=[self.THINK_START],
1734 + reasoning_end_token_ids=self.THINK_END_SINGLE,
1735 + )
1736 + holder = ThinkingBudgetStateHolder(
1737 + reasoning_config=cfg,
1738 + max_num_seqs=8,
1739 + num_spec_tokens=0,
1740 + device=torch.device("cpu"),
1741 + is_pin_memory=False,
1742 + )
1743 +
1744 + # Simulate: prompt already inside <think> (sets continue_thinking=True)
1745 + prompt_tok_ids = [self.THINK_START]
1746 + params = MagicMock()
1747 + params.thinking_token_budget = self.BUDGET
1748 + batch_update = MagicMock(
1749 + removed=[],
1750 + added=[(0, params, prompt_tok_ids, [])],
1751 + moved=[],
1752 + )
1753 + holder.sync_batch(batch_update)
1754 + assert holder._state[0]["continue_thinking"], (
1755 + "Prompt inside <think> must set continue_thinking=True"
1756 + )
1757 +
1758 + # Block 1: emit some think tokens then natural </think>
1759 + output = list(prompt_tok_ids)
1760 + for _ in range(4):
1761 + output.append(self.THINK_TOKEN)
1762 + holder.update_state([output], None, None)
1763 + output.append(self.THINK_END_SINGLE[0])
1764 + holder.update_state([output], None, None)
1765 +
1766 + assert not holder._state[0]["in_think"], "Should have exited think mode"
1767 + assert not holder._state[0]["continue_thinking"], (
1768 + "continue_thinking must be cleared after natural end"
1769 + )
1770 +
1771 + # Content tokens
1772 + for _ in range(3):
1773 + output.append(self.CONTENT_TOKEN)
1774 + holder.update_state([output], None, None)
1775 +
1776 + # Block 2: re-entry must be detected and budget enforced
1777 + output.append(self.THINK_START)
1778 + holder.update_state([output], None, None)
1779 + for _ in range(self.BUDGET + 1):
1780 + output.append(self.THINK_TOKEN)
1781 + holder.update_state([output], None, None)
1782 +
1783 + assert holder._state[0]["in_end"], (
1784 + "Block 2 after natural end of prompt-prefill session must be enforced"
1785 + )
1786 +
1787 + # --- Forced-end re-entry tests ---
vllm/v1/sample/thinking_budget_state.py
+45 -44
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any
7 import torch
8
9 from vllm.platforms import current_platform
10 -from vllm.utils.torch_utils import PIN_MEMORY, async_tensor_h2d
10 +from vllm.utils.torch_utils import async_tensor_h2d
11 from vllm.v1.sample.logits_processor.interface import (
12 BatchUpdate,
13 MoveDirectionality,
@@ -22,11 +22,12 @@ def maybe_create_thinking_budget_state_holder(
22 max_num_seqs: int,
23 num_spec_tokens: int,
24 device: torch.device,
25 + is_pin_memory: bool,
26 ) -> "ThinkingBudgetStateHolder | None":
27 if reasoning_config is None:
28 return None
29 return ThinkingBudgetStateHolder(
29 - reasoning_config, max_num_seqs, num_spec_tokens, device, PIN_MEMORY
30 + reasoning_config, max_num_seqs, num_spec_tokens, device, is_pin_memory
31 )
32
33
@@ -173,19 +174,6 @@ class ThinkingBudgetStateHolder:
174 return i
175 return -1
176
176 - @staticmethod
177 - def _find_last_sequence_index_from(
178 - target_list: list[int], token_ids: list[int], search_start: int
179 - ) -> int:
180 - """Last occurrence of ``token_ids`` at or after ``search_start``."""
181 - if not token_ids:
182 - return -1
183 - lo = max(0, search_start)
184 - for i in range(len(target_list) - len(token_ids), lo - 1, -1):
185 - if target_list[i : i + len(token_ids)] == token_ids:
186 - return i
187 - return -1
188 -
177 def _init_state_entry(
178 self, prompt_tok_ids: list[int] | None, thinking_token_budget: int
179 ) -> dict[str, Any]:
@@ -239,8 +227,6 @@ class ThinkingBudgetStateHolder:
227 "force_index": [],
228 "start_thinking": start_thinking,
229 "end_thinking": -1,
242 - "start_search_pos": 0,
243 - "end_search_pos": 0,
230 "in_spec_mode": False,
231 "bonus_token_forced": False,
232 "continue_thinking": continue_thinking,
@@ -256,41 +242,40 @@ class ThinkingBudgetStateHolder:
242 state["force_index"] = []
243 return
244
259 - output_tok_ids = state.get("output_tok_ids", [])
245 if state["start_thinking"] == -1:
261 - seq_len = len(self.think_start_token_ids)
246 scan_offset = state.get("scan_offset", 0)
263 - start_thinking = self._find_last_sequence_index_from(
264 - output_tok_ids,
265 - self.think_start_token_ids,
266 - max(scan_offset, state["start_search_pos"] - (seq_len - 1)),
247 + output_slice = state.get("output_tok_ids", [])[scan_offset:]
248 + start_thinking = self._find_last_sequence_index(
249 + output_slice, self.think_start_token_ids
250 )
268 - if start_thinking >= 0 and scan_offset > 0:
269 - # Re-entry after a forced end: budget was already exhausted
270 - # in a prior block, so immediately force-close this one.
271 - # scan_offset > 0 is only set after forced-end completion
272 - # (never after natural end), so this won't block legitimate
273 - # re-entries where budget remains.
274 - state["start_thinking"] = start_thinking
275 - state["in_think"] = False
276 - state["in_end"] = True
277 - state["end_count"] = 0
278 - state["force_index"] = [0]
279 - return
251 + if start_thinking >= 0:
252 + start_thinking += scan_offset
253 state["start_thinking"] = start_thinking
281 - if start_thinking == -1:
282 - state["start_search_pos"] = len(output_tok_ids)
254 if state["end_thinking"] == -1:
284 - seq_len = len(self.think_end_token_ids)
255 scan_offset = state.get("scan_offset", 0)
286 - end_thinking = self._find_last_sequence_index_from(
287 - output_tok_ids,
288 - self.think_end_token_ids,
289 - max(scan_offset, state["end_search_pos"] - (seq_len - 1)),
256 + output_slice = state.get("output_tok_ids", [])[scan_offset:]
257 + end_thinking = self._find_last_sequence_index(
258 + output_slice, self.think_end_token_ids
259 )
260 + if end_thinking >= 0:
261 + end_thinking += scan_offset
262 state["end_thinking"] = end_thinking
292 - if end_thinking == -1:
293 - state["end_search_pos"] = len(output_tok_ids)
263 +
264 + if (
265 + not state.get("in_end", False)
266 + and state["start_thinking"] >= 0
267 + and state["end_thinking"] >= 0
268 + and state["end_thinking"] > state["start_thinking"]
269 + and not state.get("continue_thinking", False)
270 + ):
271 + state["in_think"] = False
272 + state["think_count"] = 0
273 + state["continue_thinking"] = False
274 + state["start_thinking"] = -1
275 + state["end_thinking"] = -1
276 + state["scan_offset"] = len(state.get("output_tok_ids", []))
277 + state["check_count_down"] = state["thinking_token_budget"]
278 + return
279
280 if state["start_thinking"] == -1:
281 return
@@ -314,10 +299,18 @@ class ThinkingBudgetStateHolder:
299 predicted_countdown = current_step_countdown - len(state["spec_token_ids"]) - 1
300 # We only proceed further if we have counted down the thinking budget
301 # to 0 or less and when we are in the "in think" mode.
302 + # Exception: when continue_thinking=True and a natural </think> is
303 + # detected (end_thinking != -1), fall through to handle the exit —
304 + # even if the budget hasn't expired yet. For continue_thinking=False,
305 + # the early natural-end detection block above already handles it.
306 + natural_end_with_continue = (
307 + state.get("continue_thinking", False) and state["end_thinking"] != -1
308 + )
309 if (
310 not state.get("in_end", False)
311 and predicted_countdown >= 0
312 and state["start_thinking"] > -1
313 + and not natural_end_with_continue
314 ):
315 state["check_count_down"] = current_step_countdown
316 state["prev_output_length"] = len(state.get("output_tok_ids", []))
@@ -389,6 +382,10 @@ class ThinkingBudgetStateHolder:
382 # Case: ...<start>...<end>... - exiting think mode
383 state["in_think"] = False
384 state["think_count"] = 0
385 + state["continue_thinking"] = False
386 + state["start_thinking"] = -1
387 + state["end_thinking"] = -1
388 + state["scan_offset"] = len(state.get("output_tok_ids", []))
389
390 elif absolute_start_pos >= 0 and not state["continue_thinking"]:
391 # Found think start - entering think mode
@@ -400,6 +397,10 @@ class ThinkingBudgetStateHolder:
397 # Found think end - exiting think mode
398 state["in_think"] = False
399 state["think_count"] = 0
400 + state["continue_thinking"] = False
401 + state["start_thinking"] = -1
402 + state["end_thinking"] = -1
403 + state["scan_offset"] = len(state.get("output_tok_ids", []))
404
405 elif state["in_think"]:
406 # Continue thinking mode, increment count by new tokens
vllm/v1/worker/gpu_input_batch.py
+1
@@ -113,6 +113,7 @@ class InputBatch:
113 max_num_reqs,
114 num_spec_tokens,
115 device,
116 + PIN_MEMORY,
117 )
118 self.thinking_token_budget_reqs: set[str] = set()
119 self.is_pooling_model = is_pooling_model