[Rust Frontend] Add seed_oss and step3p5 reasoning parsers (#44552)

Signed-off-by: yzhan1 <zhanyaoming2014@gmail.com>
This commit is contained in:
Yaoming Zhan
2026-06-09 23:01:33 -07:00
committed by GitHub
parent 47930b59ca
commit 7a74f31d2e
9 changed files with 547 additions and 4 deletions
+1 -1
View File
@@ -272,6 +272,6 @@ mod tests {
)
.unwrap_err();
expect_test::expect!["reasoning parser `definitely_missing_reasoning_parser` is not registered (choose from: cohere_cmd, deepseek_r1, deepseek_v3, deepseek_v4, gemma4, glm45, kimi, kimi_k2, minimax_m2, nemotron_v3, qwen3, step3)"].assert_eq(&error.to_report_string());
expect_test::expect!["reasoning parser `definitely_missing_reasoning_parser` is not registered (choose from: cohere_cmd, deepseek_r1, deepseek_v3, deepseek_v4, gemma4, glm45, kimi, kimi_k2, minimax_m2, nemotron_v3, qwen3, seed_oss, step3, step3p5)"].assert_eq(&error.to_report_string());
}
}
+14 -2
View File
@@ -6,7 +6,8 @@ pub use vllm_reasoning_parser::{
CohereCmdReasoningParser, DeepSeekR1ReasoningParser, DeepSeekV3ReasoningParser,
DeepSeekV4ReasoningParser, Gemma4ReasoningParser, Glm45ReasoningParser, KimiK2ReasoningParser,
KimiReasoningParser, MiniMaxM2ReasoningParser, NemotronV3ReasoningParser, Qwen3ReasoningParser,
ReasoningDelta, ReasoningError, ReasoningParser, Step3ReasoningParser,
ReasoningDelta, ReasoningError, ReasoningParser, SeedOssReasoningParser, Step3ReasoningParser,
Step3p5ReasoningParser,
};
use vllm_tokenizer::DynTokenizer;
@@ -25,7 +26,9 @@ pub mod names {
pub const MINIMAX_M2: &str = "minimax_m2";
pub const NEMOTRON_V3: &str = "nemotron_v3";
pub const QWEN3: &str = "qwen3";
pub const SEED_OSS: &str = "seed_oss";
pub const STEP3: &str = "step3";
pub const STEP3P5: &str = "step3p5";
}
/// Constructor signature for one registered reasoning parser implementation.
@@ -61,7 +64,9 @@ impl ReasoningParserFactory {
.register_parser::<MiniMaxM2ReasoningParser>(names::MINIMAX_M2)
.register_parser::<NemotronV3ReasoningParser>(names::NEMOTRON_V3)
.register_parser::<Qwen3ReasoningParser>(names::QWEN3)
.register_parser::<Step3ReasoningParser>(names::STEP3);
.register_parser::<SeedOssReasoningParser>(names::SEED_OSS)
.register_parser::<Step3ReasoningParser>(names::STEP3)
.register_parser::<Step3p5ReasoningParser>(names::STEP3P5);
factory
.register_pattern("deepseek-r1", names::DEEPSEEK_R1)
@@ -77,7 +82,14 @@ impl ReasoningParserFactory {
.register_pattern("glm-4.5", names::GLM45)
.register_pattern("kimi-k2", names::KIMI_K2)
.register_pattern("kimi", names::KIMI)
// step3p5 patterns must precede `step3`: substring matching would
// otherwise route step3p5 IDs to step3.
.register_pattern("step-3p5", names::STEP3P5)
.register_pattern("step3p5", names::STEP3P5)
.register_pattern("step-3.5", names::STEP3P5)
.register_pattern("step3", names::STEP3)
.register_pattern("seed-oss", names::SEED_OSS)
.register_pattern("seedoss", names::SEED_OSS)
.register_pattern("minimax", names::MINIMAX_M2)
.register_pattern("mm-m2", names::MINIMAX_M2)
.register_pattern("cohere", names::COHERE_CMD)
@@ -32,8 +32,12 @@ fn factory_contains_and_lists_registered_parsers() {
let factory = ReasoningParserFactory::new();
assert!(factory.contains(names::QWEN3));
assert!(factory.contains(names::DEEPSEEK_V4));
assert!(factory.contains(names::SEED_OSS));
assert!(factory.contains(names::STEP3P5));
assert!(factory.list().contains(&names::QWEN3.to_string()));
assert!(factory.list().contains(&names::DEEPSEEK_V4.to_string()));
assert!(factory.list().contains(&names::SEED_OSS.to_string()));
assert!(factory.list().contains(&names::STEP3P5.to_string()));
}
#[test]
@@ -49,6 +53,41 @@ fn factory_resolves_deepseek_v4_to_qwen3_alias() {
);
}
#[test]
fn factory_routes_step3p5_models_to_dedicated_parser() {
let factory = ReasoningParserFactory::new();
// step3p5 patterns must beat the bare `step3` substring.
assert_eq!(
factory.resolve_name_for_model("step-3p5-instruct"),
Some(names::STEP3P5)
);
assert_eq!(
factory.resolve_name_for_model("step3p5"),
Some(names::STEP3P5)
);
assert_eq!(
factory.resolve_name_for_model("step-3.5-base"),
Some(names::STEP3P5)
);
assert_eq!(
factory.resolve_name_for_model("step3-base"),
Some(names::STEP3)
);
}
#[test]
fn factory_routes_seed_oss_models() {
let factory = ReasoningParserFactory::new();
assert_eq!(
factory.resolve_name_for_model("ByteDance-Seed/Seed-OSS-36B-Instruct"),
Some(names::SEED_OSS)
);
assert_eq!(
factory.resolve_name_for_model("seedoss-7b"),
Some(names::SEED_OSS)
);
}
#[test]
fn factory_rejects_unknown_parser_names() {
let tokenizer = Arc::new(FakeTokenizer);
+26
View File
@@ -144,6 +144,30 @@ impl RoundtripCase {
json_fmt: spaced_json_fmt(),
}
}
/// SeedOSS with `<seed:think>` / `</seed:think>` reasoning tags.
fn seed_oss() -> Self {
Self {
model_id: "ByteDance-Seed/Seed-OSS-36B-Instruct",
assistant_stop_suffix: "<seed:eos>",
tool_call_parser: ParserSelection::Auto,
reasoning_parser: ParserSelection::Auto,
thinking_behavior: ThinkingBehavior::Always { value: true },
json_fmt: compact_json_fmt(),
}
}
/// Step-3.5 with `<think>` / `</think>` reasoning tags and newline trimming.
fn step3p5() -> Self {
Self {
model_id: "stepfun-ai/Step-3.5-Flash",
assistant_stop_suffix: "<|im_end|>\n",
tool_call_parser: ParserSelection::Auto,
reasoning_parser: ParserSelection::Auto,
thinking_behavior: ThinkingBehavior::Always { value: true },
json_fmt: compact_json_fmt(),
}
}
}
macro_rules! roundtrip_tests {
@@ -168,6 +192,8 @@ roundtrip_tests! {
minimax_m25 => [reasoning_and_content, tool_call_mix],
deepseek_v4 => [reasoning_and_content, tool_call_mix],
glm47 => [reasoning_and_content, tool_call_mix],
seed_oss => [reasoning_and_content],
step3p5 => [reasoning_and_content],
// Note: Kimi K2.5 strips the reasoning content in history.
kimi_k25 => [tool_call_mix],
@@ -68,6 +68,11 @@ impl DelimitedReasoningParser {
.unwrap_or(self.default_in_reasoning);
}
/// Return whether the parser is currently inside a reasoning section.
pub(crate) fn in_reasoning(&self) -> bool {
self.current_in_reasoning
}
/// Parse one decoded text delta and return its reasoning/content split.
pub(crate) fn push(&mut self, delta: &str) -> ReasoningDelta {
self.buffer.push_str(delta);
+4
View File
@@ -20,6 +20,8 @@ mod delimited;
mod gemma4;
mod kimi;
mod qwen3;
mod seed_oss;
mod step3p5;
use thiserror::Error;
use vllm_tokenizer::DynTokenizer;
@@ -30,6 +32,8 @@ pub(crate) use self::delimited::DelimitedReasoningParser;
pub use self::gemma4::Gemma4ReasoningParser;
pub use self::kimi::KimiReasoningParser;
pub use self::qwen3::Qwen3ReasoningParser;
pub use self::seed_oss::SeedOssReasoningParser;
pub use self::step3p5::Step3p5ReasoningParser;
/// DeepSeek V3 currently shares the standard `<think>...</think>` parser.
pub type DeepSeekV3ReasoningParser = Qwen3ReasoningParser;
+147
View File
@@ -0,0 +1,147 @@
use vllm_tokenizer::DynTokenizer;
use super::{DelimitedReasoningParser, ReasoningDelta, ReasoningParser, Result};
/// Reasoning parser for SeedOSS models using `<seed:think>`/`</seed:think>`
/// delimiters.
pub struct SeedOssReasoningParser {
inner: DelimitedReasoningParser,
}
impl SeedOssReasoningParser {
/// Create a SeedOSS parser backed by the shared delimited state machine.
pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
Ok(Self {
inner: DelimitedReasoningParser::new(
tokenizer,
"<seed:think>",
"</seed:think>",
false,
)?,
})
}
}
impl ReasoningParser for SeedOssReasoningParser {
fn create(tokenizer: DynTokenizer) -> Result<Box<dyn ReasoningParser>>
where
Self: Sized + 'static,
{
Ok(Box::new(Self::new(tokenizer)?))
}
fn initialize(&mut self, prompt_token_ids: &[u32]) -> Result<()> {
self.inner.initialize(prompt_token_ids);
Ok(())
}
fn push(&mut self, delta: &str) -> Result<ReasoningDelta> {
Ok(self.inner.push(delta))
}
fn finish(&mut self) -> Result<ReasoningDelta> {
Ok(self.inner.finish())
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::SeedOssReasoningParser;
use crate::{ReasoningParser, tests::FakeTokenizer};
#[test]
fn without_prompt_markers_expects_start_token() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("implicit reasoning</seed:think>answer").unwrap();
assert_eq!(delta.reasoning, None);
assert_eq!(
delta.content.as_deref(),
Some("implicit reasoning</seed:think>answer")
);
}
#[test]
fn picks_up_prompt_start_boundary() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
// Prompt prefills `<seed:think>` (id 10), opening reasoning before the stream.
parser.initialize(&[10]).unwrap();
let delta = parser.push("reason</seed:think>answer").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("reason"));
assert_eq!(delta.content.as_deref(), Some("answer"));
}
#[test]
fn respects_prompt_end_boundary() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
// Prompt already closed reasoning with `</seed:think>` (id 11).
parser.initialize(&[11]).unwrap();
let delta = parser.push("answer").unwrap();
assert_eq!(delta.reasoning, None);
assert_eq!(delta.content.as_deref(), Some("answer"));
}
#[test]
fn handles_explicit_start_token() {
// An explicit start delimiter must not leak into reasoning text.
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<seed:think>reason</seed:think>answer").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("reason"));
assert_eq!(delta.content.as_deref(), Some("answer"));
}
#[test]
fn streams_explicit_start_token_across_pushes() {
// Start token, reasoning body, end token, and content arrive in separate
// streaming deltas.
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
let mut reasoning = String::new();
let mut content = String::new();
for delta_str in [
"<seed:think>",
"Some ",
"reasoning ",
"content",
"</seed:think>",
"Final ",
"answer",
] {
let delta = parser.push(delta_str).unwrap();
if let Some(r) = delta.reasoning {
reasoning.push_str(&r);
}
if let Some(c) = delta.content {
content.push_str(&c);
}
}
assert_eq!(reasoning, "Some reasoning content");
assert_eq!(content, "Final answer");
}
#[test]
fn handles_partial_delimiters_across_pushes() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = SeedOssReasoningParser::new(tokenizer).unwrap();
parser.initialize(&[10]).unwrap();
// Closing delimiter `</seed:think>` arrives in two halves.
let first = parser.push("reason</seed:").unwrap();
assert_eq!(first.reasoning.as_deref(), Some("reason"));
assert_eq!(first.content, None);
let second = parser.push("think>answer").unwrap();
assert_eq!(second.reasoning, None);
assert_eq!(second.content.as_deref(), Some("answer"));
}
}
+308
View File
@@ -0,0 +1,308 @@
use vllm_tokenizer::DynTokenizer;
use super::{DelimitedReasoningParser, ReasoningDelta, ReasoningParser, Result};
/// Reasoning parser for Step3p5 outputs.
///
/// Step3p5 uses standard `<think>`/`</think>` delimiters but emits a `\n`
/// immediately before and/or after `</think>`. The parser drops these framing
/// newlines on both sides of the boundary, holding a trailing `\n` from
/// reasoning across pushes until either more reasoning text or `</think>`
/// arrives, and dropping a leading `\n` from the first content delta after
/// the boundary.
pub struct Step3p5ReasoningParser {
inner: DelimitedReasoningParser,
/// `\n` at end of last reasoning delta, held in case `</think>` follows.
pending_reasoning_newline: bool,
/// Last push ended on `</think>` without emitting content; the next
/// content delta's leading `\n` should be dropped.
just_ended_reasoning: bool,
}
impl Step3p5ReasoningParser {
/// Create a Step3p5 parser backed by the shared delimited state machine.
pub fn new(tokenizer: DynTokenizer) -> Result<Self> {
Ok(Self {
inner: DelimitedReasoningParser::new(tokenizer, "<think>", "</think>", false)?,
pending_reasoning_newline: false,
just_ended_reasoning: false,
})
}
/// Drop framing newlines around `</think>` and track held-newline state.
fn process(
&mut self,
mut inner_delta: ReasoningDelta,
was_in_reasoning: bool,
now_in_reasoning: bool,
) -> ReasoningDelta {
// A `<think>...</think>` round-trip in one push still counts as a
// transition: the inner emits reasoning while ending in content mode.
let transitioned =
!now_in_reasoning && (was_in_reasoning || inner_delta.reasoning.is_some());
// Replay or drop a previously-held trailing reasoning newline.
if self.pending_reasoning_newline {
if let Some(reasoning) = inner_delta.reasoning.as_mut() {
reasoning.insert(0, '\n');
self.pending_reasoning_newline = false;
} else if transitioned {
// The held `\n` was the one right before `</think>`: drop it.
self.pending_reasoning_newline = false;
}
}
// Hold back a trailing reasoning `\n` until we know if `</think>` follows.
if let Some(reasoning) = inner_delta.reasoning.as_mut()
&& reasoning.ends_with('\n')
{
reasoning.pop();
if !transitioned {
self.pending_reasoning_newline = true;
}
}
// Drop a leading `\n` of content emitted right after `</think>`.
if let Some(content) = inner_delta.content.as_mut()
&& (transitioned || self.just_ended_reasoning)
&& content.starts_with('\n')
{
content.remove(0);
}
self.just_ended_reasoning = transitioned && inner_delta.content.is_none();
if inner_delta.reasoning.as_deref() == Some("") {
inner_delta.reasoning = None;
}
if inner_delta.content.as_deref() == Some("") {
inner_delta.content = None;
}
inner_delta
}
}
impl ReasoningParser for Step3p5ReasoningParser {
fn create(tokenizer: DynTokenizer) -> Result<Box<dyn ReasoningParser>>
where
Self: Sized + 'static,
{
Ok(Box::new(Self::new(tokenizer)?))
}
fn initialize(&mut self, prompt_token_ids: &[u32]) -> Result<()> {
self.inner.initialize(prompt_token_ids);
Ok(())
}
fn push(&mut self, delta: &str) -> Result<ReasoningDelta> {
let was = self.inner.in_reasoning();
let inner_delta = self.inner.push(delta);
let now = self.inner.in_reasoning();
Ok(self.process(inner_delta, was, now))
}
fn finish(&mut self) -> Result<ReasoningDelta> {
let was = self.inner.in_reasoning();
let inner_delta = self.inner.finish();
let now = self.inner.in_reasoning();
let mut delta = self.process(inner_delta, was, now);
// Emit a still-held newline rather than silently dropping it.
if self.pending_reasoning_newline {
match delta.reasoning.as_mut() {
Some(existing) => existing.push('\n'),
None => delta.reasoning = Some("\n".to_string()),
}
self.pending_reasoning_newline = false;
}
Ok(delta)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::Step3p5ReasoningParser;
use crate::{ReasoningParser, tests::FakeTokenizer};
#[test]
fn picks_up_prompt_start_boundary() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
// Prompt prefills `<think>` (id 1), opening reasoning before the stream.
parser.initialize(&[1]).unwrap();
let delta = parser.push("This is a reasoning section</think>This is the rest").unwrap();
assert_eq!(
delta.reasoning.as_deref(),
Some("This is a reasoning section")
);
assert_eq!(delta.content.as_deref(), Some("This is the rest"));
}
#[test]
fn handles_unterminated_reasoning() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let pushed = parser.push("<think>reason without end").unwrap();
assert_eq!(pushed.reasoning.as_deref(), Some("reason without end"));
assert_eq!(pushed.content, None);
let flushed = parser.finish().unwrap();
assert!(flushed.is_empty());
}
#[test]
fn handles_empty_input() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let pushed = parser.push("").unwrap();
assert!(pushed.is_empty());
let flushed = parser.finish().unwrap();
assert!(flushed.is_empty());
}
#[test]
fn complex_newline_pattern_trims_only_single_framing_newline_each_side() {
// Only the immediately-adjacent framing `\n` is dropped on each side of
// `</think>`; surrounding newlines remain part of reasoning/content.
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
parser.initialize(&[1]).unwrap();
let delta = parser
.push("\n This is a \n reasoning section\n\n\n</think>\n\nThis is the rest")
.unwrap();
assert_eq!(
delta.reasoning.as_deref(),
Some("\n This is a \n reasoning section\n\n")
);
assert_eq!(delta.content.as_deref(), Some("\nThis is the rest"));
}
#[test]
fn drops_framing_newlines_in_single_push() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<think>reason\n</think>\nanswer").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("reason"));
assert_eq!(delta.content.as_deref(), Some("answer"));
}
#[test]
fn drops_framing_newlines_across_pushes() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
// The trailing `\n` from the first push is held until we know whether
// `</think>` follows.
let first = parser.push("<think>reason\n").unwrap();
assert_eq!(first.reasoning.as_deref(), Some("reason"));
assert_eq!(first.content, None);
// `</think>` arrives standalone; the held newline should be dropped.
let second = parser.push("</think>").unwrap();
assert!(second.is_empty());
// The leading newline of the first content delta is dropped.
let third = parser.push("\nanswer").unwrap();
assert_eq!(third.reasoning, None);
assert_eq!(third.content.as_deref(), Some("answer"));
}
#[test]
fn replays_held_newline_when_more_reasoning_follows() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let first = parser.push("<think>reason\n").unwrap();
assert_eq!(first.reasoning.as_deref(), Some("reason"));
let second = parser.push("more reason").unwrap();
assert_eq!(second.reasoning.as_deref(), Some("\nmore reason"));
assert_eq!(second.content, None);
}
#[test]
fn finish_flushes_held_newline_in_unterminated_stream() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let first = parser.push("<think>reason\n").unwrap();
assert_eq!(first.reasoning.as_deref(), Some("reason"));
let flushed = parser.finish().unwrap();
assert_eq!(flushed.reasoning.as_deref(), Some("\n"));
assert_eq!(flushed.content, None);
}
#[test]
fn preserves_inner_newlines_in_reasoning() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<think>line1\nline2</think>tail").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("line1\nline2"));
assert_eq!(delta.content.as_deref(), Some("tail"));
}
#[test]
fn trims_only_one_trailing_reasoning_newline() {
// Only the single framing newline immediately before `</think>` is
// dropped; earlier newlines in the reasoning body are preserved.
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<think>reason\n\n</think>answer").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("reason\n"));
assert_eq!(delta.content.as_deref(), Some("answer"));
}
#[test]
fn drops_only_first_content_newline_after_transition() {
// The leading-`\n` drop applies only to the first content delta after
// `</think>`; later deltas pass through untouched.
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let first = parser.push("<think>reason</think>").unwrap();
assert_eq!(first.reasoning.as_deref(), Some("reason"));
assert_eq!(first.content, None);
let second = parser.push("\nfirst").unwrap();
assert_eq!(second.reasoning, None);
assert_eq!(second.content.as_deref(), Some("first"));
// A `\n` arriving in a later content delta must NOT be dropped.
let third = parser.push("\nsecond").unwrap();
assert_eq!(third.reasoning, None);
assert_eq!(third.content.as_deref(), Some("\nsecond"));
}
#[test]
fn passes_through_clean_boundary_without_framing_newlines() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<think>reason</think>tail").unwrap();
assert_eq!(delta.reasoning.as_deref(), Some("reason"));
assert_eq!(delta.content.as_deref(), Some("tail"));
}
#[test]
fn handles_empty_reasoning_section() {
let tokenizer = Arc::new(FakeTokenizer);
let mut parser = Step3p5ReasoningParser::new(tokenizer).unwrap();
let delta = parser.push("<think></think>answer").unwrap();
assert_eq!(delta.reasoning, None);
assert_eq!(delta.content.as_deref(), Some("answer"));
}
}
+3 -1
View File
@@ -6,7 +6,7 @@ use super::{
DeepSeekR1ReasoningParser, DelimitedReasoningParser, Qwen3ReasoningParser, ReasoningParser,
};
struct FakeTokenizer;
pub(crate) struct FakeTokenizer;
impl Tokenizer for FakeTokenizer {
fn encode(&self, text: &str, _add_special_tokens: bool) -> vllm_tokenizer::Result<Vec<u32>> {
@@ -32,6 +32,8 @@ impl Tokenizer for FakeTokenizer {
"<|END_THINKING|>" => Some(4),
"◁think▷" => Some(5),
"◁/think▷" => Some(6),
"<seed:think>" => Some(10),
"</seed:think>" => Some(11),
_ => None,
}
}