[Rust Frontend] Tolerate whitespace before the outer brace in JSON tool-call parsers (#48034)

Signed-off-by: Tahsin Tunan <tahsintunan@gmail.com>
This commit is contained in:
Tahsin Tunan
2026-07-15 11:03:37 +00:00
committed by GitHub
parent 66b6c684ab
commit 4e04bcbce6
3 changed files with 105 additions and 2 deletions
+25 -1
View File
@@ -238,7 +238,9 @@ fn parse_llama_arguments_event(
/// Parse the outer closing brace for one Llama JSON tool call.
fn tool_call_close_event(input: &mut JsonToolInput<'_>) -> ModalResult<LlamaJsonEvent> {
literal("}").value(LlamaJsonEvent::ToolCallClose).parse_next(input)
seq!(_: ws0, _: literal("}"))
.value(LlamaJsonEvent::ToolCallClose)
.parse_next(input)
}
/// Parse a semicolon separator after one Llama JSON tool call.
@@ -266,6 +268,28 @@ mod tests {
format!(r#"{{"name":"{function_name}","parameters":{parameters}}}"#)
}
#[test]
fn llama_tolerates_whitespace_before_outer_brace() {
// Whitespace between the parameters object's `}` and the outer `}` must
// still parse (json.loads / raw_decode parity).
let mut whole = Llama3JsonToolParser::new(&test_tools());
let whole_output = whole.parse_complete(r#"{"name":"f","parameters":{"x":1} }"#).unwrap();
assert_eq!(whole_output.calls().len(), 1);
assert_eq!(whole_output.calls()[0].name.as_deref(), Some("f"));
assert_eq!(whole_output.calls()[0].arguments, r#"{"x":1}"#);
// Same input, whitespace before the outer `}` split across a chunk boundary.
let mut chunked = Llama3JsonToolParser::new(&test_tools());
let mut output = ToolParserOutput::default();
for chunk in [r#"{"name":"f","parameters":{"x":1}"#, " ", "}"] {
output.append(chunked.parse_chunk(chunk).unwrap());
}
output.append(chunked.finish().unwrap());
let output = output.coalesce();
assert_eq!(output.calls().len(), 1);
assert_eq!(output.calls()[0].arguments, r#"{"x":1}"#);
}
#[test]
fn llama_json_parse_complete_without_tool_call_keeps_text() {
let mut parser = Llama3JsonToolParser::new(&test_tools());
+29 -1
View File
@@ -317,7 +317,7 @@ fn tool_call_close_event(
input: &mut JsonToolInput<'_>,
config: JsonToolCallConfig,
) -> ModalResult<JsonToolCallEvent> {
let _ = literal("}").parse_next(input)?;
seq!(_: ws0, _: literal("}")).parse_next(input)?;
match config.delimiter {
Some(delimiter) => alt((
@@ -406,6 +406,34 @@ mod tests {
output.coalesce()
}
#[test]
fn json_tool_call_tolerates_whitespace_before_outer_brace() {
// Pretty-printed JSON puts whitespace between the arguments object's `}`
// and the outer object's `}`; it must still parse (json.loads parity).
let mut whole = JsonToolCallParser::new(DELIMITED_CONFIG);
let whole_output = collect_chunks(
&mut whole,
&[r#"<tool_calls>{"function":"f","parameters":{"x":1} }</tool_calls>"#],
);
assert_eq!(whole_output.calls().len(), 1);
assert_eq!(whole_output.calls()[0].name.as_deref(), Some("f"));
assert_eq!(whole_output.calls()[0].arguments, r#"{"x":1}"#);
// Same input, but the whitespace before the outer `}` is split across a
// chunk boundary (exercises `ws0` returning Incomplete on `Partial`).
let mut chunked = JsonToolCallParser::new(DELIMITED_CONFIG);
let chunked_output = collect_chunks(
&mut chunked,
&[
r#"<tool_calls>{"function":"f","parameters":{"x":1}"#,
" ",
"}</tool_calls>",
],
);
assert_eq!(chunked_output.calls().len(), 1);
assert_eq!(chunked_output.calls()[0].arguments, r#"{"x":1}"#);
}
#[test]
fn json_tool_call_delimiter_extracts_multiple_calls_in_one_block() {
let input = build_tool_calls(&[