@@ -38,27 +38,28 @@ using ovms::ToolsParameterTypeMap_t;
38
38
static std::unique_ptr<ov::genai::Tokenizer> qwen3Tokenizer;
39
39
40
40
static std::map<std::string, std::string> toolSchemasInput = {
41
- {" string_tool" , R"( {"properties": {"arg1": {"type": "string", "description": "A string argument."}}, "required": ["arg1"]})" }};
42
- static rapidjson::Document testSchemasDoc;
41
+ {" string_tool" , R"( {"properties": {"arg1": {"type": "string", "description": "A string argument."}}, "required": ["arg1"]})" },
42
+ {" string_int_tool" , R"( {"properties":{"arg1":{"type":"string","description":"A string argument."},"arg2":{"type":"integer","description":"An integer argument."}},"required":["arg1", "arg2"]})" },
43
+ {" some_tool" , R"( {"properties":{"source":{"type":"string","description":"The name of the file or directory to copy."},"destination":{"type":"string","description":"The destination name to copy the file or directory to. If the destination is a directory, the source will be copied into this directory. No file paths allowed. "}},"required":[]})" }};
44
+
45
+ static std::vector<std::unique_ptr<rapidjson::Document>> schemaDocsStorage;
43
46
44
47
static ToolsSchemas_t convertStringToolSchemasStringToToolsSchemas (
45
- const std::map<std::string, std::string>& input,
46
- rapidjson::Document& doc) {
48
+ const std::map<std::string, std::string>& input) {
47
49
ToolsSchemas_t result;
48
- auto & allocator = doc. GetAllocator ();
50
+ schemaDocsStorage. clear ();
49
51
for (const auto & [name, schemaStr] : input) {
50
- rapidjson::Document schemaDoc ;
51
- if (schemaDoc. Parse (schemaStr.c_str ()).HasParseError ()) {
52
+ auto schemaDoc = std::make_unique< rapidjson::Document>() ;
53
+ if (schemaDoc-> Parse (schemaStr.c_str ()).HasParseError ()) {
52
54
throw std::runtime_error (" Failed to parse schema for tool: " + name);
53
55
}
54
- rapidjson::Value schemaCopy (schemaDoc, allocator);
55
- doc.CopyFrom (schemaCopy, allocator);
56
- result[name] = {&doc, schemaStr};
56
+ result[name] = {schemaDoc.get (), schemaStr};
57
+ schemaDocsStorage.push_back (std::move (schemaDoc));
57
58
}
58
-
59
59
return result;
60
60
}
61
- static ovms::ToolsSchemas_t toolsSchemas = convertStringToolSchemasStringToToolsSchemas(toolSchemasInput, testSchemasDoc);
61
+
62
+ static ovms::ToolsSchemas_t toolsSchemas = convertStringToolSchemasStringToToolsSchemas(toolSchemasInput);
62
63
static ToolsParameterTypeMap_t toolsParametersTypeMap = {
63
64
{" string_tool" , {{" arg1" , ParameterType::STRING}}},
64
65
{" string_string_tool" , {{" arg1" , ParameterType::STRING}, {" arg2" , ParameterType::STRING}}},
@@ -88,26 +89,25 @@ class Qwen3CoderOutputParserTest : public ::testing::Test {
88
89
}
89
90
90
91
void SetUp () override {
91
- // For Qwen3 model we use hermes3 tool parser (due to the same format of generated tool calls) and qwen3 reasoning parser
92
92
outputParser = std::make_unique<OutputParser>(*qwen3Tokenizer, " qwen3coder" , " " , toolsSchemas);
93
93
}
94
- std::tuple<ov::Tensor, std::vector<int64_t >, ParsedOutput> doTheWork (const std::string& input) {
94
+ std::tuple<ov::Tensor, std::vector<int64_t >, ParsedOutput> generateParsedOutput (const std::string& input) {
95
95
auto generatedTensor = qwen3Tokenizer->encode (input, ov::genai::add_special_tokens (false )).input_ids ;
96
96
std::vector<int64_t > generatedTokens (generatedTensor.data <int64_t >(), generatedTensor.data <int64_t >() + generatedTensor.get_size ());
97
97
ParsedOutput parsedOutput = outputParser->parse (generatedTokens, true );
98
98
return {generatedTensor, generatedTokens, parsedOutput};
99
99
}
100
100
};
101
101
TEST_F (Qwen3CoderOutputParserTest, Parse1ToolCall1Function1ArgumentTagsNewline) {
102
- std::string input = R"( io_processing/hermes3/generation_config_builder.cpp
102
+ std::string input = R"(
103
103
"<tool_call>
104
104
<function=string_tool>
105
105
<parameter=arg1>
106
106
value1
107
107
</parameter>
108
108
</function>
109
109
</tool_call>")" ;
110
- auto [generatedTensor, generatedTokens, parsedOutput] = doTheWork (input);
110
+ auto [generatedTensor, generatedTokens, parsedOutput] = generateParsedOutput (input);
111
111
112
112
ASSERT_EQ (parsedOutput.toolCalls .size (), 1 );
113
113
EXPECT_EQ (parsedOutput.toolCalls [0 ].name , " string_tool" );
@@ -124,18 +124,31 @@ TEST_F(Qwen3CoderOutputParserTest, Parse1ToolCallNestedXmlNotFromSchema) {
124
124
</parameter>
125
125
</function>
126
126
</tool_call>")" ;
127
- auto [generatedTensor, generatedTokens, parsedOutput] = doTheWork (input);
127
+ auto [generatedTensor, generatedTokens, parsedOutput] = generateParsedOutput (input);
128
128
129
129
ASSERT_EQ (parsedOutput.toolCalls .size (), 1 );
130
130
EXPECT_EQ (parsedOutput.toolCalls [0 ].name , " string_tool" );
131
131
EXPECT_EQ (parsedOutput.toolCalls [0 ].arguments , " {\" arg1\" : \" <value=abc>value1</value>\" }" );
132
132
EXPECT_EQ (parsedOutput.toolCalls [0 ].id .empty (), false );
133
133
}
134
- // FIXME check if two tool calls is a vali for outputparser as well not only for parser imple
134
+ TEST_F (Qwen3CoderOutputParserTest, ParseTwoToolCalls1Function1ArgumentTagsNoNewline) {
135
+ std::string input = R"(
136
+ "<tool_call><function=string_tool><parameter=arg1>value1</parameter></function></tool_call>"
137
+ "<tool_call><function=string_tool><parameter=arg1>value2</parameter></function></tool_call>")" ;
138
+ auto [generatedTensor, generatedTokens, parsedOutput] = generateParsedOutput (input);
139
+
140
+ ASSERT_EQ (parsedOutput.toolCalls .size (), 2 );
141
+ EXPECT_EQ (parsedOutput.toolCalls [0 ].name , " string_tool" );
142
+ EXPECT_EQ (parsedOutput.toolCalls [0 ].arguments , " {\" arg1\" : \" value1\" }" );
143
+ EXPECT_EQ (parsedOutput.toolCalls [0 ].id .empty (), false );
144
+ EXPECT_EQ (parsedOutput.toolCalls [1 ].name , " string_tool" );
145
+ EXPECT_EQ (parsedOutput.toolCalls [1 ].arguments , " {\" arg1\" : \" value2\" }" );
146
+ EXPECT_EQ (parsedOutput.toolCalls [1 ].id .empty (), false );
147
+ }
135
148
TEST_F (Qwen3CoderOutputParserTest, Parse1ToolCall1Function1ArgumentTagsNoNewline) {
136
149
std::string input = R"(
137
150
"<tool_call><function=string_tool><parameter=arg1>value1</parameter></function></tool_call>")" ;
138
- auto [generatedTensor, generatedTokens, parsedOutput] = doTheWork (input);
151
+ auto [generatedTensor, generatedTokens, parsedOutput] = generateParsedOutput (input);
139
152
140
153
ASSERT_EQ (parsedOutput.toolCalls .size (), 1 );
141
154
EXPECT_EQ (parsedOutput.toolCalls [0 ].name , " string_tool" );
@@ -152,7 +165,7 @@ value1line2
152
165
</parameter>
153
166
</function>
154
167
</tool_call>")" ;
155
- auto [generatedTensor, generatedTokens, parsedOutput] = doTheWork (input);
168
+ auto [generatedTensor, generatedTokens, parsedOutput] = generateParsedOutput (input);
156
169
157
170
ASSERT_EQ (parsedOutput.toolCalls .size (), 1 );
158
171
EXPECT_EQ (parsedOutput.toolCalls [0 ].name , " string_tool" );
@@ -526,20 +539,13 @@ INSTANTIATE_TEST_SUITE_P(
526
539
std::string name = std::get<0 >(info.param ) + " _" + std::get<2 >(info.param );
527
540
// Replace non-alphanumeric characters with underscore
528
541
std::replace_if (name.begin (), name.end (), [](char c) { return !std::isalnum (c); }, ' _' );
529
- // Limit length to 30 characters
530
- if (name.length () > 30 ) {
531
- name = name.substr (0 , 30 );
532
- }
533
542
return name;
534
543
});
535
544
536
545
TEST_F (Qwen3CoderOutputParserTest, StreamingSimpleToolCall) {
537
546
// since unary reuses streaming we don't need to test for partial tool calls
538
547
// if we don't get closing tag we don't emit tool call
539
548
int i = -1 ;
540
- // FIXME add content in between tool_calls and test what happens
541
- // Add another tool call to test for special tags handling
542
- // add content after second tool call
543
549
std::vector<std::tuple<std::string, ov::genai::GenerationFinishReason, std::optional<std::string>>> chunkToDeltaVec{
544
550
{" <too" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
545
551
{" l_cal" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
@@ -564,14 +570,20 @@ TEST_F(Qwen3CoderOutputParserTest, StreamingSimpleToolCall) {
564
570
{" POTENTIALLY EXISINT CONTENT" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
565
571
{" <tool" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
566
572
{" <tool_call>\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
567
- {" <function=string_tool " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
568
- {" >\n " , ov::genai::GenerationFinishReason::NONE, R"( {"delta":{"tool_calls":[{"id":"XXXXXXXXX","type":"function","index":1,"function":{"name":"string_tool "}}]}})" },
573
+ {" <function=string_int_tool " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
574
+ {" >\n " , ov::genai::GenerationFinishReason::NONE, R"( {"delta":{"tool_calls":[{"id":"XXXXXXXXX","type":"function","index":1,"function":{"name":"string_int_tool "}}]}})" },
569
575
{" <parameter=arg1>\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
570
576
{" \n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
571
577
{" ANOTHER_STRING_VALUE\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
572
578
{" </parameter>\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
579
+ {" <parameter=arg2>" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
580
+ {" \n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
581
+ {" 314" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
582
+ {" 1522\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
583
+ {" </parameter>\n " , ov::genai::GenerationFinishReason::NONE, std::nullopt },
573
584
{" </function>" , ov::genai::GenerationFinishReason::NONE, std::nullopt },
574
- {" </tool_call>" , ov::genai::GenerationFinishReason::NONE, R"( {"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"arg1\": \"\nANOTHER_STRING_VALUE\"}"}}]}})" }};
585
+ {" </tool_call>" , ov::genai::GenerationFinishReason::NONE, R"( {"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"arg1\": \"\nANOTHER_STRING_VALUE\", \"arg2\": 3141522}"}}]}})" },
586
+ {" CONTENT_AFTER_TOOL_CALL" , ov::genai::GenerationFinishReason::NONE, std::nullopt }};
575
587
for (const auto & [chunk, finishReason, expectedDelta] : chunkToDeltaVec) {
576
588
i++;
577
589
std::optional<rapidjson::Document> doc = outputParser->parseChunk (chunk, true , ov::genai::GenerationFinishReason::NONE);
0 commit comments