use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;
use async_stream::stream;
use dynamo_protocols::types::{
ChatCompletionMessageContent, ChatCompletionMessageToolCallChunk,
ChatCompletionToolChoiceOption, FinishReason, FunctionCallStream, FunctionType,
};
use dynamo_runtime::config::{env_is_truthy, environment_names::llm as env_llm};
use dynamo_runtime::protocols::annotated::Annotated;
use futures::{Stream, StreamExt};
use uuid::Uuid;
use dynamo_parsers::tool_calling::{
CalledFunction, ToolCallResponse, ToolCallType, ToolDefinition,
};
use dynamo_parsers_v2::{Tool as ToolV2, ToolCallDelta, ToolParser, create_tool_parser_for_family};
use super::NvCreateChatCompletionStreamResponse;
pub(crate) const V2_FAMILIES: &[&str] = &["qwen3_coder", "deepseek_v4"];
pub(crate) fn enabled() -> bool {
static ENABLED: LazyLock<bool> =
LazyLock::new(|| env_is_truthy(env_llm::DYN_ENABLE_EXPERIMENTAL_PARSERS_V2));
*ENABLED
}
pub(crate) fn supports_family(family: &str) -> bool {
V2_FAMILIES.contains(&family)
}
pub(crate) fn batch_tool_choice_eligible(
tool_choice: Option<&ChatCompletionToolChoiceOption>,
) -> bool {
matches!(
tool_choice,
None | Some(ChatCompletionToolChoiceOption::Auto)
)
}
fn to_v2_tools(tools: Option<&[ToolDefinition]>) -> Vec<ToolV2> {
tools
.unwrap_or(&[])
.iter()
.map(|t| ToolV2 {
name: t.name.clone(),
description: None,
parameters: t.parameters.clone().unwrap_or(serde_json::Value::Null),
strict: t.strict,
})
.collect()
}
pub(crate) fn parse_complete(
content: &str,
tools: Option<&[ToolDefinition]>,
family: &str,
) -> anyhow::Result<(Vec<ToolCallResponse>, String)> {
let v2_tools = to_v2_tools(tools);
let mut parser = create_tool_parser_for_family(family, &v2_tools)?;
let result = parser.parse_complete(content)?;
let tool_calls = result
.calls
.into_iter()
.map(|call| ToolCallResponse {
id: format!("call-{}", Uuid::new_v4()),
tp: ToolCallType::Function,
function: CalledFunction {
name: call.name.unwrap_or_default(),
arguments: call.arguments,
},
})
.collect();
Ok((tool_calls, result.normal_text))
}
struct ChoiceState {
parser: Box<dyn ToolParser>,
opened: HashSet<usize>,
}
impl ChoiceState {
fn new(family: &str, tools: &[ToolV2]) -> anyhow::Result<Self> {
Ok(Self {
parser: create_tool_parser_for_family(family, tools)?,
opened: HashSet::new(),
})
}
fn emit_chunks(
&mut self,
calls: Vec<ToolCallDelta>,
) -> Option<Vec<ChatCompletionMessageToolCallChunk>> {
if calls.is_empty() {
return None;
}
let chunks = calls
.into_iter()
.map(|delta| {
let first = self.opened.insert(delta.tool_index);
ChatCompletionMessageToolCallChunk {
index: delta.tool_index as u32,
id: first.then(|| format!("call-{}", Uuid::new_v4())),
r#type: first.then_some(FunctionType::Function),
function: Some(FunctionCallStream {
name: delta.name,
arguments: Some(delta.arguments),
}),
}
})
.collect();
Some(chunks)
}
}
pub(crate) fn apply_stream<S>(
stream_in: S,
tool_definitions: Option<Vec<ToolDefinition>>,
family: String,
) -> impl Stream<Item = Annotated<NvCreateChatCompletionStreamResponse>> + Send
where
S: Stream<Item = Annotated<NvCreateChatCompletionStreamResponse>> + Send + 'static,
{
let v2_tools = to_v2_tools(tool_definitions.as_deref());
stream! {
if create_tool_parser_for_family(&family, &v2_tools).is_err() {
tracing::warn!(family = %family, "no dynamo-parsers-v2 parser for family; passing stream through unchanged");
tokio::pin!(stream_in);
while let Some(response) = stream_in.next().await {
yield response;
}
return;
}
let mut states: HashMap<u32, ChoiceState> = HashMap::new();
let mut finished: HashSet<u32> = HashSet::new();
let mut tool_emitted: HashSet<u32> = HashSet::new();
let mut template: Option<NvCreateChatCompletionStreamResponse> = None;
tokio::pin!(stream_in);
while let Some(mut response) = stream_in.next().await {
let Some(chat_response) = response.data.as_mut() else {
yield response;
continue;
};
{
let mut t = chat_response.clone();
t.inner.choices.clear();
template = Some(t);
}
for choice in chat_response.inner.choices.iter_mut() {
let state = states.entry(choice.index).or_insert_with(|| {
ChoiceState::new(&family, &v2_tools)
.expect("dynamo-parsers-v2 parser construction validated above")
});
let text = match choice.delta.content.as_ref() {
Some(ChatCompletionMessageContent::Text(t)) => Some(t.clone()),
_ => None,
};
let mut result = dynamo_parsers_v2::ToolParseResult::default();
let mut parsed_any = false;
if let Some(text) = text.as_deref() {
match state.parser.push(text) {
Ok(r) => {
result.append(r);
parsed_any = true;
}
Err(e) => {
tracing::warn!(error = %e, family = %family, "v2 stream push failed; passing chunk through");
}
}
}
if choice.finish_reason.is_some() && finished.insert(choice.index) {
match state.parser.finish() {
Ok(r) => {
result.append(r);
parsed_any = true;
}
Err(e) => {
tracing::warn!(error = %e, family = %family, "v2 stream finish failed");
}
}
}
if parsed_any {
let tool_calls = state.emit_chunks(result.calls);
if tool_calls.is_some() {
tool_emitted.insert(choice.index);
}
choice.delta.content = if result.normal_text.is_empty() {
None
} else {
Some(ChatCompletionMessageContent::Text(result.normal_text))
};
choice.delta.tool_calls = tool_calls;
}
if choice.finish_reason == Some(FinishReason::Stop)
&& tool_emitted.contains(&choice.index)
{
choice.finish_reason = Some(FinishReason::ToolCalls);
}
}
yield response;
}
if let Some(template) = template {
for (index, state) in states.iter_mut() {
if finished.contains(index) {
continue;
}
let Ok(result) = state.parser.finish() else {
continue;
};
let tool_calls = state.emit_chunks(result.calls);
if result.normal_text.is_empty() && tool_calls.is_none() {
continue;
}
let mut response = template.clone();
#[allow(deprecated)]
let choice = dynamo_protocols::types::ChatChoiceStream {
index: *index,
delta: dynamo_protocols::types::ChatCompletionStreamResponseDelta {
role: None,
content: (!result.normal_text.is_empty())
.then_some(ChatCompletionMessageContent::Text(result.normal_text)),
tool_calls,
function_call: None,
refusal: None,
reasoning_content: None,
},
finish_reason: None,
logprobs: None,
};
response.inner.choices = vec![choice];
yield Annotated {
data: Some(response),
id: None,
event: None,
comment: None,
error: None,
};
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use dynamo_protocols::types::{
ChatChoiceStream, ChatCompletionStreamResponseDelta, FinishReason, Role,
};
use futures::stream;
const QWEN3_GET_WEATHER: &str = "<tool_call>\n<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n</function>\n</tool_call>";
const DSV4_GET_WEATHER: &str = "<|DSML|tool_calls> <|DSML|invoke name=\"get_weather\"> <|DSML|parameter name=\"location\" string=\"true\">NYC</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>";
fn chunk(text: &str, finish: bool) -> Annotated<NvCreateChatCompletionStreamResponse> {
#[allow(deprecated)]
let response = NvCreateChatCompletionStreamResponse {
inner: dynamo_protocols::types::CreateChatCompletionStreamResponse {
id: "test".to_string(),
choices: vec![ChatChoiceStream {
index: 0,
delta: ChatCompletionStreamResponseDelta {
role: Some(Role::Assistant),
content: Some(ChatCompletionMessageContent::Text(text.to_string())),
tool_calls: None,
function_call: None,
refusal: None,
reasoning_content: None,
},
finish_reason: finish.then_some(FinishReason::Stop),
logprobs: None,
}],
created: 0,
model: "test".to_string(),
system_fingerprint: None,
service_tier: None,
object: "chat.completion.chunk".to_string(),
usage: None,
},
nvext: None,
llm_metrics: None,
};
Annotated {
data: Some(response),
id: None,
event: None,
comment: None,
error: None,
}
}
fn reassemble(
responses: &[Annotated<NvCreateChatCompletionStreamResponse>],
) -> (Vec<(String, String)>, String) {
let mut calls: Vec<(String, String)> = Vec::new();
let mut content = String::new();
for r in responses {
let Some(data) = r.data.as_ref() else {
continue;
};
for choice in &data.inner.choices {
if let Some(ChatCompletionMessageContent::Text(t)) = &choice.delta.content {
content.push_str(t);
}
let Some(tcs) = &choice.delta.tool_calls else {
continue;
};
for tc in tcs {
let idx = tc.index as usize;
if calls.len() <= idx {
calls.resize(idx + 1, (String::new(), String::new()));
}
if let Some(f) = &tc.function {
if let Some(name) = &f.name {
calls[idx].0 = name.clone();
}
if let Some(args) = &f.arguments {
calls[idx].1.push_str(args);
}
}
}
}
}
(calls, content)
}
fn final_finish_reason(
responses: &[Annotated<NvCreateChatCompletionStreamResponse>],
) -> Option<FinishReason> {
responses
.iter()
.filter_map(|r| r.data.as_ref())
.flat_map(|d| d.inner.choices.iter())
.filter_map(|c| c.finish_reason)
.next_back()
}
#[tokio::test]
async fn qwen3_bypass_streams_incrementally_without_leaking_markup() {
let mut chunks: Vec<_> = QWEN3_GET_WEATHER
.as_bytes()
.chunks(8)
.map(|b| chunk(std::str::from_utf8(b).unwrap(), false))
.collect();
chunks.push(chunk("", true));
let out: Vec<_> = apply_stream(stream::iter(chunks), None, "qwen3_coder".to_string())
.collect::<Vec<_>>()
.await;
let (calls, content) = reassemble(&out);
assert_eq!(calls.len(), 1, "expected exactly one tool call: {calls:?}");
assert_eq!(calls[0].0, "get_weather");
let args: serde_json::Value = serde_json::from_str(&calls[0].1).unwrap();
assert_eq!(args["location"], "Paris");
for marker in ["<tool_call>", "<function=", "<parameter="] {
assert!(
!content.contains(marker),
"raw markup {marker:?} leaked into content: {content:?}"
);
}
assert_eq!(
final_finish_reason(&out),
Some(FinishReason::ToolCalls),
"finish_reason must flip Stop->ToolCalls when tool calls are emitted"
);
}
#[tokio::test]
async fn qwen3_bypass_drops_value_truncated_at_eof() {
let truncated = "<tool_call>\n<function=get_weather>\n<parameter=location>\nPar";
let chunks = vec![chunk(truncated, false), chunk("", true)];
let out: Vec<_> = apply_stream(stream::iter(chunks), None, "qwen3_coder".to_string())
.collect::<Vec<_>>()
.await;
let (calls, content) = reassemble(&out);
let complete: Vec<_> = calls.iter().filter(|(n, _)| !n.is_empty()).collect();
assert!(
complete.is_empty(),
"truncated value must not produce a finished call: {calls:?}"
);
assert!(
!content.contains("<function="),
"raw markup leaked into content: {content:?}"
);
assert_eq!(
final_finish_reason(&out),
Some(FinishReason::Stop),
"no tool calls emitted -> finish_reason stays Stop"
);
}
#[tokio::test]
async fn dsv4_bypass_streams_incrementally_without_leaking_markup() {
let glyphs: Vec<char> = DSV4_GET_WEATHER.chars().collect();
let mut chunks: Vec<_> = glyphs
.chunks(6)
.map(|c| chunk(&c.iter().collect::<String>(), false))
.collect();
chunks.push(chunk("", true));
let out: Vec<_> = apply_stream(stream::iter(chunks), None, "deepseek_v4".to_string())
.collect::<Vec<_>>()
.await;
let (calls, content) = reassemble(&out);
let complete: Vec<_> = calls.iter().filter(|(n, _)| !n.is_empty()).collect();
assert_eq!(
complete.len(),
1,
"expected exactly one tool call: {calls:?}"
);
assert_eq!(complete[0].0, "get_weather");
let args: serde_json::Value = serde_json::from_str(&complete[0].1).unwrap();
assert_eq!(args["location"], "NYC");
assert!(
!content.contains("DSML"),
"raw DSML markup leaked into content: {content:?}"
);
assert_eq!(
final_finish_reason(&out),
Some(FinishReason::ToolCalls),
"finish_reason must flip Stop->ToolCalls when tool calls are emitted"
);
}
}