From 4cfbd1de7aa44c906adac3a9d4b767f9f1701607 Mon Sep 17 00:00:00 2001 From: Pawel Rzepecki Date: Thu, 16 Jul 2026 16:52:54 +0200 Subject: [PATCH 1/5] toolcall empty array removing --- src/llm/BUILD | 2 + src/llm/io_processing/input_processor.cpp | 2 + ...ty_tool_calls_array_removing_processor.cpp | 38 ++++++++++ ...ty_tool_calls_array_removing_processor.hpp | 31 ++++++++ ...ol_calls_array_removing_processor_test.cpp | 74 +++++++++++++++++++ 5 files changed, 147 insertions(+) create mode 100644 src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp create mode 100644 src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp create mode 100644 src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp diff --git a/src/llm/BUILD b/src/llm/BUILD index b3525b17ad..a896b009b1 100644 --- a/src/llm/BUILD +++ b/src/llm/BUILD @@ -156,6 +156,7 @@ ovms_cc_library( "io_processing/input_processors/chat_template_adapter.hpp", "io_processing/chat_template/caps.hpp", "io_processing/input_processors/empty_content_array_normalization_processor.hpp", + "io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp", "io_processing/input_processors/raw_prompt_extractor.hpp", "io_processing/input_processors/text_content_normalization_processor.hpp", "io_processing/input_processors/tokenization_processor.hpp"], @@ -163,6 +164,7 @@ ovms_cc_library( "io_processing/input_processors/chat_template_processor.cpp", "io_processing/input_processors/chat_template_adapter.cpp", "io_processing/input_processors/empty_content_array_normalization_processor.cpp", + "io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp", "io_processing/input_processors/text_content_normalization_processor.cpp", "io_processing/input_processors/tokenization_processor.cpp"], deps = [ diff --git a/src/llm/io_processing/input_processor.cpp b/src/llm/io_processing/input_processor.cpp index 504fdb51b0..80f775cd1a 100644 --- a/src/llm/io_processing/input_processor.cpp +++ b/src/llm/io_processing/input_processor.cpp @@ -24,6 +24,7 @@ #include "../../logging.hpp" #include "input_processors/chat_template_processor.hpp" #include "input_processors/empty_content_array_normalization_processor.hpp" +#include "input_processors/empty_tool_calls_array_removing_processor.hpp" #include "input_processors/image_decoding_processor.hpp" #include "input_processors/chat_template_adapter.hpp" #include "input_processors/raw_prompt_extractor.hpp" @@ -41,6 +42,7 @@ InputProcessor::InputProcessor(InputProcessorContext& context, if (isChatPath) { // Normalize empty content arrays to null before any content-aware processor runs. processors.emplace_back(std::make_unique()); + processors.emplace_back(std::make_unique()); // Flatten text-only content arrays for both LM and VLM. Arrays that contain // images (or other modalities) are left untouched for ImageDecodingProcessor. diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp new file mode 100644 index 0000000000..a4aa0cf414 --- /dev/null +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp @@ -0,0 +1,38 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** + +#include "empty_tool_calls_array_removing_processor.hpp" + +#include + +namespace ovms { + +absl::Status EmptyToolCallsArrayRemovingProcessor::process(InputRequest& req) { + if (!std::holds_alternative(req.input)) { + return absl::Status(absl::StatusCode::kInternal, + "EmptyToolCallsArrayRemovingProcessor received input that is not a ChatHistory"); + } + ov::genai::ChatHistory& chatHistory = std::get(req.input); + for (size_t i = 0; i < chatHistory.size(); i++) { + const auto content = chatHistory[i]["tool_calls"]; + if (content.is_array() && content.size() == 0) { + chatHistory[i].erase("tool_calls"); + } + } + return absl::OkStatus(); +} + +} // namespace ovms \ No newline at end of file diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp new file mode 100644 index 0000000000..bb0317288b --- /dev/null +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp @@ -0,0 +1,31 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** +#pragma once + +#include "../base_input_processor.hpp" + +namespace ovms { + +// Replaces empty tool_calls arrays ("tool_calls": []) in ChatHistory messages with null. +// Runs for all chat paths (LM and VLM) and must execute before ImageDecodingProcessor +// and EmptyToolCallsArrayRemovingProcessor so downstream processors and chat templates +// see a null tool_calls instead of an empty array. +class EmptyToolCallsArrayRemovingProcessor : public BaseInputProcessor { +public: + absl::Status process(InputRequest& req) override; +}; + +} // namespace ovms \ No newline at end of file diff --git a/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp new file mode 100644 index 0000000000..b382d98f7b --- /dev/null +++ b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp @@ -0,0 +1,74 @@ +//***************************************************************************** +// Copyright 2026 Intel Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//***************************************************************************** +#include + +#include +#include + +#include "../../../llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp" +#include "../../../llm/io_processing/input_request.hpp" + +using namespace ovms; + +// Helpers ---------------------------------------------------------------- + +static InputRequest makeChatRequest(ov::genai::ChatHistory chatHistory) { + InputRequest req; + req.input = std::move(chatHistory); + return req; +} + +// Tests ------------------------------------------------------------------ + +TEST(EmptyToolCallsArrayRemovingProcessorTest, EmptyToolCallsArrayRemoved) { + ov::genai::ChatHistory history; + ov::AnyMap msg = {{"role", std::string("user")}}; + msg["content"] = ov::genai::JsonContainer::from_json_string("\"What is the weather in Szczecin?\""); + msg["tool_calls"] = ov::genai::JsonContainer::from_json_string("[]"); + history.push_back(msg); + + InputRequest req = makeChatRequest(history); + EmptyToolCallsArrayRemovingProcessor processor; + const auto status = processor.process(req); + + EXPECT_TRUE(status.ok()); + const auto& result = std::get(req.input); + EXPECT_TRUE(result[0]["content"].is_string()); + EXPECT_EQ(result[0]["content"].as_string().value_or(""), "What is the weather in Szczecin?"); + +} + +TEST(EmptyToolCallsArrayRemovingProcessorTest, NonEmptyArrayPreserved) { + ov::genai::ChatHistory history; + ov::AnyMap msg = {{"role", std::string("assistant")}}; + msg["tool_calls"] = ov::genai::JsonContainer::from_json_string( + R"([{"name":"get_weather","parameters":{"city":"Szczecin"}}])"); + + + history.push_back(msg); + + InputRequest req = makeChatRequest(history); + EmptyToolCallsArrayRemovingProcessor processor; + const auto status = processor.process(req); + + EXPECT_TRUE(status.ok()); + const auto& result = std::get(req.input); + ASSERT_TRUE(result[0]["tool_calls"].is_array()); + EXPECT_EQ(result[0]["tool_calls"].size(), 1u); + EXPECT_EQ(result[0]["tool_calls"][0]["name"].as_string().value_or(""), "get_weather"); + ASSERT_TRUE(result[0]["tool_calls"][0].contains("parameters")); + EXPECT_EQ(result[0]["tool_calls"][0]["parameters"]["city"].as_string().value_or(""), "Szczecin"); +} From 90cbfa8f331ddcb8a2064c9e9886e9d94df0dc47 Mon Sep 17 00:00:00 2001 From: Pawel Rzepecki Date: Fri, 17 Jul 2026 11:08:33 +0200 Subject: [PATCH 2/5] fixing non-toolcalling messages --- .../empty_tool_calls_array_removing_processor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp index a4aa0cf414..c583696ae3 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp @@ -27,6 +27,10 @@ absl::Status EmptyToolCallsArrayRemovingProcessor::process(InputRequest& req) { } ov::genai::ChatHistory& chatHistory = std::get(req.input); for (size_t i = 0; i < chatHistory.size(); i++) { + if(!chatHistory[i].contains("tool_calls")) { + continue; + } + const auto content = chatHistory[i]["tool_calls"]; if (content.is_array() && content.size() == 0) { chatHistory[i].erase("tool_calls"); From 2ec0aa432ede199aca11fa177bb9f428b0bcacdf Mon Sep 17 00:00:00 2001 From: Pawel Rzepecki Date: Mon, 20 Jul 2026 10:12:52 +0200 Subject: [PATCH 3/5] style --- .../empty_tool_calls_array_removing_processor.cpp | 2 +- .../empty_tool_calls_array_removing_processor_test.cpp | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp index c583696ae3..493ff22ec1 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp @@ -27,7 +27,7 @@ absl::Status EmptyToolCallsArrayRemovingProcessor::process(InputRequest& req) { } ov::genai::ChatHistory& chatHistory = std::get(req.input); for (size_t i = 0; i < chatHistory.size(); i++) { - if(!chatHistory[i].contains("tool_calls")) { + if (!chatHistory[i].contains("tool_calls")) { continue; } diff --git a/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp index b382d98f7b..ae2a8ddb56 100644 --- a/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp +++ b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp @@ -48,7 +48,6 @@ TEST(EmptyToolCallsArrayRemovingProcessorTest, EmptyToolCallsArrayRemoved) { const auto& result = std::get(req.input); EXPECT_TRUE(result[0]["content"].is_string()); EXPECT_EQ(result[0]["content"].as_string().value_or(""), "What is the weather in Szczecin?"); - } TEST(EmptyToolCallsArrayRemovingProcessorTest, NonEmptyArrayPreserved) { @@ -57,7 +56,6 @@ TEST(EmptyToolCallsArrayRemovingProcessorTest, NonEmptyArrayPreserved) { msg["tool_calls"] = ov::genai::JsonContainer::from_json_string( R"([{"name":"get_weather","parameters":{"city":"Szczecin"}}])"); - history.push_back(msg); InputRequest req = makeChatRequest(history); From 3d905ec5c6026da7caf5e2b82156c42aca72d72f Mon Sep 17 00:00:00 2001 From: Pawel Rzepecki Date: Mon, 20 Jul 2026 10:18:37 +0200 Subject: [PATCH 4/5] style v2 --- .../empty_tool_calls_array_removing_processor.cpp | 2 +- .../empty_tool_calls_array_removing_processor.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp index 493ff22ec1..7a4f43369d 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp @@ -39,4 +39,4 @@ absl::Status EmptyToolCallsArrayRemovingProcessor::process(InputRequest& req) { return absl::OkStatus(); } -} // namespace ovms \ No newline at end of file +} // namespace ovms diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp index bb0317288b..4a8965004d 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp @@ -28,4 +28,4 @@ class EmptyToolCallsArrayRemovingProcessor : public BaseInputProcessor { absl::Status process(InputRequest& req) override; }; -} // namespace ovms \ No newline at end of file +} // namespace ovms From e76e2fdfab75f49e4851da8b6180be885743a386 Mon Sep 17 00:00:00 2001 From: Pawel Rzepecki Date: Mon, 20 Jul 2026 14:07:55 +0200 Subject: [PATCH 5/5] copilot's review --- .../empty_tool_calls_array_removing_processor.cpp | 4 ++-- .../empty_tool_calls_array_removing_processor.hpp | 7 +++---- .../empty_tool_calls_array_removing_processor_test.cpp | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp index 7a4f43369d..d13462ca4e 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.cpp @@ -31,8 +31,8 @@ absl::Status EmptyToolCallsArrayRemovingProcessor::process(InputRequest& req) { continue; } - const auto content = chatHistory[i]["tool_calls"]; - if (content.is_array() && content.size() == 0) { + const auto toolCalls = chatHistory[i]["tool_calls"]; + if (toolCalls.is_array() && toolCalls.size() == 0) { chatHistory[i].erase("tool_calls"); } } diff --git a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp index 4a8965004d..96ca8dae52 100644 --- a/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp +++ b/src/llm/io_processing/input_processors/empty_tool_calls_array_removing_processor.hpp @@ -19,10 +19,9 @@ namespace ovms { -// Replaces empty tool_calls arrays ("tool_calls": []) in ChatHistory messages with null. -// Runs for all chat paths (LM and VLM) and must execute before ImageDecodingProcessor -// and EmptyToolCallsArrayRemovingProcessor so downstream processors and chat templates -// see a null tool_calls instead of an empty array. +// Removes empty tool_calls arrays ("tool_calls": []) from ChatHistory messages. +// Runs for all chat paths (LM and VLM) and must execute before ChatTemplateProcessor +// so downstream processors and chat templates do not render an empty tool_calls list. class EmptyToolCallsArrayRemovingProcessor : public BaseInputProcessor { public: absl::Status process(InputRequest& req) override; diff --git a/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp index ae2a8ddb56..5b06f9fd8c 100644 --- a/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp +++ b/src/test/llm/input_processing/empty_tool_calls_array_removing_processor_test.cpp @@ -46,6 +46,7 @@ TEST(EmptyToolCallsArrayRemovingProcessorTest, EmptyToolCallsArrayRemoved) { EXPECT_TRUE(status.ok()); const auto& result = std::get(req.input); + EXPECT_FALSE(result[0].contains("tool_calls")); EXPECT_TRUE(result[0]["content"].is_string()); EXPECT_EQ(result[0]["content"].as_string().value_or(""), "What is the weather in Szczecin?"); }