From 63f742bc74e892c59012e63a20eefe68be740995 Mon Sep 17 00:00:00 2001 From: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> Date: Thu, 22 Oct 2026 13:11:06 -0700 Subject: [PATCH] fix: preserve spaces in DSML tool parameter names I made the output parser read a DSML parameter name up to its closing quote instead of its first space, so a name like "start date" no longer breaks the tool call arguments JSON. The quotes around the name now come from the argument labels, so the output for other names is unchanged. Assisted-by: Claude Assisted-by: GPT-6 Astra Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> --- deepseek-recipe/src/stream/output_parser.rs | 18 +++++----- deepseek-recipe/src/stream/processor.rs | 38 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/deepseek-recipe/src/stream/output_parser.rs b/deepseek-recipe/src/stream/output_parser.rs index 99a3491..ed0fe43 100644 --- a/deepseek-recipe/src/stream/output_parser.rs +++ b/deepseek-recipe/src/stream/output_parser.rs @@ -161,11 +161,11 @@ pub(super) enum InvalidTokenKind { /// two distinct actions to produce both of its quotes. #[derive(Debug, Clone, Copy, PartialEq)] pub(super) enum ArgumentsLabel { - /// `{` before the first argument. + /// `{"` before the first argument name. ObjectOpen, - /// `, ` before each later argument. + /// `, "` before each later argument name. ArgumentSeparator, - /// `: ` between an argument name and its value. + /// `": ` between an argument name and its value. KeyValueSeparator, /// `"` before a string value. StringOpen, @@ -176,9 +176,9 @@ pub(super) enum ArgumentsLabel { impl ArgumentsLabel { pub(super) fn as_str(self) -> &'static str { match self { - Self::ObjectOpen => "{", - Self::ArgumentSeparator => ", ", - Self::KeyValueSeparator => ": ", + Self::ObjectOpen => "{\"", + Self::ArgumentSeparator => ", \"", + Self::KeyValueSeparator => "\": ", Self::StringOpen | Self::StringClose => "\"", } } @@ -247,7 +247,7 @@ enum Stage { /// Inside an `invoke` tag, before a `parameter` tag or the closing tag. /// `is_first_argument` holds until the first argument is written. ToolCallArguments { is_first_argument: bool }, - /// The quoted argument name of a `parameter` tag. + /// The argument name of a `parameter` tag, up to its closing quote. ToolCallArgumentName, /// The `string="true|false">` attribute of a `parameter` tag. ToolCallArgumentStringAttribute, @@ -504,7 +504,7 @@ impl State { } Stage::ToolCallArguments { is_first_argument } => { match_branches.add_literal_branch( - "parameter name=", + "parameter name=\"", Stage::ToolCallArgumentName, if is_first_argument { ActionOnMatched::LabelToolCallArguments(ArgumentsLabel::ObjectOpen) @@ -525,7 +525,7 @@ impl State { } Stage::ToolCallArgumentName => { match_branches.add_literal_branch( - " ", + "\"", Stage::ToolCallArgumentStringAttribute, ActionOnMatched::LabelToolCallArguments(ArgumentsLabel::KeyValueSeparator), ); diff --git a/deepseek-recipe/src/stream/processor.rs b/deepseek-recipe/src/stream/processor.rs index 870a419..4b3aa9c 100644 --- a/deepseek-recipe/src/stream/processor.rs +++ b/deepseek-recipe/src/stream/processor.rs @@ -748,6 +748,44 @@ mod tests { assert_eq!(finish_chunk(&chunks).0, FinishReason::ToolCalls); } + #[tokio::test] + async fn argument_names_keep_their_spaces() { + // Names with and without spaces, as first and later arguments, with + // string and raw values; an empty name ends the second call. + let block = r#"<|DSML|tool_calls> +<|DSML|invoke name="book_trip"> +<|DSML|parameter name="start date" string="true">2026-10-22 +<|DSML|parameter name="city" string="true">Tokyo +<|DSML|parameter name="party size" string="false">2 +<|DSML|parameter name="nights" string="false">3 + +<|DSML|invoke name="book_trip"> +<|DSML|parameter name="room count" string="false">1 +<|DSML|parameter name="guest name" string="true">Ada Lovelace +<|DSML|parameter name="" string="true">empty + +"#; + let expected = [ + ( + "book_trip", + r#"{"start date": "2026-10-22", "city": "Tokyo", "party size": 2, "nights": 3}"#, + ), + ( + "book_trip", + r#"{"room count": 1, "guest name": "Ada Lovelace", "": "empty"}"#, + ), + ]; + let output = format!("{PREAMBLE}\n\n{block}"); + for chunks in [ + parse_characters(&output, ParsingOptions::default()).await, + parse_texts(&[&output], ParsingOptions::default()).await, + ] { + assert_eq!(content(&chunks), PREAMBLE); + assert_tool_calls(&chunks, &expected); + assert_eq!(finish_chunk(&chunks).0, FinishReason::ToolCalls); + } + } + #[tokio::test] async fn json_text_in_a_string_value_is_escaped() { let output = format!("{PREAMBLE}\n\n{TOOL_CALLS}");