add tool schema resolve

Signed-off-by: Bugen Zhao <i@bugenzhao.com>
This commit is contained in:
Bugen Zhao
2026-07-06 16:59:48 +08:00
parent 0578b70ea9
commit 7acfc161ec
4 changed files with 41 additions and 13 deletions
+1
View File
@@ -28,6 +28,7 @@ pub use json::{
pub use kimi_k2::KimiK2ToolParser;
pub use minimax_m2::MinimaxM2ToolParser;
pub use minimax_m3::MinimaxM3ToolParser;
pub use parameters::{ToolSchema, ToolSchemas};
pub use qwen_coder::Qwen3CoderToolParser;
use serde::{Deserialize, Serialize};
use serde_json::Value;
+13 -6
View File
@@ -6,7 +6,7 @@ use crate::tool::Tool;
/// Normalized parameter schemas for all tools in one request.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct ToolSchemas {
pub struct ToolSchemas {
tools: BTreeMap<String, ToolSchema>,
}
@@ -17,7 +17,7 @@ pub(super) struct ToolSchemas {
/// coercing raw string parameter values into more specific JSON types for
/// downstream tool call execution.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(super) struct ToolSchema {
pub struct ToolSchema {
params: BTreeMap<String, JsonParamType>,
}
@@ -64,7 +64,7 @@ pub(super) enum JsonParamType {
impl ToolSchemas {
/// Normalize OpenAI-style tool parameter JSON schemas for one request.
pub(super) fn from_tools(tools: &[Tool]) -> Self {
pub(crate) fn from_tools(tools: &[Tool]) -> Self {
let tools = tools
.iter()
.map(|tool| (tool.name.clone(), ToolSchema::from_schema(&tool.parameters)))
@@ -73,6 +73,14 @@ impl ToolSchemas {
Self { tools }
}
/// Resolve the parameter schema for one named tool.
///
/// Unknown tool names resolve to the empty schema, so all parameters fall
/// back to strings or object-like JSON for structured inputs.
pub(crate) fn resolve(&self, function_name: &str) -> &ToolSchema {
self.tools.get(function_name).unwrap_or(ToolSchema::empty())
}
/// Convert parameter values for one named tool.
///
/// Unknown tool names use an empty schema, so all parameters fall back to
@@ -85,7 +93,7 @@ impl ToolSchemas {
where
P: Into<ParamInput>,
{
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
let tool_schema = self.resolve(function_name);
let mut converted = Map::with_capacity(params.len());
for (name, value) in params {
let value = tool_schema.convert(&name, value.into());
@@ -104,8 +112,7 @@ impl ToolSchemas {
where
P: Into<ParamInput>,
{
let tool_schema = self.tools.get(function_name).unwrap_or(ToolSchema::empty());
tool_schema.convert(name, value.into())
self.resolve(function_name).convert(name, value.into())
}
}
@@ -18,6 +18,7 @@ use winnow::stream::Stream;
use winnow::token::{literal, take_till, take_until};
use super::{ArgsEndScan, ConfigDrivenParser, Input, ParserFormat};
use crate::tool::ToolSchema;
use crate::utils::{incomplete, partial_prefix_len};
const CHANNEL_START: &str = "<|channel>";
@@ -52,7 +53,7 @@ impl ParserFormat for Gemma4Format {
Ok(name)
}
fn tool_args(body: &str) -> ModalResult<Map<String, Value>> {
fn tool_args(_schema: &ToolSchema, body: &str) -> ModalResult<Map<String, Value>> {
let Some(args_input) = body.strip_suffix('}') else {
return Err(ErrMode::Cut(ContextError::new()));
};
@@ -42,7 +42,7 @@ use winnow::token::{literal, rest};
use super::{Result, UnifiedParser, UnifiedParserError, UnifiedParserOutput};
use crate::reasoning::last_reasoning_boundary;
use crate::tool::{Tool, ToolCallDelta};
use crate::tool::{Tool, ToolCallDelta, ToolSchema, ToolSchemas};
use crate::unified::parsing_failed;
use crate::utils::{MarkerScanState, parse_buffered_event, safe_text_len_mul, take_until_marker};
@@ -87,6 +87,12 @@ pub trait ParserFormat: 'static {
/// Whether decoded output must keep tokenizer special tokens.
const PRESERVE_SPECIAL_TOKENS: bool = true;
/// Whether to normalize the request tools' parameter JSON schemas at
/// creation for schema-aware argument conversion. When `false`, the
/// engine skips schema construction and `tool_args` resolves to the
/// empty schema (all raw values keep their string-level interpretation).
const USES_TOOL_SCHEMAS: bool = false;
/// Resumable scanner locating `CALL_END` for the raw argument body.
type ArgsScan: ArgsEndScan;
@@ -96,7 +102,10 @@ pub trait ParserFormat: 'static {
/// Parse one complete raw argument body (end marker already stripped)
/// into a JSON object.
fn tool_args(body: &str) -> ModalResult<Map<String, Value>>;
///
/// `schema` is the parameter schema resolved for this call's tool name —
/// the empty schema unless `USES_TOOL_SCHEMAS` is enabled.
fn tool_args(schema: &ToolSchema, body: &str) -> ModalResult<Map<String, Value>>;
}
/// Resumable scanner that locates a tool-call end marker in buffered input.
@@ -229,6 +238,7 @@ pub struct ConfigDrivenParser<F: ParserFormat> {
buffer: String,
mode: Mode<F>,
markers: ModeMarkerSet,
tool_schemas: ToolSchemas,
emitted_tool_count: usize,
tokenizer: DynTokenizer,
/// Resolved reasoning boundary token IDs for prompt-state derivation.
@@ -238,7 +248,13 @@ pub struct ConfigDrivenParser<F: ParserFormat> {
impl<F: ParserFormat> ConfigDrivenParser<F> {
/// Create a parser for one request stream.
pub fn new(_tools: &[Tool], tokenizer: DynTokenizer) -> Result<Self> {
pub fn new(tools: &[Tool], tokenizer: DynTokenizer) -> Result<Self> {
let tool_schemas = if F::USES_TOOL_SCHEMAS {
ToolSchemas::from_tools(tools)
} else {
ToolSchemas::default()
};
let boundary_ids = match F::REASONING_BOUNDARY_TOKENS {
Some((start, end)) => {
let start_id = tokenizer.token_to_id(start).ok_or_else(|| {
@@ -259,6 +275,7 @@ impl<F: ParserFormat> ConfigDrivenParser<F> {
buffer: String::new(),
mode: Mode::Text,
markers: ModeMarkerSet::of::<F>(),
tool_schemas,
emitted_tool_count: 0,
tokenizer,
boundary_ids,
@@ -389,7 +406,7 @@ impl<F: ParserFormat> UnifiedParser for ConfigDrivenParser<F> {
while let Some((event, consumed_len)) = {
parse_buffered_event(&self.buffer, |input| {
parse_next_event::<F>(input, &mut self.mode, &self.markers)
parse_next_event::<F>(input, &mut self.mode, &self.markers, &self.tool_schemas)
})?
} {
self.apply_event(event, consumed_len, output)?;
@@ -430,13 +447,14 @@ fn parse_next_event<F: ParserFormat>(
input: &mut Input<'_>,
mode: &mut Mode<F>,
markers: &ModeMarkerSet,
schemas: &ToolSchemas,
) -> ModalResult<Event> {
match mode {
Mode::Text => boundary_or_content(input, &markers.text, Event::Text),
Mode::Reasoning => boundary_or_content(input, &markers.reasoning, Event::Reasoning),
Mode::ToolBetween => boundary_or_content(input, &markers.between, Event::Ignored),
Mode::ToolHeader => F::tool_header(input).map(|name| Event::CallHeader { name }),
Mode::ToolArgs { scan, .. } => args_event::<F>(input, scan),
Mode::ToolArgs { name, scan, .. } => args_event::<F>(input, scan, schemas.resolve(name)),
Mode::Done => rest.value(Event::Ignored).parse_next(input),
}
}
@@ -445,9 +463,10 @@ fn parse_next_event<F: ParserFormat>(
fn args_event<F: ParserFormat>(
input: &mut Input<'_>,
scan: &mut F::ArgsScan,
schema: &ToolSchema,
) -> ModalResult<Event> {
let body = scan.scan(input, F::CALL_END)?;
let args = F::tool_args(body)?;
let args = F::tool_args(schema, body)?;
Ok(Event::CallComplete { args })
}