diff --git a/rust/src/chat/src/renderer/deepseek.rs b/rust/src/chat/src/renderer/deepseek.rs index ab435028eb..5f900ab88b 100644 --- a/rust/src/chat/src/renderer/deepseek.rs +++ b/rust/src/chat/src/renderer/deepseek.rs @@ -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::>(&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'); } diff --git a/rust/src/chat/src/renderer/deepseek_v4/tests.rs b/rust/src/chat/src/renderer/deepseek_v4/tests.rs index a0ded7bd99..172b67463c 100644 --- a/rust/src/chat/src/renderer/deepseek_v4/tests.rs +++ b/rust/src/chat/src/renderer/deepseek_v4/tests.rs @@ -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|>")); } +#[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}" + )], + "arguments: {arguments:?}, tag: {tag:?}", + ); + } + } +} + #[test] fn tool_results_are_sorted_by_previous_assistant_tool_call_order() { let mut request = ChatRequest {