[Bugfix][Rust Frontend][Renderer] Align DeepSeek tool-call arguments with deepseek-recipe (#56260)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Codex <noreply@openai.com> Signed-off-by: JinYan Su <751080330@qq.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
JinYan Su committed
Sep 11, 2026 at 18:19 UTC
79c137c6ada6bf9384e14feb260e10d91bf58c0b
2 files changed
+57
-12
rust/src/chat/src/renderer/deepseek.rs
+11
-12
@@ -11,7 +11,7 @@ use std::collections::HashMap;
11
use std::fmt::Write as _;
12
13
use serde::Serialize;
14
-use serde_json::Value;
14
+use serde_json::{Map, Value};
15
use serde_json_fmt::JsonFormat;
16
17
use llm_multimodal::DEEPSEEK_V41_IMAGE_PLACEHOLDER;
@@ -685,19 +685,18 @@ fn encode_arguments_to_dsml(
685
dialect: DsDialect,
686
) -> Result<()> {
687
let parameter_tag = dialect.parameter_tag();
688
- let arguments: Value = serde_json::from_str(&tool_call.arguments).map_err(|error| {
689
- Error::ChatTemplate(format!(
690
- "assistant tool call has invalid JSON arguments for DeepSeek: {error}"
691
- ))
692
- })?;
693
- let Some(arguments) = arguments.as_object() else {
694
- return Err(Error::ChatTemplate(
695
- "assistant tool call arguments for DeepSeek must be a JSON object".to_string(),
696
- ));
697
- };
688
+ // Match deepseek-recipe's render_tool_arguments for both V4 and V4.1:
689
+ // https://github.com/deepseek-ai/deepseek-recipe/blob/8cadfede7063c896b944e7bae05daa3549ae97ea/deepseek-recipe-encoding/src/v4/mod.rs#L48-L58
690
+ let arguments = serde_json::from_str::<Map<String, Value>>(&tool_call.arguments)
691
+ .unwrap_or_else(|_| {
692
+ Map::from_iter([(
693
+ "arguments".to_owned(),
694
+ Value::String(tool_call.arguments.clone()),
695
+ )])
696
+ });
697
698
let mut wrote_parameter = false;
700
- for (key, value) in arguments {
699
+ for (key, value) in &arguments {
700
if wrote_parameter {
701
out.push('\n');
702
}
rust/src/chat/src/renderer/deepseek_v4/tests.rs
+46
@@ -9,6 +9,7 @@ use serde_json::Value;
9
use super::DeepSeekV4ChatRenderer;
10
use crate::ChatRenderer;
11
use crate::event::{AssistantContentBlock, AssistantToolCall};
12
+use crate::renderer::deepseek_v41::DeepSeekV41ChatRenderer;
13
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
14
use crate::request::{ChatMessage, ChatRequest, ChatTool, GenerationPromptMode, ReasoningEffort};
15
@@ -399,6 +400,51 @@ fn reasoning_effort_template_kwarg_is_ignored() {
400
assert!(rendered.ends_with("<|Assistant|><think>"));
401
}
402
403
+#[test]
404
+fn tool_call_arguments_preserve_non_objects_like_recipe() {
405
+ let renderers: [(&dyn ChatRenderer, &str); 2] = [
406
+ (&DeepSeekV4ChatRenderer::new(), "parameter"),
407
+ (&DeepSeekV41ChatRenderer::new(), " parameter"),
408
+ ];
409
+ for arguments in [
410
+ "not json",
411
+ "[1, 2]",
412
+ "null",
413
+ "42",
414
+ "true",
415
+ "",
416
+ r#""text""#,
417
+ r#""{\"query\":\"double\"}""#,
418
+ ] {
419
+ let request = ChatRequest {
420
+ messages: vec![
421
+ ChatMessage::assistant_blocks(vec![AssistantContentBlock::ToolCall(
422
+ AssistantToolCall {
423
+ id: "call".to_string(),
424
+ name: "search".to_string(),
425
+ arguments: arguments.to_string(),
426
+ },
427
+ )]),
428
+ ChatMessage::tool_response("result", "call"),
429
+ ],
430
+ ..ChatRequest::for_test()
431
+ };
432
+
433
+ for (renderer, tag) in renderers {
434
+ let prompt = renderer.render(&request).unwrap().prompt.into_text().unwrap();
435
+ let parameters: Vec<_> =
436
+ prompt.lines().filter(|line| line.contains("parameter name=")).collect();
437
+ assert_eq!(
438
+ parameters,
439
+ [format!(
440
+ "<|DSML|{tag} name=\"arguments\" string=\"true\">{arguments}</|DSML|{tag}>"
441
+ )],
442
+ "arguments: {arguments:?}, tag: {tag:?}",
443
+ );
444
+ }
445
+ }
446
+}
447
+
448
#[test]
449
fn tool_results_are_sorted_by_previous_assistant_tool_call_order() {
450
let mut request = ChatRequest {