[Rust Frontend] Add Seed-OSS tool parser (#47741)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: RickyChen / 陳昭儒 <ricky.chen@infinirc.com>
This commit is contained in:
co-authored by
mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent
f44f3d6f79
commit
ea1d65fe6d
@@ -294,7 +294,7 @@ mod tests {
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
expect_test::expect!["tool parser `definitely_missing_tool_parser` is not registered (choose from: deepseek_v3, deepseek_v31, deepseek_v32, deepseek_v4, gemma4, glm45, glm47, granite4, hermes, hy_v3, inkling, internlm, kimi_k2, llama3_json, llama4_json, minimax_m2, minimax_m3, mistral, phi4_mini_json, qwen3_coder, qwen3_xml)"].assert_eq(&error.to_report_string());
|
||||
expect_test::expect!["tool parser `definitely_missing_tool_parser` is not registered (choose from: deepseek_v3, deepseek_v31, deepseek_v32, deepseek_v4, gemma4, glm45, glm47, granite4, hermes, hy_v3, inkling, internlm, kimi_k2, llama3_json, llama4_json, minimax_m2, minimax_m3, mistral, phi4_mini_json, qwen3_coder, qwen3_xml, seed_oss)"].assert_eq(&error.to_report_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -10,7 +10,7 @@ pub use vllm_parser::tool::{
|
||||
Glm45MoeToolParser, Glm47MoeToolParser, Granite4ToolParser, HermesToolParser, HyV3ToolParser,
|
||||
Internlm2ToolParser, KimiK2ToolParser, Llama3JsonToolParser, MinimaxM2ToolParser,
|
||||
MinimaxM3ToolParser, MistralToolParser, Phi4MiniJsonToolParser, Qwen3CoderToolParser,
|
||||
Qwen3XmlToolParser, ToolParser, ToolParserError,
|
||||
Qwen3XmlToolParser, SeedOssToolParser, ToolParser, ToolParserError,
|
||||
};
|
||||
|
||||
use crate::parser::ParserFactory;
|
||||
@@ -41,6 +41,7 @@ pub mod names {
|
||||
pub const PHI4_MINI_JSON: &str = "phi4_mini_json";
|
||||
pub const QWEN3_CODER: &str = "qwen3_coder";
|
||||
pub const QWEN3_XML: &str = "qwen3_xml";
|
||||
pub const SEED_OSS: &str = "seed_oss";
|
||||
}
|
||||
|
||||
/// Constructor signature for one registered tool parser implementation.
|
||||
@@ -84,7 +85,8 @@ impl ToolParserFactory {
|
||||
.register_parser::<MistralToolParser>(names::MISTRAL)
|
||||
.register_parser::<Phi4MiniJsonToolParser>(names::PHI4_MINI_JSON)
|
||||
.register_parser::<Qwen3XmlToolParser>(names::QWEN3_XML)
|
||||
.register_parser::<Qwen3CoderToolParser>(names::QWEN3_CODER);
|
||||
.register_parser::<Qwen3CoderToolParser>(names::QWEN3_CODER)
|
||||
.register_parser::<SeedOssToolParser>(names::SEED_OSS);
|
||||
|
||||
factory
|
||||
.register_pattern("mistral-", names::MISTRAL)
|
||||
@@ -122,7 +124,9 @@ impl ToolParserFactory {
|
||||
.register_pattern("minimax-m3", names::MINIMAX_M3)
|
||||
.register_pattern("mm-m3", names::MINIMAX_M3)
|
||||
.register_pattern("minimax", names::MINIMAX_M2)
|
||||
.register_pattern("mm-m2", names::MINIMAX_M2);
|
||||
.register_pattern("mm-m2", names::MINIMAX_M2)
|
||||
.register_pattern("seed-oss", names::SEED_OSS)
|
||||
.register_pattern("seedoss", names::SEED_OSS);
|
||||
|
||||
factory
|
||||
}
|
||||
|
||||
@@ -176,6 +176,10 @@ fn factory_new_resolves_default_patterns() {
|
||||
factory.resolve_name_for_model("org/mm-m2-base"),
|
||||
Some(names::MINIMAX_M2)
|
||||
);
|
||||
assert_eq!(
|
||||
factory.resolve_name_for_model("ByteDance-Seed/Seed-OSS-36B-Instruct"),
|
||||
Some(names::SEED_OSS)
|
||||
);
|
||||
|
||||
// InternLM2 positive: both dashed and underscored versioned names route.
|
||||
assert_eq!(
|
||||
|
||||
@@ -307,7 +307,7 @@ roundtrip_tests! {
|
||||
deepseek_v32 => [tool_call_mix],
|
||||
glm45 => [reasoning_and_content, tool_call_mix],
|
||||
glm47 => [reasoning_and_content, tool_call_mix],
|
||||
seed_oss => [reasoning_and_content],
|
||||
seed_oss => [reasoning_and_content, tool_call_mix],
|
||||
step3p5 => [reasoning_and_content],
|
||||
nemotron_v3 => [reasoning_and_content],
|
||||
gemma4 => [tool_call_mix], // Gemma4 strips reasoning in history if there's no tool call
|
||||
|
||||
@@ -15,6 +15,7 @@ mod minimax_m2;
|
||||
mod minimax_m3;
|
||||
mod parameters;
|
||||
mod qwen_coder;
|
||||
mod seed_oss;
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub mod test_utils;
|
||||
use std::collections::{BTreeMap, btree_map};
|
||||
@@ -32,6 +33,7 @@ pub use kimi_k2::KimiK2ToolParser;
|
||||
pub use minimax_m2::MinimaxM2ToolParser;
|
||||
pub use minimax_m3::MinimaxM3ToolParser;
|
||||
pub use qwen_coder::Qwen3CoderToolParser;
|
||||
pub use seed_oss::SeedOssToolParser;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
pub use xgrammar_structural_tag::Model as StructuralTagModel;
|
||||
|
||||
@@ -19,6 +19,28 @@ const FUNCTION_END: &str = "</function>";
|
||||
const PARAMETER_START: &str = "<parameter=";
|
||||
const PARAMETER_END: &str = "</parameter>";
|
||||
|
||||
/// Model-specific configuration for the shared Qwen Coder grammar.
|
||||
///
|
||||
/// Only the tool-call wrapper tokens vary across models that reuse this
|
||||
/// grammar; the inner `<function=...>` / `<parameter=...>` tags are always
|
||||
/// byte-identical. Seed-OSS, for example, wraps the same body in
|
||||
/// `<seed:tool_call>` / `</seed:tool_call>`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct QwenCoderConfig {
|
||||
/// Human-readable parser name used in error messages.
|
||||
pub(crate) parser_name: &'static str,
|
||||
/// Marker that opens a tool-call block.
|
||||
pub(crate) tool_call_start: &'static str,
|
||||
/// Marker that closes a tool-call block.
|
||||
pub(crate) tool_call_end: &'static str,
|
||||
}
|
||||
|
||||
const QWEN_CODER_CONFIG: QwenCoderConfig = QwenCoderConfig {
|
||||
parser_name: "Qwen Coder",
|
||||
tool_call_start: TOOL_CALL_START,
|
||||
tool_call_end: TOOL_CALL_END,
|
||||
};
|
||||
|
||||
type QwenCoderInput<'i> = Partial<&'i str>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -60,16 +82,24 @@ pub struct Qwen3CoderToolParser {
|
||||
mode: QwenCoderMode,
|
||||
emitted_tool_count: usize,
|
||||
tool_parameters: ToolSchemas,
|
||||
config: QwenCoderConfig,
|
||||
}
|
||||
|
||||
impl Qwen3CoderToolParser {
|
||||
/// Create a Qwen Coder tool parser.
|
||||
fn new(tools: &[Tool]) -> Self {
|
||||
Self::with_config(tools, QWEN_CODER_CONFIG)
|
||||
}
|
||||
|
||||
/// Create a parser for a model that reuses the Qwen Coder grammar with a
|
||||
/// different tool-call wrapper (e.g. Seed-OSS).
|
||||
pub(crate) fn with_config(tools: &[Tool], config: QwenCoderConfig) -> Self {
|
||||
Self {
|
||||
buffer: String::new(),
|
||||
mode: QwenCoderMode::Text,
|
||||
emitted_tool_count: 0,
|
||||
tool_parameters: ToolSchemas::from_tools(tools),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +152,10 @@ impl ToolParser for Qwen3CoderToolParser {
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
self.buffer.push_str(chunk);
|
||||
let config = self.config;
|
||||
|
||||
while let Some((event, consumed_len)) = parse_buffered_event(&self.buffer, |input| {
|
||||
parse_next_qwen_coder_event(input, &mut self.mode)
|
||||
parse_next_qwen_coder_event(input, &mut self.mode, config)
|
||||
})? {
|
||||
self.apply_event(event, output)?;
|
||||
self.buffer.drain(..consumed_len);
|
||||
@@ -137,9 +168,12 @@ impl ToolParser for Qwen3CoderToolParser {
|
||||
let mut output = ToolParserOutput::default();
|
||||
if !self.buffer.is_empty() {
|
||||
if matches!(self.mode, QwenCoderMode::ToolCall { .. })
|
||||
|| self.buffer.starts_with(TOOL_CALL_START)
|
||||
|| self.buffer.starts_with(self.config.tool_call_start)
|
||||
{
|
||||
return Err(parsing_failed!("incomplete Qwen Coder tool call"));
|
||||
return Err(parsing_failed!(
|
||||
"incomplete {} tool call",
|
||||
self.config.parser_name
|
||||
));
|
||||
}
|
||||
output.push_text(&self.buffer);
|
||||
}
|
||||
@@ -156,37 +190,54 @@ impl ToolParser for Qwen3CoderToolParser {
|
||||
fn parse_next_qwen_coder_event(
|
||||
input: &mut QwenCoderInput<'_>,
|
||||
mode: &mut QwenCoderMode,
|
||||
config: QwenCoderConfig,
|
||||
) -> ModalResult<QwenCoderEvent> {
|
||||
match mode {
|
||||
QwenCoderMode::Text => parse_text_event(input),
|
||||
QwenCoderMode::ToolCall { end_marker_scan } => tool_call_event(input, end_marker_scan),
|
||||
QwenCoderMode::Text => parse_text_event(input, config),
|
||||
QwenCoderMode::ToolCall { end_marker_scan } => {
|
||||
tool_call_event(input, end_marker_scan, config.tool_call_end)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a text-mode Qwen Coder event.
|
||||
fn parse_text_event(input: &mut QwenCoderInput<'_>) -> ModalResult<QwenCoderEvent> {
|
||||
alt((tool_call_start_event, safe_text_event)).parse_next(input)
|
||||
fn parse_text_event(
|
||||
input: &mut QwenCoderInput<'_>,
|
||||
config: QwenCoderConfig,
|
||||
) -> ModalResult<QwenCoderEvent> {
|
||||
alt((
|
||||
|input: &mut QwenCoderInput<'_>| tool_call_start_event(input, config.tool_call_start),
|
||||
|input: &mut QwenCoderInput<'_>| safe_text_event(input, config.tool_call_start),
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a Qwen Coder tool-call start marker.
|
||||
fn tool_call_start_event(input: &mut QwenCoderInput<'_>) -> ModalResult<QwenCoderEvent> {
|
||||
literal(TOOL_CALL_START).value(QwenCoderEvent::ToolCallStart).parse_next(input)
|
||||
fn tool_call_start_event(
|
||||
input: &mut QwenCoderInput<'_>,
|
||||
tool_call_start: &'static str,
|
||||
) -> ModalResult<QwenCoderEvent> {
|
||||
literal(tool_call_start).value(QwenCoderEvent::ToolCallStart).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a safe text run before the next Qwen Coder marker.
|
||||
fn safe_text_event(input: &mut QwenCoderInput<'_>) -> ModalResult<QwenCoderEvent> {
|
||||
safe_text_len(input, TOOL_CALL_START).map(|len| QwenCoderEvent::Text { len })
|
||||
fn safe_text_event(
|
||||
input: &mut QwenCoderInput<'_>,
|
||||
tool_call_start: &'static str,
|
||||
) -> ModalResult<QwenCoderEvent> {
|
||||
safe_text_len(input, tool_call_start).map(|len| QwenCoderEvent::Text { len })
|
||||
}
|
||||
|
||||
/// Parse a complete Qwen Coder tool call.
|
||||
fn tool_call_event(
|
||||
input: &mut QwenCoderInput<'_>,
|
||||
end_marker_scan: &mut MarkerScanState,
|
||||
tool_call_end: &'static str,
|
||||
) -> ModalResult<QwenCoderEvent> {
|
||||
let (body,) = seq!(
|
||||
_: ws0,
|
||||
take_until_marker(TOOL_CALL_END, end_marker_scan),
|
||||
_: literal(TOOL_CALL_END),
|
||||
take_until_marker(tool_call_end, end_marker_scan),
|
||||
_: literal(tool_call_end),
|
||||
)
|
||||
.parse_next(input)?;
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
use super::qwen_coder::{Qwen3CoderToolParser, QwenCoderConfig};
|
||||
use crate::tool::{Result, Tool, ToolParser, ToolParserOutput};
|
||||
|
||||
const SEED_OSS_CONFIG: QwenCoderConfig = QwenCoderConfig {
|
||||
parser_name: "Seed-OSS",
|
||||
tool_call_start: "<seed:tool_call>",
|
||||
tool_call_end: "</seed:tool_call>",
|
||||
};
|
||||
|
||||
/// Tool parser for Seed-OSS XML-style tool calls.
|
||||
///
|
||||
/// Example tool call content:
|
||||
///
|
||||
/// ```text
|
||||
/// <seed:tool_call>
|
||||
/// <function=get_weather>
|
||||
/// <parameter=location>杭州</parameter>
|
||||
/// </function>
|
||||
/// </seed:tool_call>
|
||||
/// ```
|
||||
///
|
||||
/// Seed-OSS shares the Qwen3 Coder grammar exactly; only the two tool-call
|
||||
/// wrapper tokens differ (`<seed:tool_call>` / `</seed:tool_call>`). The inner
|
||||
/// `<function=...>` / `<parameter=...>` grammar and schema-driven argument
|
||||
/// conversion are byte-identical, so this delegates to a
|
||||
/// [`Qwen3CoderToolParser`] configured with the Seed-OSS markers. This mirrors
|
||||
/// Python `SeedOssParser(Qwen3Parser)` in `vllm/parser/seed_oss.py`.
|
||||
///
|
||||
/// Structured-output tags are intentionally unsupported: the trait default
|
||||
/// `structural_tag_model() -> None` matches Python `SeedOssEngineToolParser`,
|
||||
/// which sets `structural_tag_model = None` (xgrammar has no Seed-OSS model).
|
||||
///
|
||||
/// The `<seed:tool_call>` wrapper tokens are added-vocabulary tokens
|
||||
/// (`special = false`), not tokenizer special tokens, so they survive decoding
|
||||
/// under the production default `skip_special_tokens = true`; the trait default
|
||||
/// `preserve_special_tokens() == false` is therefore correct, matching the
|
||||
/// sibling `SeedOssReasoningParser`, which relies on the same for `<seed:think>`.
|
||||
pub struct SeedOssToolParser {
|
||||
inner: Qwen3CoderToolParser,
|
||||
}
|
||||
|
||||
impl SeedOssToolParser {
|
||||
/// Create a Seed-OSS tool parser.
|
||||
fn new(tools: &[Tool]) -> Self {
|
||||
Self {
|
||||
inner: Qwen3CoderToolParser::with_config(tools, SEED_OSS_CONFIG),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolParser for SeedOssToolParser {
|
||||
fn create(tools: &[Tool]) -> Result<Box<dyn ToolParser>>
|
||||
where
|
||||
Self: Sized + 'static,
|
||||
{
|
||||
Ok(Box::new(Self::new(tools)))
|
||||
}
|
||||
|
||||
fn parse_into(&mut self, chunk: &str, output: &mut ToolParserOutput) -> Result<()> {
|
||||
self.inner.parse_into(chunk, output)
|
||||
}
|
||||
|
||||
fn finish(&mut self) -> Result<ToolParserOutput> {
|
||||
self.inner.finish()
|
||||
}
|
||||
|
||||
fn reset(&mut self) -> String {
|
||||
self.inner.reset()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
use thiserror_ext::AsReport;
|
||||
|
||||
use super::SeedOssToolParser;
|
||||
use crate::tool::test_utils::{collect_stream, split_by_chars, test_tools};
|
||||
use crate::tool::{ToolParser, ToolParserOutput, ToolParserTestExt as _};
|
||||
|
||||
fn build_tool_call(function_name: &str, params: &[(&str, &str)]) -> String {
|
||||
let params = params
|
||||
.iter()
|
||||
.map(|(name, value)| format!("<parameter={name}>{value}</parameter>"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!(
|
||||
"<seed:tool_call>\n<function={function_name}>\n{params}\n</function>\n</seed:tool_call>"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_parse_complete_without_tool_call_keeps_text() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let output = parser.parse_complete("Hello, world!").unwrap();
|
||||
|
||||
assert_eq!(output.normal_text(), "Hello, world!");
|
||||
assert!(output.calls().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_does_not_treat_plain_tool_call_marker_as_start() {
|
||||
// The Qwen Coder wrapper `<tool_call>` must NOT trigger the Seed-OSS
|
||||
// parser; only `<seed:tool_call>` does.
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let output = parser
|
||||
.parse_complete("<tool_call>\n<function=get_weather>\n</function>\n</tool_call>")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
output.normal_text(),
|
||||
"<tool_call>\n<function=get_weather>\n</function>\n</tool_call>"
|
||||
);
|
||||
assert!(output.calls().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_parse_complete_extracts_single_tool_call() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let output = parser
|
||||
.parse_complete(&build_tool_call(
|
||||
"get_weather",
|
||||
&[("location", "SF"), ("date", "2026-04-29")],
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(output.normal_text().is_empty());
|
||||
assert_eq!(output.calls().len(), 1);
|
||||
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
|
||||
json!({ "location": "SF", "date": "2026-04-29" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_parse_complete_preserves_prefix_text() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let input = format!(
|
||||
"Thinking... {}",
|
||||
build_tool_call("get_weather", &[("location", "NYC")])
|
||||
);
|
||||
let output = parser.parse_complete(&input).unwrap();
|
||||
|
||||
assert_eq!(output.normal_text(), "Thinking... ");
|
||||
assert_eq!(output.calls().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_parse_complete_converts_schema_types() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let output = parser
|
||||
.parse_complete(&build_tool_call(
|
||||
"convert",
|
||||
&[
|
||||
("whole", "5.0"),
|
||||
("flag", "true"),
|
||||
("payload", r#"{"nested":true}"#),
|
||||
("items", "[1,2]"),
|
||||
("empty", "42"),
|
||||
],
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(output.calls().len(), 1);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
|
||||
json!({
|
||||
"whole": 5.0,
|
||||
"flag": true,
|
||||
"payload": { "nested": true },
|
||||
"items": [1, 2],
|
||||
"empty": "42",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_streaming_extracts_multiple_tool_calls_in_order() {
|
||||
let text = format!(
|
||||
"{}\n{}",
|
||||
build_tool_call("get_weather", &[("location", "SF")]),
|
||||
build_tool_call("get_weather", &[("location", "NYC")])
|
||||
);
|
||||
let chunks = split_by_chars(&text, 7);
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
|
||||
let output = collect_stream(&mut parser, &chunks);
|
||||
|
||||
assert_eq!(output.calls().len(), 2);
|
||||
assert_eq!(output.calls()[0].tool_index, 0);
|
||||
assert_eq!(output.calls()[1].tool_index, 1);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
|
||||
json!({ "location": "SF" })
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&output.calls()[1].arguments).unwrap(),
|
||||
json!({ "location": "NYC" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_streaming_handles_end_marker_split_across_chunks() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
let mut output = ToolParserOutput::default();
|
||||
output.append(
|
||||
parser
|
||||
.parse_chunk(
|
||||
"<seed:tool_call>\n\
|
||||
<function=get_weather>\n\
|
||||
<parameter=location>SF</parameter>\n\
|
||||
</function>\n\
|
||||
</seed:tool",
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
assert!(output.normal_text().is_empty());
|
||||
assert!(output.calls().is_empty());
|
||||
|
||||
output.append(parser.parse_chunk("_call>").unwrap());
|
||||
output.append(parser.finish().unwrap());
|
||||
let output = output.coalesce();
|
||||
|
||||
assert_eq!(output.calls().len(), 1);
|
||||
assert_eq!(output.calls()[0].name.as_deref(), Some("get_weather"));
|
||||
assert_eq!(
|
||||
serde_json::from_str::<Value>(&output.calls()[0].arguments).unwrap(),
|
||||
json!({ "location": "SF" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seed_oss_finish_fails_incomplete_tool_call() {
|
||||
let mut parser = SeedOssToolParser::new(&test_tools());
|
||||
parser
|
||||
.parse_chunk(
|
||||
"<seed:tool_call>\n<function=get_weather>\n<parameter=location>SF</parameter>",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = parser.finish().unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
error.to_report_string(),
|
||||
"tool parser parsing failed: incomplete Seed-OSS tool call"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user