[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>
This commit is contained in:
JinYan Su
2026-09-11 10:19:55 +00:00
committed by GitHub
co-authored by Claude Fable 5.1 Bugen Zhao Codex
parent 8c1d1c2974
commit 79c137c6ad
2 changed files with 57 additions and 12 deletions
+11 -12
View File
@@ -11,7 +11,7 @@ use std::collections::HashMap;
use std::fmt::Write as _;
use serde::Serialize;
use serde_json::Value;
use serde_json::{Map, Value};
use serde_json_fmt::JsonFormat;
use llm_multimodal::DEEPSEEK_V41_IMAGE_PLACEHOLDER;
@@ -685,19 +685,18 @@ fn encode_arguments_to_dsml(
dialect: DsDialect,
) -> Result<()> {
let parameter_tag = dialect.parameter_tag();
let arguments: Value = serde_json::from_str(&tool_call.arguments).map_err(|error| {
Error::ChatTemplate(format!(
"assistant tool call has invalid JSON arguments for DeepSeek: {error}"
))
})?;
let Some(arguments) = arguments.as_object() else {
return Err(Error::ChatTemplate(
"assistant tool call arguments for DeepSeek must be a JSON object".to_string(),
));
};
// Match deepseek-recipe's render_tool_arguments for both V4 and V4.1:
// https://github.com/deepseek-ai/deepseek-recipe/blob/8cadfede7063c896b944e7bae05daa3549ae97ea/deepseek-recipe-encoding/src/v4/mod.rs#L48-L58
let arguments = serde_json::from_str::<Map<String, Value>>(&tool_call.arguments)
.unwrap_or_else(|_| {
Map::from_iter([(
"arguments".to_owned(),
Value::String(tool_call.arguments.clone()),
)])
});
let mut wrote_parameter = false;
for (key, value) in arguments {
for (key, value) in &arguments {
if wrote_parameter {
out.push('\n');
}
@@ -9,6 +9,7 @@ use serde_json::Value;
use super::DeepSeekV4ChatRenderer;
use crate::ChatRenderer;
use crate::event::{AssistantContentBlock, AssistantToolCall};
use crate::renderer::deepseek_v41::DeepSeekV41ChatRenderer;
use crate::renderer::test_utils::{FixtureRequestOptions, fixture_chat_request};
use crate::request::{ChatMessage, ChatRequest, ChatTool, GenerationPromptMode, ReasoningEffort};
@@ -399,6 +400,51 @@ fn reasoning_effort_template_kwarg_is_ignored() {
assert!(rendered.ends_with("<|Assistant|><think>"));
}
#[test]
fn tool_call_arguments_preserve_non_objects_like_recipe() {
let renderers: [(&dyn ChatRenderer, &str); 2] = [
(&DeepSeekV4ChatRenderer::new(), "parameter"),
(&DeepSeekV41ChatRenderer::new(), " parameter"),
];
for arguments in [
"not json",
"[1, 2]",
"null",
"42",
"true",
"",
r#""text""#,
r#""{\"query\":\"double\"}""#,
] {
let request = ChatRequest {
messages: vec![
ChatMessage::assistant_blocks(vec![AssistantContentBlock::ToolCall(
AssistantToolCall {
id: "call".to_string(),
name: "search".to_string(),
arguments: arguments.to_string(),
},
)]),
ChatMessage::tool_response("result", "call"),
],
..ChatRequest::for_test()
};
for (renderer, tag) in renderers {
let prompt = renderer.render(&request).unwrap().prompt.into_text().unwrap();
let parameters: Vec<_> =
prompt.lines().filter(|line| line.contains("parameter name=")).collect();
assert_eq!(
parameters,
[format!(
"<|DSML|{tag} name=\"arguments\" string=\"true\">{arguments}</|DSML|{tag}>"
)],
"arguments: {arguments:?}, tag: {tag:?}",
);
}
}
}
#[test]
fn tool_results_are_sorted_by_previous_assistant_tool_call_order() {
let mut request = ChatRequest {