From 53b23b0692eb06e2b1ee961f397a6361dd75a835 Mon Sep 17 00:00:00 2001 From: Valeriy Selitskiy <239034+iamwavecut@users.noreply.github.com> Date: Mon, 12 Oct 2026 15:56:30 +0200 Subject: [PATCH] Accept named argument elements in a forced call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gemma also writes a forced draw_image as … …, and Qwen-style output uses or . These were rejected, which sent the turn to the GPU2 fallback; they now give the same arguments as …. --- crates/openplotva-llm/src/aifarm.rs | 44 +++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/openplotva-llm/src/aifarm.rs b/crates/openplotva-llm/src/aifarm.rs index 080b5f26..ae70b6e4 100644 --- a/crates/openplotva-llm/src/aifarm.rs +++ b/crates/openplotva-llm/src/aifarm.rs @@ -5410,10 +5410,10 @@ fn first_choice_tool_steps( Ok(ToolStepSelection::None(parsed.decision)) } -/// The arguments of a required tool written as bare `value` -/// elements, the shape a model without native tool calling falls back to when the -/// request names one tool; the engine's own call wrapper may enclose them. Every element -/// must name a parameter of that tool. Text may only follow a closed call wrapper; it is +/// The arguments of a required tool written as markup elements (`value` +/// or a named ``/`` element), the shape a model without native tool calling +/// falls back to when the request names one tool; the engine's own call wrapper may enclose +/// them. Every element must name a parameter of that tool. Text may only follow a closed call wrapper; it is /// returned as the text that accompanies the call. fn forced_tool_markup_step( response: &Value, @@ -5452,10 +5452,9 @@ fn forced_tool_markup_step( } let tail = rest.strip_prefix('<')?; let tag_end = tail.find('>')?; - let tag = &tail[..tag_end]; + let (tag, close) = markup_argument_tag(&tail[..tag_end])?; let schema = parameters.get(tag)?; let body = &tail[tag_end + 1..]; - let close = format!(""); let value_end = body.find(&close)?; let raw = body[..value_end].trim(); let value = match schema.get("type").and_then(Value::as_str) { @@ -5492,6 +5491,26 @@ fn forced_tool_markup_step( )) } +/// The argument name an opening tag carries and the tag that closes it: ``, +/// ``, `` or ``. +fn markup_argument_tag(tag: &str) -> Option<(&str, String)> { + fn quoted_name(attributes: &str) -> Option<&str> { + let value = attributes.trim().strip_prefix("name=")?; + let quote = value.chars().next().filter(|ch| matches!(ch, '"' | '\''))?; + value[1..].strip_suffix(quote) + } + if let Some(attributes) = tag.strip_prefix("arg ") { + return Some((quoted_name(attributes)?, "".to_owned())); + } + if let Some(name) = tag.strip_prefix("parameter=") { + return Some((name.trim(), "".to_owned())); + } + if let Some(attributes) = tag.strip_prefix("parameter ") { + return Some((quoted_name(attributes)?, "".to_owned())); + } + Some((tag, format!(""))) +} + fn tool_protocol_completion_error(message: impl Into) -> CompletionError { Box::new(AifarmDialogError::Response(format!( "tool protocol error: {}", @@ -11168,6 +11187,19 @@ mod tests { assert_eq!(commented.tool_calls.len(), 1); assert_eq!(commented.tool_calls[0].step.prompt, "A cat on a roof"); assert_eq!(commented.text, "Держи кота на крыше."); + + for named in [ + "Кот\nA cat on a roof", + "Кот\nA cat on a roof", + "КотA cat on a roof", + ] { + let output = forced_draw_step(named, true) + .await + .expect("named argument elements are that tool's arguments"); + assert_eq!(output.tool_calls.len(), 1, "{named}"); + assert_eq!(output.tool_calls[0].step.caption, "Кот"); + assert_eq!(output.tool_calls[0].step.prompt, "A cat on a roof"); + } } #[tokio::test]