Skip to content

Commit e9a9bb2

Browse files
authored
Fix qwen3 coder capabilities detection (google#80)
* fix items filter * test qwen3coder capabilities * fix detection of tool calls and obj args requirement for qwen3 coder
1 parent 269260f commit e9a9bb2

File tree

6 files changed

+142
-16
lines changed

6 files changed

+142
-16
lines changed

include/minja/chat-template.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,12 @@ class chat_template {
198198
dummy_user_msg,
199199
make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj.dump())})),
200200
}), {}, false);
201-
auto tool_call_renders_str_arguments = contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':");
201+
auto tool_call_renders_str_arguments = contains(out, "<parameter=argument_needle>") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':");
202202
out = try_raw_render(json::array({
203203
dummy_user_msg,
204204
make_tool_calls_msg(json::array({make_tool_call("ipython", dummy_args_obj)})),
205205
}), {}, false);
206-
auto tool_call_renders_obj_arguments = contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':");
206+
auto tool_call_renders_obj_arguments = contains(out, "<parameter=argument_needle>") || contains(out, "\"argument_needle\":") || contains(out, "'argument_needle':");
207207

208208
caps_.supports_tool_calls = tool_call_renders_str_arguments || tool_call_renders_obj_arguments;
209209
caps_.requires_object_arguments = !tool_call_renders_str_arguments && tool_call_renders_obj_arguments;

include/minja/minja.hpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2664,15 +2664,11 @@ inline std::shared_ptr<Context> Context::builtins() {
26642664
auto items = Value::array();
26652665
if (args.contains("object")) {
26662666
auto & obj = args.at("object");
2667-
if (obj.is_string()) {
2668-
auto json_obj = json::parse(obj.get<std::string>());
2669-
for (const auto & kv : json_obj.items()) {
2670-
items.push_back(Value::array({kv.key(), kv.value()}));
2671-
}
2672-
} else if (!obj.is_null()) {
2673-
for (auto & key : obj.keys()) {
2674-
items.push_back(Value::array({key, obj.at(key)}));
2675-
}
2667+
if (!obj.is_object()) {
2668+
throw std::runtime_error("Can only get item pairs from a mapping");
2669+
}
2670+
for (auto & key : obj.keys()) {
2671+
items.push_back(Value::array({key, obj.at(key)}));
26762672
}
26772673
}
26782674
return items;

scripts/fetch_templates_and_goldens.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,12 @@ def make_tool_call(tool_name, arguments):
192192
dummy_user_msg,
193193
make_tool_calls_msg([make_tool_call("ipython", json.dumps(dummy_args_obj))]),
194194
])
195-
tool_call_renders_str_arguments = '"argument_needle":' in out or "'argument_needle':" in out
195+
tool_call_renders_str_arguments = "<parameter=argument_needle>" in out or '"argument_needle":' in out or "'argument_needle':" in out
196196
out = self.try_raw_render([
197197
dummy_user_msg,
198198
make_tool_calls_msg([make_tool_call("ipython", dummy_args_obj)]),
199199
])
200-
tool_call_renders_obj_arguments = '"argument_needle":' in out or "'argument_needle':" in out
200+
tool_call_renders_obj_arguments = "<parameter=argument_needle>" in out or '"argument_needle":' in out or "'argument_needle':" in out
201201

202202
caps.supports_tool_calls = tool_call_renders_str_arguments or tool_call_renders_obj_arguments
203203
caps.requires_object_arguments = not tool_call_renders_str_arguments and tool_call_renders_obj_arguments

tests/test-capabilities.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ TEST(CapabilitiesTest, QwQ32B) {
8787
EXPECT_FALSE(caps.requires_typed_content);
8888
}
8989

90+
TEST(CapabilitiesTest, Qwen3Coder) {
91+
auto caps = get_caps("tests/Qwen-Qwen3-Coder-30B-A3B-Instruct.jinja");
92+
EXPECT_TRUE(caps.supports_system_role);
93+
EXPECT_TRUE(caps.supports_tools);
94+
EXPECT_TRUE(caps.supports_tool_calls);
95+
EXPECT_TRUE(caps.supports_tool_responses);
96+
EXPECT_TRUE(caps.supports_parallel_tool_calls);
97+
EXPECT_TRUE(caps.requires_object_arguments);
98+
// EXPECT_TRUE(caps.requires_non_null_content);
99+
EXPECT_FALSE(caps.requires_typed_content);
100+
}
101+
90102
#ifndef _WIN32
91103
TEST(CapabilitiesTest, DeepSeekR1Distill)
92104
{

tests/test-syntax.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,9 +430,6 @@ TEST(SyntaxTest, SimpleCases) {
430430
{{- foo() }} {{ foo() -}})", {}, {}));
431431

432432
if (!getenv("USE_JINJA2")) {
433-
EXPECT_EQ(
434-
"[]",
435-
render(R"({{ None | items | list | tojson }})", {}, {}));
436433
EXPECT_EQ(
437434
"Foo",
438435
render(R"({% generation %}Foo{% endgeneration %})", {}, {}));
@@ -561,6 +558,10 @@ TEST(SyntaxTest, SimpleCases) {
561558
if (!getenv("USE_JINJA2")) {
562559
// TODO: capture stderr from jinja2 and test these.
563560

561+
EXPECT_THAT([]() { render("{{ '' | items }}", {}, {}); }, ThrowsWithSubstr("Can only get item pairs from a mapping"));
562+
EXPECT_THAT([]() { render("{{ [] | items }}", {}, {}); }, ThrowsWithSubstr("Can only get item pairs from a mapping"));
563+
EXPECT_THAT([]() { render("{{ None | items }}", {}, {}); }, ThrowsWithSubstr("Can only get item pairs from a mapping"));
564+
564565
EXPECT_THAT([]() { render("{% break %}", {}, {}); }, ThrowsWithSubstr("break outside of a loop"));
565566
EXPECT_THAT([]() { render("{% continue %}", {}, {}); }, ThrowsWithSubstr("continue outside of a loop"));
566567

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
{% macro render_extra_keys(json_dict, handled_keys) %}
2+
{%- if json_dict is mapping %}
3+
{%- for json_key in json_dict if json_key not in handled_keys %}
4+
{%- if json_dict[json_key] is mapping %}
5+
{{- '\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | tojson | safe) ~ '</' ~ json_key ~ '>' }}
6+
{%- else %}
7+
{{-'\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | string) ~ '</' ~ json_key ~ '>' }}
8+
{%- endif %}
9+
{%- endfor %}
10+
{%- endif %}
11+
{% endmacro %}
12+
13+
{%- if messages[0]["role"] == "system" %}
14+
{%- set system_message = messages[0]["content"] %}
15+
{%- set loop_messages = messages[1:] %}
16+
{%- else %}
17+
{%- set loop_messages = messages %}
18+
{%- endif %}
19+
20+
{%- if not tools is defined %}
21+
{%- set tools = [] %}
22+
{%- endif %}
23+
24+
{%- if system_message is defined %}
25+
{{- "<|im_start|>system\n" + system_message }}
26+
{%- else %}
27+
{%- if tools is iterable and tools | length > 0 %}
28+
{{- "<|im_start|>system\nYou are Qwen, a helpful AI assistant that can interact with a computer to solve tasks." }}
29+
{%- endif %}
30+
{%- endif %}
31+
{%- if tools is iterable and tools | length > 0 %}
32+
{{- "\n\nYou have access to the following functions:\n\n" }}
33+
{{- "<tools>" }}
34+
{%- for tool in tools %}
35+
{%- if tool.function is defined %}
36+
{%- set tool = tool.function %}
37+
{%- endif %}
38+
{{- "\n<function>\n<name>" ~ tool.name ~ "</name>" }}
39+
{%- if tool.description is defined %}
40+
{{- '\n<description>' ~ (tool.description | trim) ~ '</description>' }}
41+
{%- endif %}
42+
{{- '\n<parameters>' }}
43+
{%- if tool.parameters is defined and tool.parameters is mapping and tool.parameters.properties is defined and tool.parameters.properties is mapping %}
44+
{%- for param_name, param_fields in tool.parameters.properties|items %}
45+
{{- '\n<parameter>' }}
46+
{{- '\n<name>' ~ param_name ~ '</name>' }}
47+
{%- if param_fields.type is defined %}
48+
{{- '\n<type>' ~ (param_fields.type | string) ~ '</type>' }}
49+
{%- endif %}
50+
{%- if param_fields.description is defined %}
51+
{{- '\n<description>' ~ (param_fields.description | trim) ~ '</description>' }}
52+
{%- endif %}
53+
{%- set handled_keys = ['name', 'type', 'description'] %}
54+
{{- render_extra_keys(param_fields, handled_keys) }}
55+
{{- '\n</parameter>' }}
56+
{%- endfor %}
57+
{%- endif %}
58+
{% set handled_keys = ['type', 'properties'] %}
59+
{{- render_extra_keys(tool.parameters, handled_keys) }}
60+
{{- '\n</parameters>' }}
61+
{%- set handled_keys = ['type', 'name', 'description', 'parameters'] %}
62+
{{- render_extra_keys(tool, handled_keys) }}
63+
{{- '\n</function>' }}
64+
{%- endfor %}
65+
{{- "\n</tools>" }}
66+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
67+
{%- endif %}
68+
{%- if system_message is defined %}
69+
{{- '<|im_end|>\n' }}
70+
{%- else %}
71+
{%- if tools is iterable and tools | length > 0 %}
72+
{{- '<|im_end|>\n' }}
73+
{%- endif %}
74+
{%- endif %}
75+
{%- for message in loop_messages %}
76+
{%- if message.role == "assistant" and message.tool_calls is defined and message.tool_calls is iterable and message.tool_calls | length > 0 %}
77+
{{- '<|im_start|>' + message.role }}
78+
{%- if message.content is defined and message.content is string and message.content | trim | length > 0 %}
79+
{{- '\n' + message.content | trim + '\n' }}
80+
{%- endif %}
81+
{%- for tool_call in message.tool_calls %}
82+
{%- if tool_call.function is defined %}
83+
{%- set tool_call = tool_call.function %}
84+
{%- endif %}
85+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
86+
{%- if tool_call.arguments is defined %}
87+
{%- for args_name, args_value in tool_call.arguments|items %}
88+
{{- '<parameter=' + args_name + '>\n' }}
89+
{%- set args_value = args_value | tojson | safe if args_value is mapping else args_value | string %}
90+
{{- args_value }}
91+
{{- '\n</parameter>\n' }}
92+
{%- endfor %}
93+
{%- endif %}
94+
{{- '</function>\n</tool_call>' }}
95+
{%- endfor %}
96+
{{- '<|im_end|>\n' }}
97+
{%- elif message.role == "user" or message.role == "system" or message.role == "assistant" %}
98+
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
99+
{%- elif message.role == "tool" %}
100+
{%- if loop.previtem and loop.previtem.role != "tool" %}
101+
{{- '<|im_start|>user\n' }}
102+
{%- endif %}
103+
{{- '<tool_response>\n' }}
104+
{{- message.content }}
105+
{{- '\n</tool_response>\n' }}
106+
{%- if not loop.last and loop.nextitem.role != "tool" %}
107+
{{- '<|im_end|>\n' }}
108+
{%- elif loop.last %}
109+
{{- '<|im_end|>\n' }}
110+
{%- endif %}
111+
{%- else %}
112+
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' }}
113+
{%- endif %}
114+
{%- endfor %}
115+
{%- if add_generation_prompt %}
116+
{{- '<|im_start|>assistant\n' }}
117+
{%- endif %}

0 commit comments

Comments
 (0)