# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import json from collections.abc import Generator from unittest.mock import MagicMock, patch import partial_json_parser import pytest import regex as re from mistral_common.protocol.instruct.messages import AssistantMessage from mistral_common.protocol.instruct.request import InstructRequest from mistral_common.protocol.instruct.tool_calls import ( FunctionCall, ToolCall, ) from mistral_common.protocol.instruct.tool_calls import ( NamedToolChoice as MistralNamedToolChoice, ) from mistral_common.protocol.instruct.tool_calls import ( ToolChoice as MistralToolChoice, ) from mistral_common.protocol.instruct.tool_calls import ( ToolChoiceEnum as MistralToolChoiceEnum, ) from partial_json_parser.core.options import Allow from vllm.entrypoints.generate.base.protocol import ( DeltaFunctionCall, DeltaMessage, DeltaToolCall, ExtractedToolCallInformation, StructuralTagResponseFormat, ) from vllm.entrypoints.openai.chat_completion.protocol import ( ChatCompletionRequest, ) from vllm.parser.engine.events import EventType from vllm.parser.engine.streaming_parser_engine import StreamingParserEngine from vllm.parser.mistral import _DEFAULT_JSON_SCHEMA, MistralParser, mistral_config from vllm.parser.parser_manager import ParserManager from vllm.sampling_params import StructuredOutputsParams from vllm.tokenizers import TokenizerLike, get_tokenizer from vllm.tokenizers.detokenizer_utils import detokenize_incrementally from vllm.tokenizers.mistral import MistralTokenizer from vllm.tool_parsers.mistral_tool_parser import MistralToolParser _DUMMY_REQUEST = ChatCompletionRequest(messages=[], model="test") # Minimal request for the engine driver: tool_choice=None falls through to # auto extraction without triggering "tools must be set" validation. _ENGINE_REQUEST = ChatCompletionRequest(messages=[], model="test", tool_choice=None) @pytest.fixture(scope="module") def mistral_pre_v11_tokenizer(): MODEL = "mistralai/Mistral-7B-Instruct-v0.3" return get_tokenizer(tokenizer_name=MODEL) @pytest.fixture(scope="module") def mistral_tokenizer(): MODEL = "mistralai/Mistral-Small-3.2-24B-Instruct-2506" return get_tokenizer(tokenizer_name=MODEL, tokenizer_mode="mistral") @pytest.fixture def mistral_pre_v11_tool_parser(mistral_pre_v11_tokenizer): return MistralToolParser(mistral_pre_v11_tokenizer) @pytest.fixture def mistral_tool_parser(mistral_tokenizer): return MistralToolParser(mistral_tokenizer) @pytest.fixture def non_mistral_parser() -> MistralToolParser: mock_tokenizer = MagicMock() mock_tokenizer.get_vocab.return_value = {"[TOOL_CALLS]": 1} # Ensure the legacy (non-grammar) path is taken by MistralParser. mock_tokenizer.supports_grammar = False return MistralToolParser(mock_tokenizer) def assert_tool_calls( actual_tool_calls: list[ToolCall] | list[DeltaToolCall], expected_tool_calls: list[ToolCall], ): assert len(actual_tool_calls) == len(expected_tool_calls) for actual_tool_call, expected_tool_call in zip( actual_tool_calls, expected_tool_calls ): assert isinstance(actual_tool_call.id, str) assert len(actual_tool_call.id) == 9 if isinstance(actual_tool_call, ToolCall): assert actual_tool_call.type == "function" elif isinstance(actual_tool_call, DeltaToolCall): assert actual_tool_call.function is not None assert actual_tool_call.function.name is not None assert actual_tool_call.function.arguments is not None assert actual_tool_call.function is not None assert actual_tool_call.function.name == expected_tool_call.function.name, ( f"got wrong function name:${actual_tool_call.function.name}" ) actual_args = actual_tool_call.function.arguments # Streaming deltas (DeltaToolCall) may have partial JSON because the # engine holds back the final closing brace until finish() is called. # Apply partial_json_parser so the comparison works for both the # legacy (complete JSON) and engine (potentially partial) paths. if isinstance(actual_tool_call, DeltaToolCall) and actual_args is not None: actual_args = partial_json_parser.ensure_json( actual_args, Allow.OBJ | Allow.STR ) assert actual_args == expected_tool_call.function.arguments, ( f"got wrong function argument:${actual_tool_call.function.arguments}" ) def fix_tool_call_tokenization( tokens: list[int], mistral_tool_parser: MistralToolParser, mistral_tokenizer: TokenizerLike, ): """Replace textual token sequences for the control markers ([TOOL_CALLS] and [ARGS]) with their single special token IDs. Real generation emits these as control tokens; encoding the marker *string* can instead yield literal text tokens. The engine detects markers by id, so the test stream must carry the special ids. """ vocab = mistral_tokenizer.get_vocab() markers: list[tuple[str, int]] = [ ( mistral_tool_parser._parser_engine.bot_token, mistral_tool_parser._parser_engine.bot_token_id, ) ] args_id = vocab.get("[ARGS]") if args_id is not None: markers.append(("[ARGS]", args_id)) # (textual token sequence, special id) pairs to replace. replacements: list[tuple[list[int], int]] = [] for text, special_id in markers: if special_id is None: continue textual = mistral_tokenizer.encode(text=text, add_special_tokens=False) if textual: replacements.append((textual, special_id)) result_tokens: list[int] = [] i = 0 while i < len(tokens): for textual, special_id in replacements: if tokens[i : i + len(textual)] == textual: result_tokens.append(special_id) i += len(textual) break else: result_tokens.append(tokens[i]) i += 1 return result_tokens def encode_mistral_output(tokenizer: MistralTokenizer, text: str) -> list[int]: """Encode generated output with control markers as their special-token ids. Real generation emits ``[TOOL_CALLS]``/``[ARGS]`` as single control tokens. Encoding the marker *strings* can instead glue them to neighbours (e.g. ``}[``) and yield literal text tokens, which the id-based engine never sees. Split on the markers and insert their special ids so the token stream matches real generation. """ vocab = tokenizer.get_vocab() marker_ids = { "[TOOL_CALLS]": vocab.get("[TOOL_CALLS]"), "[ARGS]": vocab.get("[ARGS]"), } ids: list[int] = [] for part in re.split(r"(\[TOOL_CALLS\]|\[ARGS\])", text): if part in marker_ids: # Fall back to text encoding here and the id-based engine path # silently stops being exercised while the tests still pass. assert marker_ids[part] is not None, f"{part} missing from vocab" ids.append(marker_ids[part]) elif part: ids.extend(tokenizer.encode(part, add_special_tokens=False)) return ids def stream_delta_message_generator( mistral_tool_parser: MistralToolParser, mistral_tokenizer: TokenizerLike, model_output: str | None, tools: list[tuple[str, str]] | None, chunk_size: int = 1, driver: str = "legacy", reasoning_parser: str | None = None, ) -> Generator[DeltaMessage, None, None]: if ( isinstance(mistral_tokenizer, MistralTokenizer) and mistral_tokenizer.version >= 11 ): # With the newer versions of the tokenizer, # we cannot tokenize free text # so we need to create a list of messages to get tokenized assert tools is not None assistant_msg = AssistantMessage( tool_calls=[ ToolCall( function=FunctionCall( name=name, arguments=arg, ) ) for (name, arg) in tools ], ) request = InstructRequest( messages=[assistant_msg], ) all_token_ids = mistral_tokenizer.instruct.encode_instruct(request).tokens else: # Older versions of the tokenizer are # able to encode directly the model's output (free text) into tokens assert model_output is not None all_token_ids = mistral_tokenizer.encode(model_output, add_special_tokens=False) all_token_ids = fix_tool_call_tokenization( all_token_ids, mistral_tool_parser, mistral_tokenizer ) if isinstance(mistral_tokenizer, MistralTokenizer): # Real serving streams only generated tokens; encode_instruct adds # framing (BOS/EOS) the parser never receives. Strip them so the # id-based engine detection sees a realistic stream. if all_token_ids and all_token_ids[0] == mistral_tokenizer.bos_token_id: all_token_ids = all_token_ids[1:] if all_token_ids and all_token_ids[-1] == mistral_tokenizer.eos_token_id: all_token_ids = all_token_ids[:-1] if driver == "engine": _parser_cls = ParserManager.get_parser( tool_parser_name="mistral", reasoning_parser_name=reasoning_parser, enable_auto_tools=True, ) _engine_parser = _parser_cls(mistral_tokenizer, None) if reasoning_parser is not None: _req_with_tools = ChatCompletionRequest( messages=[], model="test", tools=SAMPLE_TOOLS_DICTS, tool_choice="auto", ) _engine_req = _engine_parser.adjust_request(_req_with_tools) else: _engine_req = _ENGINE_REQUEST previous_text = "" previous_tokens = None prefix_offset = 0 read_offset = 0 pending_text = "" pending_token_ids: list[int] = [] for i, delta_token in enumerate(all_token_ids): (new_tokens, delta_text, new_prefix_offset, new_read_offset) = ( detokenize_incrementally( tokenizer=mistral_tokenizer, all_input_ids=all_token_ids[: i + 1], prev_tokens=previous_tokens, prefix_offset=prefix_offset, read_offset=read_offset, skip_special_tokens=isinstance(mistral_tokenizer, MistralTokenizer), spaces_between_special_tokens=True, ) ) previous_tokens = ( previous_tokens + new_tokens if previous_tokens else new_tokens ) prefix_offset = new_prefix_offset read_offset = new_read_offset # Buffer tokens so each streamed delta can carry ``chunk_size`` tokens, # reproducing the multi-token deltas produced by async scheduling / # stream_interval > 1. pending_text += delta_text pending_token_ids.append(delta_token) if len(pending_token_ids) < chunk_size and i != len(all_token_ids) - 1: continue previous_token_ids = all_token_ids[: i + 1 - len(pending_token_ids)] current_token_ids = all_token_ids[: i + 1] current_text = previous_text + pending_text if driver == "legacy": delta_message = mistral_tool_parser.extract_tool_calls_streaming( previous_text, current_text, pending_text, previous_token_ids, current_token_ids, pending_token_ids, request=_DUMMY_REQUEST, ) else: # engine: per-delta windows, mirroring parse_delta behaviour delta_message = _engine_parser.parse_delta( delta_text=pending_text, delta_token_ids=list(pending_token_ids), request=_engine_req, prompt_token_ids=[1], finished=(i == len(all_token_ids) - 1), ) if delta_message: yield delta_message previous_text = current_text pending_text = "" pending_token_ids = [] # For the legacy driver the engine holds back the final closing brace # in _args_buffer until finish_streaming() is called, mirroring how # the real serving layer flushes at end of stream. if driver == "legacy": flush_delta = mistral_tool_parser.finish_streaming() if flush_delta: yield flush_delta @pytest.mark.parametrize( "parser_fixture", ["mistral_pre_v11_tool_parser", "mistral_tool_parser"], ids=["pre_v11", "v11"], ) def test_extract_tool_calls_no_tools(parser_fixture, request): parser = request.getfixturevalue(parser_fixture) model_output = "This is a test" result = parser.extract_tool_calls(model_output, request=_DUMMY_REQUEST) assert result == ExtractedToolCallInformation( tools_called=False, tool_calls=[], content=model_output ) @pytest.mark.parametrize( ids=[ "single_tool_add", "single_tool_weather", "argument_before_name", "argument_before_name_and_name_in_argument", "multiple_tools", "content_before_tool", "trailing_data_after_json", ], argnames=["model_output", "expected_tool_calls", "expected_content"], argvalues=[ ( """[TOOL_CALLS][{"name": "add", "arguments":{"a": 3.5, "b": 4}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ) ], None, ), ( """[TOOL_CALLS] [{"name": "get_current_weather", "arguments":{"city": "San Francisco", "state": "CA", "unit": "celsius"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], None, ), ( """[TOOL_CALLS] [{"arguments":{"city": "San Francisco", "state": "CA", "unit": "celsius"}, "name": "get_current_weather"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], None, ), ( """[TOOL_CALLS] [{"arguments":{"name": "John Doe"}, "name": "get_age"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_age", arguments=json.dumps( { "name": "John Doe", } ), ) ) ], None, ), ( """[TOOL_CALLS] [{"name": "add", "arguments": {"a": 3.5, "b": 4}}, {"name": "get_current_weather", "arguments":{"city": "San Francisco", "state": "CA", "unit": "celsius"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ), ], None, ), ( """Hello[TOOL_CALLS] [{"name": "add", "arguments":{"a": 1, "b": 2}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 1, "b": 2}) ) ) ], "Hello", ), ( """[TOOL_CALLS] [{"name": "get_current_weather", "arguments":{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}]\nextra trailing data""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( { "city": "Dallas", "state": "TX", "unit": "fahrenheit", } ), ) ) ], None, ), ], ) def test_extract_tool_calls_pre_v11_tokenizer( mistral_pre_v11_tool_parser, model_output, expected_tool_calls, expected_content ): extracted_tool_calls = mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert extracted_tool_calls.tools_called assert_tool_calls(extracted_tool_calls.tool_calls, expected_tool_calls) assert extracted_tool_calls.content == expected_content def test_extract_tool_calls_pre_v11_multiple_bot_tokens_raises( mistral_pre_v11_tool_parser, ): model_output = ( '[TOOL_CALLS] [{"name": "add", "arguments":{"a": 1}}]' '[TOOL_CALLS] [{"name": "sub", "arguments":{"b": 2}}]' ) with pytest.raises(ValueError, match="Only one BOT token"): mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) def test_extract_tool_calls_pre_v11_regex_fallback( mistral_pre_v11_tool_parser, ): """The regex fallback path finds valid JSON via regex when the primary raw_decode fails on leading junk. It should re-serialize arguments and return a valid tool call.""" model_output = ( '[TOOL_CALLS] junk [{"name": "add", "arguments":{"a": 1, "b": 2}}] trail' ) result = mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "add" assert result.tool_calls[0].function.arguments == json.dumps({"a": 1, "b": 2}) def test_extract_tool_calls_pre_v11_regex_fallback_fails( mistral_pre_v11_tool_parser, ): model_output = "[TOOL_CALLS] not json at all" result = mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result == ExtractedToolCallInformation( tools_called=False, tool_calls=[], content="not json at all" ) @pytest.mark.parametrize( ids=[ "single_tool_add", "single_tool_weather", "multiple_tool_calls", "single_tool_add_args", "single_tool_weather_args", "multiple_tool_calls_args", "content_before_tool_args", "complex", "wrong_json", "trailing_text_after_json", "trailing_text_after_args_json", ], argnames=["model_output", "expected_tool_calls", "expected_content"], argvalues=[ ( """[TOOL_CALLS]add_this_and_that{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], None, ), ( """[TOOL_CALLS]get_current_weather{"city": "San Francisco", "state": "CA", "unit": "celsius"}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], None, ), ( """[TOOL_CALLS]add{"a": 3.5, "b": 4}[TOOL_CALLS]multiply{"a": 3, "b": 6}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="multiply", arguments=json.dumps({"a": 3, "b": 6}) ) ), ], None, ), ( # v11+ emits an explicit [ARGS] separator between name and args. """[TOOL_CALLS]add_this_and_that[ARGS]{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], None, ), ( """[TOOL_CALLS]get_current_weather[ARGS]{"city": "San Francisco", "state": "CA", "unit": "celsius"}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], None, ), ( """[TOOL_CALLS]add[ARGS]{"a": 3.5, "b": 4}[TOOL_CALLS]multiply[ARGS]{"a": 3, "b": 6}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="multiply", arguments=json.dumps({"a": 3, "b": 6}) ) ), ], None, ), ( """hi[TOOL_CALLS]add[ARGS]{"a": 1, "b": 2}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 1, "b": 2}) ) ) ], "hi", ), ( # Complex """hi{hi[TOOL_CALLS]bash{"command": "print(\\"hello world!\\")\\nre.compile(r\'{}\')""", # noqa: E501 [ ToolCall( function=FunctionCall( name="bash", arguments=json.dumps( {"command": "print(\"hello world!\")\nre.compile(r'{}')"} )[:-2], ) ) ], "hi{hi", ), ( # Wrong json """hi{hi[TOOL_CALLS]bash{"command": "print(\\"hello world!\\")\\nre.compile(r\'{}\')"}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="bash", arguments=json.dumps( {"command": "print(\"hello world!\")\nre.compile(r'{}')"} ), ) ) ], "hi{hi", ), ( # gh#48975: trailing text after the JSON object must not leak into # arguments (v11+ name{args} format has no terminator). """[TOOL_CALLS]get_current_weather{"city": "San Francisco"}Estimating the weather...""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps({"city": "San Francisco"}), ) ) ], None, ), ( # gh#48975 with the explicit [ARGS] separator. """[TOOL_CALLS]get_current_weather[ARGS]{"city": "San Francisco"}Estimating the weather...""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps({"city": "San Francisco"}), ) ) ], None, ), ], ) def test_extract_tool_calls( mistral_tool_parser, model_output, expected_tool_calls, expected_content ): extracted_tool_calls = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert extracted_tool_calls.tools_called assert_tool_calls(extracted_tool_calls.tool_calls, expected_tool_calls) assert extracted_tool_calls.content == expected_content def test_extract_tool_calls_v11_without_args_skipped(mistral_tool_parser): # Before Stage 2: the legacy path skipped tool calls with no arg separator, # returning tools_called=True with an empty list. After Stage 2 the engine # path is used for v11; it recognises the name and emits a tool call with # empty arguments ("{}"). The semantic result is equivalent — a valid zero- # argument call — and is strictly more correct than silently dropping it. model_output = "[TOOL_CALLS]toolname_no_args" result = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "toolname_no_args" assert result.tool_calls[0].function.arguments == "{}" assert result.content is None def test_extract_tool_calls_malformed_name_before_marker_emits_empty_name( mistral_tool_parser, ): model_output = 'bash[TOOL_CALLS]{"command": "cat file.py"}' result = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "" assert result.tool_calls[0].function.arguments == json.dumps( {"command": "cat file.py"} ) assert result.content == "bash" def test_extract_tool_calls_well_formed_name_unaffected(mistral_tool_parser): model_output = '[TOOL_CALLS]bash[ARGS]{"command": "ls -la"}' result = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "bash" assert result.tool_calls[0].function.arguments == json.dumps({"command": "ls -la"}) assert result.content is None def test_extract_tool_calls_empty_name_unparsable_args_no_crash( mistral_tool_parser, ): # Empty name AND args that never form valid JSON: still emitted (raw # text as arguments) instead of raising. model_output = "bash[TOOL_CALLS]{not valid json at all" result = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "" assert result.tool_calls[0].function.arguments == "{not valid json at all" assert result.content == "bash" def test_extract_tool_calls_streaming_malformed_name_before_marker( mistral_tool_parser, ): # Streaming counterpart: once the args start the slot can never go back to # naming, so the (empty) name is already final and streams immediately # alongside the args. model_output = 'bash[TOOL_CALLS]{"command": "cat file.py"}' mid_delta = mistral_tool_parser.extract_tool_calls_streaming( previous_text="", current_text=model_output, delta_text=model_output, previous_token_ids=[], current_token_ids=[], delta_token_ids=[], request=_DUMMY_REQUEST, ) assert mid_delta is not None assert mid_delta.content == "bash" assert len(mid_delta.tool_calls) == 1 assert mid_delta.tool_calls[0].function.name == "" assert mid_delta.tool_calls[0].function.arguments == '{"command": "cat file.py"' final_delta = mistral_tool_parser.finish_streaming() assert final_delta is not None assert len(final_delta.tool_calls) == 1 assert final_delta.tool_calls[0].function.name is None assert final_delta.tool_calls[0].function.arguments == "}" def test_extract_tool_calls_parallel_malformed_then_well_formed( mistral_tool_parser, ): # A malformed empty-name call followed by a well-formed one must not # regress into brace-splitting: nested braces in the second call's # arguments must stay intact, and both calls must be correctly separated. model_output = ( 'bash[TOOL_CALLS]{"command": "ls"}' '[TOOL_CALLS]submit[ARGS]{"answer": {"nested": 1}}' ) result = mistral_tool_parser.extract_tool_calls( model_output, request=_DUMMY_REQUEST ) assert result.tools_called assert len(result.tool_calls) == 2 assert result.tool_calls[0].function.name == "" assert result.tool_calls[0].function.arguments == json.dumps({"command": "ls"}) assert result.tool_calls[1].function.name == "submit" assert result.tool_calls[1].function.arguments == json.dumps( {"answer": {"nested": 1}} ) assert result.content == "bash" def _test_extract_tool_calls_streaming( tool_parser, tokenizer, model_output, tools, expected_tool_calls, expected_content, driver: str = "legacy", reasoning_parser: str | None = None, ): other_content: str = "" function_names: list[str] = [] function_args_strs: list[str] = [] tool_call_idx: int = -1 tool_call_ids: list[str | None] = [] for delta_message in stream_delta_message_generator( tool_parser, tokenizer, model_output, tools, driver=driver, reasoning_parser=reasoning_parser, ): # role should never be streamed from tool parser assert not delta_message.role if delta_message.content: other_content += delta_message.content streamed_tool_calls = delta_message.tool_calls if streamed_tool_calls and len(streamed_tool_calls) > 0: # On the final finished=True delta the engine's _flush_engine_parsers # may append the flushed '}' as a separate DeltaToolCall for the # same index without coalescing. Merge entries sharing an index so # the "one diff per delta" invariant can be checked after merging. if len(streamed_tool_calls) > 1: assert len({tc.index for tc in streamed_tool_calls}) == 1, ( "only within-chunk finish/flush of one tool is allowed; " "distinct indices in one delta indicate a regression: " f"{[tc.index for tc in streamed_tool_calls]}" ) by_index: dict[int, DeltaToolCall] = {} for tc in streamed_tool_calls: existing = by_index.get(tc.index) if existing is None: by_index[tc.index] = tc else: if tc.id and not existing.id: existing.id = tc.id if tc.type and not existing.type: existing.type = tc.type if tc.function and existing.function and tc.function.arguments: existing.function.arguments = ( existing.function.arguments or "" ) + tc.function.arguments streamed_tool_calls = list(by_index.values()) # make sure only one diff is present - correct even for parallel assert len(streamed_tool_calls) == 1 tool_call = streamed_tool_calls[0] # if a new tool is being called, set up empty arguments if tool_call.index != tool_call_idx: tool_call_idx = tool_call.index function_args_strs.append("") tool_call_ids.append(None) # if a tool call ID is streamed, make sure one hasn't been already if tool_call.id and not tool_call_ids[tool_call.index]: tool_call_ids[tool_call.index] = tool_call.id # if parts of the function start being streamed if tool_call.function: # if the function name is defined, set it. it should be streamed # IN ENTIRETY, exactly one time. if tool_call.function.name: assert isinstance(tool_call.function.name, str) function_names.append(tool_call.function.name) if tool_call.function.arguments: # make sure they're a string and then add them to the list assert isinstance(tool_call.function.arguments, str) function_args_strs[tool_call.index] += tool_call.function.arguments assert other_content == expected_content actual_tool_calls = [ ToolCall( id=tool_call_id, function=FunctionCall( name=function_name, arguments=partial_json_parser.ensure_json( function_args_str, Allow.OBJ | Allow.STR ), ), ) for tool_call_id, function_name, function_args_str in zip( tool_call_ids, function_names, function_args_strs ) ] assert_tool_calls(actual_tool_calls, expected_tool_calls) @pytest.mark.parametrize("driver", ["legacy", "engine"]) @pytest.mark.parametrize( ids=[ "no_tools", "single_tool_add", "single_tool_add_strings", "single_tool_weather", "argument_before_name", "argument_before_name_and_name_in_argument", "multiple_tools", "trailing_data_after_json", ], argnames=["model_output", "expected_tool_calls", "expected_content"], argvalues=[ ("""This is a test""", [], """This is a test"""), ( """[TOOL_CALLS] [ {"name":"add" , "arguments" : {"a": 3, "b": 4} } ]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3, "b": 4}) ) ) ], "", ), ( """[TOOL_CALLS] [{"name": "add", "arguments":{"a": "3", "b": "4"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": "3", "b": "4"}) ) ) ], "", ), ( """[TOOL_CALLS] [{"name": "get_current_weather", "arguments": {"city": "San Francisco", "state": "CA", "unit": "celsius"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], "", ), ( """[TOOL_CALLS] [{"arguments": {"city": "San Francisco", "state": "CA", "unit": "celsius"}, "name": "get_current_weather"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], "", ), ( """[TOOL_CALLS] [{"arguments": {"name": "John Doe"}, "name": "get_age"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_age", arguments=json.dumps( { "name": "John Doe", } ), ) ) ], "", ), ( """[TOOL_CALLS] [{"name": "add", "arguments": {"a": 3.5, "b": 4}}, {"name": "get_current_weather", "arguments":{"city": "San Francisco", "state": "CA", "unit": "celsius"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ), ], "", ), ( """[TOOL_CALLS] [{"name": "get_current_weather", "arguments":{"city": "Dallas", "state": "TX", "unit": "fahrenheit"}}]\nextra trailing data""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( { "city": "Dallas", "state": "TX", "unit": "fahrenheit", } ), ) ) ], "\nextra trailing data", ), ], ) def test_extract_tool_calls_streaming_pre_v11_tokenizer( mistral_pre_v11_tool_parser, mistral_pre_v11_tokenizer, model_output, expected_tool_calls, expected_content, driver, ): _test_extract_tool_calls_streaming( mistral_pre_v11_tool_parser, mistral_pre_v11_tokenizer, model_output, None, expected_tool_calls, expected_content, driver=driver, ) @pytest.mark.parametrize( "driver,reasoning_parser", [("legacy", None), ("engine", None), ("engine", "mistral")], ids=["legacy", "engine_no_reasoning", "engine_with_reasoning"], ) @pytest.mark.parametrize( ids=[ "single_tool_add", "single_tool_add_strings", "multiple_tools", ], argnames=["tools", "expected_tool_calls", "expected_content"], argvalues=[ ( [("add", '{"a": 3, "b": 4}')], # [TOOL_CALLS]add{"a": 3, "b": 4} [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3, "b": 4}) ) ) ], "", ), ( [("add_two_strings", '{"a": "3", "b": "4"}')], # [TOOL_CALLS]add_two_strings{"a": "3", "b": "4"} [ ToolCall( function=FunctionCall( name="add_two_strings", arguments=json.dumps({"a": "3", "b": "4"}), ) ) ], "", ), ( [ ("add", '{"a": 3.5, "b": 4}'), ( "get_current_weather", '{"city": "San Francisco", "state": "CA", "unit": "celsius"}', # noqa: E501 ), ], # [TOOL_CALLS]add{"a": 3.5, "b": 4}[TOOL_CALLS]get_current_weather{"city": "San Francisco", "state": "CA", "unit": "celsius"} # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ), ], "", ), ], ) def test_extract_tool_calls_streaming( mistral_tool_parser, mistral_tokenizer, tools, expected_tool_calls, expected_content, driver, reasoning_parser, ): _test_extract_tool_calls_streaming( mistral_tool_parser, mistral_tokenizer, None, tools, expected_tool_calls, expected_content, driver=driver, reasoning_parser=reasoning_parser, ) # Drives extract_tool_calls_streaming directly (bespoke detokenization loop), # not via stream_delta_message_generator — driver parametrization not needed. def test_extract_tool_calls_streaming_v11_no_tools( mistral_tool_parser, mistral_tokenizer ): model_output = "This is a test" # add_special_tokens=False: real serving streams only generated tokens. all_token_ids = mistral_tokenizer.encode(model_output, add_special_tokens=False) skip_special = isinstance(mistral_tokenizer, MistralTokenizer) collected_content = "" previous_text = "" previous_tokens = None prefix_offset = 0 read_offset = 0 for i in range(len(all_token_ids)): current_token_ids = all_token_ids[: i + 1] previous_token_ids = all_token_ids[:i] delta_token_ids = [all_token_ids[i]] new_tokens, delta_text, prefix_offset, read_offset = detokenize_incrementally( tokenizer=mistral_tokenizer, all_input_ids=current_token_ids, prev_tokens=previous_tokens, prefix_offset=prefix_offset, read_offset=read_offset, skip_special_tokens=skip_special, spaces_between_special_tokens=True, ) current_text = previous_text + delta_text previous_tokens = ( previous_tokens + new_tokens if previous_tokens else new_tokens ) delta_message = mistral_tool_parser.extract_tool_calls_streaming( previous_text=previous_text, current_text=current_text, delta_text=delta_text, previous_token_ids=previous_token_ids, current_token_ids=current_token_ids, delta_token_ids=delta_token_ids, request=_DUMMY_REQUEST, ) if delta_message and delta_message.content: collected_content += delta_message.content if delta_message: assert not delta_message.tool_calls previous_text = current_text assert collected_content == model_output def test_mistral_parser_drops_eos_from_output(mistral_tokenizer): """EOS token must never surface as content/reasoning; literal EOS string must be preserved when the EOS token id is absent. The engine drops special tokens by id, so: Case A: the real EOS token id causes the text to be dropped. Case B: the same EOS string with a non-EOS token id is preserved. """ if not isinstance(mistral_tokenizer, MistralTokenizer): pytest.skip("Requires MistralTokenizer") eos_id: int = mistral_tokenizer.eos_token_id eos_text: str = mistral_tokenizer.decode([eos_id]) assert eos_text # Case A: real EOS token — text must be dropped. parser_a = MistralParser(mistral_tokenizer) parser_a.initialize_streaming() events_a = parser_a._feed(eos_text, [eos_id]) events_a.extend(parser_a._engine.finish()) delta_a = parser_a._events_to_delta(events_a, finished=True) content_a = (delta_a.content if delta_a else None) or "" reasoning_a = (delta_a.reasoning if delta_a else None) or "" assert eos_text not in content_a assert eos_text not in reasoning_a # Case B: EOS text with a non-EOS token id — text must be preserved. # This proves content equal to the EOS string is not silently dropped # when it did not come from the real EOS token. non_eos_ids = mistral_tokenizer.encode(text="hello", add_special_tokens=False) non_eos_id = next(tid for tid in non_eos_ids if tid != eos_id) parser_b = MistralParser(mistral_tokenizer) parser_b.initialize_streaming() events_b = parser_b._feed(eos_text, [non_eos_id]) events_b.extend(parser_b._engine.finish()) delta_b = parser_b._events_to_delta(events_b, finished=True) content_b = (delta_b.content if delta_b else None) or "" reasoning_b = (delta_b.reasoning if delta_b else None) or "" assert eos_text in content_b or eos_text in reasoning_b @pytest.mark.parametrize( "parser_fixture, tokenizer_fixture, model_output," " expected_tool_calls, expected_content", [ pytest.param( "mistral_tool_parser", "mistral_tokenizer", """[TOOL_CALLS]add_this_and_that{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], "", id="v11-single_tool_add", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """[TOOL_CALLS]get_current_weather{"city": "San Francisco", "state": "CA", "unit": "celsius"}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], "", id="v11-single_tool_weather", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """[TOOL_CALLS]add{"a": 3.5, "b": 4}[TOOL_CALLS]multiply{"a": 3, "b": 6}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="multiply", arguments=json.dumps({"a": 3, "b": 6}) ) ), ], "", id="v11-multiple_tool_calls", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """bla[TOOL_CALLS]add_this_and_that{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], "bla", id="v11-content_before_tool", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """[TOOL_CALLS]add_this_and_that[ARGS]{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], "", id="v11-single_tool_add_args", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """[TOOL_CALLS]add[ARGS]{"a": 3.5, "b": 4}[TOOL_CALLS]multiply[ARGS]{"a": 3, "b": 6}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="multiply", arguments=json.dumps({"a": 3, "b": 6}) ) ), ], "", id="v11-multiple_tool_calls_args", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """bla[TOOL_CALLS]add_this_and_that[ARGS]{"a": 3.5, "b": 4}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add_this_and_that", arguments=json.dumps({"a": 3.5, "b": 4}), ) ) ], "bla", id="v11-content_before_tool_args", ), pytest.param( "mistral_tool_parser", "mistral_tokenizer", """hi{hi[TOOL_CALLS]bash{"command": "print(\\"hello world!\\")\\nre.compile(r\'{}\')"}""", # noqa: E501 [ ToolCall( function=FunctionCall( name="bash", arguments=json.dumps( {"command": "print(\"hello world!\")\nre.compile(r'{}')"} ), ) ) ], "hi{hi", id="v11-complex", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """This is a test""", [], """This is a test""", id="pre_v11-no_tools", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [ {"name":"add" , "arguments" : {"a": 3, "b": 4} } ]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3, "b": 4}) ) ) ], "", id="pre_v11-single_tool_add", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [{"name": "add", "arguments":{"a": "3", "b": "4"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": "3", "b": "4"}) ) ) ], "", id="pre_v11-single_tool_add_strings", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [{"name": "get_current_weather", "arguments": {"city": "San Francisco", "state": "CA", "unit": "celsius"}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], "", id="pre_v11-single_tool_weather", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [{"arguments": {"city": "San Francisco", "state": "CA", "unit": "celsius"}, "name": "get_current_weather"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ) ], "", id="pre_v11-argument_before_name", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [{"arguments": {"name": "John Doe"}, "name": "get_age"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="get_age", arguments=json.dumps( { "name": "John Doe", } ), ) ) ], "", id="pre_v11-argument_before_name_and_name_in_argument", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """[TOOL_CALLS] [{"arguments": {"a": 3.5, "b": 4}, "name": "add"}, {"arguments":{"city": "San Francisco", "state": "CA", "unit": "celsius"}, "name": "get_current_weather"}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( {"city": "San Francisco", "state": "CA", "unit": "celsius"} ), ) ), ], "", id="pre_v11-multiple_tools", ), pytest.param( "mistral_pre_v11_tool_parser", "mistral_pre_v11_tokenizer", """Some text[TOOL_CALLS] [{"name": "add", "arguments":{"a": 1, "b": 2}}]""", # noqa: E501 [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 1, "b": 2}) ) ) ], "Some text", id="pre_v11-content_before_tool", ), ], ) def test_extract_tool_calls_streaming_one_chunk( parser_fixture, tokenizer_fixture, model_output, expected_tool_calls, expected_content, request, ): tool_parser = request.getfixturevalue(parser_fixture) tokenizer = request.getfixturevalue(tokenizer_fixture) # Real serving streams only generated tokens (no BOS/EOS framing) with the # control markers as special ids. if isinstance(tokenizer, MistralTokenizer) and tokenizer.version >= 11: all_token_ids = encode_mistral_output(tokenizer, model_output) else: all_token_ids = tokenizer.encode(model_output, add_special_tokens=False) all_token_ids = fix_tool_call_tokenization( all_token_ids, tool_parser, tokenizer ) delta_message = tool_parser.extract_tool_calls_streaming( previous_text="", current_text=model_output, delta_text=model_output, previous_token_ids=[], current_token_ids=all_token_ids, delta_token_ids=all_token_ids, request=_DUMMY_REQUEST, ) # The engine buffers the final closing brace ('}') in _args_buffer until # finish_streaming() is called, mirroring real serving. Flush it and # merge the result so the asserted arguments are complete. flush_delta = tool_parser.finish_streaming() if flush_delta is not None and flush_delta.tool_calls: if delta_message is not None and delta_message.tool_calls: for flush_tc in flush_delta.tool_calls: for existing_tc in delta_message.tool_calls: if existing_tc.index == flush_tc.index and flush_tc.function: existing_tc.function = ( existing_tc.function or DeltaFunctionCall() ) existing_tc.function.arguments = ( existing_tc.function.arguments or "" ) + (flush_tc.function.arguments or "") elif delta_message is not None: delta_message.tool_calls = flush_delta.tool_calls else: delta_message = flush_delta assert isinstance(delta_message, DeltaMessage) assert len(delta_message.tool_calls) == len(expected_tool_calls) assert_tool_calls(delta_message.tool_calls, expected_tool_calls) if delta_message.content is None: assert expected_content == "" else: assert delta_message.content == expected_content @pytest.mark.parametrize( "parser_fixture, model_output, fake_count, two_phase", [ pytest.param( "mistral_pre_v11_tool_parser", '[TOOL_CALLS] [{"name": "add", "arguments":{"a": 1, "b": 2}}]', 30, False, id="pre_v11", ), ], ) def test_fast_detokenization_text_detection( parser_fixture, model_output, fake_count, two_phase, request ): """Regression: bot_token in text but not token_ids (PR #37209). Only the pre-v11 legacy path detects the marker from text; v11+ routes through the engine and detects the marker by token id. """ parser = request.getfixturevalue(parser_fixture) # Token IDs that do NOT contain bot_token_id. fake_token_ids = list(range(99, 99 + fake_count)) if two_phase: # First delta: pure content, no bot token yet delta_message_before = parser.extract_tool_calls_streaming( previous_text="", current_text="Hello", delta_text="Hello", previous_token_ids=[], current_token_ids=[99], delta_token_ids=[99], request=_DUMMY_REQUEST, ) assert delta_message_before is not None assert delta_message_before.content == "Hello" assert not delta_message_before.tool_calls previous_text = "Hello" current_text = "Hello" + model_output previous_token_ids = [99] delta_token_ids = fake_token_ids[1:] else: previous_text = "" current_text = model_output previous_token_ids = [] delta_token_ids = fake_token_ids delta_message = parser.extract_tool_calls_streaming( previous_text=previous_text, current_text=current_text, delta_text=model_output, previous_token_ids=previous_token_ids, current_token_ids=fake_token_ids, delta_token_ids=delta_token_ids, request=_DUMMY_REQUEST, ) assert delta_message is not None assert delta_message.tool_calls is not None assert len(delta_message.tool_calls) == 1 assert delta_message.tool_calls[0].function is not None assert delta_message.tool_calls[0].function.name == "add" def test_extract_tool_calls_streaming_exception_returns_none( mistral_pre_v11_tool_parser, ): # v11 routes through the ParserEngine and does not swallow exceptions; # only the pre-v11 legacy state-machine path has explicit exception handling. parser = mistral_pre_v11_tool_parser patched_method = "_extract_tool_calls_streaming_pre_v11_tokenizer" current_text = '[TOOL_CALLS] [{"name":"a","arguments":{}}]' with patch.object( parser._parser_engine, patched_method, side_effect=RuntimeError("boom") ): result = parser.extract_tool_calls_streaming( previous_text="", current_text=current_text, delta_text=current_text, previous_token_ids=[], current_token_ids=[parser._parser_engine.bot_token_id], delta_token_ids=[parser._parser_engine.bot_token_id], request=_DUMMY_REQUEST, ) assert result is None SAMPLE_TOOLS_DICTS = [ { "type": "function", "function": { "name": "get_weather", "description": "Get the weather", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "add", "description": "Add two numbers", "parameters": { "type": "object", "properties": { "a": {"type": "number"}, "b": {"type": "number"}, }, "required": ["a", "b"], }, }, }, ] def _make_request(**kwargs) -> ChatCompletionRequest: defaults: dict = { "messages": [], "model": "mistralai/Mistral-Small-3.2-24B-Instruct-2506", "tools": SAMPLE_TOOLS_DICTS, "tool_choice": "auto", } defaults.update(kwargs) return ChatCompletionRequest(**defaults) @pytest.mark.parametrize( "request_kwargs,expected_mode,expected_parallel", [ ({"tool_choice": "auto"}, MistralToolChoiceEnum.auto, True), ({"tool_choice": "none"}, MistralToolChoiceEnum.none, True), ({"tool_choice": "required"}, MistralToolChoiceEnum.required, True), ({"tool_choice": None, "tools": None}, MistralToolChoiceEnum.auto, True), ( { "tool_choice": { "type": "function", "function": {"name": "get_weather"}, } }, MistralNamedToolChoice.model_validate( {"type": "function", "function": {"name": "get_weather"}} ), True, ), ( {"tool_choice": "auto", "parallel_tool_calls": False}, MistralToolChoiceEnum.auto, False, ), ( {"tool_choice": "auto", "response_format": {"type": "text"}}, MistralToolChoiceEnum.auto, True, ), ], ids=[ "auto", "none", "required", "null_tool_choice", "named_tool_choice", "parallel_false", "response_format_text", ], ) def test_adjust_request_grammar_factory( mistral_tool_parser: MistralToolParser, request_kwargs: dict, expected_mode: MistralToolChoice, expected_parallel: bool, ) -> None: request = _make_request(**request_kwargs) factory = mistral_tool_parser.model_tokenizer.grammar_factory with patch.object( factory, "get_lark_from_jinja", wraps=factory.get_lark_from_jinja, ) as mock_get_lark: result = mistral_tool_parser.adjust_request(request) mock_get_lark.assert_called_once() call_kwargs = mock_get_lark.call_args assert call_kwargs.kwargs["mode"] == expected_mode assert call_kwargs.kwargs["json_schema"] is None assert call_kwargs.kwargs["parallel_tool_calls"] == expected_parallel assert result.structured_outputs is not None assert isinstance(result.structured_outputs.grammar, str) assert len(result.structured_outputs.grammar) > 0 def test_adjust_request_unsupported_grammar_for_tokenizer(mistral_tokenizer) -> None: with patch.object( type(mistral_tokenizer), "supports_grammar", new_callable=lambda: property(lambda self: False), ): parser = MistralToolParser(mistral_tokenizer) request = _make_request() result = parser.adjust_request(request) assert result.structured_outputs is None @pytest.mark.parametrize( "tool_choice,expected_skip", [("auto", False), ("none", False)], ids=["auto_skip_false", "none_skip_false"], ) def test_adjust_request_non_mistral_tokenizer( non_mistral_parser: MistralToolParser, tool_choice: str, expected_skip: bool, ) -> None: # MistralParser (ParserEngine) always sets skip_special_tokens=False so # that special tokens like [TOOL_CALLS] are visible to the parser even # when tool_choice="none". request = _make_request(tool_choice=tool_choice) result = non_mistral_parser.adjust_request(request) assert result.skip_special_tokens is expected_skip @pytest.mark.parametrize( "so_kwargs", [ {"regex": r"\d+"}, {"choice": ["a", "b"]}, { "structural_tag": json.dumps( { "structures": [ { "begin": "", "schema": {"type": "object"}, "end": "", } ], "triggers": [""], } ) }, {"grammar": "start: 'hello'"}, ], ids=["regex", "choice", "structural_tag", "grammar"], ) def test_adjust_request_unsupported_structured_outputs( mistral_tool_parser: MistralToolParser, so_kwargs: dict, ) -> None: request = _make_request( structured_outputs=StructuredOutputsParams(**so_kwargs), ) result = mistral_tool_parser.adjust_request(request) assert result.structured_outputs == request.structured_outputs def test_adjust_request_unsupported_response_format( mistral_tool_parser: MistralToolParser, ) -> None: request = _make_request( response_format=StructuralTagResponseFormat( type="structural_tag", format={ "type": "triggered_tags", "tags": [ { "begin": "", "content": {"type": "any_text"}, "end": "", } ], "triggers": [""], }, ), ) result = mistral_tool_parser.adjust_request(request) assert result.structured_outputs is None assert result.response_format == request.response_format @pytest.mark.parametrize( "so_kwargs,expected_json_schema", [ ({"json_object": True}, _DEFAULT_JSON_SCHEMA), ({"json": '{"type": "object"}'}, {"type": "object"}), ( {"json": {"type": "object", "properties": {"x": {"type": "integer"}}}}, {"type": "object", "properties": {"x": {"type": "integer"}}}, ), ], ids=["json_object", "json_str", "json_dict"], ) def test_adjust_request_structured_outputs_generates_grammar( mistral_tool_parser: MistralToolParser, so_kwargs: dict, expected_json_schema: str, ) -> None: request = _make_request( structured_outputs=StructuredOutputsParams(**so_kwargs), ) factory = mistral_tool_parser.model_tokenizer.grammar_factory with patch.object( factory, "get_lark_from_jinja", wraps=factory.get_lark_from_jinja, ) as mock_get_lark: result = mistral_tool_parser.adjust_request(request) mock_get_lark.assert_called_once() assert mock_get_lark.call_args.kwargs["json_schema"] == expected_json_schema assert result.structured_outputs is not None assert isinstance(result.structured_outputs.grammar, str) assert len(result.structured_outputs.grammar) > 0 @pytest.mark.parametrize( "response_format_kwargs,expected_json_schema", [ ({"type": "json_object"}, _DEFAULT_JSON_SCHEMA), ( { "type": "json_schema", "json_schema": { "name": "my_schema", "schema": { "type": "object", "properties": {"x": {"type": "integer"}}, }, }, }, {"type": "object", "properties": {"x": {"type": "integer"}}}, ), ], ids=["json_object", "json_schema_with_schema"], ) def test_adjust_request_response_format_generates_grammar( mistral_tool_parser: MistralToolParser, response_format_kwargs: dict, expected_json_schema: str, ) -> None: request = _make_request(response_format=response_format_kwargs) factory = mistral_tool_parser.model_tokenizer.grammar_factory with patch.object( factory, "get_lark_from_jinja", wraps=factory.get_lark_from_jinja, ) as mock_get_lark: result = mistral_tool_parser.adjust_request(request) mock_get_lark.assert_called_once() assert mock_get_lark.call_args.kwargs["json_schema"] == expected_json_schema assert result.structured_outputs is not None assert isinstance(result.structured_outputs.grammar, str) assert len(result.structured_outputs.grammar) > 0 @pytest.mark.parametrize( "tool_choice, expected_method, not_called_method", [ ("none", "get_lark_for_json_schema", None), ("auto", "get_lark_from_jinja", "get_lark_for_json_schema"), ], ids=["none_uses_json_schema_factory", "auto_uses_jinja_factory"], ) def test_adjust_request_tool_choice_with_json_schema_factory_routing( mistral_tool_parser: MistralToolParser, tool_choice: str, expected_method: str, not_called_method: str | None, ) -> None: request = _make_request( tool_choice=tool_choice, structured_outputs=StructuredOutputsParams(json='{"type": "object"}'), ) factory = mistral_tool_parser.model_tokenizer.grammar_factory patches = { expected_method: patch.object( factory, expected_method, wraps=getattr(factory, expected_method), ), } if not_called_method: patches[not_called_method] = patch.object( factory, not_called_method, wraps=getattr(factory, not_called_method), ) with patches[expected_method] as mock_expected: ctx = patches[not_called_method] if not_called_method else None if ctx: with ctx as mock_not_called: result = mistral_tool_parser.adjust_request(request) mock_not_called.assert_not_called() else: result = mistral_tool_parser.adjust_request(request) mock_expected.assert_called_once() assert mock_expected.call_args.kwargs["json_schema"] == {"type": "object"} assert result.structured_outputs is not None assert isinstance(result.structured_outputs.grammar, str) assert len(result.structured_outputs.grammar) > 0 def test_grammar_from_parser_default_false() -> None: request = _make_request() assert request._grammar_from_parser is False def test_grammar_from_parser_set_by_adjust_request( mistral_tool_parser: MistralToolParser, ) -> None: request = _make_request() result = mistral_tool_parser.adjust_request(request) assert result._grammar_from_parser is True @pytest.mark.parametrize("driver", ["legacy", "engine"]) @pytest.mark.parametrize("chunk_size", [2, 3, 4, 5]) def test_streaming_pre_v11_parallel_calls_batched_deltas( mistral_pre_v11_tool_parser, mistral_pre_v11_tokenizer, chunk_size, driver ): """A batched delta spanning the boundary between two parallel calls must keep them on distinct indices (the bug collapsed both onto index 0).""" model_output = ( '[TOOL_CALLS] [{"name": "add", "arguments": {"a": 3.5, "b": 4}}, ' '{"name": "get_current_weather", "arguments": ' '{"city": "San Francisco", "state": "CA", "unit": "celsius"}}]' ) names: list[str] = [] args: list[str] = [] idx = -1 for delta_message in stream_delta_message_generator( mistral_pre_v11_tool_parser, mistral_pre_v11_tokenizer, model_output, tools=None, chunk_size=chunk_size, driver=driver, ): for tool_call in delta_message.tool_calls or []: if tool_call.index != idx: idx = tool_call.index args.append("") if tool_call.function and tool_call.function.name: names.append(tool_call.function.name) if tool_call.function and tool_call.function.arguments: args[tool_call.index] += tool_call.function.arguments assert names == ["add", "get_current_weather"] assert len(args) == 2 # trailing args of the final call are flushed by the serving layer assert json.loads(args[0]) == {"a": 3.5, "b": 4} @pytest.mark.parametrize( "reasoning_encoding", ["text", "special_token"], ids=["text_encoding", "special_token_encoding"], ) def test_content_tool_calls_transition_emits_reasoning_end( mistral_tokenizer, reasoning_encoding, ): """(CONTENT, TOOL_CALLS) must emit REASONING_END when reasoning is enabled.""" cfg = mistral_config(reasoning_encoding=reasoning_encoding) engine = StreamingParserEngine( config=cfg, tokenizer=mistral_tokenizer, vocab=mistral_tokenizer.get_vocab() ) engine.skip_tool_parsing = True bot_token_id = mistral_tokenizer.get_vocab().get("[TOOL_CALLS]") assert bot_token_id is not None events = engine.feed(delta_text="[TOOL_CALLS]", delta_token_ids=[bot_token_id]) event_types = [e.type for e in events] assert EventType.REASONING_END in event_types, ( f"REASONING_END missing for reasoning_encoding={reasoning_encoding!r}; " f"got events: {event_types}" ) # Tool call start must NOT appear in skip_tool_parsing mode assert EventType.TOOL_CALL_START not in event_types @pytest.mark.parametrize("chunk_size", [1, 3]) @pytest.mark.parametrize( "tools,expected_tool_calls,expected_content", [ ( [("add", '{"a": 3, "b": 4}')], [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3, "b": 4}) ) ) ], "", ), ( [ ("add", '{"a": 3.5, "b": 4}'), ( "get_current_weather", '{"city": "San Francisco", "state": "CA", "unit": "celsius"}', ), ], [ ToolCall( function=FunctionCall( name="add", arguments=json.dumps({"a": 3.5, "b": 4}) ) ), ToolCall( function=FunctionCall( name="get_current_weather", arguments=json.dumps( { "city": "San Francisco", "state": "CA", "unit": "celsius", } ), ) ), ], "", ), ], ids=["single_tool_add", "parallel_tools"], ) def test_reasoning_active_no_think_block_no_leak( mistral_tool_parser, mistral_tokenizer, tools, expected_tool_calls, expected_content, chunk_size, ): """Tool call without a preceding think block must not leak as content when the reasoning parser is active.""" accumulated_content = "" function_names: list[str] = [] function_args_strs: list[str] = [] tool_call_idx = -1 tool_call_ids: list[str | None] = [] for delta_message in stream_delta_message_generator( mistral_tool_parser, mistral_tokenizer, model_output=None, tools=tools, chunk_size=chunk_size, driver="engine", reasoning_parser="mistral", ): if delta_message.content: accumulated_content += delta_message.content for tool_call in delta_message.tool_calls or []: if tool_call.index != tool_call_idx: tool_call_idx = tool_call.index function_args_strs.append("") tool_call_ids.append(None) if tool_call.id and not tool_call_ids[tool_call.index]: tool_call_ids[tool_call.index] = tool_call.id if tool_call.function: if tool_call.function.name: function_names.append(tool_call.function.name) if tool_call.function.arguments: function_args_strs[tool_call.index] += tool_call.function.arguments assert "[TOOL_CALLS]" not in accumulated_content assert "[ARGS]" not in accumulated_content assert accumulated_content == expected_content actual_tool_calls = [ ToolCall( id=tc_id, function=FunctionCall( name=name, arguments=partial_json_parser.ensure_json(args, Allow.OBJ | Allow.STR), ), ) for tc_id, name, args in zip(tool_call_ids, function_names, function_args_strs) ] assert_tool_calls(actual_tool_calls, expected_tool_calls) # --------------------------------------------------------------------------- # Pre-v11 guided schema injection and bare-array extraction tests # --------------------------------------------------------------------------- @pytest.mark.parametrize( "tool_choice,expected_items_count", [ ("required", 2), # all tools ( {"type": "function", "function": {"name": "get_weather"}}, 1, ), # named: single tool ], ids=["required", "named"], ) def test_adjust_request_pre_v11_guided_schema_injected( mistral_pre_v11_tool_parser: MistralToolParser, tool_choice: object, expected_items_count: int, ) -> None: request = _make_request(tool_choice=tool_choice) result = mistral_pre_v11_tool_parser.adjust_request(request) assert result.structured_outputs is not None schema = result.structured_outputs.json if isinstance(schema, str): schema = json.loads(schema) assert schema["type"] == "array" assert schema["minItems"] == 1 items = schema["items"] assert "anyOf" in items assert len(items["anyOf"]) == expected_items_count for entry in items["anyOf"]: props = entry["properties"] assert "name" in props assert "arguments" in props @pytest.mark.parametrize( "response_format", [{"type": "json_object"}, {"type": "json_schema", "json_schema": {"name": "s"}}], ids=["json_object", "json_schema"], ) def test_adjust_request_pre_v11_required_clears_response_format( mistral_pre_v11_tool_parser: MistralToolParser, response_format: dict, ) -> None: """required + response_format must inject the tool schema and clear response_format so the tool schema is the sole structured-output constraint, matching the base ToolParser (otherwise the request either hits the "multiple constraints" engine error or, with no constraint, rambles to finish_reason='length').""" request = _make_request(tool_choice="required", response_format=response_format) result = mistral_pre_v11_tool_parser.adjust_request(request) assert result.response_format is None assert result.structured_outputs is not None schema = result.structured_outputs.json if isinstance(schema, str): schema = json.loads(schema) assert schema["type"] == "array" def test_adjust_request_non_mistral_tokenizer_required_injects_schema( non_mistral_parser: MistralToolParser, ) -> None: """The guided-schema injection must also fire for non-Mistral (e.g. HF-mode) tokenizers driving the Mistral tool parser, mirroring the base ToolParser so required + response_format does not fall back to an unconstrained ramble.""" request = _make_request( tool_choice="required", response_format={"type": "json_object"} ) result = non_mistral_parser.adjust_request(request) assert result.response_format is None assert result.structured_outputs is not None assert result.structured_outputs.json is not None @pytest.mark.parametrize( "tool_choice", ["auto", "none"], ids=["auto", "none"], ) def test_adjust_request_pre_v11_no_injection_for_auto_none( mistral_pre_v11_tool_parser: MistralToolParser, tool_choice: str, ) -> None: request = _make_request(tool_choice=tool_choice) result = mistral_pre_v11_tool_parser.adjust_request(request) assert result.structured_outputs is None def test_legacy_extract_tool_calls_guided_bare_array_required( mistral_pre_v11_tool_parser: MistralToolParser, ) -> None: model_output = '[{"name": "get_current_weather", "arguments": {"city": "Dallas"}}]' request = _make_request(tool_choice="required") result = mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=request ) assert result.tools_called assert len(result.tool_calls) == 1 tc = result.tool_calls[0] assert tc.function.name == "get_current_weather" assert json.loads(tc.function.arguments) == {"city": "Dallas"} assert isinstance(tc.id, str) assert len(tc.id) == 9 assert result.content is None def test_legacy_extract_tool_calls_none_with_tools( mistral_pre_v11_tool_parser: MistralToolParser, ) -> None: model_output = ( '[TOOL_CALLS] [{"name": "get_current_weather",' ' "arguments": {"city": "Dallas"}}]' ) request = _make_request(tool_choice="none") result = mistral_pre_v11_tool_parser.extract_tool_calls( model_output, request=request ) assert not result.tools_called assert result.tool_calls == [] assert result.content == model_output def test_guided_streaming_required_pre_v11( mistral_pre_v11_tool_parser: MistralToolParser, mistral_pre_v11_tokenizer, ) -> None: model_output = '[{"name": "get_current_weather", "arguments": {"city": "Dallas"}}]' request = _make_request(tool_choice="required") all_token_ids = mistral_pre_v11_tokenizer.encode( model_output, add_special_tokens=False ) function_name: str | None = None function_args = "" tool_call_id: str | None = None previous_text = "" previous_tokens = None prefix_offset = 0 read_offset = 0 for i, token_id in enumerate(all_token_ids): (new_tokens, delta_text, prefix_offset, read_offset) = detokenize_incrementally( tokenizer=mistral_pre_v11_tokenizer, all_input_ids=all_token_ids[: i + 1], prev_tokens=previous_tokens, prefix_offset=prefix_offset, read_offset=read_offset, skip_special_tokens=False, spaces_between_special_tokens=True, ) previous_tokens = ( (previous_tokens + new_tokens) if previous_tokens else new_tokens ) current_text = previous_text + delta_text delta_message = mistral_pre_v11_tool_parser.extract_tool_calls_streaming( previous_text=previous_text, current_text=current_text, delta_text=delta_text, previous_token_ids=all_token_ids[:i], current_token_ids=all_token_ids[: i + 1], delta_token_ids=[token_id], request=request, ) previous_text = current_text if delta_message and delta_message.tool_calls: for tc in delta_message.tool_calls: if tc.id and not tool_call_id: tool_call_id = tc.id if tc.function: if tc.function.name: function_name = tc.function.name if tc.function.arguments: function_args += tc.function.arguments assert function_name == "get_current_weather" assert tool_call_id is not None assert len(tool_call_id) == 9 assert json.loads(function_args) == {"city": "Dallas"} # --------------------------------------------------------------------------- # Malformed-name-before-marker matrix: {malformed, well-formed, mixed # parallel} x {streaming, non-streaming}. # # These tests drive a real MistralParser built from the module's # `mistral_tokenizer` fixture (grammar-capable v11+ tokenizer, so the # declarative ParserEngine path is exercised, not the pre-v11 legacy state # machine) with actual bash/search_replace/submit tools, mirroring the tools # present in the real captured traces this bug was found in. # --------------------------------------------------------------------------- _BASH_SEARCH_SUBMIT_TOOLS_DICTS = [ { "type": "function", "function": { "name": "bash", "description": "Run a bash command", "parameters": { "type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"], }, }, }, { "type": "function", "function": { "name": "search_replace", "description": "Search and replace text in a file", "parameters": { "type": "object", "properties": { "path": {"type": "string"}, "search": {"type": "string"}, "replace": {"type": "string"}, }, "required": ["path", "search", "replace"], }, }, }, { "type": "function", "function": { "name": "submit", "description": "Submit the final answer", "parameters": { "type": "object", "properties": {"answer": {"type": "object"}}, "required": ["answer"], }, }, }, ] def _malformed_matrix_request() -> ChatCompletionRequest: return ChatCompletionRequest( messages=[], model="test", tools=_BASH_SEARCH_SUBMIT_TOOLS_DICTS, tool_choice="auto", ) def _stream_via_parse_delta( parser: MistralParser, tokenizer: MistralTokenizer, model_output: str, request: ChatCompletionRequest, ) -> tuple[str, list[str | None], list[str]]: """Feed `model_output` through `parser.parse_delta` token-by-token. This is the exact API production serving drives on every generated token (see `ParserEngine.parse_delta`), unlike `extract_tool_calls_streaming` which some legacy call sites drive with pre-computed previous/current text windows. """ parser.initialize_streaming() all_token_ids = encode_mistral_output(tokenizer, model_output) content = "" tool_call_idx = -1 names: list[str | None] = [] args: list[str] = [] previous_tokens = None prefix_offset = 0 read_offset = 0 for i, token_id in enumerate(all_token_ids): new_tokens, delta_text, prefix_offset, read_offset = detokenize_incrementally( tokenizer=tokenizer, all_input_ids=all_token_ids[: i + 1], prev_tokens=previous_tokens, prefix_offset=prefix_offset, read_offset=read_offset, skip_special_tokens=False, spaces_between_special_tokens=True, ) previous_tokens = ( previous_tokens + new_tokens if previous_tokens else new_tokens ) finished = i == len(all_token_ids) - 1 delta_message = parser.parse_delta( delta_text=delta_text, delta_token_ids=[token_id], request=request, prompt_token_ids=[1], finished=finished, ) if delta_message is None: continue if delta_message.content: content += delta_message.content for tool_call in delta_message.tool_calls or []: if tool_call.index != tool_call_idx: tool_call_idx = tool_call.index names.append(None) args.append("") if tool_call.function: if tool_call.function.name is not None: names[tool_call.index] = tool_call.function.name if tool_call.function.arguments: args[tool_call.index] += tool_call.function.arguments return content, names, args _MALFORMED_MATRIX_CASES = [ pytest.param( # 2x2 corner: name present, `[ARGS]` token present. '[TOOL_CALLS]bash[ARGS]{"command": "ls -la"}', ["bash"], [json.dumps({"command": "ls -la"})], None, id="wellformed_single", ), pytest.param( # 2x2 corner: name present, no `[ARGS]` token. '[TOOL_CALLS]bash{"command": "ls -la"}', ["bash"], [json.dumps({"command": "ls -la"})], None, id="wellformed_single_no_args_token", ), pytest.param( # 2x2 corner: name skipped, no `[ARGS]` token. "bash" lands in # content before the marker, then generation jumps straight to # args (no NAME slot, no [ARGS]). 'bash[TOOL_CALLS]{"command": "cat file.py"}', [""], [json.dumps({"command": "cat file.py"})], "bash", id="malformed_single", ), pytest.param( # 2x2 corner: name skipped, `[ARGS]` token present. 'bash[TOOL_CALLS][ARGS]{"command": "cat file.py"}', [""], [json.dumps({"command": "cat file.py"})], "bash", id="malformed_single_args_token", ), pytest.param( # parallel well-formed x2; the second call's nested braces in its # arguments must not confuse call splitting. '[TOOL_CALLS]bash[ARGS]{"command": "ls"}' '[TOOL_CALLS]submit[ARGS]{"answer": {"nested": 1}}', ["bash", "submit"], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], None, id="parallel_wellformed", ), pytest.param( # parallel malformed x2: both calls skip the NAME slot entirely # (marker immediately followed by `{`), each ending with an empty # name and correctly separated (nested-brace) arguments. 'bash[TOOL_CALLS]{"command": "ls"}[TOOL_CALLS]{"answer": {"nested": 1}}', ["", ""], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], "bash", id="parallel_malformed", ), pytest.param( # parallel malformed x2, `[ARGS]` token variant: both calls skip # the NAME slot but enter TOOL_ARGS via `[ARGS]` instead of `{`. 'bash[TOOL_CALLS][ARGS]{"command": "ls"}' '[TOOL_CALLS][ARGS]{"answer": {"nested": 1}}', ["", ""], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], "bash", id="parallel_malformed_args_token", ), pytest.param( # mixed: malformed first call, well-formed second call. The # important case: proves calls are split on the `[TOOL_CALLS]` # marker, not on braces — the first call's simple args and the # second call's nested-brace args must not bleed into each other. 'bash[TOOL_CALLS]{"command": "ls"}' '[TOOL_CALLS]submit[ARGS]{"answer": {"nested": 1}}', ["", "submit"], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], "bash", id="mixed_malformed_then_wellformed", ), pytest.param( # mixed, `[ARGS]` token variant: malformed first call enters # TOOL_ARGS via `[ARGS]` instead of `{`, second call well-formed. 'bash[TOOL_CALLS][ARGS]{"command": "ls"}' '[TOOL_CALLS]submit[ARGS]{"answer": {"nested": 1}}', ["", "submit"], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], "bash", id="mixed_malformed_args_token_then_wellformed", ), pytest.param( # mixed: well-formed first call, malformed second call (NAME slot # skipped entirely for the second call). '[TOOL_CALLS]bash[ARGS]{"command": "ls"}[TOOL_CALLS]{"answer": {"nested": 1}}', ["bash", ""], [json.dumps({"command": "ls"}), json.dumps({"answer": {"nested": 1}})], None, id="mixed_wellformed_then_malformed", ), ] @pytest.mark.parametrize( "model_output,expected_names,expected_args,expected_content", _MALFORMED_MATRIX_CASES, ) def test_malformed_tool_call_matrix_non_streaming( mistral_tokenizer, model_output, expected_names, expected_args, expected_content, ): parser = MistralParser(mistral_tokenizer) request = _malformed_matrix_request() result = parser.extract_tool_calls_from_content(model_output, request) assert result.tools_called assert [tc.function.name for tc in result.tool_calls] == expected_names assert [tc.function.arguments for tc in result.tool_calls] == expected_args assert result.content == expected_content @pytest.mark.parametrize( "model_output,expected_names,expected_args,expected_content", _MALFORMED_MATRIX_CASES, ) def test_malformed_tool_call_matrix_streaming( mistral_tokenizer, model_output, expected_names, expected_args, expected_content, ): parser = MistralParser(mistral_tokenizer) request = _malformed_matrix_request() content, names, args = _stream_via_parse_delta( parser, mistral_tokenizer, model_output, request ) assert content == (expected_content or "") assert names == expected_names assert args == expected_args # Real payload captured from a production SWE-bench-agent. _REAL_LEAK_CONTENT = 'bash[TOOL_CALLS]{"command": "cd /testbed && git diff xarray/core/weighted.py"}ometracediff --git a/xarray/core/weighted.py b/xarray/core/weighted.py\\nindex 5f2d138e..9c38b0b7 100644\\n--- a/xarray/core/weighted.py\\n+++ b/xarray/core/weighted.py\\n@@ -1,4 +1,6 @@\\n from typing import TYPE_CHECKING, Hashable, Iterable, Optional, Union, overload\\n+\\n+import numpy as np\\n \\n from .computation import dot\\n from .options import _get_keep_attrs\\n@@ -105,6 +107,10 @@ class Weighted:\\n )\\n \\n self.obj = obj\\n- self.weights = weights\\n+ # Ensure weights are numeric (float64) to avoid boolean arithmetic issues\\n+ # in dot products and weighted operations\\n+ if not np.issubdtype(weights.dtype, np.number):\\n+ weights = weights.astype(np.float64)\\n+ self.weights = weights\\n "}现在 hareview the changes we made:bash[ARGS]{"command": "cat /testbed/xarray/core/weighted.py | head -120 | tail -20"}' # noqa: E501 def test_extract_tool_calls_real_captured_malformed_output(mistral_tokenizer): parser = MistralParser(mistral_tokenizer) request = _malformed_matrix_request() result = parser.extract_tool_calls_from_content(_REAL_LEAK_CONTENT, request) assert result.tools_called assert len(result.tool_calls) == 1 assert result.tool_calls[0].function.name == "" assert result.tool_calls[0].function.arguments == json.dumps( {"command": "cd /testbed && git diff xarray/core/weighted.py"} ) assert result.content == "bash"