[Rust Frontend] Snapshot structural-tag grammars as readable outlines (#59393)
Signed-off-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
parent
36229e1337
commit
ac9126e58a
@@ -52,6 +52,7 @@ tempfile.workspace = true
|
||||
tokio.workspace = true
|
||||
uuid.workspace = true
|
||||
vllm-engine-core-client = { workspace = true, features = ["test-util"] }
|
||||
vllm-parser = { workspace = true, features = ["test-util"] }
|
||||
vllm-tokenizer = { workspace = true, features = ["test-utils"] }
|
||||
zeromq.workspace = true
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<|DSML|function_calls>`] excluding [`<think>`, `</think>`]
|
||||
tag `<|DSML|function_calls>\n` .. `</|DSML|function_calls>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
tag `<|DSML|invoke name="add">\n` deepseek_xml({ y: number, x: number, items: string[] }) `</|DSML|invoke>\n`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML|function_calls>\n`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
tag `<|DSML|invoke name="add">\n` deepseek_xml({ y: number, x: number, items: string[] }) `</|DSML|invoke>\n`
|
||||
`</|DSML|function_calls>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML|function_calls>\n`
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
`</|DSML|function_calls>`
|
||||
@@ -0,0 +1,26 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<|DSML|tool_calls>`] excluding [`<think>`, `</think>`]
|
||||
tag `<|DSML|tool_calls>\n` .. `</|DSML|tool_calls>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
tag `<|DSML|invoke name="add">\n` deepseek_xml({ y: number, x: number, items: string[] }) `</|DSML|invoke>\n`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML|tool_calls>\n`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
tag `<|DSML|invoke name="add">\n` deepseek_xml({ y: number, x: number, items: string[] }) `</|DSML|invoke>\n`
|
||||
`</|DSML|tool_calls>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML|tool_calls>\n`
|
||||
tag `<|DSML|invoke name="get_weather">\n` deepseek_xml({ location: string }) `</|DSML|invoke>\n`
|
||||
`</|DSML|tool_calls>`
|
||||
@@ -0,0 +1,26 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<|DSML| calls>`] excluding [`<think>`, `</think>`]
|
||||
tag `<|DSML| calls>\n` .. `</|DSML| calls>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML| invoke name="get_weather">\n` deepseek_v4_1_xml({ location: string }) `</|DSML| invoke>\n`
|
||||
tag `<|DSML| invoke name="add">\n` deepseek_v4_1_xml({ y: number, x: number, items: string[] }) `</|DSML| invoke>\n`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML| calls>\n`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|DSML| invoke name="get_weather">\n` deepseek_v4_1_xml({ location: string }) `</|DSML| invoke>\n`
|
||||
tag `<|DSML| invoke name="add">\n` deepseek_v4_1_xml({ y: number, x: number, items: string[] }) `</|DSML| invoke>\n`
|
||||
`</|DSML| calls>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`\n\n<|DSML| calls>\n`
|
||||
tag `<|DSML| invoke name="get_weather">\n` deepseek_v4_1_xml({ location: string }) `</|DSML| invoke>\n`
|
||||
`</|DSML| calls>`
|
||||
@@ -0,0 +1,18 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
tag `<tool_call>add` glm_xml({ y: number, x: number, items: string[] }) `</tool_call>`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`] at_least_one
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
tag `<tool_call>add` glm_xml({ y: number, x: number, items: string[] }) `</tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
@@ -0,0 +1,18 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
tag `<tool_call>add` glm_xml({ y: number, x: number, items: string[] }) `</tool_call>`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`] at_least_one
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
tag `<tool_call>add` glm_xml({ y: number, x: number, items: string[] }) `</tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
tag `<tool_call>get_weather` glm_xml({ location: string }) `</tool_call>`
|
||||
@@ -0,0 +1,112 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think:opensource>`
|
||||
triggered_tags [`<tool_calls:opensource>`]
|
||||
tag `<tool_calls:opensource>\n` .. `\n</tool_calls:opensource>`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call:opensource>get_weather<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`location`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
tag `<tool_call:opensource>add<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`y`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`x`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`items`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think:opensource>`
|
||||
sequence
|
||||
`<tool_calls:opensource>\n`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call:opensource>get_weather<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`location`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
tag `<tool_call:opensource>add<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`y`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`x`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`items`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
`\n</tool_calls:opensource>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think:opensource>`
|
||||
sequence
|
||||
`<tool_calls:opensource>\n`
|
||||
tag `<tool_call:opensource>get_weather<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`location`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
`\n</tool_calls:opensource>`
|
||||
@@ -0,0 +1,46 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
triggered_tags [`<|tool_calls_section_begin|>`] excluding [`<think>`, `</think>`, `<|tool_call_begin|>`]
|
||||
tag `<|tool_calls_section_begin|>` .. `<|tool_calls_section_end|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|tool_call_begin|>functions.get_weather:` .. `<|tool_call_end|>`
|
||||
sequence
|
||||
/\d+/
|
||||
`<|tool_call_argument_begin|>`
|
||||
json({ location: string })
|
||||
tag `<|tool_call_begin|>functions.add:` .. `<|tool_call_end|>`
|
||||
sequence
|
||||
/\d+/
|
||||
`<|tool_call_argument_begin|>`
|
||||
json({ y: number, x: number, items: string[] })
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`<|tool_calls_section_begin|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|tool_call_begin|>functions.get_weather:` .. `<|tool_call_end|>`
|
||||
sequence
|
||||
/\d+/
|
||||
`<|tool_call_argument_begin|>`
|
||||
json({ location: string })
|
||||
tag `<|tool_call_begin|>functions.add:` .. `<|tool_call_end|>`
|
||||
sequence
|
||||
/\d+/
|
||||
`<|tool_call_argument_begin|>`
|
||||
json({ y: number, x: number, items: string[] })
|
||||
`<|tool_calls_section_end|>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
sequence
|
||||
`<|tool_calls_section_begin|>`
|
||||
tag `<|tool_call_begin|>functions.get_weather:` .. `<|tool_call_end|>`
|
||||
sequence
|
||||
/\d+/
|
||||
`<|tool_call_argument_begin|>`
|
||||
json({ location: string })
|
||||
`<|tool_calls_section_end|>`
|
||||
@@ -0,0 +1,67 @@
|
||||
# auto (final_output_only)
|
||||
sequence
|
||||
optional `<|open|>response<|sep|>`
|
||||
tag `` text excluding [`<|open|>`, `<|close|>`, `<|end_of_msg|>`] `<|close|>response<|sep|>`
|
||||
optional
|
||||
tag `<|open|>tools<|sep|>` .. `<|close|>tools<|sep|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|open|>call tool="get_weather" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus tag `<|open|>argument key="location" type="string"<|sep|>` text excluding [`<|close|>argument<|sep|>`, `<|close|>call<|sep|>`] `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ location: string }) `<|close|>json<|sep|>`
|
||||
tag `<|open|>call tool="add" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus
|
||||
or
|
||||
tag `<|open|>argument key="y" type="number"<|sep|>` json(number) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>argument key="x" type="number"<|sep|>` json(number) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>argument key="items" type="array"<|sep|>` json(string[]) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ y: number, x: number, items: string[] }) `<|close|>json<|sep|>`
|
||||
optional `<|close|>message<|sep|>`
|
||||
|
||||
# required (final_output_only)
|
||||
sequence
|
||||
optional `<|open|>response<|sep|>`
|
||||
tag `` text excluding [`<|open|>`, `<|close|>`, `<|end_of_msg|>`] `<|close|>response<|sep|>`
|
||||
tag `<|open|>tools<|sep|>` .. `<|close|>tools<|sep|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|open|>call tool="get_weather" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus tag `<|open|>argument key="location" type="string"<|sep|>` text excluding [`<|close|>argument<|sep|>`, `<|close|>call<|sep|>`] `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ location: string }) `<|close|>json<|sep|>`
|
||||
tag `<|open|>call tool="add" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus
|
||||
or
|
||||
tag `<|open|>argument key="y" type="number"<|sep|>` json(number) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>argument key="x" type="number"<|sep|>` json(number) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>argument key="items" type="array"<|sep|>` json(string[]) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ y: number, x: number, items: string[] }) `<|close|>json<|sep|>`
|
||||
optional `<|close|>message<|sep|>`
|
||||
|
||||
# named (final_output_only)
|
||||
sequence
|
||||
optional `<|open|>response<|sep|>`
|
||||
tag `` text excluding [`<|open|>`, `<|close|>`, `<|end_of_msg|>`] `<|close|>response<|sep|>`
|
||||
tag `<|open|>tools<|sep|>` .. `<|close|>tools<|sep|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|open|>call tool="get_weather" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus tag `<|open|>argument key="location" type="string"<|sep|>` text excluding [`<|close|>argument<|sep|>`, `<|close|>call<|sep|>`] `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ location: string }) `<|close|>json<|sep|>`
|
||||
optional `<|close|>message<|sep|>`
|
||||
@@ -0,0 +1,18 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
optional tag `<think>` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<function=`]
|
||||
tag `<tool_call><function=get_weather>` qwen_xml({ location: string }) `</function></tool_call>`
|
||||
tag `<tool_call><function=add>` qwen_xml({ y: number, x: number, items: string[] }) `</function></tool_call>`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
optional tag `<think>` text `</think>`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`, `</tool_call>`, `<function=`] at_least_one
|
||||
tag `<tool_call><function=get_weather>` qwen_xml({ location: string }) `</function></tool_call>`
|
||||
tag `<tool_call><function=add>` qwen_xml({ y: number, x: number, items: string[] }) `</function></tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
optional tag `<think>` text `</think>`
|
||||
tag `<tool_call><function=get_weather>` qwen_xml({ location: string }) `</function></tool_call>`
|
||||
@@ -0,0 +1,32 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
triggered_tags [`<minimax:tool_call>`] excluding [`<think>`, `</think>`]
|
||||
tag `<minimax:tool_call>\n` .. `</minimax:tool_call>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<invoke name="get_weather">\n` minimax_xml({ location: string }) `</invoke>\n`
|
||||
tag `<invoke name="add">\n` minimax_xml({ y: number, x: number, items: string[] }) `</invoke>\n`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
sequence
|
||||
`\n<minimax:tool_call>\n`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<invoke name="get_weather">\n` minimax_xml({ location: string }) `</invoke>\n`
|
||||
tag `<invoke name="add">\n` minimax_xml({ y: number, x: number, items: string[] }) `</invoke>\n`
|
||||
`</minimax:tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
sequence
|
||||
`\n<minimax:tool_call>\n`
|
||||
tag `<invoke name="get_weather">\n` minimax_xml({ location: string }) `</invoke>\n`
|
||||
`</minimax:tool_call>`
|
||||
@@ -0,0 +1,27 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
optional
|
||||
sequence
|
||||
tag `<think>` text `</think>`
|
||||
`\n\n`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`]
|
||||
tag `<tool_call>\n{"name": "get_weather", "arguments": ` json({ location: string }) `}\n</tool_call>`
|
||||
tag `<tool_call>\n{"name": "add", "arguments": ` json({ y: number, x: number, items: string[] }) `}\n</tool_call>`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
optional
|
||||
sequence
|
||||
tag `<think>` text `</think>`
|
||||
`\n\n`
|
||||
triggered_tags [`<tool_call>`] excluding [`<think>`, `</think>`] at_least_one
|
||||
tag `<tool_call>\n{"name": "get_weather", "arguments": ` json({ location: string }) `}\n</tool_call>`
|
||||
tag `<tool_call>\n{"name": "add", "arguments": ` json({ y: number, x: number, items: string[] }) `}\n</tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
optional
|
||||
sequence
|
||||
tag `<think>` text `</think>`
|
||||
`\n\n`
|
||||
tag `<tool_call>\n{"name": "get_weather", "arguments": ` json({ location: string }) `}\n</tool_call>`
|
||||
@@ -0,0 +1,24 @@
|
||||
# auto (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
triggered_tags [`<tool_call>\n<function=`] excluding [`<think>`, `</think>`]
|
||||
tag `<tool_call>\n<function=get_weather>\n` qwen_xml({ location: string }) `\n</function>\n</tool_call>`
|
||||
tag `<tool_call>\n<function=add>\n` qwen_xml({ y: number, x: number, items: string[] }) `\n</function>\n</tool_call>`
|
||||
|
||||
# required (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
triggered_tags [`<tool_call>\n<function=`] excluding [`<think>`, `</think>`] at_least_one
|
||||
tag `<tool_call>\n<function=get_weather>\n` qwen_xml({ location: string }) `\n</function>\n</tool_call>`
|
||||
tag `<tool_call>\n<function=add>\n` qwen_xml({ y: number, x: number, items: string[] }) `\n</function>\n</tool_call>`
|
||||
|
||||
# named (from_token_zero)
|
||||
sequence
|
||||
sequence
|
||||
tag `` text `</think>`
|
||||
`\n\n`
|
||||
tag `<tool_call>\n<function=get_weather>\n` qwen_xml({ location: string }) `\n</function>\n</tool_call>`
|
||||
@@ -1150,6 +1150,7 @@ fn check_grammar_replay_file(
|
||||
) -> Result<()> {
|
||||
let model_name = case.model_id.rsplit_once('/').map_or(case.model_id, |(_, name)| name);
|
||||
let path = grammar_replay_dir().join(format!("{model_name}.json"));
|
||||
let outline_path = path.with_extension("txt");
|
||||
let results = results
|
||||
.into_iter()
|
||||
.filter_map(|(variant, result)| {
|
||||
@@ -1157,11 +1158,13 @@ fn check_grammar_replay_file(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if results.is_empty() {
|
||||
ensure!(
|
||||
!path.exists(),
|
||||
"{} builds no output grammar, but grammar replay file {path:?} exists",
|
||||
case.model_id
|
||||
);
|
||||
for path in [&path, &outline_path] {
|
||||
ensure!(
|
||||
!path.exists(),
|
||||
"{} builds no output grammar, but grammar replay file {path:?} exists",
|
||||
case.model_id
|
||||
);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1205,6 +1208,25 @@ fn check_grammar_replay_file(
|
||||
);
|
||||
}
|
||||
|
||||
// A readable outline of the same grammars, for review only.
|
||||
let mut outline = String::new();
|
||||
for (variant, case) in &cases {
|
||||
if !outline.is_empty() {
|
||||
outline.push('\n');
|
||||
}
|
||||
let variant = serde_json::to_value(variant)?;
|
||||
let coverage = serde_json::to_value(case.grammar.coverage)?;
|
||||
outline.push_str(&format!(
|
||||
"# {} ({})\n",
|
||||
variant.as_str().context("variant name")?,
|
||||
coverage.as_str().context("coverage name")?,
|
||||
));
|
||||
outline.push_str(&vllm_parser::output_grammar::test_utils::outline(
|
||||
&case.grammar.format,
|
||||
));
|
||||
}
|
||||
expect_test::expect_file![outline_path].assert_eq(&outline);
|
||||
|
||||
let file = GrammarReplayFile {
|
||||
model_id: case.model_id,
|
||||
vocab_size: backends.text_backend.model_vocab_size(),
|
||||
|
||||
@@ -17,6 +17,9 @@ use xgrammar_structural_tag::{
|
||||
|
||||
use crate::tool::Tool;
|
||||
|
||||
#[cfg(any(test, feature = "test-util"))]
|
||||
pub mod test_utils;
|
||||
|
||||
/// Result alias for output grammar construction.
|
||||
pub type Result<T> = std::result::Result<T, OutputGrammarError>;
|
||||
|
||||
|
||||
@@ -0,0 +1,676 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
//! Readable renderings of structural-tag formats for test snapshots.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use xgrammar_structural_tag::format::{EndBoundary, Format, TagBoundary, TagFormat, TokenValue};
|
||||
|
||||
/// Render `format` as an indented outline, one node per line.
|
||||
///
|
||||
/// Each node starts with its format type, and composite nodes list their
|
||||
/// children one level deeper. Literal text is quoted in backticks with `\`,
|
||||
/// `` ` ``, and control characters escaped, so markers read as written. Other
|
||||
/// leaves are compact: `/regex/`, `text`, `json(SCHEMA)` or `STYLE(SCHEMA)`
|
||||
/// for styled schemas, and `token ID` or ``token `TEXT` ``. Lists such as
|
||||
/// exclusions and triggers are bracketed, and schemas read as types, see
|
||||
/// [`schema`]. A single leaf child of `optional`, `star`, `plus`, `repeat`,
|
||||
/// and dispatch rules stays on its parent's line, as does a tag's leaf
|
||||
/// content between its boundaries. Options that keep their defaults are
|
||||
/// omitted.
|
||||
pub fn outline(format: &Format) -> String {
|
||||
let mut out = String::new();
|
||||
write(&mut out, 0, format_node(format));
|
||||
out
|
||||
}
|
||||
|
||||
/// A child line of an outline node.
|
||||
enum Child<'a> {
|
||||
Format(&'a Format),
|
||||
Tag(&'a TagFormat),
|
||||
Rule(String, &'a Format),
|
||||
Text(&'a str),
|
||||
}
|
||||
|
||||
/// An outline node: its header line and children.
|
||||
struct Node<'a> {
|
||||
header: String,
|
||||
children: Vec<Child<'a>>,
|
||||
/// Whether a single leaf child joins the header line.
|
||||
inline_leaf: bool,
|
||||
}
|
||||
|
||||
impl<'a> Node<'a> {
|
||||
fn leaf(header: String) -> Self {
|
||||
Self {
|
||||
header,
|
||||
children: vec![],
|
||||
inline_leaf: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn branch(header: impl Into<String>, children: Vec<Child<'a>>) -> Self {
|
||||
Self {
|
||||
header: header.into(),
|
||||
children,
|
||||
inline_leaf: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn wrapper(header: impl Into<String>, child: Child<'a>) -> Self {
|
||||
Self {
|
||||
header: header.into(),
|
||||
children: vec![child],
|
||||
inline_leaf: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write(out: &mut String, depth: usize, node: Node<'_>) {
|
||||
let Node {
|
||||
mut header,
|
||||
children,
|
||||
inline_leaf,
|
||||
} = node;
|
||||
let mut children = children.into_iter().map(self::node).collect::<Vec<_>>();
|
||||
if inline_leaf
|
||||
&& let [child] = &children[..]
|
||||
&& child.children.is_empty()
|
||||
{
|
||||
let _ = write!(header, " {}", child.header);
|
||||
children.clear();
|
||||
}
|
||||
let _ = writeln!(out, "{}{header}", " ".repeat(depth));
|
||||
for child in children {
|
||||
write(out, depth + 1, child);
|
||||
}
|
||||
}
|
||||
|
||||
fn node(child: Child<'_>) -> Node<'_> {
|
||||
match child {
|
||||
Child::Format(format) => format_node(format),
|
||||
Child::Tag(tag) => tag_node(tag),
|
||||
Child::Rule(pattern, format) => {
|
||||
Node::wrapper(format!("on {pattern}"), Child::Format(format))
|
||||
}
|
||||
Child::Text(text) => Node::leaf(text.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_node(format: &Format) -> Node<'_> {
|
||||
match format {
|
||||
Format::ConstString(format) => Node::leaf(quote(&format.value)),
|
||||
Format::Regex(format) => Node::leaf(format!("/{}/", format.pattern)),
|
||||
Format::AnyText(format) => {
|
||||
let mut header = "text".to_string();
|
||||
push_excluding(&mut header, format.excludes.iter().map(|text| quote(text)));
|
||||
push_limit(&mut header, "max_tokens", format.max_tokens);
|
||||
push_limit(&mut header, "max_chars", format.max_chars);
|
||||
Node::leaf(header)
|
||||
}
|
||||
Format::JsonSchema(format) => {
|
||||
let style = serde_json::to_value(format.style)
|
||||
.ok()
|
||||
.and_then(|style| style.as_str().map(str::to_string))
|
||||
.unwrap_or_else(|| format!("{:?}", format.style));
|
||||
let mut header = format!("{style}({})", schema(&format.json_schema));
|
||||
if format.any_order {
|
||||
header.push_str(" any_order");
|
||||
}
|
||||
if let Some(max) = format.max_whitespace_cnt {
|
||||
let _ = write!(header, " max_whitespace={max}");
|
||||
}
|
||||
push_excluding(&mut header, format.excludes.iter().map(|text| quote(text)));
|
||||
Node::leaf(header)
|
||||
}
|
||||
Format::Grammar(format) => {
|
||||
Node::branch("grammar", format.grammar.lines().map(Child::Text).collect())
|
||||
}
|
||||
Format::Token(format) => Node::leaf(format!("token {}", token(&format.token))),
|
||||
Format::ExcludeToken(format) => {
|
||||
let mut header = "token".to_string();
|
||||
push_excluding(&mut header, format.exclude_tokens.iter().map(token));
|
||||
Node::leaf(header)
|
||||
}
|
||||
Format::AnyTokens(format) => {
|
||||
let mut header = "tokens".to_string();
|
||||
push_excluding(&mut header, format.exclude_tokens.iter().map(token));
|
||||
push_limit(&mut header, "max_tokens", format.max_tokens);
|
||||
Node::leaf(header)
|
||||
}
|
||||
Format::Sequence(format) => Node::branch(
|
||||
"sequence",
|
||||
format.elements.iter().map(Child::Format).collect(),
|
||||
),
|
||||
Format::Or(format) => {
|
||||
Node::branch("or", format.elements.iter().map(Child::Format).collect())
|
||||
}
|
||||
Format::Optional(format) => Node::wrapper("optional", Child::Format(&format.content)),
|
||||
Format::Plus(format) => Node::wrapper("plus", Child::Format(&format.content)),
|
||||
Format::Star(format) => Node::wrapper("star", Child::Format(&format.content)),
|
||||
Format::Repeat(format) => {
|
||||
let max = if format.max < 0 {
|
||||
String::new()
|
||||
} else {
|
||||
format.max.to_string()
|
||||
};
|
||||
Node::wrapper(
|
||||
format!("repeat {}..{max}", format.min),
|
||||
Child::Format(&format.content),
|
||||
)
|
||||
}
|
||||
Format::Tag(tag) => tag_node(tag),
|
||||
Format::TriggeredTags(format) => {
|
||||
let triggers = format.triggers.iter().map(|trigger| quote(trigger));
|
||||
let mut header = format!(
|
||||
"triggered_tags [{}]",
|
||||
triggers.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
push_excluding(&mut header, format.excludes.iter().map(|text| quote(text)));
|
||||
push_flags(&mut header, format.at_least_one, format.stop_after_first);
|
||||
Node::branch(header, format.tags.iter().map(Child::Tag).collect())
|
||||
}
|
||||
Format::TokenTriggeredTags(format) => {
|
||||
let triggers = format.trigger_tokens.iter().map(token);
|
||||
let mut header = format!(
|
||||
"token_triggered_tags [{}]",
|
||||
triggers.collect::<Vec<_>>().join(", ")
|
||||
);
|
||||
push_excluding(&mut header, format.exclude_tokens.iter().map(token));
|
||||
push_flags(&mut header, format.at_least_one, format.stop_after_first);
|
||||
Node::branch(header, format.tags.iter().map(Child::Tag).collect())
|
||||
}
|
||||
Format::TagsWithSeparator(format) => {
|
||||
let mut header = format!("tags_with_separator {}", quote(&format.separator));
|
||||
push_flags(&mut header, format.at_least_one, format.stop_after_first);
|
||||
Node::branch(header, format.tags.iter().map(Child::Tag).collect())
|
||||
}
|
||||
Format::Dispatch(format) => {
|
||||
let mut header = "dispatch".to_string();
|
||||
if !format.r#loop {
|
||||
header.push_str(" once");
|
||||
}
|
||||
push_excluding(&mut header, format.excludes.iter().map(|text| quote(text)));
|
||||
Node::branch(
|
||||
header,
|
||||
format
|
||||
.rules
|
||||
.iter()
|
||||
.map(|(pattern, format)| Child::Rule(quote(pattern), format))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
Format::TokenDispatch(format) => {
|
||||
let mut header = "token_dispatch".to_string();
|
||||
if !format.r#loop {
|
||||
header.push_str(" once");
|
||||
}
|
||||
push_excluding(&mut header, format.exclude_tokens.iter().map(token));
|
||||
Node::branch(
|
||||
header,
|
||||
format
|
||||
.rules
|
||||
.iter()
|
||||
.map(|(trigger, format)| Child::Rule(token(trigger), format))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `tag BEGIN CONTENT END` for leaf content, or `tag BEGIN .. END` above the
|
||||
/// content's own node.
|
||||
fn tag_node(tag: &TagFormat) -> Node<'_> {
|
||||
let begin = match &tag.begin {
|
||||
TagBoundary::Text(text) => quote(text),
|
||||
TagBoundary::Token(boundary) => format!("token {}", token(&boundary.token)),
|
||||
};
|
||||
let end = match &tag.end {
|
||||
EndBoundary::Text(text) => quote(text),
|
||||
EndBoundary::Texts(texts) => {
|
||||
texts.iter().map(|text| quote(text)).collect::<Vec<_>>().join(" | ")
|
||||
}
|
||||
EndBoundary::Token(boundary) => format!("token {}", token(&boundary.token)),
|
||||
};
|
||||
let content = format_node(&tag.content);
|
||||
if content.children.is_empty() {
|
||||
Node::leaf(format!("tag {begin} {} {end}", content.header))
|
||||
} else {
|
||||
Node::branch(
|
||||
format!("tag {begin} .. {end}"),
|
||||
vec![Child::Format(&tag.content)],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Schema keywords that do not constrain the generated text.
|
||||
const ANNOTATIONS: &[&str] = &[
|
||||
"$comment",
|
||||
"$id",
|
||||
"$schema",
|
||||
"default",
|
||||
"deprecated",
|
||||
"description",
|
||||
"examples",
|
||||
"readOnly",
|
||||
"title",
|
||||
"writeOnly",
|
||||
];
|
||||
|
||||
/// Render a JSON schema as a TypeScript-like type.
|
||||
///
|
||||
/// Objects are `{ key: T, optional?: T }`, with `...` for any additional
|
||||
/// property and `...: T` for additional properties under `T`. Arrays are
|
||||
/// `T[]`. Unions, type arrays, and enums join their options with `|`,
|
||||
/// `oneOf` and `allOf` keep their names, local references show the
|
||||
/// definition name, a `type` next to a reference intersects it as
|
||||
/// `NAME & T`, and `true` / `false` are `any` / `never`. Every other
|
||||
/// keyword follows the type as `(key=VALUE, ...)`, and definitions follow the
|
||||
/// root as `where NAME = T, ...`. Annotations such as `description` are
|
||||
/// dropped.
|
||||
pub fn schema(schema: &Value) -> String {
|
||||
typed(schema).text
|
||||
}
|
||||
|
||||
/// A rendered type, and whether it is a top-level union or intersection that
|
||||
/// an operand position must parenthesize.
|
||||
struct Typed {
|
||||
text: String,
|
||||
compound: bool,
|
||||
}
|
||||
|
||||
impl Typed {
|
||||
fn plain(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
compound: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn compound(text: String) -> Self {
|
||||
Self {
|
||||
text,
|
||||
compound: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn union(options: Vec<String>) -> Self {
|
||||
Self {
|
||||
compound: options.len() > 1,
|
||||
text: options.join(" | "),
|
||||
}
|
||||
}
|
||||
|
||||
/// The type as an operand of `&` or of a suffix such as `[]` or `(...)`.
|
||||
fn grouped(self) -> String {
|
||||
if self.compound {
|
||||
format!("({})", self.text)
|
||||
} else {
|
||||
self.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn typed(schema: &Value) -> Typed {
|
||||
let schema = match schema {
|
||||
Value::Bool(true) => return Typed::plain("any"),
|
||||
Value::Bool(false) => return Typed::plain("never"),
|
||||
Value::Object(schema) => schema,
|
||||
schema => return Typed::plain(schema.to_string()),
|
||||
};
|
||||
let mut rendered = SchemaType::new(schema).render();
|
||||
let definitions = ["$defs", "definitions"]
|
||||
.into_iter()
|
||||
.filter_map(|key| schema.get(key)?.as_object())
|
||||
.flatten()
|
||||
.map(|(name, definition)| format!("{name} = {}", self::schema(definition)))
|
||||
.collect::<Vec<_>>();
|
||||
if !definitions.is_empty() {
|
||||
rendered = Typed::plain(format!(
|
||||
"{} where {}",
|
||||
rendered.text,
|
||||
definitions.join(", ")
|
||||
));
|
||||
}
|
||||
rendered
|
||||
}
|
||||
|
||||
/// One schema object being rendered, with the keywords its type consumed.
|
||||
struct SchemaType<'a> {
|
||||
schema: &'a Map<String, Value>,
|
||||
consumed: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl<'a> SchemaType<'a> {
|
||||
fn new(schema: &'a Map<String, Value>) -> Self {
|
||||
Self {
|
||||
schema,
|
||||
consumed: vec!["$defs", "definitions"],
|
||||
}
|
||||
}
|
||||
|
||||
fn take(&mut self, key: &'static str) -> Option<&'a Value> {
|
||||
let value = self.schema.get(key)?;
|
||||
self.consumed.push(key);
|
||||
Some(value)
|
||||
}
|
||||
|
||||
fn render(mut self) -> Typed {
|
||||
let mut rendered = self.body();
|
||||
let rest = self
|
||||
.schema
|
||||
.iter()
|
||||
.filter(|(key, _)| {
|
||||
!self.consumed.contains(&key.as_str()) && !ANNOTATIONS.contains(&key.as_str())
|
||||
})
|
||||
.map(|(key, value)| format!("{key}={value}"))
|
||||
.collect::<Vec<_>>();
|
||||
if !rest.is_empty() {
|
||||
rendered = Typed::plain(format!("{}({})", rendered.grouped(), rest.join(", ")));
|
||||
}
|
||||
rendered
|
||||
}
|
||||
|
||||
fn body(&mut self) -> Typed {
|
||||
if let Some(Value::String(reference)) = self.take("$ref") {
|
||||
let name = ["#/$defs/", "#/definitions/"]
|
||||
.into_iter()
|
||||
.find_map(|prefix| reference.strip_prefix(prefix))
|
||||
.unwrap_or(reference);
|
||||
// Keywords next to `$ref` apply together with it.
|
||||
return match self.take("type") {
|
||||
Some(type_name) => {
|
||||
Typed::compound(format!("{name} & {}", type_names(type_name).grouped()))
|
||||
}
|
||||
None => Typed::plain(name),
|
||||
};
|
||||
}
|
||||
if let Some(value) = self.take("const") {
|
||||
self.take_redundant_type([value]);
|
||||
return Typed::plain(value.to_string());
|
||||
}
|
||||
if let Some(Value::Array(values)) = self.take("enum") {
|
||||
self.take_redundant_type(values);
|
||||
return Typed::union(values.iter().map(Value::to_string).collect());
|
||||
}
|
||||
if let Some(Value::Array(options)) = self.take("anyOf") {
|
||||
return Typed::union(options.iter().map(schema).collect());
|
||||
}
|
||||
for combinator in ["oneOf", "allOf"] {
|
||||
if let Some(Value::Array(options)) = self.take(combinator) {
|
||||
let options = options.iter().map(schema).collect::<Vec<_>>();
|
||||
return Typed::plain(format!("{combinator}({})", options.join(", ")));
|
||||
}
|
||||
}
|
||||
let is = |name: &str| self.schema.get("type").is_none_or(|type_name| type_name == name);
|
||||
if is("object")
|
||||
&& (self.schema.contains_key("properties")
|
||||
|| self.schema.contains_key("additionalProperties"))
|
||||
{
|
||||
self.take("type");
|
||||
return Typed::plain(self.object());
|
||||
}
|
||||
if is("array") && self.schema.contains_key("items") {
|
||||
self.take("type");
|
||||
let items = self.take("items").map_or_else(|| Typed::plain("any"), typed);
|
||||
return Typed::plain(format!("{}[]", items.grouped()));
|
||||
}
|
||||
match self.take("type") {
|
||||
Some(type_name) => type_names(type_name),
|
||||
None => Typed::plain("any"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume `type` when every literal already has that type.
|
||||
fn take_redundant_type<'v>(&mut self, values: impl IntoIterator<Item = &'v Value>) {
|
||||
let Some(Value::String(type_name)) = self.schema.get("type") else {
|
||||
return;
|
||||
};
|
||||
let matches = |value: &Value| match value {
|
||||
Value::String(_) => type_name == "string",
|
||||
Value::Number(number) => {
|
||||
type_name == "number" || (type_name == "integer" && !number.is_f64())
|
||||
}
|
||||
Value::Bool(_) => type_name == "boolean",
|
||||
Value::Null => type_name == "null",
|
||||
Value::Object(_) => type_name == "object",
|
||||
Value::Array(_) => type_name == "array",
|
||||
};
|
||||
if values.into_iter().all(matches) {
|
||||
self.take("type");
|
||||
}
|
||||
}
|
||||
|
||||
fn object(&mut self) -> String {
|
||||
let required = self
|
||||
.take("required")
|
||||
.and_then(Value::as_array)
|
||||
.map(|required| required.iter().filter_map(Value::as_str).collect::<Vec<_>>())
|
||||
.unwrap_or_default();
|
||||
let properties = self.take("properties").and_then(Value::as_object);
|
||||
let mut fields = properties
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(key, value)| {
|
||||
let optional = if required.contains(&key.as_str()) {
|
||||
""
|
||||
} else {
|
||||
"?"
|
||||
};
|
||||
format!("{key}{optional}: {}", schema(value))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
// Required keys without a declared schema.
|
||||
fields.extend(
|
||||
required
|
||||
.iter()
|
||||
.filter(|key| properties.is_none_or(|properties| !properties.contains_key(**key)))
|
||||
.map(|key| format!("{key}: any")),
|
||||
);
|
||||
match self.take("additionalProperties") {
|
||||
Some(Value::Bool(false)) | None => {}
|
||||
Some(Value::Bool(true)) => fields.push("...".to_string()),
|
||||
Some(additional) => fields.push(format!("...: {}", schema(additional))),
|
||||
}
|
||||
if fields.is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
format!("{{ {} }}", fields.join(", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `type` keyword's type names.
|
||||
fn type_names(type_name: &Value) -> Typed {
|
||||
match type_name {
|
||||
Value::String(name) if name == "array" => Typed::plain("any[]"),
|
||||
Value::String(name) => Typed::plain(name.clone()),
|
||||
Value::Array(names) => Typed::union(
|
||||
names
|
||||
.iter()
|
||||
.map(|name| name.as_str().map_or_else(|| name.to_string(), str::to_string))
|
||||
.collect(),
|
||||
),
|
||||
type_name => Typed::plain(type_name.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn quote(text: &str) -> String {
|
||||
let mut quoted = String::with_capacity(text.len() + 2);
|
||||
quoted.push('`');
|
||||
for char in text.chars() {
|
||||
match char {
|
||||
'\\' => quoted.push_str("\\\\"),
|
||||
'`' => quoted.push_str("\\`"),
|
||||
'\n' => quoted.push_str("\\n"),
|
||||
'\r' => quoted.push_str("\\r"),
|
||||
'\t' => quoted.push_str("\\t"),
|
||||
char if char.is_control() => {
|
||||
let _ = write!(quoted, "\\u{{{:x}}}", char as u32);
|
||||
}
|
||||
char => quoted.push(char),
|
||||
}
|
||||
}
|
||||
quoted.push('`');
|
||||
quoted
|
||||
}
|
||||
|
||||
fn token(token: &TokenValue) -> String {
|
||||
match token {
|
||||
TokenValue::Id(id) => id.to_string(),
|
||||
TokenValue::Text(text) => quote(text),
|
||||
}
|
||||
}
|
||||
|
||||
fn push_excluding(header: &mut String, excludes: impl Iterator<Item = String>) {
|
||||
let excludes = excludes.collect::<Vec<_>>();
|
||||
if !excludes.is_empty() {
|
||||
let _ = write!(header, " excluding [{}]", excludes.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
fn push_limit(header: &mut String, name: &str, limit: Option<u32>) {
|
||||
if let Some(limit) = limit {
|
||||
let _ = write!(header, " {name}={limit}");
|
||||
}
|
||||
}
|
||||
|
||||
fn push_flags(header: &mut String, at_least_one: bool, stop_after_first: bool) {
|
||||
if at_least_one {
|
||||
header.push_str(" at_least_one");
|
||||
}
|
||||
if stop_after_first {
|
||||
header.push_str(" stop_after_first");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use expect_test::expect;
|
||||
use serde_json::json;
|
||||
use xgrammar_structural_tag::format::{
|
||||
AnyTokensFormat, DispatchFormat, ExcludeTokenFormat, GrammarFormat, JsonSchemaFormat,
|
||||
JsonSchemaStyle, TokenBoundary, TokenDispatchFormat, TokenFormat, TokenTriggeredTagsFormat,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn outline_renders_every_format_type() {
|
||||
let format = Format::sequence(vec![
|
||||
Format::Token(TokenFormat {
|
||||
token: TokenValue::Id(7),
|
||||
}),
|
||||
Format::ExcludeToken(ExcludeTokenFormat {
|
||||
exclude_tokens: vec![TokenValue::Id(1), TokenValue::Text("<eos>".to_string())],
|
||||
}),
|
||||
Format::AnyTokens(AnyTokensFormat {
|
||||
exclude_tokens: vec![TokenValue::Id(2)],
|
||||
max_tokens: Some(16),
|
||||
}),
|
||||
Format::Grammar(GrammarFormat {
|
||||
grammar: "root ::= a\na ::= \"x\"".to_string(),
|
||||
}),
|
||||
Format::repeat(Format::regex("[0-9]"), 1, -1),
|
||||
Format::JsonSchema(
|
||||
JsonSchemaFormat::new(json!({ "type": "string" }))
|
||||
.with_style(JsonSchemaStyle::KimiK3Xml)
|
||||
.with_any_order(true)
|
||||
.with_max_whitespace_cnt(Some(2))
|
||||
.with_excludes(&["</arg>"]),
|
||||
),
|
||||
Format::TokenTriggeredTags(TokenTriggeredTagsFormat {
|
||||
trigger_tokens: vec![TokenValue::Id(9)],
|
||||
tags: vec![TagFormat::new(
|
||||
TokenBoundary::new(9),
|
||||
Format::optional(Format::const_string("`\t")),
|
||||
vec!["</a>", "</b>"],
|
||||
)],
|
||||
exclude_tokens: vec![],
|
||||
at_least_one: false,
|
||||
stop_after_first: true,
|
||||
}),
|
||||
Format::Dispatch(DispatchFormat {
|
||||
rules: vec![(
|
||||
"<call>".to_string(),
|
||||
Format::sequence(vec![Format::any_text(), Format::const_string("</call>")]),
|
||||
)],
|
||||
r#loop: false,
|
||||
excludes: vec!["<stop>".to_string()],
|
||||
}),
|
||||
Format::TokenDispatch(TokenDispatchFormat {
|
||||
rules: vec![(
|
||||
TokenValue::Id(3),
|
||||
Format::JsonSchema(JsonSchemaFormat::new(json!(true))),
|
||||
)],
|
||||
r#loop: true,
|
||||
exclude_tokens: vec![],
|
||||
}),
|
||||
]);
|
||||
|
||||
expect![[r#"
|
||||
sequence
|
||||
token 7
|
||||
token excluding [1, `<eos>`]
|
||||
tokens excluding [2] max_tokens=16
|
||||
grammar
|
||||
root ::= a
|
||||
a ::= "x"
|
||||
repeat 1.. /[0-9]/
|
||||
kimi_k3_xml(string) any_order max_whitespace=2 excluding [`</arg>`]
|
||||
token_triggered_tags [9] stop_after_first
|
||||
tag token 9 .. `</a>` | `</b>`
|
||||
optional `\`\t`
|
||||
dispatch once excluding [`<stop>`]
|
||||
on `<call>`
|
||||
sequence
|
||||
text
|
||||
`</call>`
|
||||
token_dispatch
|
||||
on 3 json(any)
|
||||
"#]]
|
||||
.assert_eq(&outline(&format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_renders_common_shapes_as_types() {
|
||||
let rendered = schema(&json!({
|
||||
"$defs": { "place": { "type": "object", "properties": { "city": { "type": "string" } } } },
|
||||
"type": "object",
|
||||
"description": "ignored",
|
||||
"properties": {
|
||||
"location": { "type": "string" },
|
||||
"id": { "type": ["integer", "null"] },
|
||||
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] },
|
||||
"tags": { "type": "array", "items": { "anyOf": [{ "type": "string" }, { "type": "number" }] } },
|
||||
"place": { "$ref": "#/$defs/place" },
|
||||
"days": { "type": "integer", "minimum": 1 },
|
||||
"free": { "type": "object", "additionalProperties": true }
|
||||
},
|
||||
"required": ["location", "id"],
|
||||
"additionalProperties": false
|
||||
}));
|
||||
|
||||
expect![[r#"{ location: string, id: integer | null, unit?: "celsius" | "fahrenheit", tags?: (string | number)[], place?: place, days?: integer(minimum=1), free?: { ... } } where place = { city?: string }"#]].assert_eq(&rendered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schema_keeps_other_keywords_after_the_type() {
|
||||
let rendered = schema(&json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": { "type": "string", "pattern": "[A-Z]{3}" },
|
||||
"ids": { "type": "array", "items": { "type": ["integer", "null"] }, "maxItems": 3 },
|
||||
"place": { "$ref": "#/$defs/place", "type": "object" },
|
||||
"either": { "not": { "type": "null" } }
|
||||
},
|
||||
"required": ["code", "tenant"],
|
||||
"additionalProperties": { "type": "boolean" },
|
||||
"minProperties": 1
|
||||
}));
|
||||
|
||||
expect![[r#"{ code: string(pattern="[A-Z]{3}"), ids?: (integer | null)[](maxItems=3), place?: place & object, either?: any(not={"type":"null"}), tenant: any, ...: boolean }(minProperties=1)"#]].assert_eq(&rendered);
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::HyStructuralTagBuilder;
|
||||
use crate::output_grammar::test_utils::outline;
|
||||
use crate::tool::{HyDialect, HyToolMarkers};
|
||||
|
||||
fn tool(name: &str, parameters: Value) -> ToolParam {
|
||||
@@ -219,7 +220,33 @@ mod tests {
|
||||
ToolChoice::required(),
|
||||
);
|
||||
|
||||
expect![[r#"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"const_string","value":"<tool_calls:opensource>\n"},{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<tool_call:opensource>get_weather<tool_sep:opensource>\n","content":{"type":"sequence","elements":[{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key:opensource>"},{"type":"const_string","value":"city"},{"type":"const_string","value":"</arg_key:opensource>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value:opensource>"},{"type":"any_text","excludes":["<tool_calls:opensource>","</tool_calls:opensource>","<tool_call:opensource>","</tool_call:opensource>","<tool_sep:opensource>","<arg_key:opensource>","</arg_key:opensource>","<arg_value:opensource>","</arg_value:opensource>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value:opensource>"},{"type":"const_string","value":"\n"}]},{"type":"star","content":{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key:opensource>"},{"type":"const_string","value":"days"},{"type":"const_string","value":"</arg_key:opensource>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value:opensource>"},{"type":"any_text","excludes":["<tool_calls:opensource>","</tool_calls:opensource>","<tool_call:opensource>","</tool_call:opensource>","<tool_sep:opensource>","<arg_key:opensource>","</arg_key:opensource>","<arg_value:opensource>","</arg_value:opensource>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value:opensource>"},{"type":"const_string","value":"\n"}]}}]},"end":"</tool_call:opensource>"}],"separator":"\n","at_least_one":true,"stop_after_first":false},{"type":"const_string","value":"\n</tool_calls:opensource>"}]}}"#]].assert_eq(&tag.to_json_string().unwrap());
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls:opensource>\n`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call:opensource>get_weather<tool_sep:opensource>\n` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`city`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
star
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`days`
|
||||
`</arg_key:opensource>`
|
||||
`\n`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<tool_sep:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`\n`
|
||||
`\n</tool_calls:opensource>`
|
||||
"#]].assert_eq(&outline(&tag.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -238,7 +265,21 @@ mod tests {
|
||||
ToolChoice::required(),
|
||||
);
|
||||
|
||||
expect![[r#"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"const_string","value":"<tool_calls:opensource>"},{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<tool_call:opensource>get_weather","content":{"type":"sequence","elements":[{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key:opensource>"},{"type":"const_string","value":"city"},{"type":"const_string","value":"</arg_key:opensource>"},{"type":"const_string","value":"<arg_value:opensource>"},{"type":"any_text","excludes":["<tool_calls:opensource>","</tool_calls:opensource>","<tool_call:opensource>","</tool_call:opensource>","<arg_key:opensource>","</arg_key:opensource>","<arg_value:opensource>","</arg_value:opensource>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value:opensource>"}]}]},"end":"</tool_call:opensource>"}],"separator":"","at_least_one":true,"stop_after_first":false},{"type":"const_string","value":"</tool_calls:opensource>"}]}}"#]].assert_eq(&tag.to_json_string().unwrap());
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls:opensource>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<tool_call:opensource>get_weather` .. `</tool_call:opensource>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key:opensource>`
|
||||
`city`
|
||||
`</arg_key:opensource>`
|
||||
`<arg_value:opensource>`
|
||||
text excluding [`<tool_calls:opensource>`, `</tool_calls:opensource>`, `<tool_call:opensource>`, `</tool_call:opensource>`, `<arg_key:opensource>`, `</arg_key:opensource>`, `<arg_value:opensource>`, `</arg_value:opensource>`]
|
||||
`</arg_value:opensource>`
|
||||
`</tool_calls:opensource>`
|
||||
"#]].assert_eq(&outline(&tag.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -257,14 +298,34 @@ mod tests {
|
||||
)],
|
||||
ToolChoice::required(),
|
||||
);
|
||||
let value = serde_json::to_value(tag).unwrap();
|
||||
|
||||
expect![[r#"{"type":"sequence","elements":[{"type":"star","content":{"type":"or","elements":[{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key>"},{"type":"const_string","value":"query"},{"type":"const_string","value":"</arg_key>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value>"},{"type":"any_text","excludes":["<tool_calls>","</tool_calls>","<tool_call>","</tool_call>","<tool_sep>","<arg_key>","</arg_key>","<arg_value>","</arg_value>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value>"},{"type":"const_string","value":"\n"}]},{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key>"},{"type":"const_string","value":"limit"},{"type":"const_string","value":"</arg_key>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value>"},{"type":"any_text","excludes":["<tool_calls>","</tool_calls>","<tool_call>","</tool_call>","<tool_sep>","<arg_key>","</arg_key>","<arg_value>","</arg_value>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value>"},{"type":"const_string","value":"\n"}]}]}}]}"#]].assert_eq(
|
||||
&serde_json::to_string(
|
||||
&value["format"]["elements"][1]["tags"][0]["content"],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls>\n`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call>lookup<tool_sep>\n` .. `</tool_call>`
|
||||
sequence
|
||||
star
|
||||
or
|
||||
sequence
|
||||
`<arg_key>`
|
||||
`query`
|
||||
`</arg_key>`
|
||||
`\n`
|
||||
`<arg_value>`
|
||||
text excluding [`<tool_calls>`, `</tool_calls>`, `<tool_call>`, `</tool_call>`, `<tool_sep>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
`</arg_value>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key>`
|
||||
`limit`
|
||||
`</arg_key>`
|
||||
`\n`
|
||||
`<arg_value>`
|
||||
text excluding [`<tool_calls>`, `</tool_calls>`, `<tool_call>`, `</tool_call>`, `<tool_sep>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
`</arg_value>`
|
||||
`\n`
|
||||
`\n</tool_calls>`
|
||||
"#]].assert_eq(&outline(&tag.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -273,11 +334,24 @@ mod tests {
|
||||
tool("search", json!({ "type": "object" })),
|
||||
tool("lookup", json!({ "type": "object" })),
|
||||
];
|
||||
let auto = build("", &tools, ToolChoice::auto()).to_json_string().unwrap();
|
||||
let forced = build("", &tools, ToolChoice::function("lookup")).to_json_string().unwrap();
|
||||
let auto = build("", &tools, ToolChoice::auto());
|
||||
let forced = build("", &tools, ToolChoice::function("lookup"));
|
||||
|
||||
expect![[r#"{"type":"structural_tag","format":{"type":"triggered_tags","triggers":["<tool_calls>"],"tags":[{"type":"tag","begin":"<tool_calls>\n","content":{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<tool_call>search<tool_sep>\n","content":{"type":"any_text","excludes":[],"max_tokens":null,"max_chars":null},"end":"</tool_call>"},{"type":"tag","begin":"<tool_call>lookup<tool_sep>\n","content":{"type":"any_text","excludes":[],"max_tokens":null,"max_chars":null},"end":"</tool_call>"}],"separator":"\n","at_least_one":true,"stop_after_first":false},"end":"\n</tool_calls>"}],"at_least_one":false,"stop_after_first":false,"excludes":[]}}"#]].assert_eq(&auto);
|
||||
expect![[r#"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"const_string","value":"<tool_calls>\n"},{"type":"tag","begin":"<tool_call>lookup<tool_sep>\n","content":{"type":"any_text","excludes":[],"max_tokens":null,"max_chars":null},"end":"</tool_call>"},{"type":"const_string","value":"\n</tool_calls>"}]}}"#]].assert_eq(&forced);
|
||||
expect![[r#"
|
||||
triggered_tags [`<tool_calls>`]
|
||||
tag `<tool_calls>\n` .. `\n</tool_calls>`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call>search<tool_sep>\n` text `</tool_call>`
|
||||
tag `<tool_call>lookup<tool_sep>\n` text `</tool_call>`
|
||||
"#]]
|
||||
.assert_eq(&outline(&auto.format));
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls>\n`
|
||||
tag `<tool_call>lookup<tool_sep>\n` text `</tool_call>`
|
||||
`\n</tool_calls>`
|
||||
"#]]
|
||||
.assert_eq(&outline(&forced.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -291,11 +365,13 @@ mod tests {
|
||||
)],
|
||||
ToolChoice::function("get_current_date"),
|
||||
);
|
||||
let value = serde_json::to_value(tag).unwrap();
|
||||
|
||||
expect![[r#"{"type":"tag","begin":"<tool_call>get_current_date","content":{"type":"const_string","value":""},"end":"</tool_call>"}"#]].assert_eq(
|
||||
&serde_json::to_string(&value["format"]["elements"][1]).unwrap(),
|
||||
);
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls>`
|
||||
tag `<tool_call>get_current_date` `` `</tool_call>`
|
||||
`</tool_calls>`
|
||||
"#]]
|
||||
.assert_eq(&outline(&tag.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -312,13 +388,31 @@ mod tests {
|
||||
)],
|
||||
ToolChoice::required(),
|
||||
);
|
||||
let value = serde_json::to_value(tag).unwrap();
|
||||
|
||||
expect![[r#"{"type":"sequence","elements":[{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key>"},{"type":"const_string","value":"query"},{"type":"const_string","value":"</arg_key>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value>"},{"type":"any_text","excludes":["<tool_calls>","</tool_calls>","<tool_call>","</tool_call>","<tool_sep>","<arg_key>","</arg_key>","<arg_value>","</arg_value>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value>"},{"type":"const_string","value":"\n"}]},{"type":"sequence","elements":[{"type":"const_string","value":"<arg_key>"},{"type":"const_string","value":"tenant"},{"type":"const_string","value":"</arg_key>"},{"type":"const_string","value":"\n"},{"type":"const_string","value":"<arg_value>"},{"type":"any_text","excludes":["<tool_calls>","</tool_calls>","<tool_call>","</tool_call>","<tool_sep>","<arg_key>","</arg_key>","<arg_value>","</arg_value>"],"max_tokens":null,"max_chars":null},{"type":"const_string","value":"</arg_value>"},{"type":"const_string","value":"\n"}]}]}"#]].assert_eq(
|
||||
&serde_json::to_string(
|
||||
&value["format"]["elements"][1]["tags"][0]["content"],
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
expect![[r#"
|
||||
sequence
|
||||
`<tool_calls>\n`
|
||||
tags_with_separator `\n` at_least_one
|
||||
tag `<tool_call>search<tool_sep>\n` .. `</tool_call>`
|
||||
sequence
|
||||
sequence
|
||||
`<arg_key>`
|
||||
`query`
|
||||
`</arg_key>`
|
||||
`\n`
|
||||
`<arg_value>`
|
||||
text excluding [`<tool_calls>`, `</tool_calls>`, `<tool_call>`, `</tool_call>`, `<tool_sep>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
`</arg_value>`
|
||||
`\n`
|
||||
sequence
|
||||
`<arg_key>`
|
||||
`tenant`
|
||||
`</arg_key>`
|
||||
`\n`
|
||||
`<arg_value>`
|
||||
text excluding [`<tool_calls>`, `</tool_calls>`, `<tool_call>`, `</tool_call>`, `<tool_sep>`, `<arg_key>`, `</arg_key>`, `<arg_value>`, `</arg_value>`]
|
||||
`</arg_value>`
|
||||
`\n`
|
||||
`\n</tool_calls>`
|
||||
"#]].assert_eq(&outline(&tag.format));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,6 +364,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::KimiK3StructuralTagBuilder;
|
||||
use crate::output_grammar::test_utils::outline;
|
||||
|
||||
fn tool(name: &str, parameters: serde_json::Value) -> ToolParam {
|
||||
ToolParam::Function(FunctionToolParam::new(
|
||||
@@ -395,7 +396,27 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
expect![[r##"{"type":"structural_tag","format":{"type":"sequence","elements":[{"type":"optional","content":{"type":"const_string","value":"<|open|>response<|sep|>"}},{"type":"tag","begin":"","content":{"type":"any_text","excludes":["<|open|>","<|close|>","<|end_of_msg|>"],"max_tokens":null,"max_chars":null},"end":"<|close|>response<|sep|>"},{"type":"tag","begin":"<|open|>tools<|sep|>","content":{"type":"tags_with_separator","tags":[{"type":"tag","begin":"<|open|>call tool=\"get_weather\" index=\"","content":{"type":"sequence","elements":[{"type":"regex","pattern":"[1-9][0-9]*"},{"type":"const_string","value":"\"<|sep|>"},{"type":"or","elements":[{"type":"plus","content":{"type":"or","elements":[{"type":"tag","begin":"<|open|>argument key=\"unit\" type=\"string\"<|sep|>","content":{"type":"or","elements":[{"type":"const_string","value":"celsius"},{"type":"const_string","value":"fahrenheit"}]},"end":"<|close|>argument<|sep|>"},{"type":"tag","begin":"<|open|>argument key=\"place\" type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$ref":"#/$defs/place","type":"object","$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}}},"style":"json","any_order":false,"max_whitespace_cnt":null,"excludes":[]},"end":"<|close|>argument<|sep|>"}]}},{"type":"tag","begin":"<|open|>json type=\"object\"<|sep|>","content":{"type":"json_schema","json_schema":{"$defs":{"place":{"type":"object","properties":{"city":{"type":"string"}}}},"type":"object","properties":{"unit":{"type":"string","enum":["celsius","fahrenheit"]},"place":{"$ref":"#/$defs/place","type":"object"}},"required":["place"]},"style":"json","any_order":false,"max_whitespace_cnt":null,"excludes":[]},"end":"<|close|>json<|sep|>"}]}]},"end":"<|close|>call<|sep|>"}],"separator":"","at_least_one":true,"stop_after_first":false},"end":"<|close|>tools<|sep|>"},{"type":"optional","content":{"type":"const_string","value":"<|close|>message<|sep|>"}}]}}"##]].assert_eq(&tag.to_json_string().unwrap());
|
||||
expect![[r#"
|
||||
sequence
|
||||
optional `<|open|>response<|sep|>`
|
||||
tag `` text excluding [`<|open|>`, `<|close|>`, `<|end_of_msg|>`] `<|close|>response<|sep|>`
|
||||
tag `<|open|>tools<|sep|>` .. `<|close|>tools<|sep|>`
|
||||
tags_with_separator `` at_least_one
|
||||
tag `<|open|>call tool="get_weather" index="` .. `<|close|>call<|sep|>`
|
||||
sequence
|
||||
/[1-9][0-9]*/
|
||||
`"<|sep|>`
|
||||
or
|
||||
plus
|
||||
or
|
||||
tag `<|open|>argument key="unit" type="string"<|sep|>` .. `<|close|>argument<|sep|>`
|
||||
or
|
||||
`celsius`
|
||||
`fahrenheit`
|
||||
tag `<|open|>argument key="place" type="object"<|sep|>` json(place & object where place = { city?: string }) `<|close|>argument<|sep|>`
|
||||
tag `<|open|>json type="object"<|sep|>` json({ unit?: "celsius" | "fahrenheit", place: place & object } where place = { city?: string }) `<|close|>json<|sep|>`
|
||||
optional `<|close|>message<|sep|>`
|
||||
"#]].assert_eq(&outline(&tag.format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user