@@ -1792,6 +1792,123 @@ mod tests {
17921792 ) ;
17931793 }
17941794
1795+ #[ tokio:: test]
1796+ async fn test_deepseek_v3_1 ( ) {
1797+ // DeepSeek v3.1 format with two tool calls encoded in special tags
1798+ let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Berlin", "units": "metric"}<|tool▁call▁end|><|tool▁call▁begin|>get_weather_forecast<|tool▁sep|>{"location": "Berlin", "days": 7, "units": "imperial"}<|tool▁call▁end|><|tool▁call▁begin|>get_air_quality<|tool▁sep|>{"location": "Berlin", "radius": 50}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"# ;
1799+
1800+ let chunks = vec ! [ create_mock_response_chunk( text. to_string( ) , 0 ) ] ;
1801+
1802+ let input_stream = stream:: iter ( chunks) ;
1803+
1804+ let jail = JailedStream :: builder ( )
1805+ . tool_call_parser ( "deepseek_v3_1" )
1806+ . build ( ) ;
1807+ let jailed_stream = jail. apply ( input_stream) ;
1808+ let results: Vec < _ > = jailed_stream. collect ( ) . await ;
1809+
1810+ // Should have at least one output containing both analysis text and parsed tool call
1811+ assert ! ( !results. is_empty( ) ) ;
1812+
1813+ // Verify a tool call was parsed with expected name and args
1814+ let tool_call_idx = results
1815+ . iter ( )
1816+ . position ( test_utils:: has_tool_call)
1817+ . expect ( "Should have a tool call result" ) ;
1818+ test_utils:: assert_tool_call (
1819+ & results[ tool_call_idx] ,
1820+ "get_current_weather" ,
1821+ json ! ( { "location" : "Berlin" , "units" : "metric" } ) ,
1822+ ) ;
1823+ for result in results {
1824+ let Some ( data) = result. data else {
1825+ continue ;
1826+ } ;
1827+ for choice in data. choices {
1828+ if let Some ( content) = choice. delta . content {
1829+ assert ! (
1830+ !content. contains( "<|tool▁calls▁end|>" ) ,
1831+ "Should not contain deepseek special tokens in content"
1832+ ) ;
1833+ }
1834+ }
1835+ }
1836+ }
1837+
1838+ #[ tokio:: test]
1839+ async fn test_deepseek_v3_1_chunk ( ) {
1840+ // DeepSeek v3.1 format with two tool calls encoded in special tags
1841+ let text = r#"<|tool▁calls▁begin|><|tool▁call▁begin|>get_current_weather<|tool▁sep|>{"location": "Berlin", "units": "metric"}<|tool▁call▁end|><|tool▁call▁begin|>get_weather_forecast<|tool▁sep|>{"location": "Berlin", "days": 7, "units": "imperial"}<|tool▁call▁end|><|tool▁call▁begin|>get_air_quality<|tool▁sep|>{"location": "Berlin", "radius": 50}<|tool▁call▁end|><|tool▁calls▁end|><|end▁of▁sentence|>"# ;
1842+
1843+ // Split text into words, treating angle-bracketed tokens as one word
1844+ let mut words = Vec :: new ( ) ;
1845+ let mut i = 0 ;
1846+ let chars: Vec < char > = text. chars ( ) . collect ( ) ;
1847+ while i < chars. len ( ) {
1848+ if chars[ i] == '<' {
1849+ // Find the next '>'
1850+ if let Some ( end) = chars[ i..] . iter ( ) . position ( |& c| c == '>' ) {
1851+ let word: String = chars[ i..=i + end] . iter ( ) . collect ( ) ;
1852+ words. push ( word) ;
1853+ i += end + 1 ;
1854+ } else {
1855+ // Malformed, just push the rest
1856+ words. push ( chars[ i..] . iter ( ) . collect ( ) ) ;
1857+ break ;
1858+ }
1859+ } else if chars[ i] . is_whitespace ( ) {
1860+ i += 1 ;
1861+ } else {
1862+ // Collect until next whitespace or '<'
1863+ let start = i;
1864+ while i < chars. len ( ) && !chars[ i] . is_whitespace ( ) && chars[ i] != '<' {
1865+ i += 1 ;
1866+ }
1867+ words. push ( chars[ start..i] . iter ( ) . collect ( ) ) ;
1868+ }
1869+ }
1870+
1871+ let chunks = words
1872+ . into_iter ( )
1873+ . map ( |word| create_mock_response_chunk ( word, 0 ) )
1874+ . collect :: < Vec < _ > > ( ) ;
1875+
1876+ let input_stream = stream:: iter ( chunks) ;
1877+
1878+ let jail = JailedStream :: builder ( )
1879+ . tool_call_parser ( "deepseek_v3_1" )
1880+ . build ( ) ;
1881+ let jailed_stream = jail. apply ( input_stream) ;
1882+ let results: Vec < _ > = jailed_stream. collect ( ) . await ;
1883+
1884+ // Should have at least one output containing both analysis text and parsed tool call
1885+ assert ! ( !results. is_empty( ) ) ;
1886+
1887+ // Verify a tool call was parsed with expected name and args
1888+ let tool_call_idx = results
1889+ . iter ( )
1890+ . position ( test_utils:: has_tool_call)
1891+ . expect ( "Should have a tool call result" ) ;
1892+ test_utils:: assert_tool_call (
1893+ & results[ tool_call_idx] ,
1894+ "get_current_weather" ,
1895+ json ! ( { "location" : "Berlin" , "units" : "metric" } ) ,
1896+ ) ;
1897+ for result in results {
1898+ let Some ( data) = result. data else {
1899+ continue ;
1900+ } ;
1901+ for choice in data. choices {
1902+ if let Some ( content) = choice. delta . content {
1903+ assert ! (
1904+ !content. contains( "<|tool▁calls▁end|>" ) ,
1905+ "Should not contain deepseek special tokens in content"
1906+ ) ;
1907+ }
1908+ }
1909+ }
1910+ }
1911+
17951912 #[ tokio:: test]
17961913 async fn test_jailed_stream_mistral_false_positive_curly ( ) {
17971914 // Curly brace in normal text should not trigger tool call detection for mistral
0 commit comments