Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
zhaoyu6
sglang
Commits
b658be6f
Unverified
Commit
b658be6f
authored
Oct 02, 2025
by
Chang Su
Committed by
GitHub
Oct 02, 2025
Browse files
[router][grpc] Support tool call parser in streaming (#11160)
parent
5e786cca
Changes
38
Show whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
865 additions
and
1329 deletions
+865
-1329
sgl-router/src/tool_parser/registry.rs
sgl-router/src/tool_parser/registry.rs
+0
-245
sgl-router/src/tool_parser/state.rs
sgl-router/src/tool_parser/state.rs
+0
-186
sgl-router/src/tool_parser/tests.rs
sgl-router/src/tool_parser/tests.rs
+21
-146
sgl-router/src/tool_parser/traits.rs
sgl-router/src/tool_parser/traits.rs
+14
-8
sgl-router/src/tool_parser/types.rs
sgl-router/src/tool_parser/types.rs
+20
-0
sgl-router/tests/common/mod.rs
sgl-router/tests/common/mod.rs
+283
-0
sgl-router/tests/tool_parser_deepseek.rs
sgl-router/tests/tool_parser_deepseek.rs
+11
-13
sgl-router/tests/tool_parser_edge_cases.rs
sgl-router/tests/tool_parser_edge_cases.rs
+54
-39
sgl-router/tests/tool_parser_glm4_moe.rs
sgl-router/tests/tool_parser_glm4_moe.rs
+11
-13
sgl-router/tests/tool_parser_gpt_oss.rs
sgl-router/tests/tool_parser_gpt_oss.rs
+11
-13
sgl-router/tests/tool_parser_kimik2.rs
sgl-router/tests/tool_parser_kimik2.rs
+12
-14
sgl-router/tests/tool_parser_llama.rs
sgl-router/tests/tool_parser_llama.rs
+69
-71
sgl-router/tests/tool_parser_mixed_edge_cases.rs
sgl-router/tests/tool_parser_mixed_edge_cases.rs
+14
-15
sgl-router/tests/tool_parser_pythonic.rs
sgl-router/tests/tool_parser_pythonic.rs
+142
-165
sgl-router/tests/tool_parser_qwen.rs
sgl-router/tests/tool_parser_qwen.rs
+28
-28
sgl-router/tests/tool_parser_registry.rs
sgl-router/tests/tool_parser_registry.rs
+0
-192
sgl-router/tests/tool_parser_step3.rs
sgl-router/tests/tool_parser_step3.rs
+11
-13
sgl-router/tests/tool_parser_streaming.rs
sgl-router/tests/tool_parser_streaming.rs
+164
-168
No files found.
sgl-router/src/tool_parser/registry.rs
deleted
100644 → 0
View file @
5e786cca
use
crate
::
tool_parser
::
parsers
::{
DeepSeekParser
,
Glm4MoeParser
,
GptOssHarmonyParser
,
GptOssParser
,
JsonParser
,
KimiK2Parser
,
LlamaParser
,
MistralParser
,
PythonicParser
,
QwenParser
,
Step3Parser
,
};
use
crate
::
tool_parser
::
traits
::
ToolParser
;
use
once_cell
::
sync
::
Lazy
;
use
std
::{
collections
::
HashMap
,
env
,
sync
::
Arc
};
/// Global singleton registry instance - created once and reused
pub
static
GLOBAL_REGISTRY
:
Lazy
<
ParserRegistry
>
=
Lazy
::
new
(
ParserRegistry
::
new_internal
);
/// Registry for tool parsers and model mappings
pub
struct
ParserRegistry
{
/// Map of parser name to parser instance
parsers
:
HashMap
<
String
,
Arc
<
dyn
ToolParser
>>
,
/// Map of model name/pattern to parser name
model_mapping
:
HashMap
<
String
,
String
>
,
/// Default parser to use when no match found
default_parser
:
String
,
}
impl
ParserRegistry
{
/// Get the global singleton instance
pub
fn
new
()
->
&
'static
Self
{
&
GLOBAL_REGISTRY
}
/// Create a new instance for testing (not the singleton)
#[cfg(test)]
pub
fn
new_for_testing
()
->
Self
{
Self
::
new_internal
()
}
/// Internal constructor for creating the singleton instance
fn
new_internal
()
->
Self
{
let
mut
registry
=
Self
{
parsers
:
HashMap
::
new
(),
model_mapping
:
HashMap
::
new
(),
default_parser
:
"json"
.to_string
(),
};
// Register default parsers
registry
.register_default_parsers
();
// Register default model mappings
registry
.register_default_mappings
();
registry
}
/// Register a parser
pub
fn
register_parser
(
&
mut
self
,
name
:
impl
Into
<
String
>
,
parser
:
Arc
<
dyn
ToolParser
>
)
{
self
.parsers
.insert
(
name
.into
(),
parser
);
}
/// Map a model name/pattern to a parser
pub
fn
map_model
(
&
mut
self
,
model
:
impl
Into
<
String
>
,
parser
:
impl
Into
<
String
>
)
{
self
.model_mapping
.insert
(
model
.into
(),
parser
.into
());
}
/// Get parser for a specific model
pub
fn
get_parser
(
&
self
,
model
:
&
str
)
->
Option
<
Arc
<
dyn
ToolParser
>>
{
// Try exact match first
if
let
Some
(
parser_name
)
=
self
.model_mapping
.get
(
model
)
{
if
let
Some
(
parser
)
=
self
.parsers
.get
(
parser_name
)
{
return
Some
(
parser
.clone
());
}
}
// Try prefix matching with more specific patterns first
// Collect all matching patterns and sort by specificity (longer = more specific)
let
mut
matches
:
Vec
<
(
&
String
,
&
String
)
>
=
self
.model_mapping
.iter
()
.filter
(|(
pattern
,
_
)|
{
if
pattern
.ends_with
(
'*'
)
{
let
prefix
=
&
pattern
[
..
pattern
.len
()
-
1
];
model
.starts_with
(
prefix
)
}
else
{
false
}
})
.collect
();
// Sort by pattern length in descending order (longer patterns are more specific)
matches
.sort_by_key
(|(
pattern
,
_
)|
std
::
cmp
::
Reverse
(
pattern
.len
()));
// Return the first matching parser
for
(
_
,
parser_name
)
in
matches
{
if
let
Some
(
parser
)
=
self
.parsers
.get
(
parser_name
)
{
return
Some
(
parser
.clone
());
}
}
// Fall back to default parser if it exists
self
.parsers
.get
(
&
self
.default_parser
)
.cloned
()
}
/// List all registered parsers
pub
fn
list_parsers
(
&
self
)
->
Vec
<&
str
>
{
self
.parsers
.keys
()
.map
(|
s
|
s
.as_str
())
.collect
()
}
/// List all model mappings
pub
fn
list_mappings
(
&
self
)
->
Vec
<
(
&
str
,
&
str
)
>
{
self
.model_mapping
.iter
()
.map
(|(
k
,
v
)|
(
k
.as_str
(),
v
.as_str
()))
.collect
()
}
/// Register default parsers
fn
register_default_parsers
(
&
mut
self
)
{
// JSON parser - most common format
self
.register_parser
(
"json"
,
Arc
::
new
(
JsonParser
::
new
()));
// Mistral parser - [TOOL_CALLS] [...] format
self
.register_parser
(
"mistral"
,
Arc
::
new
(
MistralParser
::
new
()));
// Qwen parser - <tool_call>...</tool_call> format
self
.register_parser
(
"qwen"
,
Arc
::
new
(
QwenParser
::
new
()));
// Pythonic parser - [func(arg=val)] format
self
.register_parser
(
"pythonic"
,
Arc
::
new
(
PythonicParser
::
new
()));
// Llama parser - <|python_tag|>{...} or plain JSON format
self
.register_parser
(
"llama"
,
Arc
::
new
(
LlamaParser
::
new
()));
// DeepSeek V3 parser - Unicode tokens with JSON blocks
self
.register_parser
(
"deepseek"
,
Arc
::
new
(
DeepSeekParser
::
new
()));
// GLM-4 MoE parser - XML-style key-value format
self
.register_parser
(
"glm4_moe"
,
Arc
::
new
(
Glm4MoeParser
::
new
()));
// Step3 parser - StepTML XML format
self
.register_parser
(
"step3"
,
Arc
::
new
(
Step3Parser
::
new
()));
// Kimi K2 parser - Token-based with indexed functions
self
.register_parser
(
"kimik2"
,
Arc
::
new
(
KimiK2Parser
::
new
()));
// GPT-OSS parsers - register legacy and Harmony variants
let
gpt_oss_legacy
=
Arc
::
new
(
GptOssParser
::
new
());
let
gpt_oss_harmony
=
Arc
::
new
(
GptOssHarmonyParser
::
new
());
self
.register_parser
(
"gpt_oss_legacy"
,
gpt_oss_legacy
.clone
());
self
.register_parser
(
"gpt_oss_harmony"
,
gpt_oss_harmony
.clone
());
if
use_harmony_gpt_oss
()
{
self
.register_parser
(
"gpt_oss"
,
gpt_oss_harmony
);
}
else
{
self
.register_parser
(
"gpt_oss"
,
gpt_oss_legacy
);
}
}
/// Register default model mappings
fn
register_default_mappings
(
&
mut
self
)
{
// OpenAI models
self
.map_model
(
"gpt-4*"
,
"json"
);
self
.map_model
(
"gpt-3.5*"
,
"json"
);
self
.map_model
(
"gpt-4o*"
,
"json"
);
// Anthropic models
self
.map_model
(
"claude-*"
,
"json"
);
// Mistral models - use Mistral parser
self
.map_model
(
"mistral-*"
,
"mistral"
);
self
.map_model
(
"mixtral-*"
,
"mistral"
);
// Qwen models - use Qwen parser
self
.map_model
(
"qwen*"
,
"qwen"
);
self
.map_model
(
"Qwen*"
,
"qwen"
);
// Llama models
// Llama 4 uses pythonic format
self
.map_model
(
"llama-4*"
,
"pythonic"
);
self
.map_model
(
"meta-llama-4*"
,
"pythonic"
);
// Llama 3.2 uses python_tag format
self
.map_model
(
"llama-3.2*"
,
"llama"
);
self
.map_model
(
"meta-llama-3.2*"
,
"llama"
);
// Other Llama models use JSON
self
.map_model
(
"llama-*"
,
"json"
);
self
.map_model
(
"meta-llama-*"
,
"json"
);
// DeepSeek models
// DeepSeek V3 uses custom Unicode token format
self
.map_model
(
"deepseek-v3*"
,
"deepseek"
);
self
.map_model
(
"deepseek-ai/DeepSeek-V3*"
,
"deepseek"
);
// DeepSeek V2 uses pythonic format
self
.map_model
(
"deepseek-*"
,
"pythonic"
);
// GLM models
// GLM-4.5 and GLM-4.6 uses XML-style format
self
.map_model
(
"glm-4.5*"
,
"glm4_moe"
);
self
.map_model
(
"glm-4.6*"
,
"glm4_moe"
);
// Other GLM models may use JSON
self
.map_model
(
"glm-*"
,
"json"
);
// Step3 models
self
.map_model
(
"step3*"
,
"step3"
);
self
.map_model
(
"Step-3*"
,
"step3"
);
// Kimi models
self
.map_model
(
"kimi-k2*"
,
"kimik2"
);
self
.map_model
(
"Kimi-K2*"
,
"kimik2"
);
self
.map_model
(
"moonshot*/Kimi-K2*"
,
"kimik2"
);
// GPT-OSS models (T4-style)
self
.map_model
(
"gpt-oss*"
,
"gpt_oss"
);
self
.map_model
(
"t4-*"
,
"gpt_oss"
);
// Other models default to JSON
self
.map_model
(
"gemini-*"
,
"json"
);
self
.map_model
(
"palm-*"
,
"json"
);
self
.map_model
(
"gemma-*"
,
"json"
);
}
/// Set the default parser
pub
fn
set_default_parser
(
&
mut
self
,
name
:
impl
Into
<
String
>
)
{
self
.default_parser
=
name
.into
();
}
/// Check if a parser is registered
pub
fn
has_parser
(
&
self
,
name
:
&
str
)
->
bool
{
self
.parsers
.contains_key
(
name
)
}
}
fn
use_harmony_gpt_oss
()
->
bool
{
env
::
var
(
"ROUTER_USE_HARMONY_GPT_OSS"
)
.ok
()
.map
(|
value
|
{
let
normalized
=
value
.trim
();
matches!
(
normalized
,
"1"
|
"true"
|
"TRUE"
|
"True"
|
"yes"
|
"YES"
|
"Yes"
|
"on"
|
"ON"
|
"On"
)
})
.unwrap_or
(
false
)
}
impl
Default
for
&
'static
ParserRegistry
{
fn
default
()
->
Self
{
ParserRegistry
::
new
()
}
}
sgl-router/src/tool_parser/state.rs
View file @
b658be6f
use
crate
::
tool_parser
::
types
::{
PartialToolCall
,
ToolCall
};
/// Current phase of parsing
#[derive(Debug,
Clone,
Copy,
PartialEq,
Eq)]
pub
enum
ParsePhase
{
/// Looking for start of tool call
Searching
,
/// Parsing function name
InName
,
/// Parsing function arguments
InArguments
,
/// Tool call complete
Complete
,
}
/// State for streaming parser
#[derive(Debug,
Clone)]
pub
struct
ParseState
{
/// Buffer for accumulating input
pub
buffer
:
String
,
/// Position of last consumed character
pub
consumed
:
usize
,
/// Current partial tool being parsed
pub
partial_tool
:
Option
<
PartialToolCall
>
,
/// Completed tool calls
pub
completed_tools
:
Vec
<
ToolCall
>
,
/// Current parsing phase
pub
phase
:
ParsePhase
,
/// Bracket/brace depth for JSON parsing
pub
bracket_depth
:
i32
,
/// Whether currently inside a string literal
pub
in_string
:
bool
,
/// Whether next character should be escaped
pub
escape_next
:
bool
,
/// Current tool index (for streaming)
pub
tool_index
:
usize
,
/// Optional Harmony-specific streaming state (populated by token-aware parsers)
pub
harmony_stream
:
Option
<
HarmonyStreamState
>
,
}
impl
ParseState
{
/// Create a new parse state
pub
fn
new
()
->
Self
{
Self
{
buffer
:
String
::
new
(),
consumed
:
0
,
partial_tool
:
None
,
completed_tools
:
Vec
::
new
(),
phase
:
ParsePhase
::
Searching
,
bracket_depth
:
0
,
in_string
:
false
,
escape_next
:
false
,
tool_index
:
0
,
harmony_stream
:
None
,
}
}
/// Reset state for parsing next tool
pub
fn
reset
(
&
mut
self
)
{
self
.partial_tool
=
None
;
self
.phase
=
ParsePhase
::
Searching
;
self
.bracket_depth
=
0
;
self
.in_string
=
false
;
self
.escape_next
=
false
;
self
.harmony_stream
=
None
;
}
/// Process a single character for JSON parsing
pub
fn
process_char
(
&
mut
self
,
ch
:
char
)
{
// Handle escape sequences
if
self
.escape_next
{
self
.escape_next
=
false
;
self
.buffer
.push
(
ch
);
return
;
}
if
ch
==
'\\'
&&
self
.in_string
{
self
.escape_next
=
true
;
self
.buffer
.push
(
ch
);
return
;
}
// Track string boundaries
if
ch
==
'"'
&&
!
self
.escape_next
{
self
.in_string
=
!
self
.in_string
;
}
// Track bracket depth for JSON
if
!
self
.in_string
{
match
ch
{
'{'
|
'['
=>
{
self
.bracket_depth
+=
1
;
}
'}'
|
']'
=>
{
self
.bracket_depth
-=
1
;
if
self
.bracket_depth
==
0
&&
self
.partial_tool
.is_some
()
{
// Complete tool call found
self
.phase
=
ParsePhase
::
Complete
;
}
}
_
=>
{}
}
}
self
.buffer
.push
(
ch
);
}
/// Check if we have a complete JSON object/array
pub
fn
has_complete_json
(
&
self
)
->
bool
{
self
.bracket_depth
==
0
&&
!
self
.in_string
&&
!
self
.buffer
.is_empty
()
}
/// Extract content from buffer starting at position
pub
fn
extract_from
(
&
self
,
start
:
usize
)
->
&
str
{
if
start
>=
self
.buffer
.len
()
{
return
""
;
}
// Find the nearest character boundary at or after start
let
mut
safe_start
=
start
;
while
safe_start
<
self
.buffer
.len
()
&&
!
self
.buffer
.is_char_boundary
(
safe_start
)
{
safe_start
+=
1
;
}
if
safe_start
<
self
.buffer
.len
()
{
&
self
.buffer
[
safe_start
..
]
}
else
{
""
}
}
/// Mark content as consumed up to position
pub
fn
consume_to
(
&
mut
self
,
position
:
usize
)
{
if
position
>
self
.consumed
{
self
.consumed
=
position
;
}
}
/// Get unconsumed content
pub
fn
unconsumed
(
&
self
)
->
&
str
{
if
self
.consumed
>=
self
.buffer
.len
()
{
return
""
;
}
// Find the nearest character boundary at or after consumed
let
mut
safe_consumed
=
self
.consumed
;
while
safe_consumed
<
self
.buffer
.len
()
&&
!
self
.buffer
.is_char_boundary
(
safe_consumed
)
{
safe_consumed
+=
1
;
}
if
safe_consumed
<
self
.buffer
.len
()
{
&
self
.buffer
[
safe_consumed
..
]
}
else
{
""
}
}
/// Clear consumed content from buffer
pub
fn
clear_consumed
(
&
mut
self
)
{
if
self
.consumed
>
0
{
// Find the nearest character boundary at or before consumed
let
mut
safe_consumed
=
self
.consumed
;
while
safe_consumed
>
0
&&
!
self
.buffer
.is_char_boundary
(
safe_consumed
)
{
safe_consumed
-=
1
;
}
if
safe_consumed
>
0
{
self
.buffer
.drain
(
..
safe_consumed
);
self
.consumed
=
self
.consumed
.saturating_sub
(
safe_consumed
);
}
}
}
/// Add completed tool
pub
fn
add_completed_tool
(
&
mut
self
,
tool
:
ToolCall
)
{
self
.completed_tools
.push
(
tool
);
self
.tool_index
+=
1
;
}
}
impl
Default
for
ParseState
{
fn
default
()
->
Self
{
Self
::
new
()
}
}
/// Placeholder for Harmony streaming metadata captured during token-aware parsing.
#[derive(Debug,
Clone,
Default)]
pub
struct
HarmonyStreamState
{
...
...
sgl-router/src/tool_parser/tests.rs
View file @
b658be6f
...
...
@@ -5,64 +5,27 @@ use crate::tool_parser::partial_json::{
};
use
crate
::
tool_parser
::
traits
::
ToolParser
;
#[test]
fn
test_parse_state_new
()
{
let
state
=
ParseState
::
new
();
assert_eq!
(
state
.phase
,
ParsePhase
::
Searching
);
assert_eq!
(
state
.buffer
,
""
);
assert_eq!
(
state
.consumed
,
0
);
assert_eq!
(
state
.bracket_depth
,
0
);
assert
!
(
!
state
.in_string
);
assert
!
(
!
state
.escape_next
);
}
#[test]
fn
test_parse_state_process_char
()
{
let
mut
state
=
ParseState
::
new
();
state
.process_char
(
'{'
);
assert_eq!
(
state
.bracket_depth
,
1
);
state
.process_char
(
'}'
);
assert_eq!
(
state
.bracket_depth
,
0
);
state
.process_char
(
'"'
);
assert
!
(
state
.in_string
);
state
.process_char
(
'"'
);
assert
!
(
!
state
.in_string
);
state
.process_char
(
'"'
);
state
.process_char
(
'\\'
);
assert
!
(
state
.escape_next
);
state
.process_char
(
'"'
);
assert
!
(
!
state
.escape_next
);
assert
!
(
state
.in_string
);
// Still in string because quote was escaped
}
#[test]
fn
test_parser_registry
()
{
let
registry
=
ParserRegistry
::
new
();
assert
!
(
!
registry
.list_mappings
()
.is_empty
());
#[tokio::test]
async
fn
test_tool_parser_factory
()
{
let
factory
=
ToolParserFactory
::
new
();
let
mappings
=
registry
.list_mappings
();
let
has_gpt
=
mappings
.iter
()
.any
(|(
m
,
_
)|
m
.starts_with
(
"gpt"
));
assert
!
(
has_gpt
);
// Test that we can get a pooled parser
let
pooled_parser
=
factory
.get_pooled
(
"gpt-4"
);
let
parser
=
pooled_parser
.lock
()
.await
;
assert
!
(
parser
.detect_format
(
r#"{"name": "test", "arguments": {}}"#
));
}
#[test]
fn
test_parser_
registry_pattern_match
ing
()
{
let
mut
regist
ry
=
Parser
Regist
ry
::
new
_for_testing
();
#[
tokio::
test]
async
fn
test_
tool_
parser_
factory_model_mapp
ing
()
{
let
facto
ry
=
Tool
Parser
Facto
ry
::
new
();
registry
.map_model
(
"test-model"
,
"json"
);
// Test model mapping
factory
.registry
()
.map_model
(
"test-model"
,
"json"
);
let
mappings
=
registry
.list_mappings
();
let
has_test
=
mappings
.iter
()
.any
(|(
m
,
p
)|
*
m
==
"test-model"
&&
*
p
==
"json"
);
assert
!
(
has_test
);
// Get parser for the test model
let
pooled_parser
=
factory
.get_pooled
(
"test-model"
);
let
parser
=
pooled_parser
.lock
()
.await
;
assert
!
(
parser
.detect_format
(
r#"{"name": "test", "arguments": {}}"#
));
}
#[test]
...
...
@@ -165,37 +128,7 @@ fn test_compute_diff() {
assert_eq!
(
compute_diff
(
"test"
,
"hello"
),
"hello"
);
}
#[test]
fn
test_stream_result_variants
()
{
let
result
=
StreamResult
::
Incomplete
;
matches!
(
result
,
StreamResult
::
Incomplete
);
let
result
=
StreamResult
::
ToolName
{
index
:
0
,
name
:
"test"
.to_string
(),
};
if
let
StreamResult
::
ToolName
{
index
,
name
}
=
result
{
assert_eq!
(
index
,
0
);
assert_eq!
(
name
,
"test"
);
}
else
{
panic!
(
"Expected ToolName variant"
);
}
let
tool
=
ToolCall
{
id
:
"123"
.to_string
(),
r
#
type
:
"function"
.to_string
(),
function
:
FunctionCall
{
name
:
"test"
.to_string
(),
arguments
:
"{}"
.to_string
(),
},
};
let
result
=
StreamResult
::
ToolComplete
(
tool
.clone
());
if
let
StreamResult
::
ToolComplete
(
t
)
=
result
{
assert_eq!
(
t
.id
,
"123"
);
}
else
{
panic!
(
"Expected ToolComplete variant"
);
}
}
// NOTE: test_stream_result_variants removed - StreamResult enum replaced by StreamingParseResult
#[test]
fn
test_partial_tool_call
()
{
...
...
@@ -310,14 +243,12 @@ fn test_json_parser_format_detection() {
}
#[tokio::test]
async
fn
test_registry_with_json_parser
()
{
let
registry
=
ParserRegistry
::
new
();
// JSON parser should be registered by default
assert
!
(
registry
.has_parser
(
"json"
));
async
fn
test_factory_with_json_parser
()
{
let
factory
=
ToolParserFactory
::
new
();
// Should get JSON parser for OpenAI models
let
parser
=
registry
.get_parser
(
"gpt-4-turbo"
)
.unwrap
();
let
pooled_parser
=
factory
.get_pooled
(
"gpt-4-turbo"
);
let
parser
=
pooled_parser
.lock
()
.await
;
let
input
=
r#"{"name": "test", "arguments": {"x": 1}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
input
)
.await
.unwrap
();
...
...
@@ -546,62 +477,6 @@ mod edge_cases {
assert
!
(
tools
[
0
]
.function.arguments
.contains
(
"null"
));
}
#[tokio::test]
async
fn
test_streaming_with_partial_chunks
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state1
=
ParseState
::
new
();
let
partial
=
r#"{"#
;
let
result
=
parser
.parse_incremental
(
partial
,
&
mut
state1
)
.await
.unwrap
();
assert
!
(
matches!
(
result
,
StreamResult
::
Incomplete
),
"Should return Incomplete for just opening brace"
);
let
mut
state2
=
ParseState
::
new
();
let
complete
=
r#"{"name": "get_weather", "arguments": {"location": "SF"}}"#
;
let
result
=
parser
.parse_incremental
(
complete
,
&
mut
state2
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"SF"
);
}
_
=>
panic!
(
"Expected ToolComplete for complete JSON"
),
}
// The PartialJson parser can complete partial JSON by filling in missing values
let
mut
state3
=
ParseState
::
new
();
let
partial_with_name
=
r#"{"name": "test", "argum"#
;
let
result
=
parser
.parse_incremental
(
partial_with_name
,
&
mut
state3
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"test"
);
// Arguments will be empty object since "argum" is incomplete
assert_eq!
(
tool
.function.arguments
,
"{}"
);
}
StreamResult
::
ToolName
{
name
,
..
}
=>
{
assert_eq!
(
name
,
"test"
);
}
StreamResult
::
Incomplete
=>
{
// Also acceptable if parser decides to wait
}
_
=>
panic!
(
"Unexpected result for partial JSON with name"
),
}
}
#[tokio::test]
async
fn
test_special_json_values
()
{
let
parser
=
JsonParser
::
new
();
...
...
sgl-router/src/tool_parser/traits.rs
View file @
b658be6f
use
crate
::
protocols
::
spec
::
Tool
;
use
crate
::
tool_parser
::{
errors
::
ToolParserResult
,
state
::
ParseState
,
types
::{
StreamResult
,
ToolCall
},
types
::{
StreamingParseResult
,
ToolCall
},
};
use
async_trait
::
async_trait
;
...
...
@@ -13,11 +13,16 @@ pub trait ToolParser: Send + Sync {
async
fn
parse_complete
(
&
self
,
output
:
&
str
)
->
ToolParserResult
<
(
String
,
Vec
<
ToolCall
>
)
>
;
/// Parse tool calls from model output (streaming)
/// Parsers now maintain internal state, so self is mutable
///
/// # Arguments
/// * `chunk` - New text chunk from model output
/// * `tools` - List of available tools for validation
async
fn
parse_incremental
(
&
self
,
&
mut
self
,
chunk
:
&
str
,
state
:
&
mut
ParseState
,
)
->
ToolParserResult
<
StreamResult
>
;
tools
:
&
[
Tool
]
,
)
->
ToolParserResult
<
Stream
ingParse
Result
>
;
/// Check if text contains tool calls in this parser's format
fn
detect_format
(
&
self
,
text
:
&
str
)
->
bool
;
...
...
@@ -50,9 +55,10 @@ pub trait TokenToolParser: ToolParser {
)
->
ToolParserResult
<
(
String
,
Vec
<
ToolCall
>
)
>
;
/// Streaming parser entrypoint for token chunks.
/// Parsers maintain internal state, so self is mutable
async
fn
parse_incremental_tokens
(
&
self
,
&
mut
self
,
tokens
:
&
[
u32
],
state
:
&
mut
ParseState
,
)
->
ToolParserResult
<
StreamResult
>
;
tools
:
&
[
Tool
]
,
)
->
ToolParserResult
<
Stream
ingParse
Result
>
;
}
sgl-router/src/tool_parser/types.rs
View file @
b658be6f
...
...
@@ -71,3 +71,23 @@ pub struct PartialToolCall {
/// Arguments already streamed
pub
streamed_args
:
String
,
}
/// Result of streaming parse operation (matches Python StreamingParseResult)
#[derive(Debug,
Clone,
Default)]
pub
struct
StreamingParseResult
{
/// Normal text that's not part of tool calls
pub
normal_text
:
String
,
/// Tool call items parsed from the chunk
pub
calls
:
Vec
<
ToolCallItem
>
,
}
/// Simple encapsulation of parsed tool call for streaming (matches Python ToolCallItem)
#[derive(Debug,
Clone)]
pub
struct
ToolCallItem
{
/// Tool index in the array
pub
tool_index
:
usize
,
/// Tool name (only present on first chunk)
pub
name
:
Option
<
String
>
,
/// Incremental JSON arguments
pub
parameters
:
String
,
}
sgl-router/tests/common/mod.rs
View file @
b658be6f
...
...
@@ -6,7 +6,9 @@ pub mod mock_openai_server;
pub
mod
mock_worker
;
pub
mod
test_app
;
use
serde_json
::
json
;
use
sglang_router_rs
::
config
::
RouterConfig
;
use
sglang_router_rs
::
protocols
::
spec
::{
Function
,
Tool
};
use
sglang_router_rs
::
server
::
AppContext
;
use
std
::
fs
;
use
std
::
path
::
PathBuf
;
...
...
@@ -100,3 +102,284 @@ pub const EXPECTED_HASHES: [u64; 4] = [
6245658446118930933
,
5097285695902185237
,
];
/// Create a comprehensive set of test tools covering all parser test scenarios
#[allow(dead_code)]
pub
fn
create_test_tools
()
->
Vec
<
Tool
>
{
vec!
[
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"search"
.to_string
(),
description
:
Some
(
"Search for information"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"query"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"get_weather"
.to_string
(),
description
:
Some
(
"Get weather information"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"city"
:
{
"type"
:
"string"
},
"location"
:
{
"type"
:
"string"
},
"date"
:
{
"type"
:
"string"
},
"units"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"calculate"
.to_string
(),
description
:
Some
(
"Perform calculations"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"x"
:
{
"type"
:
"number"
},
"y"
:
{
"type"
:
"number"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"translate"
.to_string
(),
description
:
Some
(
"Translate text"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"text"
:
{
"type"
:
"string"
},
"to"
:
{
"type"
:
"string"
},
"target_lang"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"get_time"
.to_string
(),
description
:
Some
(
"Get current time"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"timezone"
:
{
"type"
:
"string"
},
"format"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"get_current_time"
.to_string
(),
description
:
Some
(
"Get current time"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"timezone"
:
{
"type"
:
"string"
},
"format"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"update_settings"
.to_string
(),
description
:
Some
(
"Update settings"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"preferences"
:
{
"type"
:
"object"
},
"notifications"
:
{
"type"
:
"boolean"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"ping"
.to_string
(),
description
:
Some
(
"Ping service"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{}}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"test"
.to_string
(),
description
:
Some
(
"Test function"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{}}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"process"
.to_string
(),
description
:
Some
(
"Process data"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"count"
:
{
"type"
:
"number"
},
"rate"
:
{
"type"
:
"number"
},
"enabled"
:
{
"type"
:
"boolean"
},
"data"
:
{
"type"
:
"object"
},
"text"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"web_search"
.to_string
(),
description
:
Some
(
"Search the web"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"query"
:
{
"type"
:
"string"
},
"num_results"
:
{
"type"
:
"number"
},
"search_type"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"get_tourist_attractions"
.to_string
(),
description
:
Some
(
"Get tourist attractions"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"city"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"config"
.to_string
(),
description
:
Some
(
"Configuration function"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"debug"
:
{
"type"
:
"boolean"
},
"verbose"
:
{
"type"
:
"boolean"
},
"optional"
:
{
"type"
:
"null"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"test_func"
.to_string
(),
description
:
Some
(
"Test function"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"bool_true"
:
{
"type"
:
"boolean"
},
"bool_false"
:
{
"type"
:
"boolean"
},
"none_val"
:
{
"type"
:
"null"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"create"
.to_string
(),
description
:
Some
(
"Create resource"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"name"
:
{
"type"
:
"string"
},
"email"
:
{
"type"
:
"string"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"add"
.to_string
(),
description
:
Some
(
"Add operation"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"x"
:
{
"type"
:
"number"
},
"y"
:
{
"type"
:
"number"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"calc"
.to_string
(),
description
:
Some
(
"Calculate"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"x"
:
{
"type"
:
"number"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"func1"
.to_string
(),
description
:
Some
(
"Function 1"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{}}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"func2"
.to_string
(),
description
:
Some
(
"Function 2"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"y"
:
{
"type"
:
"number"
}
}
}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"tool1"
.to_string
(),
description
:
Some
(
"Tool 1"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{}}),
},
},
Tool
{
tool_type
:
"function"
.to_string
(),
function
:
Function
{
name
:
"tool2"
.to_string
(),
description
:
Some
(
"Tool 2"
.to_string
()),
parameters
:
json!
({
"type"
:
"object"
,
"properties"
:
{
"y"
:
{
"type"
:
"number"
}
}
}),
},
},
]
}
sgl-router/tests/tool_parser_deepseek.rs
View file @
b658be6f
//! DeepSeek V3 Parser Integration Tests
use
sglang_router_rs
::
tool_parser
::{
DeepSeekParser
,
ParseState
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
DeepSeekParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_deepseek_complete_parsing
()
{
...
...
@@ -46,8 +49,9 @@ async fn test_deepseek_multiple_tools() {
#[tokio::test]
async
fn
test_deepseek_streaming
()
{
let
parser
=
DeepSeekParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
DeepSeekParser
::
new
();
// Simulate streaming chunks
let
chunks
=
vec!
[
...
...
@@ -61,25 +65,19 @@ async fn test_deepseek_streaming() {
];
let
mut
found_name
=
false
;
let
mut
found_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolName
{
name
,
..
}
=>
{
for
call
in
result
.calls
{
if
let
Some
(
name
)
=
call
.name
{
assert_eq!
(
name
,
"get_weather"
);
found_name
=
true
;
}
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
found_complete
=
true
;
}
_
=>
{}
}
}
assert
!
(
found_name
||
found_complete
);
assert
!
(
found_name
,
"Should have found tool name during streaming"
);
}
#[tokio::test]
...
...
sgl-router/tests/tool_parser_edge_cases.rs
View file @
b658be6f
...
...
@@ -3,27 +3,46 @@
//! Tests for malformed input, edge cases, and error recovery
use
sglang_router_rs
::
tool_parser
::{
JsonParser
,
MistralParser
,
ParseState
,
ParserRegistry
,
PythonicParser
,
QwenParser
,
StreamResult
,
ToolParser
,
JsonParser
,
MistralParser
,
PythonicParser
,
QwenParser
,
ToolParser
,
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_empty_input
()
{
let
registry
=
ParserRegistry
::
new
();
let
parsers
=
vec!
[
"json"
,
"mistral"
,
"qwen"
,
"pythonic"
,
"llama"
];
// Test that all parsers handle empty input correctly
let
json_parser
=
JsonParser
::
new
();
let
(
_
normal_text
,
tools
)
=
json_parser
.parse_complete
(
""
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
0
,
"JSON parser should return empty for empty input"
);
for
parser_name
in
parsers
{
let
parser
=
registry
.get_parser
(
&
format!
(
"test-{}"
,
parser_name
))
.unwrap
();
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
""
)
.await
.unwrap
();
let
mistral_parser
=
MistralParser
::
new
();
let
(
_
normal_text
,
tools
)
=
mistral_parser
.parse_complete
(
""
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
0
,
"Parser {} should return empty for empty input"
,
parser_name
"Mistral parser should return empty for empty input"
);
let
qwen_parser
=
QwenParser
::
new
();
let
(
_
normal_text
,
tools
)
=
qwen_parser
.parse_complete
(
""
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
0
,
"Qwen parser should return empty for empty input"
);
let
pythonic_parser
=
PythonicParser
::
new
();
let
(
_
normal_text
,
tools
)
=
pythonic_parser
.parse_complete
(
""
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
0
,
"Pythonic parser should return empty for empty input"
);
}
}
#[tokio::test]
...
...
@@ -277,38 +296,39 @@ async fn test_null_and_boolean_values() {
#[tokio::test]
async
fn
test_partial_token_at_buffer_boundary
()
{
let
parser
=
QwenParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
QwenParser
::
new
();
let
tools
=
create_test_tools
();
// Send exactly "<tool" which is a 5-character prefix of "<tool_call>\n"
let
result
=
parser
.parse_incremental
(
"<tool"
,
&
mut
state
)
.await
.unwrap
();
assert
!
(
matches!
(
result
,
StreamResult
::
Incomplete
));
assert_eq!
(
state
.buffer
,
"<tool"
);
let
result
=
parser
.parse_incremental
(
"<tool"
,
&
tools
)
.await
.unwrap
();
assert
!
(
result
.calls
.is_empty
(),
"Should be incomplete for partial tag"
);
// Complete the token
let
result
=
parser
.parse_incremental
(
"_call>
\n
{
\"
name
\"
:
\"
test
\"
,
\"
arguments
\"
: {}}
\n
</tool_call>"
,
&
mut
state
,
&
tools
,
)
.await
.unwrap
();
// Should successfully parse after completing
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"test"
);
}
_
=>
{
// In Phase 2 simplified streaming, might get Incomplete
// The important thing is it didn't fail to recognize the partial token
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"test"
);
}
}
}
#[tokio::test]
async
fn
test_exact_prefix_lengths
()
{
let
parser
=
QwenParser
::
new
();
let
mut
parser
=
QwenParser
::
new
();
let
tools
=
create_test_tools
();
let
test_cases
=
vec!
[
(
"<"
,
1
),
// 1-char prefix
...
...
@@ -319,18 +339,13 @@ async fn test_exact_prefix_lengths() {
];
for
(
prefix
,
expected_len
)
in
test_cases
{
let
mut
state
=
ParseState
::
new
();
let
result
=
parser
.parse_incremental
(
prefix
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
prefix
,
&
tools
)
.await
.unwrap
();
assert
!
(
matches!
(
result
,
StreamResult
::
Incomplete
),
result
.calls
.is_empty
(
),
"Prefix '{}' (len {}) should be incomplete"
,
prefix
,
expected_len
);
assert_eq!
(
state
.buffer
,
prefix
,
"Buffer should contain the prefix '{}'"
,
prefix
);
// Buffer is now internal to parser - can't assert on it
}
}
sgl-router/tests/tool_parser_glm4_moe.rs
View file @
b658be6f
//! GLM-4 MoE Parser Integration Tests
use
sglang_router_rs
::
tool_parser
::{
Glm4MoeParser
,
ParseState
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
Glm4MoeParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_glm4_complete_parsing
()
{
...
...
@@ -78,8 +81,9 @@ async fn test_glm4_type_conversion() {
#[tokio::test]
async
fn
test_glm4_streaming
()
{
let
parser
=
Glm4MoeParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
Glm4MoeParser
::
new
();
let
tools
=
create_test_tools
();
// Simulate streaming chunks
let
chunks
=
vec!
[
...
...
@@ -93,25 +97,19 @@ async fn test_glm4_streaming() {
];
let
mut
found_name
=
false
;
let
mut
found_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolName
{
name
,
..
}
=>
{
for
call
in
result
.calls
{
if
let
Some
(
name
)
=
call
.name
{
assert_eq!
(
name
,
"get_weather"
);
found_name
=
true
;
}
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
found_complete
=
true
;
}
_
=>
{}
}
}
assert
!
(
found_name
||
found_complete
);
assert
!
(
found_name
,
"Should have found tool name during streaming"
);
}
#[test]
...
...
sgl-router/tests/tool_parser_gpt_oss.rs
View file @
b658be6f
//! GPT-OSS Parser Integration Tests
use
sglang_router_rs
::
tool_parser
::{
GptOssParser
,
ParseState
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
GptOssParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_gpt_oss_complete_parsing
()
{
...
...
@@ -71,8 +74,9 @@ async fn test_gpt_oss_empty_args() {
#[tokio::test]
async
fn
test_gpt_oss_streaming
()
{
let
parser
=
GptOssParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
GptOssParser
::
new
();
// Simulate streaming chunks
let
chunks
=
vec!
[
...
...
@@ -84,26 +88,20 @@ async fn test_gpt_oss_streaming() {
"<|call|>"
,
];
let
mut
found_name
=
false
;
let
mut
found_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolName
{
name
,
..
}
=>
{
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"calculate"
);
found_name
=
true
;
}
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"calculate"
);
found_complete
=
true
;
}
_
=>
{}
}
}
assert
!
(
found_name
||
found_complete
);
assert
!
(
found_complete
);
}
#[test]
...
...
sgl-router/tests/tool_parser_kimik2.rs
View file @
b658be6f
//! Kimi K2 Parser Integration Tests
use
sglang_router_rs
::
tool_parser
::{
KimiK2Parser
,
ParseState
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
KimiK2Parser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_kimik2_complete_parsing
()
{
...
...
@@ -58,8 +61,9 @@ async fn test_kimik2_with_whitespace() {
#[tokio::test]
async
fn
test_kimik2_streaming
()
{
let
parser
=
KimiK2Parser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
KimiK2Parser
::
new
();
// Simulate streaming chunks
let
chunks
=
vec!
[
...
...
@@ -74,25 +78,19 @@ async fn test_kimik2_streaming() {
];
let
mut
found_name
=
false
;
let
mut
found_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolName
{
name
,
..
}
=>
{
for
call
in
result
.calls
{
if
let
Some
(
name
)
=
call
.name
{
assert_eq!
(
name
,
"calculate"
);
found_name
=
true
;
}
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"calculate"
);
found_complete
=
true
;
}
_
=>
{}
}
}
assert
!
(
found_name
||
found_complete
);
assert
!
(
found_name
,
"Should have found tool name during streaming"
);
}
#[test]
...
...
@@ -156,5 +154,5 @@ async fn test_namespace_extraction() {
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"search"
);
//
Should extract after last dot
assert_eq!
(
tools
[
0
]
.function.name
,
"
api.tools.
search"
);
//
Includes full namespace
}
sgl-router/tests/tool_parser_llama.rs
View file @
b658be6f
...
...
@@ -4,6 +4,9 @@
use
sglang_router_rs
::
tool_parser
::{
LlamaParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_llama_python_tag_format
()
{
let
parser
=
LlamaParser
::
new
();
...
...
@@ -228,29 +231,27 @@ async fn test_with_python_tag_prefix() {
#[tokio::test]
async
fn
test_llama_streaming_simple
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
// Send complete JSON at once
let
full_json
=
r#"<|python_tag|>{"name": "search", "parameters": {"query": "weather"}}"#
;
let
result
=
parser
.parse_incremental
(
full_json
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_json
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"search"
);
}
_
=>
panic!
(
"Expected ToolComplete for complete JSON input"
),
}
assert
!
(
!
result
.calls
.is_empty
(),
"Expected tool call for complete JSON input"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"search"
);
}
#[tokio::test]
async
fn
test_llama_streaming_partial
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
// Stream in chunks
let
chunks
=
vec!
[
...
...
@@ -264,20 +265,23 @@ async fn test_llama_streaming_partial() {
let
mut
got_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"calculate"
);
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"calculate"
);
got_complete
=
true
;
}
}
}
assert
!
(
got_complete
,
"Should have completed parsing"
);
}
#[tokio::test]
async
fn
test_llama_streaming_plain_json
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
// Stream plain JSON without python_tag
let
chunks
=
vec!
[
...
...
@@ -291,20 +295,23 @@ async fn test_llama_streaming_plain_json() {
let
mut
got_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"search"
);
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"search"
);
got_complete
=
true
;
}
}
}
assert
!
(
got_complete
,
"Should have completed parsing"
);
}
#[tokio::test]
async
fn
test_llama_streaming_with_text_before
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
let
chunks
=
vec!
[
r#"Let me help you. "#
,
...
...
@@ -317,86 +324,77 @@ async fn test_llama_streaming_with_text_before() {
let
mut
got_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"get_time"
);
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"get_time"
);
got_complete
=
true
;
}
}
}
assert
!
(
got_complete
,
"Should have completed parsing"
);
}
#[tokio::test]
async
fn
test_llama_streaming_multiple_tools
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
let
text
=
r#"<|python_tag|>{"name": "func1", "parameters": {}};{"name": "func2", "parameters": {}}"#
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
// Should get first tool complete
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"func1"
);
}
_
=>
panic!
(
"Expected first tool to be complete, got: {:?}"
,
result
),
assert
!
(
!
result
.calls
.is_empty
(),
"Expected first tool to be complete"
);
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"func1"
);
}
// Process remaining buffer to get second tool
let
result2
=
parser
.parse_incremental
(
""
,
&
mut
state
)
.await
.unwrap
();
match
result2
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.
name
,
"func2"
);
let
result2
=
parser
.parse_incremental
(
""
,
&
tools
)
.await
.unwrap
();
if
!
result2
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result2
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"func2"
);
}
_
=>
panic!
(
"Expected second tool to be complete"
),
}
}
#[tokio::test]
async
fn
test_llama_streaming_multiple_tools_chunked
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
LlamaParser
::
new
();
let
tools
=
create_test_tools
();
// First chunk - incomplete first JSON
let
chunk1
=
r#"<|python_tag|>{"name": "get_weather", "parameters""#
;
let
result1
=
parser
.parse_incremental
(
chunk1
,
&
mut
state
)
.await
.unwrap
();
// Should be incomplete or have tool name
match
result1
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
Incomplete
|
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolName
{
..
}
|
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolArguments
{
..
}
=>
{
// Expected - could get tool name or be incomplete or even partial args
let
result1
=
parser
.parse_incremental
(
chunk1
,
&
tools
)
.await
.unwrap
();
if
!
result1
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result1
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"get_weather"
);
}
_
=>
panic!
(
"Expected incomplete or tool name for partial JSON, got: {:?}"
,
result1
),
}
// Second chunk - complete first JSON and separator
let
chunk2
=
r#": {"city": "Paris"}};{"name": "#
;
let
result2
=
parser
.parse_incremental
(
chunk2
,
&
mut
state
)
.await
.unwrap
();
let
result2
=
parser
.parse_incremental
(
chunk2
,
&
tools
)
.await
.unwrap
();
// Should get first tool complete
match
result2
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
// Should get parameters for first tool (name already sent in result1)
if
!
result2
.calls
.is_empty
()
{
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result2
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"city"
],
"Paris"
);
}
_
=>
panic!
(
"Expected first tool complete, got: {:?}"
,
result2
),
}
let
chunk3
=
r#""get_time", "parameters": {"timezone": "UTC"}}"#
;
let
result3
=
parser
.parse_incremental
(
chunk3
,
&
mut
state
)
.await
.unwrap
();
match
result3
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.
name
,
"get_time"
);
let
result3
=
parser
.parse_incremental
(
chunk3
,
&
tools
)
.await
.unwrap
();
if
!
result3
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result3
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"get_time"
);
}
_
=>
panic!
(
"Expected tool to be complete, got: {:?}"
,
result3
),
}
}
sgl-router/tests/tool_parser_mixed_edge_cases.rs
View file @
b658be6f
...
...
@@ -4,10 +4,12 @@
use
serde_json
::
json
;
use
sglang_router_rs
::
tool_parser
::{
JsonParser
,
LlamaParser
,
MistralParser
,
ParseState
,
PythonicParser
,
QwenParser
,
StreamResult
,
ToolParser
,
JsonParser
,
LlamaParser
,
MistralParser
,
PythonicParser
,
QwenParser
,
ToolParser
,
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_mixed_formats_in_text
()
{
let
json_parser
=
JsonParser
::
new
();
...
...
@@ -152,25 +154,22 @@ async fn test_special_json_values() {
#[tokio::test]
async
fn
test_parser_recovery_after_invalid_input
()
{
let
mut
state
=
ParseState
::
new
();
let
parser
=
JsonParser
::
new
();
let
mut
parser
=
JsonParser
::
new
();
let
tools
=
create_test_tools
();
// Send invalid JSON first
let
_
=
parser
.parse_incremental
(
r#"{"broken": "#
,
&
mut
state
)
.await
;
let
_
=
parser
.parse_incremental
(
r#"{"broken": "#
,
&
tools
)
.await
;
// C
l
ea
r state and try valid JSON
state
.buffer
.clear
();
let
result
=
parser
.parse_incremental
(
r#"{"name": "valid", "arguments": {}}"#
,
&
mut
state
)
// C
r
ea
te a new parser instance for clean state
let
mut
parser2
=
JsonParser
::
new
();
let
result
=
parser
2
.parse_incremental
(
r#"{"name": "valid", "arguments": {}}"#
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"valid"
);
}
_
=>
{
// Might be incomplete depending on implementation
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"valid"
);
}
}
}
...
...
sgl-router/tests/tool_parser_pythonic.rs
View file @
b658be6f
...
...
@@ -5,6 +5,9 @@
use
serde_json
::
json
;
use
sglang_router_rs
::
tool_parser
::{
PythonicParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_pythonic_single_function
()
{
let
parser
=
PythonicParser
::
new
();
...
...
@@ -246,260 +249,231 @@ async fn test_pythonic_complex_nesting() {
#[tokio::test]
async
fn
test_parse_streaming_no_brackets
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"This is just normal text without any tool calls."
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
Incomplete
=>
{
// Expected - no tool calls found
assert_eq!
(
state
.buffer
,
text
);
}
_
=>
panic!
(
"Should return Incomplete for text without tool calls"
),
}
assert
!
(
result
.calls
.is_empty
());
}
#[tokio::test]
async
fn
test_parse_streaming_complete_tool_call
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"Here's a tool call: [get_weather(location='New York', unit='celsius')]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse complete tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"New York"
);
assert_eq!
(
args
[
"unit"
],
"celsius"
);
assert_eq!
(
state
.buffer
,
""
);
}
_
=>
panic!
(
"Should return ToolComplete for complete tool call"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_text_before_tool_call
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"This is some text before [get_weather(location='London')]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"London"
);
}
_
=>
panic!
(
"Should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_partial_tool_call
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
// First chunk with opening bracket but no closing bracket
let
text1
=
"Let me check the weather: [get_weather(location="
;
let
result1
=
parser
.parse_incremental
(
text1
,
&
mut
state
)
.await
.unwrap
();
let
result1
=
parser
.parse_incremental
(
text1
,
&
tools
)
.await
.unwrap
();
match
result1
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
Incomplete
=>
{
assert
!
(
state
.buffer
.contains
(
"[get_weather(location="
));
}
_
=>
panic!
(
"First chunk should return Incomplete"
),
}
// First chunk should be incomplete
assert
!
(
result1
.calls
.is_empty
(),
"First chunk should not return tool call"
);
// Second chunk completing the tool call
let
text2
=
"'Paris')]"
;
let
result2
=
parser
.parse_incremental
(
text2
,
&
mut
state
)
.await
.unwrap
();
match
result2
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
let
result2
=
parser
.parse_incremental
(
text2
,
&
tools
)
.await
.unwrap
();
assert
!
(
!
result2
.calls
.is_empty
(),
"Second chunk should complete tool call"
);
assert_eq!
(
result2
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result2
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"Paris"
);
assert_eq!
(
state
.buffer
,
""
);
}
_
=>
panic!
(
"Second chunk should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_bracket_without_text_before
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"[search(query='python programming')]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"search"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"search"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"query"
],
"python programming"
);
}
_
=>
panic!
(
"Should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_text_after_tool_call
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
// First chunk with complete tool call and some text after
let
text
=
"[get_weather(location='Tokyo')] Here's the forecast:"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
// Text after tool call should remain in buffer
// Note: Current implementation may clear buffer, this behavior needs verification
}
_
=>
panic!
(
"Should return ToolComplete"
),
}
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
// Text after tool call is handled by parser internally
}
#[tokio::test]
async
fn
test_parse_streaming_multiple_tool_calls
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"[get_weather(location='Berlin'), search(query='restaurants')]"
;
// Current implementation may handle this as a single parse
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
// The parser should handle multiple tools in one bracket pair
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
_
)
=>
{
// Expected behavior - parses first tool
}
_
=>
{
// Also acceptable if it returns Incomplete waiting for more
}
// This test is flexible about the implementation behavior
if
!
result
.calls
.is_empty
()
{
// Parser found at least one tool
assert
!
(
result
.calls
[
0
]
.name
.is_some
());
}
// Also acceptable if parser returns empty waiting for more context
}
#[tokio::test]
async
fn
test_parse_streaming_opening_bracket_only
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"Let's try this: ["
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
Incomplete
=>
{
assert
!
(
state
.buffer
.ends_with
(
"["
));
}
_
=>
panic!
(
"Should return Incomplete for partial bracket"
),
}
// Should be incomplete - no complete tool call
assert
!
(
result
.calls
.is_empty
(),
"Should not return tool call for partial bracket"
);
}
#[tokio::test]
async
fn
test_parse_streaming_nested_brackets
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
text
=
"[get_weather(location='New York', unit='celsius', data=[1, 2, 3])]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
tools
=
create_test_tools
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
let
text
=
"[get_weather(location='New York', unit='celsius', data=[1, 2, 3])]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse tool call with nested brackets"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"New York"
);
assert_eq!
(
args
[
"unit"
],
"celsius"
);
assert_eq!
(
args
[
"data"
],
json!
([
1
,
2
,
3
]));
}
_
=>
panic!
(
"Should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_nested_brackets_dict
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
r#"[search(query='test', config={'options': [1, 2], 'nested': {'key': 'value'}})]"#
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"search"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse tool call with nested dict"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"search"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"query"
],
"test"
);
assert_eq!
(
args
[
"config"
][
"options"
],
json!
([
1
,
2
]));
assert_eq!
(
args
[
"config"
][
"nested"
][
"key"
],
"value"
);
}
_
=>
panic!
(
"Should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_multiple_tools_with_nested_brackets
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
text
=
"[get_weather(location='Paris', data=[10, 20]), search(query='test', filters=['a', 'b'])]"
;
let
result
=
parser
.parse_incremental
(
text
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
text
,
&
tools
)
.await
.unwrap
();
// Should parse both tools successfully
match
result
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
// Should parse tools successfully
if
!
result
.calls
.is_empty
()
{
// At least gets the first tool
assert_eq!
(
tool
.function.name
,
"get_weather"
);
}
_
=>
panic!
(
"Should return ToolComplete"
),
assert
!
(
result
.calls
[
0
]
.name
.is_some
());
}
}
#[tokio::test]
async
fn
test_parse_streaming_partial_nested_brackets
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
// First chunk with nested brackets but incomplete
let
text1
=
"Here's a call: [get_weather(location='Tokyo', data=[1, 2"
;
let
result1
=
parser
.parse_incremental
(
text1
,
&
mut
state
)
.await
.unwrap
();
let
result1
=
parser
.parse_incremental
(
text1
,
&
tools
)
.await
.unwrap
();
match
result1
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
Incomplete
=>
{
assert
!
(
state
.buffer
.contains
(
"[get_weather(location='Tokyo', data=[1, 2"
));
}
_
=>
panic!
(
"First chunk should return Incomplete"
),
}
// First chunk should be incomplete
assert
!
(
result1
.calls
.is_empty
(),
"First chunk should not complete"
);
// Second chunk completing the nested brackets
let
text2
=
", 3])]"
;
let
result2
=
parser
.parse_incremental
(
text2
,
&
mut
state
)
.await
.unwrap
();
match
result2
{
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
let
result2
=
parser
.parse_incremental
(
text2
,
&
tools
)
.await
.unwrap
();
assert
!
(
!
result2
.calls
.is_empty
(),
"Second chunk should complete tool call"
);
assert_eq!
(
result2
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result2
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"Tokyo"
);
assert_eq!
(
args
[
"data"
],
json!
([
1
,
2
,
3
]));
}
_
=>
panic!
(
"Second chunk should return ToolComplete"
),
}
}
#[tokio::test]
async
fn
test_parse_streaming_with_python_start_and_end_token
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
sglang_router_rs
::
tool_parser
::
ParseState
::
new
();
let
mut
parser
=
PythonicParser
::
new
();
let
tools
=
create_test_tools
();
let
chunks
=
vec!
[
"Here's a call: "
,
...
...
@@ -512,15 +486,18 @@ async fn test_parse_streaming_with_python_start_and_end_token() {
let
mut
got_tool
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
sglang_router_rs
::
tool_parser
::
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"Tokyo"
);
assert_eq!
(
args
[
"data"
],
json!
([
1
,
2
,
3
]));
got_tool
=
true
;
}
}
}
assert
!
(
got_tool
,
"Should have parsed the tool call"
);
}
...
...
sgl-router/tests/tool_parser_qwen.rs
View file @
b658be6f
...
...
@@ -3,7 +3,10 @@
//! Tests for the Qwen parser which handles <tool_call>...</tool_call> format
use
serde_json
::
json
;
use
sglang_router_rs
::
tool_parser
::{
ParseState
,
QwenParser
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
QwenParser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_qwen_single_tool
()
{
...
...
@@ -189,43 +192,43 @@ These tools will provide the information you need."#;
#[tokio::test]
async
fn
test_buffer_drain_optimization
()
{
let
parser
=
QwenParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
QwenParser
::
new
();
let
tools
=
create_test_tools
();
// First chunk - incomplete tool call
let
chunk1
=
"<tool_call>
\n
{
\"
name
\"
:
\"
test1
\"
, "
;
let
_
result
=
parser
.parse_incremental
(
chunk1
,
&
mut
state
)
.await
.unwrap
();
let
_
result
=
parser
.parse_incremental
(
chunk1
,
&
tools
)
.await
.unwrap
();
// The important thing is buffer accumulation works
assert
!
(
!
state
.buffer
.is_empty
());
// Complete first tool and start second
let
chunk2
=
"
\"
arguments
\"
: {}}
\n
</tool_call><tool_call>
\n
{
\"
name
\"
:
\"
test2
\"
, "
;
let
result
=
parser
.parse_incremental
(
chunk2
,
&
mut
state
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"test1"
);
// After consuming the first tool, buffer should contain only the second tool start
assert
!
(
state
.buffer
.starts_with
(
"<tool_call>"
));
assert
!
(
state
.buffer
.contains
(
"test2"
));
}
else
{
// The important thing is the buffer is managed correctly
let
result
=
parser
.parse_incremental
(
chunk2
,
&
tools
)
.await
.unwrap
();
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
_
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"test1"
);
// After consuming the first tool, buffer is managed internally
}
}
// Complete the second tool
let
chunk3
=
"
\"
arguments
\"
: {
\"
x
\"
: 1}}
\n
</tool_call>"
;
let
result
=
parser
.parse_incremental
(
chunk3
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk3
,
&
tools
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"test2"
);
// Buffer should be empty after consuming all tools
assert
!
(
state
.buffer
.is_empty
()
||
!
state
.buffer
.contains
(
"</tool_call>"
));
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
_
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"test2"
);
// Buffer is managed internally
}
}
}
#[tokio::test]
async
fn
test_buffer_efficiency_with_multiple_tools
()
{
let
parser
=
QwenParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
QwenParser
::
new
();
let
tools
=
create_test_tools
();
// Send multiple complete tools at once
let
input
=
r#"<tool_call>
...
...
@@ -237,16 +240,13 @@ async fn test_buffer_efficiency_with_multiple_tools() {
</tool_call>"#
;
// This should efficiently process tools using drain() without creating new strings
let
result
=
parser
.parse_incremental
(
input
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
input
,
&
tools
)
.await
.unwrap
();
// In Phase 2, this will likely parse only the first tool
// The important thing is that drain() doesn't cause any issues
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert
!
([
"tool1"
,
"tool2"
,
"tool3"
]
.contains
(
&
tool
.function.name
.as_str
()));
}
_
=>
{
// Simplified streaming might return Incomplete
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert
!
([
"tool1"
,
"tool2"
,
"tool3"
]
.contains
(
&
name
.as_str
()));
}
}
}
sgl-router/tests/tool_parser_registry.rs
deleted
100644 → 0
View file @
5e786cca
//! Parser Registry Integration Tests
//!
//! Tests for model-to-parser mappings and registry functionality
use
sglang_router_rs
::
tool_parser
::
ParserRegistry
;
#[tokio::test]
async
fn
test_registry_has_all_parsers
()
{
let
registry
=
ParserRegistry
::
new
();
let
parsers
=
registry
.list_parsers
();
assert
!
(
parsers
.contains
(
&
"json"
));
assert
!
(
parsers
.contains
(
&
"mistral"
));
assert
!
(
parsers
.contains
(
&
"qwen"
));
assert
!
(
parsers
.contains
(
&
"pythonic"
));
assert
!
(
parsers
.contains
(
&
"llama"
));
}
#[tokio::test]
async
fn
test_openai_models_use_json
()
{
let
registry
=
ParserRegistry
::
new
();
let
models
=
vec!
[
"gpt-4"
,
"gpt-4-turbo"
,
"gpt-3.5-turbo"
,
"gpt-4o"
];
for
model
in
models
{
let
parser
=
registry
.get_parser
(
model
)
.unwrap
();
let
test_input
=
r#"{"name": "test", "arguments": {}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"test"
);
}
}
#[tokio::test]
async
fn
test_anthropic_models_use_json
()
{
let
registry
=
ParserRegistry
::
new
();
let
models
=
vec!
[
"claude-3-opus"
,
"claude-3-sonnet"
,
"claude-2.1"
];
for
model
in
models
{
let
parser
=
registry
.get_parser
(
model
)
.unwrap
();
let
test_input
=
r#"{"name": "test", "arguments": {}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
}
}
#[tokio::test]
async
fn
test_mistral_models
()
{
let
registry
=
ParserRegistry
::
new
();
let
models
=
vec!
[
"mistral-large"
,
"mistral-medium"
,
"mixtral-8x7b"
];
for
model
in
models
{
let
parser
=
registry
.get_parser
(
model
)
.unwrap
();
let
test_input
=
r#"[TOOL_CALLS] [{"name": "test", "arguments": {}}]"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"test"
);
}
}
#[tokio::test]
async
fn
test_qwen_models
()
{
let
registry
=
ParserRegistry
::
new
();
let
models
=
vec!
[
"qwen2.5-72b"
,
"Qwen2-7B"
,
"qwen-max"
];
for
model
in
models
{
let
parser
=
registry
.get_parser
(
model
)
.unwrap
();
let
test_input
=
r#"<tool_call>
{"name": "test", "arguments": {}}
</tool_call>"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"test"
);
}
}
#[tokio::test]
async
fn
test_llama_model_variants
()
{
let
registry
=
ParserRegistry
::
new
();
// Llama 4 uses pythonic
let
parser
=
registry
.get_parser
(
"llama-4-70b"
)
.unwrap
();
let
test_input
=
r#"[get_weather(city="NYC")]"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"get_weather"
);
// Llama 3.2 uses python_tag
let
parser
=
registry
.get_parser
(
"llama-3.2-8b"
)
.unwrap
();
let
test_input
=
r#"<|python_tag|>{"name": "test", "arguments": {}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"test"
);
// Other Llama models use JSON
let
parser
=
registry
.get_parser
(
"llama-2-70b"
)
.unwrap
();
let
test_input
=
r#"{"name": "test", "arguments": {}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
}
#[tokio::test]
async
fn
test_deepseek_models
()
{
let
registry
=
ParserRegistry
::
new
();
// DeepSeek uses pythonic format (simplified, v3 would need custom parser)
let
parser
=
registry
.get_parser
(
"deepseek-coder"
)
.unwrap
();
let
test_input
=
r#"[function(arg="value")]"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"function"
);
}
#[tokio::test]
async
fn
test_unknown_model_fallback
()
{
let
registry
=
ParserRegistry
::
new
();
// Unknown models should fall back to JSON parser
let
parser
=
registry
.get_parser
(
"unknown-model-xyz"
)
.unwrap
();
let
test_input
=
r#"{"name": "fallback", "arguments": {}}"#
;
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
test_input
)
.await
.unwrap
();
assert_eq!
(
tools
.len
(),
1
);
assert_eq!
(
tools
[
0
]
.function.name
,
"fallback"
);
}
#[tokio::test]
async
fn
test_pattern_specificity
()
{
let
registry
=
ParserRegistry
::
new
();
// llama-4* should match before llama-*
let
parser
=
registry
.get_parser
(
"llama-4-70b"
)
.unwrap
();
assert
!
(
parser
.detect_format
(
r#"[test_function(x=1)]"#
));
// Pythonic format
let
parser
=
registry
.get_parser
(
"llama-3-70b"
)
.unwrap
();
assert
!
(
parser
.detect_format
(
r#"{"name": "test", "arguments": {}}"#
));
// JSON format
}
#[tokio::test]
async
fn
test_real_world_model_outputs
()
{
let
registry
=
ParserRegistry
::
new
();
let
test_cases
=
vec!
[
(
"gpt-4"
,
r#"I'll help you with that.
{"name": "search_web", "arguments": {"query": "latest AI news", "max_results": 5}}
Let me search for that information."#
,
"search_web"
,
),
(
"mistral-large"
,
r#"Let me search for information about Rust.
[TOOL_CALLS] [
{"name": "search", "arguments": {"query": "Rust programming"}},
{"name": "get_weather", "arguments": {"city": "San Francisco"}}
]
I've initiated the search."#
,
"search"
,
),
(
"qwen2.5"
,
r#"I'll check the weather for you.
<tool_call>
{
"name": "get_weather",
"arguments": {
"location": "Tokyo",
"units": "celsius"
}
}
</tool_call>
The weather information has been requested."#
,
"get_weather"
,
),
];
for
(
model
,
output
,
expected_name
)
in
test_cases
{
let
parser
=
registry
.get_parser
(
model
)
.unwrap
();
let
(
_
normal_text
,
tools
)
=
parser
.parse_complete
(
output
)
.await
.unwrap
();
assert
!
(
!
tools
.is_empty
(),
"No tools parsed for model {}"
,
model
);
assert_eq!
(
tools
[
0
]
.function.name
,
expected_name
,
"Wrong function name for model {}"
,
model
);
}
}
sgl-router/tests/tool_parser_step3.rs
View file @
b658be6f
//! Step3 Parser Integration Tests
use
sglang_router_rs
::
tool_parser
::{
ParseState
,
Step3Parser
,
StreamResult
,
ToolParser
};
use
sglang_router_rs
::
tool_parser
::{
Step3Parser
,
ToolParser
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_step3_complete_parsing
()
{
...
...
@@ -72,8 +75,9 @@ async fn test_step3_type_conversion() {
#[tokio::test]
async
fn
test_step3_streaming
()
{
let
parser
=
Step3Parser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
Step3Parser
::
new
();
let
tools
=
create_test_tools
();
// Simulate streaming chunks
let
chunks
=
vec!
[
...
...
@@ -86,26 +90,20 @@ async fn test_step3_streaming() {
"
\n
<|tool_calls_end|>"
,
];
let
mut
found_name
=
false
;
let
mut
found_complete
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolName
{
name
,
..
}
=>
{
if
!
result
.calls
.is_empty
()
{
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"calc"
);
found_name
=
true
;
}
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"calc"
);
found_complete
=
true
;
}
_
=>
{}
}
}
assert
!
(
found_name
||
found_complete
);
assert
!
(
found_complete
);
}
#[test]
...
...
sgl-router/tests/tool_parser_streaming.rs
View file @
b658be6f
...
...
@@ -3,36 +3,31 @@
//! Tests for incremental/streaming parsing capabilities across all parsers
use
sglang_router_rs
::
tool_parser
::{
JsonParser
,
LlamaParser
,
MistralParser
,
ParseState
,
PythonicParser
,
QwenParser
,
StreamResult
,
ToolParser
,
JsonParser
,
LlamaParser
,
MistralParser
,
PythonicParser
,
QwenParser
,
ToolParser
,
};
mod
common
;
use
common
::
create_test_tools
;
#[tokio::test]
async
fn
test_json_streaming_simple
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
JsonParser
::
new
();
let
full_json
=
r#"{"name": "get_weather", "arguments": {"location": "San Francisco"}}"#
;
let
result
=
parser
.parse_incremental
(
full_json
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_json
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
}
_
=>
{
panic!
(
"Expected ToolComplete for complete JSON input"
);
}
}
assert
!
(
!
result
.calls
.is_empty
(),
"Should have parsed a tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
,
Some
(
"get_weather"
.to_string
()));
}
#[tokio::test]
async
fn
test_json_streaming_array
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
JsonParser
::
new
();
let
chunks
=
vec!
[
r#"["#
,
...
...
@@ -46,11 +41,13 @@ async fn test_json_streaming_array() {
let
mut
tool_count
=
0
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
_
)
=
result
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
for
call
in
result
.calls
{
if
call
.name
.is_some
()
{
tool_count
+=
1
;
}
}
}
// Current implementation may handle this differently
assert
!
(
tool_count
<=
2
,
"Should parse at most 2 tools"
);
...
...
@@ -58,8 +55,9 @@ async fn test_json_streaming_array() {
#[tokio::test]
async
fn
test_mistral_streaming
()
{
let
parser
=
MistralParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
MistralParser
::
new
();
let
chunks
=
vec!
[
r#"Here is the result: "#
,
...
...
@@ -72,47 +70,42 @@ async fn test_mistral_streaming() {
r#"}}]"#
,
];
let
mut
got_
complet
e
=
false
;
let
mut
got_
tool_nam
e
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"search"
);
got_complete
=
true
;
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
for
call
in
result
.calls
{
if
let
Some
(
name
)
=
call
.name
{
assert_eq!
(
name
,
"search"
);
got_tool_name
=
true
;
}
}
}
assert
!
(
got_
complet
e
,
"Should have
completed parsing
"
);
assert
!
(
got_
tool_nam
e
,
"Should have
found tool name
"
);
}
#[tokio::test]
async
fn
test_pythonic_streaming
()
{
let
parser
=
PythonicParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
PythonicParser
::
new
();
let
full_input
=
r#"[get_weather(city="London", units="celsius")]"#
;
let
result
=
parser
.parse_incremental
(
full_input
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_input
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"get_weather"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Should have parsed a tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
,
Some
(
"get_weather"
.to_string
()));
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
result
.calls
[
0
]
.parameters
)
.unwrap
();
assert_eq!
(
args
[
"city"
],
"London"
);
}
_
=>
{
panic!
(
"Expected ToolComplete for complete pythonic input"
);
}
}
}
#[tokio::test]
async
fn
test_llama_streaming_with_python_tag
()
{
let
parser
=
LlamaParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
LlamaParser
::
new
();
let
chunks
=
vec!
[
r#"Let me help. "#
,
...
...
@@ -125,194 +118,197 @@ async fn test_llama_streaming_with_python_tag() {
r#"}"#
,
];
let
mut
got_
complet
e
=
false
;
let
mut
got_
tool_nam
e
=
false
;
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"calculate"
);
got_complete
=
true
;
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
for
call
in
result
.calls
{
if
let
Some
(
name
)
=
call
.name
{
assert_eq!
(
name
,
"calculate"
);
got_tool_name
=
true
;
}
}
}
assert
!
(
got_
complet
e
,
"Should have
completed parsing
"
);
assert
!
(
got_
tool_nam
e
,
"Should have
found tool name
"
);
}
#[tokio::test]
async
fn
test_qwen_streaming
()
{
let
parser
=
QwenParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
QwenParser
::
new
();
// Note: Parser expects newline after both tags
let
full_input
=
"<tool_call>
\n
{
\"
name
\"
:
\"
translate
\"
,
\"
arguments
\"
: {
\"
text
\"
:
\"
hello
\"
,
\"
to
\"
:
\"
zh
\"
}}
\n
</tool_call>"
;
let
result
=
parser
.parse_incremental
(
full_input
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_input
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"translate"
);
}
other
=>
{
panic!
(
"Expected ToolComplete for complete Qwen input, got: {:?}"
,
other
);
}
}
assert
!
(
!
result
.calls
.is_empty
(),
"Should have parsed a tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
,
Some
(
"translate"
.to_string
()));
}
#[tokio::test]
async
fn
test_streaming_incomplete_stays_incomplete
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
JsonParser
::
new
();
let
chunks
=
vec!
[
r#"{"na"#
,
r#"me": "#
];
for
chunk
in
chunks
{
let
result
=
parser
.parse_incremental
(
chunk
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
chunk
,
&
tools
)
.await
.unwrap
();
assert
!
(
matches!
(
result
,
StreamResult
::
Incomplete
),
"Should return
Incomplete
for partial JSON, got: {:?}"
,
result
.calls
.is_empty
(
),
"Should return
empty calls
for partial JSON, got: {:?}"
,
result
);
}
assert
!
(
!
state
.buffer
.is_empty
());
}
#[tokio::test]
async
fn
test_streaming_with_text_before_tool
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
full_input
=
r#"{"name": "test", "arguments": {}}"#
;
let
result
=
parser
.parse_incremental
(
full_input
,
&
mut
state
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"test"
);
}
other
=>
{
panic!
(
"Expected ToolComplete, got: {:?}"
,
other
);
}
}
}
#[tokio::test]
async
fn
test_streaming_buffer_accumulation
()
{
let
parser
=
JsonParser
::
new
();
let
tools
=
create_test_tools
();
let
mut
state
=
ParseState
::
new
();
let
mut
parser
=
JsonParser
::
new
();
let
result1
=
parser
.parse_incremental
(
r#"{"na"#
,
&
mut
state
)
.await
.unwrap
();
let
result1
=
parser
.parse_incremental
(
r#"{"na"#
,
&
tools
)
.await
.unwrap
();
assert
!
(
matches!
(
result1
,
StreamResult
::
Incomplete
));
assert
!
(
!
state
.buffer
.is_empty
(),
"Buffer should accumulate incomplete JSON"
);
assert
!
(
result1
.calls
.is_empty
(),
"Should not parse incomplete JSON"
);
let
result2
=
parser
.parse_incremental
(
r#"me": "test", "arguments": {}}"#
,
&
mut
state
)
.parse_incremental
(
r#"me": "test", "arguments": {}}"#
,
&
tools
)
.await
.unwrap
();
match
result2
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"test"
);
assert
!
(
state
.buffer
.is_empty
(),
"
Buffer should be cleared after complete parse
"
!
result2
.calls
.is_empty
(),
"
Should parse complete JSON after buffering
"
);
}
_
=>
panic!
(
"Expected ToolComplete for complete JSON, got: {:?}"
,
result2
),
}
assert_eq!
(
result2
.calls
[
0
]
.name
,
Some
(
"test"
.to_string
()));
}
#[tokio::test]
async
fn
test_streaming_multiple_tools_sequential
()
{
let
parser
=
QwenParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
QwenParser
::
new
();
let
full_input
=
r#"<tool_call>
{"name": "tool1", "arguments": {}}
</tool_call>"#
;
let
result
=
parser
.parse_incremental
(
full_input
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_input
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"tool1"
);
}
_
=>
{
panic!
(
"Expected ToolComplete for first tool"
);
}
}
assert
!
(
!
result
.calls
.is_empty
(),
"Should have parsed a tool call"
);
assert_eq!
(
result
.calls
[
0
]
.name
,
Some
(
"tool1"
.to_string
()));
}
#[tokio::test]
async
fn
test_streaming_reset_after_error
()
{
let
parser
=
JsonParser
::
new
();
let
tools
=
create_test_tools
();
let
mut
state1
=
ParseState
::
new
();
let
_
=
parser
.parse_incremental
(
r#"{"name": invalid}"#
,
&
mut
state1
)
let
mut
parser1
=
JsonParser
::
new
();
let
_
=
parser1
.parse_incremental
(
r#"{"name": invalid}"#
,
&
tools
)
.await
;
let
mut
state2
=
ParseState
::
new
();
let
result
=
parser
.parse_incremental
(
r#"{"name": "test", "arguments": {}}"#
,
&
mut
state2
)
// Use a new parser instance for clean state
let
mut
parser2
=
JsonParser
::
new
();
let
result
=
parser2
.parse_incremental
(
r#"{"name": "test", "arguments": {}}"#
,
&
tools
)
.await
.unwrap
();
if
let
StreamResult
::
ToolComplete
(
tool
)
=
result
{
assert_eq!
(
tool
.function.name
,
"test"
);
}
assert
!
(
!
result
.calls
.is_empty
(),
"Should parse valid JSON"
);
assert_eq!
(
result
.calls
[
0
]
.name
,
Some
(
"test"
.to_string
()));
}
#[tokio::test]
async
fn
test_streaming_with_unicode_chunks
()
{
let
parser
=
JsonParser
::
new
();
let
mut
state
=
ParseState
::
new
();
let
tools
=
create_test_tools
();
let
mut
parser
=
JsonParser
::
new
();
let
full_input
=
r#"{"name": "translate", "arguments": {"text": "Hello 世界 🌍"}}"#
;
let
result
=
parser
.parse_incremental
(
full_input
,
&
mut
state
)
.await
.unwrap
();
let
result
=
parser
.parse_incremental
(
full_input
,
&
tools
)
.await
.unwrap
();
match
result
{
StreamResult
::
ToolComplete
(
tool
)
=>
{
assert_eq!
(
tool
.function.name
,
"translate"
);
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
tool
.function.arguments
)
.unwrap
();
assert
!
(
args
[
"text"
]
.as_str
()
.unwrap
()
.contains
(
"世界"
));
}
StreamResult
::
ToolName
{
name
,
..
}
=>
{
assert
!
(
!
result
.calls
.is_empty
(),
"Should have parsed a tool call"
);
// Check if we got the tool name
if
let
Some
(
name
)
=
&
result
.calls
[
0
]
.name
{
assert_eq!
(
name
,
"translate"
);
}
StreamResult
::
ToolArguments
{
arguments
,
..
}
=>
{
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
&
arguments
)
.unwrap
();
// In streaming mode, need to make another call to get parameters
let
result2
=
parser
.parse_incremental
(
""
,
&
tools
)
.await
.unwrap
();
// Parameters should be in either result.calls[1] or result2.calls[0]
let
params
=
if
result
.calls
.len
()
>
1
{
&
result
.calls
[
1
]
.parameters
}
else
if
!
result2
.calls
.is_empty
()
{
&
result2
.calls
[
0
]
.parameters
}
else
{
&
result
.calls
[
0
]
.parameters
};
if
!
params
.is_empty
()
{
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
params
)
.unwrap
();
assert
!
(
args
[
"text"
]
.as_str
()
.unwrap
()
.contains
(
"世界"
));
}
other
=>
{
panic!
(
"Unexpected result: {:?}"
,
other
);
}
#[tokio::test]
async
fn
test_streaming_with_partial_chunks
()
{
let
mut
parser
=
JsonParser
::
new
();
let
tools
=
create_test_tools
();
let
partial
=
r#"{"#
;
let
result
=
parser
.parse_incremental
(
partial
,
&
tools
)
.await
.unwrap
();
assert
!
(
result
.calls
.is_empty
(),
"Should return empty calls for just opening brace"
);
let
mut
parser2
=
JsonParser
::
new
();
let
complete
=
r#"{"name": "get_weather", "arguments": {"location": "SF"}}"#
;
let
result
=
parser2
.parse_incremental
(
complete
,
&
tools
)
.await
.unwrap
();
assert
!
(
!
result
.calls
.is_empty
(),
"Expected tool call for complete JSON"
);
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"get_weather"
);
// In streaming mode, need to make another call to get parameters
let
result2
=
parser2
.parse_incremental
(
""
,
&
tools
)
.await
.unwrap
();
// Parameters should be in either result.calls[1] or result2.calls[0]
let
params
=
if
result
.calls
.len
()
>
1
{
&
result
.calls
[
1
]
.parameters
}
else
if
!
result2
.calls
.is_empty
()
{
&
result2
.calls
[
0
]
.parameters
}
else
{
&
result
.calls
[
0
]
.parameters
};
if
!
params
.is_empty
()
{
let
args
:
serde_json
::
Value
=
serde_json
::
from_str
(
params
)
.unwrap
();
assert_eq!
(
args
[
"location"
],
"SF"
);
}
// The PartialJson parser can complete partial JSON by filling in missing values
let
mut
parser3
=
JsonParser
::
new
();
let
partial_with_name
=
r#"{"name": "test", "argum"#
;
let
result
=
parser3
.parse_incremental
(
partial_with_name
,
&
tools
)
.await
.unwrap
();
// Parser behavior may vary - either complete with partial data or wait for more
if
!
result
.calls
.is_empty
()
{
assert_eq!
(
result
.calls
[
0
]
.name
.as_ref
()
.unwrap
(),
"test"
);
}
}
Prev
1
2
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment