diff --git a/.gitignore b/.gitignore index 7f17193c13..5ec33f756f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out *.errors.txt tmp/ *.zip +*.tar.gz \ No newline at end of file diff --git a/src/BUILD b/src/BUILD index 80088630ff..bbfb0f282f 100644 --- a/src/BUILD +++ b/src/BUILD @@ -615,8 +615,14 @@ cc_library( "//src/embeddings:embeddingscalculator", "//src/rerank:rerankcalculator",], }) + select({ - "//:enable_drogon": ["libdrogon_http_server"], - "//conditions:default" : ["libnet_http_server"], + "//:enable_drogon": [ + "libdrogon_http_server", + "libovmsmulti_part_parser_drogon_impl", + ], + "//conditions:default" : [ + "libnet_http_server", + # TODO + ], }) + [ "cpp_headers", "ovms_header", @@ -810,6 +816,7 @@ cc_library( deps = [ "@com_github_tencent_rapidjson//:rapidjson", "libovmsclient_connection", + "libovmsmulti_part_parser", ], visibility = ["//visibility:public"], copts = COPTS_ADJUSTED, @@ -828,6 +835,7 @@ cc_library( linkopts = LINKOPTS_ADJUSTED, alwayslink = 1, ) + cc_library( name = "libhttpclientconnection", hdrs = ["http_frontend/http_client_connection.hpp"], @@ -842,6 +850,33 @@ cc_library( alwayslink = 1, ) +cc_library( + name = "libovmsmulti_part_parser", + hdrs = ["multi_part_parser.hpp"], + deps = [ + ], + visibility = ["//visibility:public",], + local_defines = COMMON_LOCAL_DEFINES, + copts = COPTS_ADJUSTED, + linkopts = LINKOPTS_ADJUSTED, + alwayslink = 1, +) + +cc_library( + name = "libovmsmulti_part_parser_drogon_impl", + hdrs = ["http_frontend/multi_part_parser_drogon_impl.hpp"], + srcs = ["http_frontend/multi_part_parser_drogon_impl.cpp"], + deps = [ + "libovmsmulti_part_parser", + "@drogon//:drogon_cmake", + ], + visibility = ["//visibility:public",], + local_defines = COMMON_LOCAL_DEFINES, + copts = COPTS_ADJUSTED, + linkopts = LINKOPTS_ADJUSTED, + alwayslink = 1, +) + cc_library( name = "libovms_module", hdrs = ["module.hpp"], @@ -2756,6 +2791,7 @@ cc_test( "test/get_mediapipe_graph_metadata_response_test.cpp", "test/mediapipe_framework_test.cpp", "test/http_openai_handler_test.cpp", + "test/multipart_calculator_test.cpp", ], "//:disable_mediapipe" : [ "test/disabled_mediapipe_test.cpp", @@ -2823,6 +2859,7 @@ cc_test( "test/mediapipe/config_mediapipe_graph_with_side_packets.json", "test/mediapipe/config_mediapipe_two_inputs.json", "test/mediapipe/config_mediapipe_two_outputs_dag.json", + "test/mediapipe/config_mediapipe_multipart_mock.json", "test/mediapipe/config_mp_tf_passthrough.json", "test/mediapipe/config_standard_add.json", "test/mediapipe/config_standard_dummy.json", @@ -2841,6 +2878,7 @@ cc_test( "test/mediapipe/graphdummyadapterfull_dummyinputnames.pbtxt", "test/mediapipe/graphadapterfull_two_outputs_dag.pbtxt", "test/mediapipe/graphdummyadapterfull_two_outputs.pbtxt", + "test/mediapipe/graph_multipart.pbtxt", "test/mediapipe/negative/config_exception_during_process.json", "test/mediapipe/negative/config_no_calc_output_stream.json", "test/mediapipe/negative/graph_exception_during_process.pbtxt", diff --git a/src/http_frontend/multi_part_parser_drogon_impl.cpp b/src/http_frontend/multi_part_parser_drogon_impl.cpp new file mode 100644 index 0000000000..cab6c2177a --- /dev/null +++ b/src/http_frontend/multi_part_parser_drogon_impl.cpp @@ -0,0 +1,42 @@ +//***************************************************************************** +// Copyright 2025 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 "multi_part_parser_drogon_impl.hpp" + +namespace ovms { + +// TODO (dkalinow): Test actual drogon parser with mocked request +bool DrogonMultiPartParser::parse() { + this->hasParseError_ = this->parser->parse(request) != 0; + return !this->hasParseError_; +} + +bool DrogonMultiPartParser::hasParseError() const { + return this->hasParseError_; +} + +std::string DrogonMultiPartParser::getFieldByName(const std::string& name) const { + return this->parser->getParameter(name); +} + +std::string_view DrogonMultiPartParser::getFileContentByName(const std::string& name) const { + auto it = this->parser->getFilesMap().find(name); + if (it == this->parser->getFilesMap().end()) { + return ""; + } + return it->second.fileContent(); +} + +} // namespace ovms diff --git a/src/http_frontend/multi_part_parser_drogon_impl.hpp b/src/http_frontend/multi_part_parser_drogon_impl.hpp new file mode 100644 index 0000000000..33c5e26af0 --- /dev/null +++ b/src/http_frontend/multi_part_parser_drogon_impl.hpp @@ -0,0 +1,49 @@ +//***************************************************************************** +// Copyright 2024 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 "../multi_part_parser.hpp" + +#pragma warning(push) +#pragma warning(disable : 6326) +#include +#pragma warning(pop) + +#include +#include +#include + +namespace ovms { + +class DrogonMultiPartParser : public MultiPartParser { + bool hasParseError_{true}; + const drogon::HttpRequestPtr request{nullptr}; + const std::shared_ptr parser{nullptr}; + +public: + DrogonMultiPartParser(const drogon::HttpRequestPtr& request) : + request(request), + parser(std::make_shared()) {} + + bool parse() override; + + bool hasParseError() const override; + + std::string getFieldByName(const std::string& name) const override; + std::string_view getFileContentByName(const std::string& name) const override; +}; + +} // namespace ovms diff --git a/src/http_payload.hpp b/src/http_payload.hpp index 8a6b69426f..b4415a1616 100644 --- a/src/http_payload.hpp +++ b/src/http_payload.hpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #pragma warning(push) @@ -25,15 +26,17 @@ #pragma warning(pop) #include "client_connection.hpp" +#include "multi_part_parser.hpp" namespace ovms { struct HttpPayload { std::string uri; - std::vector> headers; + std::unordered_map headers; std::string body; // always std::shared_ptr parsedJson; // pre-parsed body = null std::shared_ptr client; + std::shared_ptr multipartParser; }; } // namespace ovms diff --git a/src/http_rest_api_handler.cpp b/src/http_rest_api_handler.cpp index 1e7bcfa06a..0f587488ff 100644 --- a/src/http_rest_api_handler.cpp +++ b/src/http_rest_api_handler.cpp @@ -163,7 +163,7 @@ void HttpRestApiHandler::registerHandler(RequestType type, HandlerCallbackFn f) } void HttpRestApiHandler::registerAll() { - registerHandler(Predict, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(Predict, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { if (request_components.processing_method == "predict") { return processPredictRequest(request_components.model_name, request_components.model_version, request_components.model_version_label, request_body, &response); @@ -173,44 +173,44 @@ void HttpRestApiHandler::registerAll() { } }); - registerHandler(GetModelMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) { + registerHandler(GetModelMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) { return processModelMetadataRequest(request_components.model_name, request_components.model_version, request_components.model_version_label, &response); }); - registerHandler(GetModelStatus, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) { + registerHandler(GetModelStatus, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) { return processModelStatusRequest(request_components.model_name, request_components.model_version, request_components.model_version_label, &response); }); - registerHandler(ConfigReload, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(ConfigReload, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processConfigReloadRequest(response, this->modelManager); }); - registerHandler(ConfigStatus, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(ConfigStatus, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processConfigStatusRequest(response, this->modelManager); }); - registerHandler(KFS_GetModelReady, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_GetModelReady, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processModelReadyKFSRequest(request_components, response, request_body); }); - registerHandler(KFS_GetModelMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_GetModelMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processModelMetadataKFSRequest(request_components, response, request_body); }); - registerHandler(KFS_Infer, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_Infer, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processInferKFSRequest(request_components, response, request_body, response_components.inferenceHeaderContentLength); }); - registerHandler(KFS_GetServerReady, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_GetServerReady, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processServerReadyKFSRequest(request_components, response, request_body); }); - registerHandler(KFS_GetServerLive, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_GetServerLive, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processServerLiveKFSRequest(request_components, response, request_body); }); - registerHandler(KFS_GetServerMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(KFS_GetServerMetadata, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processServerMetadataKFSRequest(request_components, response, request_body); }); - registerHandler(V3, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(V3, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { OVMS_PROFILE_FUNCTION(); - return processV3(uri, request_components, response, request_body, std::move(serverReaderWriter)); + return processV3(uri, request_components, response, request_body, std::move(serverReaderWriter), std::move(multiPartParser)); }); - registerHandler(Metrics, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter) -> Status { + registerHandler(Metrics, [this](const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, HttpResponseComponents& response_components, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) -> Status { return processMetrics(request_components, response, request_body); }); } @@ -441,18 +441,19 @@ Status HttpRestApiHandler::dispatchToProcessor( std::string* response, const HttpRequestComponents& request_components, HttpResponseComponents& response_components, - std::shared_ptr serverReaderWriter) { + std::shared_ptr serverReaderWriter, + std::shared_ptr multiPartParser) { auto handler = handlers.find(request_components.type); if (handler != handlers.end()) { - return handler->second(uri, request_components, *response, request_body, response_components, std::move(serverReaderWriter)); + return handler->second(uri, request_components, *response, request_body, response_components, std::move(serverReaderWriter), std::move(multiPartParser)); } else { return StatusCode::UNKNOWN_REQUEST_COMPONENTS_TYPE; } return StatusCode::UNKNOWN_REQUEST_COMPONENTS_TYPE; } -Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr serverReaderWriter) { +Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser) { #if (MEDIAPIPE_DISABLE == 0) OVMS_PROFILE_FUNCTION(); HttpPayload request; @@ -460,51 +461,87 @@ Status HttpRestApiHandler::processV3(const std::string_view uri, const HttpReque std::shared_ptr executor; bool streamFieldVal = false; { - OVMS_PROFILE_SCOPE("rapidjson parse body"); - doc->Parse(request_body.c_str()); - } - { - OVMS_PROFILE_SCOPE("rapidjson validate"); - if (doc->HasParseError()) { - return Status(StatusCode::JSON_INVALID, "Cannot parse JSON body"); - } - - if (!doc->IsObject()) { - return Status(StatusCode::JSON_INVALID, "JSON body must be an object"); - } + auto it = request_components.headers.find("content-type"); + bool isApplicationJson = it != request_components.headers.end() && it->second.find("application/json") != std::string::npos; + bool isMultiPart = it != request_components.headers.end() && it->second.find("multipart/form-data") != std::string::npos; + bool isDefault = !isApplicationJson && !isMultiPart; + + std::string model_name; + + // TODO (dkalinow): Test new routing + if (isMultiPart) { + OVMS_PROFILE_SCOPE("multipart parse"); + if (!multiPartParser->parse()) { + return StatusCode::REST_INVALID_URL; + } + model_name = multiPartParser->getFieldByName("model"); + if (model_name.empty()) { + isDefault = true; + } else { + SPDLOG_DEBUG("Model name from deduced from MultiPart field: {}", model_name); + } + } else if (isApplicationJson) { + { + OVMS_PROFILE_SCOPE("rapidjson parse"); + doc->Parse(request_body.c_str()); + } + OVMS_PROFILE_SCOPE("rapidjson validate"); + if (doc->HasParseError()) { + return Status(StatusCode::JSON_INVALID, "Cannot parse JSON body"); + } - auto modelNameIt = doc->FindMember("model"); - if (modelNameIt == doc->MemberEnd()) { - return Status(StatusCode::JSON_INVALID, "model field is missing in JSON body"); - } + if (!doc->IsObject()) { + return Status(StatusCode::JSON_INVALID, "JSON body must be an object"); + } - if (!modelNameIt->value.IsString()) { - return Status(StatusCode::JSON_INVALID, "model field is not a string"); - } + auto modelNameIt = doc->FindMember("model"); + if (modelNameIt == doc->MemberEnd()) { + return Status(StatusCode::JSON_INVALID, "model field is missing in JSON body"); + } - const std::string model_name = modelNameIt->value.GetString(); + if (!modelNameIt->value.IsString()) { + return Status(StatusCode::JSON_INVALID, "model field is not a string"); + } - bool isTextGenerationEndpoint = uri.find("completions") != std::string_view::npos; - if (isTextGenerationEndpoint) { - auto streamIt = doc->FindMember("stream"); - if (streamIt != doc->MemberEnd()) { - if (!streamIt->value.IsBool()) { - return Status(StatusCode::JSON_INVALID, "stream field is not a boolean"); + bool isTextGenerationEndpoint = uri.find("completions") != std::string_view::npos; + if (isTextGenerationEndpoint) { + auto streamIt = doc->FindMember("stream"); + if (streamIt != doc->MemberEnd()) { + if (!streamIt->value.IsBool()) { + return Status(StatusCode::JSON_INVALID, "stream field is not a boolean"); + } + streamFieldVal = streamIt->value.GetBool(); } - streamFieldVal = streamIt->value.GetBool(); + } + + model_name = modelNameIt->value.GetString(); + if (model_name.empty()) { + isDefault = true; + } else { + SPDLOG_DEBUG("Model name from deduced from JSON: {}", model_name); } } + // Deduce Graph Name from URI since there is no info in JSON or MultiPart + if (isDefault) { + if (uri.size() <= 4) { // nothing after "/v3/..." + return StatusCode::REST_INVALID_URL; + } + model_name = std::string(uri.substr(4)); + SPDLOG_DEBUG("Model name from deduced from URI: {}", model_name); + } + auto status = this->modelManager.createPipeline(executor, model_name); if (!status.ok()) { return status; } - // TODO: Possibly avoid making copy + request.headers = request_components.headers; request.body = request_body; request.parsedJson = std::move(doc); request.uri = std::string(uri); request.client = std::make_shared(serverReaderWriter); + request.multipartParser = std::move(multiPartParser); } if (streamFieldVal == false) { ExecutionContext executionContext{ExecutionContext::Interface::REST, ExecutionContext::Method::V3Unary}; @@ -661,7 +698,7 @@ Status HttpRestApiHandler::processModelMetadataKFSRequest(const HttpRequestCompo } static Status parseInferenceHeaderContentLength(HttpRequestComponents& requestComponents, - const std::vector>& headers) { + const std::unordered_map& headers) { for (auto& header : headers) { if (toLower(header.first) == "inference-header-content-length") { // drogon automatically converts all headers to lowercase, net_http does not requestComponents.inferenceHeaderContentLength = stoi32(header.second); @@ -676,7 +713,7 @@ static Status parseInferenceHeaderContentLength(HttpRequestComponents& requestCo Status HttpRestApiHandler::parseRequestComponents(HttpRequestComponents& requestComponents, const std::string_view http_method, const std::string& request_path, - const std::vector>& headers) { + const std::unordered_map& headers) { std::smatch sm; requestComponents.http_method = http_method; if (http_method != "POST" && http_method != "GET") { @@ -821,10 +858,12 @@ Status HttpRestApiHandler::processRequest( const std::string_view http_method, const std::string_view request_path, const std::string& request_body, - std::vector>* headers, + // std::vector>* headers, + std::unordered_map* headers, std::string* response, HttpResponseComponents& responseComponents, - std::shared_ptr serverReaderWriter) { + std::shared_ptr serverReaderWriter, + std::shared_ptr multiPartParser) { std::smatch sm; std::string request_path_str(request_path); @@ -838,11 +877,11 @@ Status HttpRestApiHandler::processRequest( headers->clear(); response->clear(); - headers->push_back({"Content-Type", "application/json"}); + (*headers)["content-type"] = "application/json"; if (!status.ok()) return status; - return dispatchToProcessor(request_path, request_body, response, requestComponents, responseComponents, std::move(serverReaderWriter)); + return dispatchToProcessor(request_path, request_body, response, requestComponents, responseComponents, std::move(serverReaderWriter), std::move(multiPartParser)); } Status HttpRestApiHandler::processPredictRequest( diff --git a/src/http_rest_api_handler.hpp b/src/http_rest_api_handler.hpp index c235e6a1f4..8535e1ee86 100644 --- a/src/http_rest_api_handler.hpp +++ b/src/http_rest_api_handler.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -33,6 +34,7 @@ #pragma warning(pop) #include "http_async_writer_interface.hpp" +#include "multi_part_parser.hpp" #include "rest_parser.hpp" #include "status.hpp" @@ -66,14 +68,21 @@ struct HttpRequestComponents { std::string processing_method; std::string model_subresource; std::optional inferenceHeaderContentLength; - std::vector> headers; + std::unordered_map headers; }; struct HttpResponseComponents { std::optional inferenceHeaderContentLength; }; -using HandlerCallbackFn = std::function)>; +using HandlerCallbackFn = std::function, + std::shared_ptr)>; std::string urlDecode(const std::string& encoded); @@ -105,7 +114,7 @@ class HttpRestApiHandler { Status parseRequestComponents(HttpRequestComponents& components, const std::string_view http_method, const std::string& request_path, - const std::vector>& headers = {}); + const std::unordered_map& headers = {}); Status parseModelVersion(std::string& model_version_str, std::optional& model_version); static Status prepareGrpcRequest(const std::string modelName, const std::optional& modelVersion, const std::string& request_body, ::KFSRequest& grpc_request, const std::optional& inferenceHeaderContentLength = {}); @@ -119,7 +128,8 @@ class HttpRestApiHandler { std::string* response, const HttpRequestComponents& request_components, HttpResponseComponents& response_components, - std::shared_ptr writer); + std::shared_ptr writer, + std::shared_ptr multiPartParser); /** * @brief Process Request @@ -136,10 +146,11 @@ class HttpRestApiHandler { const std::string_view http_method, const std::string_view request_path, const std::string& request_body, - std::vector>* headers, + std::unordered_map* headers, std::string* response, HttpResponseComponents& responseComponents, - std::shared_ptr writer); + std::shared_ptr writer, + std::shared_ptr multiPartParser); /** * @brief Process predict request @@ -220,7 +231,7 @@ class HttpRestApiHandler { Status processServerLiveKFSRequest(const HttpRequestComponents& request_components, std::string& response, const std::string& request_body); Status processServerMetadataKFSRequest(const HttpRequestComponents& request_components, std::string& response, const std::string& request_body); - Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr serverReaderWriter); + Status processV3(const std::string_view uri, const HttpRequestComponents& request_components, std::string& response, const std::string& request_body, std::shared_ptr serverReaderWriter, std::shared_ptr multiPartParser); private: const std::regex predictionRegex; diff --git a/src/http_server.cpp b/src/http_server.cpp index cd318cc02d..c567a8c592 100644 --- a/src/http_server.cpp +++ b/src/http_server.cpp @@ -55,6 +55,7 @@ #include #include "drogon_http_async_writer_impl.hpp" +#include "http_frontend/multi_part_parser_drogon_impl.hpp" // TODO: net_http #endif namespace ovms { @@ -191,10 +192,12 @@ std::unique_ptr createAndStartDrogonHttpServer(const std::stri server->registerRequestDispatcher([handler, &pool](const drogon::HttpRequestPtr& req, std::function drogonResponseInitializeCallback) { SPDLOG_DEBUG("REST request {}", req->getOriginalPath()); - std::vector> headers; + std::unordered_map headers; + SPDLOG_ERROR("Drogon headers:"); for (const auto& header : req->headers()) { - headers.emplace_back(header.first, header.second); + SPDLOG_ERROR("\t[{}]->[{}]", header.first, header.second); + headers[header.first] = header.second; } SPDLOG_DEBUG("Processing HTTP request: {} {} body: {} bytes", @@ -206,6 +209,7 @@ std::unique_ptr createAndStartDrogonHttpServer(const std::stri std::string output; HttpResponseComponents responseComponents; std::shared_ptr writer = std::make_shared(drogonResponseInitializeCallback, pool, req); + std::shared_ptr multiPartParser = std::make_shared(req); const auto status = handler->processRequest( drogon::to_string_view(req->getMethod()), @@ -214,7 +218,8 @@ std::unique_ptr createAndStartDrogonHttpServer(const std::stri &headers, &output, responseComponents, - writer); + writer, + multiPartParser); if (status == StatusCode::PARTIAL_END) { // No further messaging is required. // Partial responses were delivered via "req" object. @@ -233,8 +238,7 @@ std::unique_ptr createAndStartDrogonHttpServer(const std::stri resp->setContentTypeCode(drogon::CT_APPLICATION_JSON); if (responseComponents.inferenceHeaderContentLength.has_value()) { - std::pair header{"Inference-Header-Content-Length", std::to_string(responseComponents.inferenceHeaderContentLength.value())}; - headers.emplace_back(header); + headers["inference-header-content-length"] = std::to_string(responseComponents.inferenceHeaderContentLength.value()); } for (const auto& [key, value] : headers) { resp->addHeader(key, value); @@ -318,7 +322,7 @@ class RestApiRequestDispatcher { body.size()); HttpResponseComponents responseComponents; std::shared_ptr writer = std::make_shared(req); - const auto status = handler_->processRequest(req->http_method(), req->uri_path(), body, &headers, &output, responseComponents, writer); + const auto status = handler_->processRequest(req->http_method(), req->uri_path(), body, &headers, &output, responseComponents, writer); // TODO: vector of headers is no longer a vector if (status == StatusCode::PARTIAL_END) { // No further messaging is required. // Partial responses were delivered via "req" object. diff --git a/src/llm/servable.cpp b/src/llm/servable.cpp index 3818b12cfa..a7054cb56d 100644 --- a/src/llm/servable.cpp +++ b/src/llm/servable.cpp @@ -38,6 +38,9 @@ namespace ovms { absl::Status GenAiServable::loadRequest(std::shared_ptr& executionContext, const ovms::HttpPayload& payload) { SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Request body: {}", payload.body); SPDLOG_LOGGER_DEBUG(llm_calculator_logger, "Request uri: {}", payload.uri); + if (payload.parsedJson->HasParseError()) { + return absl::InvalidArgumentError("Cannot parse JSON body"); + } if (payload.uri == "/v3/chat/completions" || payload.uri == "/v3/v1/chat/completions") { executionContext->endpoint = Endpoint::CHAT_COMPLETIONS; } else if (payload.uri == "/v3/completions" || payload.uri == "/v3/v1/completions") { diff --git a/src/multi_part_parser.hpp b/src/multi_part_parser.hpp new file mode 100644 index 0000000000..6e65922893 --- /dev/null +++ b/src/multi_part_parser.hpp @@ -0,0 +1,31 @@ +//***************************************************************************** +// Copyright 2025 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 + +namespace ovms { + +class MultiPartParser { +public: + virtual bool parse() = 0; + + virtual bool hasParseError() const = 0; + + virtual std::string getFieldByName(const std::string& name) const = 0; + virtual std::string_view getFileContentByName(const std::string& name) const = 0; +}; + +} // namespace ovms diff --git a/src/test/disabled_mediapipe_test.cpp b/src/test/disabled_mediapipe_test.cpp index 308bbaa22a..9d51c1524b 100644 --- a/src/test/disabled_mediapipe_test.cpp +++ b/src/test/disabled_mediapipe_test.cpp @@ -33,7 +33,7 @@ class MediapipeDisabledTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointChatCompletions = "/v3/chat/completions"; const std::string endpointCompletions = "/v3/completions"; @@ -83,7 +83,7 @@ TEST_F(MediapipeDisabledTest, completionsRequest) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::NOT_IMPLEMENTED); } @@ -106,6 +106,6 @@ TEST_F(MediapipeDisabledTest, chatCompletionsRequest) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::NOT_IMPLEMENTED); } diff --git a/src/test/embeddingsnode_test.cpp b/src/test/embeddingsnode_test.cpp index ee040ea1cc..de599c7200 100644 --- a/src/test/embeddingsnode_test.cpp +++ b/src/test/embeddingsnode_test.cpp @@ -30,11 +30,12 @@ class V3HttpTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointEmbeddings = "/v3/embeddings"; const std::string endpointRerank = "/v3/rerank"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; ovms::HttpResponseComponents responseComponents; @@ -47,6 +48,7 @@ class V3HttpTest : public ::testing::Test { void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); ovms::Server& server = ovms::Server::instance(); handler = std::make_unique(server, 5); ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpointEmbeddings, headers), ovms::StatusCode::OK); @@ -91,7 +93,7 @@ TEST_F(EmbeddingsHttpTest, simplePositive) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -125,7 +127,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveNoNorm) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -160,7 +162,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveBase64) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -187,7 +189,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveInt) { "input": [111, 222, 121] } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); @@ -209,7 +211,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveMultipleInts) { "input": [[111, 222, 121], [123, 221, 311]] } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); @@ -234,7 +236,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveMultipleIntLengths) { "input": [[1, 2, 3, 4, 5, 6], [4, 5, 6, 7], [7, 8]] } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); @@ -261,7 +263,7 @@ TEST_F(EmbeddingsHttpTest, simplePositiveMultipleStrings) { "input": ["one", "two"] } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); @@ -286,7 +288,7 @@ TEST_F(EmbeddingsHttpTest, positiveLongInput) { } std::string requestBody = "{ \"model\": \"embeddings\", \"input\": \"" + words + " \"}"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); @@ -305,7 +307,7 @@ TEST_F(EmbeddingsHttpTest, negativeTooLongInput) { } std::string requestBody = "{ \"model\": \"embeddings\", \"input\": \"" + words + " \"}"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); @@ -323,7 +325,7 @@ TEST_F(EmbeddingsHttpTest, negativeTooLongInputPair) { } std::string requestBody = "{ \"model\": \"embeddings\", \"input\": [\"" + words + " \", \"short prompt\"]}"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); @@ -340,10 +342,11 @@ class EmbeddingsExtensionTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointEmbeddings = "/v3/embeddings"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; ovms::HttpResponseComponents responseComponents; @@ -376,6 +379,7 @@ class EmbeddingsExtensionTest : public ::testing::Test { GTEST_SKIP() << "Skipping test because we have no custom extension built for Windows"; #endif writer = std::make_shared(); + multiPartParser = std::make_shared(); ovms::Server& server = ovms::Server::instance(); handler = std::make_unique(server, 5); ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpointEmbeddings, headers), ovms::StatusCode::OK); @@ -407,7 +411,7 @@ TEST_F(EmbeddingsExtensionTest, simplePositive) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -445,7 +449,7 @@ TEST_F(EmbeddingsInvalidConfigTest, simpleNegative) { "input": "dummyInput" } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); } @@ -473,6 +477,6 @@ TEST_F(EmbeddingsInvalidTokenizerConfigTest, simpleNegative) { "input": "dummyInput" } )"; - Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer); + Status status = handler->dispatchToProcessor(endpointEmbeddings, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); } diff --git a/src/test/http_openai_handler_test.cpp b/src/test/http_openai_handler_test.cpp index 48d14eefc5..76d1b1d3c4 100644 --- a/src/test/http_openai_handler_test.cpp +++ b/src/test/http_openai_handler_test.cpp @@ -37,10 +37,11 @@ class HttpOpenAIHandlerTest : public ::testing::Test { std::unique_ptr t; std::string port = "9173"; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpoint = "/v3/chat/completions"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; ovms::HttpResponseComponents responseComponents; @@ -52,6 +53,7 @@ class HttpOpenAIHandlerTest : public ::testing::Test { void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); SetUpServer(getGenericFullPathForSrcTest("/ovms/src/test/mediapipe/config_mediapipe_openai_chat_completions_mock.json").c_str()); ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); } @@ -74,11 +76,11 @@ TEST_F(HttpOpenAIHandlerTest, Unary) { )"; ASSERT_EQ( - handler->dispatchToProcessor("/v3/v1/completions/", requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor("/v3/v1/completions/", requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); std::string expectedResponse = R"(/v3/v1/completions/ - +content-typeapplication/json { "model": "gpt", "stream": false, @@ -96,15 +98,15 @@ TEST_F(HttpOpenAIHandlerTest, UnaryWithHeaders) { "messages": [] } )"; - comp.headers.push_back(std::pair("test1", "header")); - comp.headers.push_back(std::pair("test2", "header")); + comp.headers["test1"] = "header"; + comp.headers["test2"] = "header"; ASSERT_EQ( - handler->dispatchToProcessor("/v3/completions/", requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor("/v3/completions/", requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); std::string expectedResponse = R"(/v3/completions/ -test1headertest2header +content-typeapplication/jsontest1headertest2header { "model": "gpt", "stream": false, @@ -129,7 +131,7 @@ TEST_F(HttpOpenAIHandlerTest, Stream) { EXPECT_CALL(*writer, IsDisconnected()).Times(9); ASSERT_EQ( - handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_EQ(response, ""); @@ -142,7 +144,7 @@ TEST_F(HttpOpenAIHandlerTest, BodyNotAJson) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::JSON_INVALID); ASSERT_EQ(status.string(), "The file is not valid json - Cannot parse JSON body"); } @@ -154,7 +156,7 @@ TEST_F(HttpOpenAIHandlerTest, JsonBodyValidButNotAnObject) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::JSON_INVALID); ASSERT_EQ(status.string(), "The file is not valid json - JSON body must be an object"); } @@ -171,7 +173,7 @@ TEST_F(HttpOpenAIHandlerTest, ModelFieldMissing) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::JSON_INVALID); ASSERT_EQ(status.string(), "The file is not valid json - model field is missing in JSON body"); } @@ -189,7 +191,7 @@ TEST_F(HttpOpenAIHandlerTest, ModelFieldNotAString) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::JSON_INVALID); ASSERT_EQ(status.string(), "The file is not valid json - model field is not a string"); } @@ -208,7 +210,7 @@ TEST_F(HttpOpenAIHandlerTest, StreamFieldNotABoolean) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::JSON_INVALID); ASSERT_EQ(status.string(), "The file is not valid json - stream field is not a boolean"); } @@ -226,7 +228,7 @@ TEST_F(HttpOpenAIHandlerTest, GraphWithANameDoesNotExist) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_DEFINITION_NAME_MISSING); } @@ -604,6 +606,6 @@ TEST_F(HttpOpenAIHandlerTest, V3ApiWithNonLLMCalculator) { EXPECT_CALL(*writer, PartialReply(::testing::_)).Times(0); EXPECT_CALL(*writer, IsDisconnected()).Times(0); - auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor("/v3/completions", requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_GRAPH_ADD_PACKET_INPUT_STREAM); } diff --git a/src/test/kfs_rest_test.cpp b/src/test/kfs_rest_test.cpp index 428ae400f0..29bbe3e7b6 100644 --- a/src/test/kfs_rest_test.cpp +++ b/src/test/kfs_rest_test.cpp @@ -178,9 +178,8 @@ std::unique_ptr HttpRestApiHandlerTest::thread = nullptr; #pragma GCC diagnostic ignored "-Wnarrowing" static void testInference(int headerLength, std::string& request_body, std::unique_ptr& handler, const std::string endpoint = "/v2/models/mediapipeAdd/versions/1/infer") { - std::vector> headers; - std::pair binaryInputsHeader{"inference-header-content-length", std::to_string(headerLength)}; - headers.emplace_back(binaryInputsHeader); + std::unordered_map headers; + headers["inference-header-content-length"] = std::to_string(headerLength); ovms::HttpRequestComponents comp; @@ -189,7 +188,8 @@ static void testInference(int headerLength, std::string& request_body, std::uniq std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -210,9 +210,8 @@ static void testInference(int headerLength, std::string& request_body, std::uniq static void testInferenceNegative(int headerLength, std::string& request_body, std::unique_ptr& handler, ovms::Status processorStatus) { std::string request = "/v2/models/mediapipeAdd/versions/1/infer"; - std::vector> headers; - std::pair binaryInputsHeader{"inference-header-content-length", std::to_string(headerLength)}; - headers.emplace_back(binaryInputsHeader); + std::unordered_map headers; + headers["inference-header-content-length"] = std::to_string(headerLength); ovms::HttpRequestComponents comp; @@ -221,7 +220,8 @@ static void testInferenceNegative(int headerLength, std::string& request_body, s std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), processorStatus); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), processorStatus); } class HttpRestApiHandlerWithMediapipe : public ::testing::TestWithParam { @@ -366,7 +366,8 @@ TEST_F(HttpRestApiHandlerWithMediapipePassthrough, inferRequestBYTES) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -518,27 +519,24 @@ TEST_F(HttpRestApiHandlerTest, RegexParseServerLive) { TEST_F(HttpRestApiHandlerTest, RegexParseInferWithBinaryInputs) { std::string request = "/v2/models/dummy/versions/1/infer"; ovms::HttpRequestComponents comp; - std::vector> headers; - std::pair binaryInputsHeader{"inference-header-content-length", "15"}; - headers.emplace_back(binaryInputsHeader); + std::unordered_map headers; + headers["inference-header-content-length"] = "15"; ASSERT_EQ(handler->parseRequestComponents(comp, "POST", request, headers), StatusCode::OK); } TEST_F(HttpRestApiHandlerTest, RegexParseInferWithBinaryInputsSizeNegative) { std::string request = "/v2/models/dummy/versions/1/infer"; ovms::HttpRequestComponents comp; - std::vector> headers; - std::pair binaryInputsHeader{"inference-header-content-length", "-15"}; - headers.emplace_back(binaryInputsHeader); + std::unordered_map headers; + headers["inference-header-content-length"] = "-15"; ASSERT_EQ(handler->parseRequestComponents(comp, "POST", request, headers), StatusCode::REST_INFERENCE_HEADER_CONTENT_LENGTH_INVALID); } TEST_F(HttpRestApiHandlerTest, RegexParseInferWithBinaryInputsSizeNotInt) { std::string request = "/v2/models/dummy/versions/1/infer"; ovms::HttpRequestComponents comp; - std::vector> headers; - std::pair binaryInputsHeader{"inference-header-content-length", "value"}; - headers.emplace_back(binaryInputsHeader); + std::unordered_map headers; + headers["inference-header-content-length"] = "value"; ASSERT_EQ(handler->parseRequestComponents(comp, "POST", request, headers), StatusCode::REST_INFERENCE_HEADER_CONTENT_LENGTH_INVALID); } @@ -547,7 +545,7 @@ TEST_F(HttpRestApiHandlerTest, dispatchMetadata) { ovms::HttpRequestComponents comp; int c = 0; - handler->registerHandler(KFS_GetModelMetadata, [&](const std::string_view uri, const ovms::HttpRequestComponents& request_components, std::string& response, const std::string& request_body, ovms::HttpResponseComponents& response_components, std::shared_ptr) { + handler->registerHandler(KFS_GetModelMetadata, [&](const std::string_view uri, const ovms::HttpRequestComponents& request_components, std::string& response, const std::string& request_body, ovms::HttpResponseComponents& response_components, std::shared_ptr, std::shared_ptr) { c++; return ovms::StatusCode::OK; }); @@ -555,7 +553,8 @@ TEST_F(HttpRestApiHandlerTest, dispatchMetadata) { std::string discard; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - handler->dispatchToProcessor("", std::string(), &discard, comp, responseComponents, writer); + std::shared_ptr multiPartParser{nullptr}; + handler->dispatchToProcessor("", std::string(), &discard, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(c, 1); } @@ -565,7 +564,7 @@ TEST_F(HttpRestApiHandlerTest, dispatchReady) { ovms::HttpRequestComponents comp; int c = 0; - handler->registerHandler(KFS_GetModelReady, [&](const std::string_view, const ovms::HttpRequestComponents& request_components, std::string& response, const std::string& request_body, ovms::HttpResponseComponents& response_components, std::shared_ptr writer) { + handler->registerHandler(KFS_GetModelReady, [&](const std::string_view, const ovms::HttpRequestComponents& request_components, std::string& response, const std::string& request_body, ovms::HttpResponseComponents& response_components, std::shared_ptr, std::shared_ptr) { c++; return ovms::StatusCode::OK; }); @@ -573,7 +572,8 @@ TEST_F(HttpRestApiHandlerTest, dispatchReady) { std::string discard; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - handler->dispatchToProcessor("", std::string(), &discard, comp, responseComponents, writer); + std::shared_ptr multiPartParser{nullptr}; + handler->dispatchToProcessor("", std::string(), &discard, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(c, 1); } @@ -586,7 +586,8 @@ TEST_F(HttpRestApiHandlerTest, modelMetadataRequest) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", std::string(), &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", std::string(), &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -622,7 +623,8 @@ TEST_F(HttpRestApiHandlerWithScalarModelTest, modelMetadataRequest) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", std::string(), &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", std::string(), &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -649,7 +651,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithMultidimensionalMatrix) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -670,7 +673,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequest) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -693,7 +697,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithSpecificBinaryOutputNotBool) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -715,7 +720,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithDefaultBinaryOutputFalse) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -737,7 +743,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithSpecificBinaryOutputFalse) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -759,7 +766,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithDefaultBinaryOutputTrue) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -783,7 +791,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithSpecificBinaryOutputTrue) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -807,7 +816,8 @@ TEST_F(HttpRestApiHandlerTest, inferRequestWithSpecificBinaryOutputTrueDefaultFa std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); ASSERT_EQ(doc["model_name"].GetString(), std::string("dummy")); @@ -831,7 +841,8 @@ TEST_F(HttpRestApiHandlerWithScalarModelTest, inferRequestScalar) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -854,7 +865,8 @@ TEST_F(HttpRestApiHandlerWithDynamicModelTest, inferRequestZeroBatch) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -878,7 +890,8 @@ TEST_F(HttpRestApiHandlerWithDynamicModelTest, inferRequestZeroDim) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -1441,7 +1454,8 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, invalidPrecision) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::REST_COULD_NOT_PARSE_INPUT); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::REST_COULD_NOT_PARSE_INPUT); } TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape) { @@ -1453,7 +1467,8 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::INVALID_VALUE_COUNT); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::INVALID_VALUE_COUNT); } TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape_noData) { @@ -1465,7 +1480,8 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape_noData) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::INVALID_VALUE_COUNT); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::INVALID_VALUE_COUNT); } TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape_emptyData) { @@ -1477,7 +1493,8 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, invalidShape_emptyData) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::INVALID_VALUE_COUNT); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::INVALID_VALUE_COUNT); } void assertStringMetadataOutput(rapidjson::Document& doc) { @@ -1512,7 +1529,8 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, positivePassthrough) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->dispatchToProcessor("", request_body, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document doc; doc.Parse(response.c_str()); @@ -1555,14 +1573,15 @@ TEST_F(HttpRestApiHandlerWithStringModelTest, positivePassthrough_binaryInput) { std::string binaryInputData{0x05, 0x00, 0x00, 0x00, 'H', 'e', 'l', 'l', 'o', 0x02, 0x00, 0x00, 0x00, '1', '2'}; request_body += binaryInputData; - std::vector> headers{ + std::unordered_map headers{ {"inference-header-content-length", std::to_string(jsonEnd)}, {"Content-Type", "application/json"}, }; ovms::HttpResponseComponents responseComponents; std::string output; std::shared_ptr writer{nullptr}; - ASSERT_EQ(handler->processRequest("POST", request, request_body, &headers, &output, responseComponents, writer), ovms::StatusCode::OK); + std::shared_ptr multiPartParser{nullptr}; + ASSERT_EQ(handler->processRequest("POST", request, request_body, &headers, &output, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); ASSERT_TRUE(responseComponents.inferenceHeaderContentLength.has_value()); ASSERT_EQ(responseComponents.inferenceHeaderContentLength.value(), 272); @@ -1589,7 +1608,8 @@ TEST_F(HttpRestApiHandlerTest, serverReady) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer); + std::shared_ptr multiPartParser{nullptr}; + ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK); } @@ -1601,7 +1621,8 @@ TEST_F(HttpRestApiHandlerTest, serverLive) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer); + std::shared_ptr multiPartParser{nullptr}; + ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK); } @@ -1613,7 +1634,8 @@ TEST_F(HttpRestApiHandlerTest, serverMetadata) { std::string response; ovms::HttpResponseComponents responseComponents; std::shared_ptr writer{nullptr}; - ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer); + std::shared_ptr multiPartParser{nullptr}; + ovms::Status status = handler->dispatchToProcessor("", request, &response, comp, responseComponents, writer, multiPartParser); rapidjson::Document doc; doc.Parse(response.c_str()); diff --git a/src/test/llm/llmnode_test.cpp b/src/test/llm/llmnode_test.cpp index da2b967b82..f885142124 100644 --- a/src/test/llm/llmnode_test.cpp +++ b/src/test/llm/llmnode_test.cpp @@ -65,11 +65,12 @@ class LLMFlowHttpTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointChatCompletions = "/v3/chat/completions"; const std::string endpointCompletions = "/v3/completions"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; rapidjson::Document parsedResponse; ovms::HttpResponseComponents responseComponents; @@ -140,6 +141,7 @@ class LLMFlowHttpTest : public ::testing::Test { void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); ON_CALL(*writer, PartialReplyBegin(::testing::_)).WillByDefault(testing::Invoke([](std::function fn) { fn(); })); // make the streaming flow sequential ovms::Server& server = ovms::Server::instance(); handler = std::make_unique(server, 5); @@ -203,7 +205,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJson) { // TODO: In the next step we should break this suite into smaller ones, use proper configuration instead of such if...else... if (params.modelName.find("vlm") == std::string::npos) { ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -228,7 +230,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJson) { EXPECT_STREQ(parsedResponse["object"].GetString(), "text_completion"); } else { // Completions endpoint not supported for VLM servable ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } } @@ -262,7 +264,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonSpeculativeDecoding) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -287,7 +289,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonSpeculativeDecoding) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -329,7 +331,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonEchoWithCompletion) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -410,7 +412,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsEchoWithCompletion) { }); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); // Since prompt is treated as a single entity and streamer returns chunk only after space or newline @@ -439,7 +441,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonEchoOnly) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -529,7 +531,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsEchoOnly) { EXPECT_STREQ(d["object"].GetString(), "text_completion.chunk"); }); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } else { // In legacy servable streaming with echo, prompt can be sent back in multiple chunks @@ -540,7 +542,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsEchoOnly) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); std::string mergedContent; for (const auto& response : responses) { @@ -572,7 +574,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonFinishReasonLength) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -613,7 +615,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonSingleStopString) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -656,7 +658,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonSpaceStopString) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse.HasMember("choices")); @@ -687,7 +689,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonNFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -720,7 +722,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonN) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -770,7 +772,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonNFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -804,7 +806,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonN) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -848,10 +850,10 @@ TEST_P(LLMFlowHttpTestParameterized, KFSApiRequestToChatCompletionsGraph) { } ] })"; - std::vector> headers; + std::unordered_map headers; ASSERT_EQ(handler->parseRequestComponents(comp, "POST", "/v2/models/" + params.modelName + "/versions/1/infer", headers), ovms::StatusCode::OK); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_GRAPH_ADD_PACKET_INPUT_STREAM); } @@ -875,7 +877,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJson) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -935,7 +937,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonSpeculativeDecoding )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -972,7 +974,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonSpeculativeDecoding )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -1011,7 +1013,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonContentArray) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -1061,11 +1063,11 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonContentArrayWithIma if (params.modelName.find("vlm") != std::string::npos) { ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } else { ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } } @@ -1093,7 +1095,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonNMultipleStopString )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -1138,7 +1140,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsJsonLogprobs) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -1177,7 +1179,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsJsonLogprobs) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -1216,7 +1218,7 @@ TEST_P(LLMFlowHttpTestParameterized, ChatCompletionsJsonLogprobsStream) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -1239,7 +1241,7 @@ TEST_P(LLMFlowHttpTestParameterized, CompletionsJsonLogprobsStream) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1263,7 +1265,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsStopStringBadType) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1288,7 +1290,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsIncludeStopStringInOutp )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1311,7 +1313,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsStopStringElementBadType) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1335,7 +1337,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsStopStringExceedingSize )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1367,7 +1369,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsPromptTokensWithMaxToke )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1399,7 +1401,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsPromptTokensWithMaxComp )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1430,7 +1432,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsPromptTokensEqualToMaxM )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1461,7 +1463,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsStoppedByMaxModelLength )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); // parsedResponse.Parse(response.c_str()); // ASSERT_TRUE(parsedResponse["usage"].IsObject()); @@ -1490,7 +1492,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsStopStringEmpty) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -1511,7 +1513,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamBeamSearchCompletionsFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -1533,7 +1535,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamBeamSearchChatCompletionsFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -1583,7 +1585,7 @@ TEST_P(LLMFlowHttpTestParameterized, inferCompletionsStream) { EXPECT_STREQ(d["object"].GetString(), "text_completion.chunk"); }); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -1635,7 +1637,7 @@ TEST_P(LLMFlowHttpTestParameterized, inferChatCompletionsStream) { EXPECT_STREQ(d["object"].GetString(), "chat.completion.chunk"); }); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -1659,7 +1661,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryChatCompletionsStreamOptionsSetFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1682,7 +1684,7 @@ TEST_P(LLMFlowHttpTestParameterized, unaryCompletionsStreamOptionsSetFail) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -1713,7 +1715,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsFinishReasonLength) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (params.checkFinishReason) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"length\"") != std::string::npos); @@ -1751,7 +1753,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsSingleStopString) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (params.checkFinishReason) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"stop\"") != std::string::npos); @@ -1792,7 +1794,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsFinishReasonLength) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (params.checkFinishReason) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"length\"") != std::string::npos); @@ -1829,7 +1831,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsSingleStopString) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (params.checkFinishReason) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"stop\"") != std::string::npos); @@ -1873,7 +1875,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsSpaceStopString) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_GE(responses.size(), 1); if (params.checkFinishReason) { @@ -1910,7 +1912,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsUsage) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_TRUE(responses.back().find("\"completion_tokens\":5") != std::string::npos); ASSERT_TRUE(responses.back().find("\"prompt_tokens\"") != std::string::npos); @@ -1947,7 +1949,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsUsage) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_TRUE(responses.back().find("\"completion_tokens\":5") != std::string::npos); ASSERT_TRUE(responses.back().find("\"prompt_tokens\"") != std::string::npos); @@ -1988,7 +1990,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsBadStopStringType) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2021,7 +2023,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsBadStopStringElementType) }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2056,7 +2058,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsIncludeStopStrInOutputFals }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2090,7 +2092,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsBadIncludeStopStrInOutputT }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2124,7 +2126,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsBadStreamOptionsBadTyp }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2157,7 +2159,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsStreamOptionsBadType) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2191,7 +2193,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsStreamOptionsBadConten }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2224,7 +2226,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsStreamOptionsBadContent) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2258,7 +2260,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamChatCompletionsBadIncludeUsage) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2291,7 +2293,7 @@ TEST_P(LLMFlowHttpTestParameterized, streamCompletionsBadIncludeUsage) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } @@ -2325,7 +2327,7 @@ TEST_P(LLMFlowHttpTestParameterized, inferChatCompletionsUnaryClientDisconnected fn(); // disconnect immediately, even before read_all is called }); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2365,7 +2367,7 @@ TEST_P(LLMFlowHttpTestParameterized, inferChatCompletionsStreamClientDisconnecte }); // no results ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_EQ(i, 1); ASSERT_EQ(response, ""); @@ -2406,7 +2408,7 @@ TEST_P(LLMFlowHttpTestParameterized, inferCompletionsStreamClientDisconnectedImm }); // no results ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_EQ(i, 1); ASSERT_EQ(response, ""); @@ -2462,7 +2464,7 @@ TEST_P(LLMHttpParametersValidationTest, maxTokensInvalid) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2484,7 +2486,7 @@ TEST_P(LLMHttpParametersValidationTest, maxTokensExceedsUint32Size) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2506,7 +2508,7 @@ TEST_P(LLMHttpParametersValidationTest, maxCompletionsTokensInvalid) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2528,7 +2530,7 @@ TEST_P(LLMHttpParametersValidationTest, maxCompletionsTokensExceedsUint32Size) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2537,7 +2539,7 @@ TEST_P(LLMHttpParametersValidationTest, streamInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "stream", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::JSON_INVALID); } @@ -2554,7 +2556,7 @@ TEST_P(LLMHttpParametersValidationTest, messagesInvalid) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2570,7 +2572,7 @@ TEST_P(LLMHttpParametersValidationTest, messagesMissing) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2589,7 +2591,7 @@ TEST_P(LLMHttpParametersValidationTest, messageNotAnObject) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2611,7 +2613,7 @@ TEST_P(LLMHttpParametersValidationTest, messageNotAString) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2633,7 +2635,7 @@ TEST_P(LLMHttpParametersValidationTest, roleNotAString) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2654,7 +2656,7 @@ TEST_P(LLMHttpParametersValidationTest, promptInvalid) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2670,7 +2672,7 @@ TEST_P(LLMHttpParametersValidationTest, promptMissing) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2690,7 +2692,7 @@ TEST_P(LLMHttpParametersValidationTest, modelMissing) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::JSON_INVALID); } @@ -2711,7 +2713,7 @@ TEST_P(LLMHttpParametersValidationTest, modelInvalid) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::JSON_INVALID); } @@ -2720,7 +2722,7 @@ TEST_P(LLMHttpParametersValidationTest, ignoreEosValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "ignore_eos", "false"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2729,7 +2731,7 @@ TEST_P(LLMHttpParametersValidationTest, ignoreEosInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "ignore_eos", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2738,13 +2740,13 @@ TEST_P(LLMHttpParametersValidationTest, repetitionPenaltyValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "repetition_penalty", "2.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "repetition_penalty", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2753,7 +2755,7 @@ TEST_P(LLMHttpParametersValidationTest, repetitionPenaltyInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "repetition_penalty", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2762,13 +2764,13 @@ TEST_P(LLMHttpParametersValidationTest, lengthPenaltyValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "length_penalty", "2.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "length_penalty", "2"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2777,7 +2779,7 @@ TEST_P(LLMHttpParametersValidationTest, lengthPenaltyInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "length_penalty", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2786,19 +2788,19 @@ TEST_P(LLMHttpParametersValidationTest, temperatureValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "temperature", "1.5"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "temperature", "0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "temperature", "2"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2807,7 +2809,7 @@ TEST_P(LLMHttpParametersValidationTest, temperatureInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "temperature", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2816,7 +2818,7 @@ TEST_P(LLMHttpParametersValidationTest, temperatureOutOfRange) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "temperature", "3.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2825,13 +2827,13 @@ TEST_P(LLMHttpParametersValidationTest, frequencyPenaltyValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "frequency_penalty", "1.5"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "frequency_penalty", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2840,7 +2842,7 @@ TEST_P(LLMHttpParametersValidationTest, frequencyPenaltyInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "frequency_penalty", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2849,7 +2851,7 @@ TEST_P(LLMHttpParametersValidationTest, frequencyPenaltyOutOfRange) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "frequency_penalty", "3.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2858,13 +2860,13 @@ TEST_P(LLMHttpParametersValidationTest, presencePenaltyValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "presence_penalty", "1.5"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "presence_penalty", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2873,7 +2875,7 @@ TEST_P(LLMHttpParametersValidationTest, presencePenaltyInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "presence_penalty", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2882,7 +2884,7 @@ TEST_P(LLMHttpParametersValidationTest, presencePenaltyOutOfRange) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "presence_penalty", "3.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2891,13 +2893,13 @@ TEST_P(LLMHttpParametersValidationTest, topPValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "top_p", "0.5"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); requestBody = validRequestBodyWithParameter(params.modelName, "top_p", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2906,7 +2908,7 @@ TEST_P(LLMHttpParametersValidationTest, topPInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "top_p", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2915,7 +2917,7 @@ TEST_P(LLMHttpParametersValidationTest, topPOutOfRange) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "top_p", "3.0"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2924,7 +2926,7 @@ TEST_P(LLMHttpParametersValidationTest, topKValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "top_k", "2"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2933,7 +2935,7 @@ TEST_P(LLMHttpParametersValidationTest, topKInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "top_k", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2942,7 +2944,7 @@ TEST_P(LLMHttpParametersValidationTest, seedValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "seed", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2951,7 +2953,7 @@ TEST_P(LLMHttpParametersValidationTest, seedInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "seed", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2960,7 +2962,7 @@ TEST_P(LLMHttpParametersValidationTest, bestOfValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "best_of", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -2969,7 +2971,7 @@ TEST_P(LLMHttpParametersValidationTest, bestOfInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "best_of", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2978,7 +2980,7 @@ TEST_P(LLMHttpParametersValidationTest, bestOfNegative) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "best_of", "-1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2987,7 +2989,7 @@ TEST_P(LLMHttpParametersValidationTest, bestOfExceedsLimit) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "best_of", "40"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -2996,7 +2998,7 @@ TEST_P(LLMHttpParametersValidationTest, nValid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "n", "1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -3005,7 +3007,7 @@ TEST_P(LLMHttpParametersValidationTest, nInvalid) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "n", "\"INVALID\""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3014,7 +3016,7 @@ TEST_P(LLMHttpParametersValidationTest, nNegative) { std::string requestBody = validRequestBodyWithParameter(params.modelName, "n", "-1"); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3038,7 +3040,7 @@ TEST_P(LLMHttpParametersValidationTest, nGreaterThanBestOf) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3054,7 +3056,7 @@ TEST_P(LLMHttpParametersValidationTest, MessagesEmpty) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3069,7 +3071,7 @@ TEST_P(LLMHttpParametersValidationTest, MessagesWithEmptyObject) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3088,7 +3090,7 @@ TEST_P(LLMHttpParametersValidationTest, EmptyPrompt) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3103,7 +3105,7 @@ TEST_P(LLMHttpParametersValidationTest, MessagesWithOnlyRole) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3127,7 +3129,7 @@ TEST_P(LLMHttpParametersValidationTest, SpeculativeDecodingExclusiveParametersPr )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3147,7 +3149,7 @@ TEST_P(LLMHttpParametersValidationTest, SpeculativeDecodingExclusiveParametersPr )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3169,7 +3171,7 @@ TEST_P(LLMHttpParametersValidationTest, SpeculativeDecodingNoSDSpecificParameter )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3187,7 +3189,7 @@ TEST_P(LLMHttpParametersValidationTest, SpeculativeDecodingNoSDSpecificParameter )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3203,7 +3205,7 @@ TEST_P(LLMHttpParametersValidationTest, MessagesWithOnlyContent) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -3219,7 +3221,7 @@ TEST_P(LLMHttpParametersValidationTest, MessagesWithMoreMessageFields) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } diff --git a/src/test/llm/llmtemplate_test.cpp b/src/test/llm/llmtemplate_test.cpp index 86d1bb24bd..111e765c6e 100644 --- a/src/test/llm/llmtemplate_test.cpp +++ b/src/test/llm/llmtemplate_test.cpp @@ -551,16 +551,18 @@ class LLMChatTemplateHttpTest : public TestWithTempDir { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointChatCompletions = "/v3/chat/completions"; const std::string endpointCompletions = "/v3/completions"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; ovms::HttpResponseComponents responseComponents; void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); ON_CALL(*writer, PartialReplyBegin(::testing::_)).WillByDefault(testing::Invoke([](std::function fn) { fn(); })); TestWithTempDir::SetUp(); tokenizerConfigFilePath = directoryPath + "/tokenizer_config.json"; @@ -663,7 +665,7 @@ TEST_F(LLMJinjaChatTemplateHttpTest, inferChatCompletionsUnary) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); // Assertion split in two parts to avoid timestamp mismatch // const size_t timestampLength = 10; @@ -685,7 +687,7 @@ TEST_F(LLMJinjaChatTemplateHttpTest, inferCompletionsUnary) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); // Assertion split in two parts to avoid timestamp mismatch // const size_t timestampLength = 10; @@ -727,7 +729,7 @@ TEST_F(LLMJinjaChatTemplateHttpTest, inferChatCompletionsStream) { // TODO: New output EXPECT_CALL(writer, IsDisconnected()).Times(7); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_EQ(response, ""); @@ -767,7 +769,7 @@ TEST_F(LLMJinjaChatTemplateHttpTest, inferCompletionsStream) { // TODO: New output EXPECT_CALL(writer, IsDisconnected()).Times(7); ASSERT_EQ( - handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); ASSERT_EQ(response, ""); @@ -793,7 +795,7 @@ TEST_F(LLMJinjaChatTemplateHttpTest, inferDefaultChatCompletionsUnary) { )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); // Assertion split in two parts to avoid timestamp mismatch // const size_t timestampLength = 10; diff --git a/src/test/llm/visual_language_model/complete_flow_test.cpp b/src/test/llm/visual_language_model/complete_flow_test.cpp index 28f666023d..4afbd99f31 100644 --- a/src/test/llm/visual_language_model/complete_flow_test.cpp +++ b/src/test/llm/visual_language_model/complete_flow_test.cpp @@ -46,10 +46,11 @@ class VLMServableExecutionTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointChatCompletions = "/v3/chat/completions"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; rapidjson::Document parsedResponse; ovms::HttpResponseComponents responseComponents; @@ -61,6 +62,7 @@ class VLMServableExecutionTest : public ::testing::Test { void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); ON_CALL(*writer, PartialReplyBegin(::testing::_)).WillByDefault(testing::Invoke([](std::function fn) { fn(); })); // make the streaming flow sequential ovms::Server& server = ovms::Server::instance(); handler = std::make_unique(server, 5); @@ -141,7 +143,7 @@ TEST_P(VLMServableExecutionTestParameterized, unaryBasic) { std::string requestBody = createRequestBody(modelName, fields); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -179,7 +181,7 @@ TEST_P(VLMServableExecutionTestParameterized, unaryBasicOnlyImage) { std::string requestBody = createRequestBody(modelName, fields, false, 1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -217,7 +219,7 @@ TEST_P(VLMServableExecutionTestParameterized, unaryMultipleImageTagOrderPasses) std::string requestBody = createRequestBody(modelName, fields, false, 3); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); parsedResponse.Parse(response.c_str()); ASSERT_TRUE(parsedResponse["choices"].IsArray()); @@ -254,7 +256,7 @@ TEST_P(VLMServableExecutionTestParameterized, UnaryRestrictedTagUsed) { std::string requestBody = createRequestBody(modelName, fields, true, 1, ""); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); } @@ -277,7 +279,7 @@ TEST_P(VLMServableExecutionTestParameterized, streamBasic) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (modelName.find("legacy") == std::string::npos) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"length\"") != std::string::npos); @@ -302,7 +304,7 @@ TEST_P(VLMServableExecutionTestParameterized, streamBasicOnlyImage) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (modelName.find("legacy") == std::string::npos) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"length\"") != std::string::npos); @@ -327,7 +329,7 @@ TEST_P(VLMServableExecutionTestParameterized, streamMultipleImageTagOrderPasses) }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); if (modelName.find("legacy") == std::string::npos) { ASSERT_TRUE(responses.back().find("\"finish_reason\":\"length\"") != std::string::npos); @@ -355,7 +357,7 @@ TEST_P(VLMServableExecutionTestParameterized, streamRestrictedTagUsed) { }); EXPECT_CALL(*writer, PartialReplyEnd()).Times(1); ASSERT_EQ( - handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointChatCompletions, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::PARTIAL_END); } diff --git a/src/test/mediapipe/calculators/BUILD b/src/test/mediapipe/calculators/BUILD index 8ffc1952dc..e8ef37fca3 100644 --- a/src/test/mediapipe/calculators/BUILD +++ b/src/test/mediapipe/calculators/BUILD @@ -49,6 +49,7 @@ cc_library( "streaming_test_calculator.cpp", "two_input_calculator.cpp", "openai_chat_completions_mock_calculator.cpp", + "multipart_accepting_calculator.cpp", ], copts = select({ "//conditions:default": [ diff --git a/src/test/mediapipe/calculators/multipart_accepting_calculator.cpp b/src/test/mediapipe/calculators/multipart_accepting_calculator.cpp new file mode 100644 index 0000000000..2fc3b983f7 --- /dev/null +++ b/src/test/mediapipe/calculators/multipart_accepting_calculator.cpp @@ -0,0 +1,65 @@ +//***************************************************************************** +// Copyright 2025 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 "../../../http_payload.hpp" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#include "mediapipe/framework/calculator_framework.h" +#include "mediapipe/framework/port/canonical_errors.h" +#pragma GCC diagnostic pop + +namespace mediapipe { + +class MultipartAcceptingCalculator : public CalculatorBase { + static const std::string INPUT_TAG_NAME; + static const std::string OUTPUT_TAG_NAME; + +public: + static absl::Status GetContract(CalculatorContract* cc) { + RET_CHECK(!cc->Inputs().GetTags().empty()); + RET_CHECK(!cc->Outputs().GetTags().empty()); + cc->Inputs().Tag(INPUT_TAG_NAME).Set(); + cc->Outputs().Tag(OUTPUT_TAG_NAME).Set(); + return absl::OkStatus(); + } + + absl::Status Close(CalculatorContext* cc) final { + return absl::OkStatus(); + } + + absl::Status Open(CalculatorContext* cc) final { + return absl::OkStatus(); + } + + absl::Status Process(CalculatorContext* cc) final { + auto payload = cc->Inputs().Tag(INPUT_TAG_NAME).Get(); + RET_CHECK(payload.multipartParser != nullptr); + std::string email = payload.multipartParser->getFieldByName("email"); + std::string username = payload.multipartParser->getFieldByName("username"); + std::string_view fileContent = payload.multipartParser->getFileContentByName("file"); + + cc->Outputs().Tag(OUTPUT_TAG_NAME).Add(new std::string{email + std::string{"+"} + username + std::string{"\n"} + std::string(fileContent)}, cc->InputTimestamp()); + return absl::OkStatus(); + } +}; +#pragma GCC diagnostic pop + +const std::string MultipartAcceptingCalculator::INPUT_TAG_NAME{"HTTP_REQUEST_PAYLOAD"}; +const std::string MultipartAcceptingCalculator::OUTPUT_TAG_NAME{"HTTP_RESPONSE_PAYLOAD"}; + +REGISTER_CALCULATOR(MultipartAcceptingCalculator); +} // namespace mediapipe diff --git a/src/test/mediapipe/calculators/openai_chat_completions_mock_calculator.cpp b/src/test/mediapipe/calculators/openai_chat_completions_mock_calculator.cpp index d62c5a4b42..a1fa7d9d10 100644 --- a/src/test/mediapipe/calculators/openai_chat_completions_mock_calculator.cpp +++ b/src/test/mediapipe/calculators/openai_chat_completions_mock_calculator.cpp @@ -75,7 +75,18 @@ class OpenAIChatCompletionsMockCalculator : public CalculatorBase { // - Request body // - timestamps 0-8 (appended in cycles) this->body = data.uri + std::string{"\n"}; - for (auto header : data.headers) { + + // Sort alphabetically to get determinism + std::vector> sorted_elements( + data.headers.begin(), data.headers.end()); + + // Sort the vector by key + std::sort(sorted_elements.begin(), sorted_elements.end(), + [](const auto& a, const auto& b) { + return a.first < b.first; + }); + + for (auto header : sorted_elements) { this->body += header.first; this->body += header.second; } diff --git a/src/test/mediapipe/config_mediapipe_multipart_mock.json b/src/test/mediapipe/config_mediapipe_multipart_mock.json new file mode 100644 index 0000000000..e070f5882a --- /dev/null +++ b/src/test/mediapipe/config_mediapipe_multipart_mock.json @@ -0,0 +1,9 @@ +{ + "model_config_list": [], + "mediapipe_config_list": [ + { + "name": "multipart", + "graph_path": "/ovms/src/test/mediapipe/graph_multipart.pbtxt" + } + ] +} \ No newline at end of file diff --git a/src/test/mediapipe/graph_multipart.pbtxt b/src/test/mediapipe/graph_multipart.pbtxt new file mode 100644 index 0000000000..5051c04c4a --- /dev/null +++ b/src/test/mediapipe/graph_multipart.pbtxt @@ -0,0 +1,23 @@ +# +# Copyright 2025 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. +# +input_stream: "HTTP_REQUEST_PAYLOAD:input" +output_stream: "HTTP_RESPONSE_PAYLOAD:output" + +node: { + calculator: "MultipartAcceptingCalculator" + input_stream: "HTTP_REQUEST_PAYLOAD:input" + output_stream: "HTTP_RESPONSE_PAYLOAD:output" +} diff --git a/src/test/mediapipeflow_test.cpp b/src/test/mediapipeflow_test.cpp index 8f67aecb60..be3f0e0a7c 100644 --- a/src/test/mediapipeflow_test.cpp +++ b/src/test/mediapipeflow_test.cpp @@ -3827,6 +3827,7 @@ TEST(WhitelistRegistered, MediapipeCalculatorsList) { "ModelInferHttpRequestCalculator", "ModelInferRequestImageCalculator", "MotionAnalysisCalculator", + "MultipartAcceptingCalculator", "MuxCalculator", "NegativeCalculator", "NoOutputStreamsProducedCalculator", diff --git a/src/test/metrics_flow_test.cpp b/src/test/metrics_flow_test.cpp index c2ff3cd443..884e62ba4a 100644 --- a/src/test/metrics_flow_test.cpp +++ b/src/test/metrics_flow_test.cpp @@ -823,6 +823,7 @@ TEST_F(MetricFlowTest, ModelReady) { TEST_F(MetricFlowTest, RestV3Unary) { HttpRestApiHandler handler(server, 0); std::shared_ptr stream = std::make_shared(); + std::shared_ptr multiPartParser = std::make_shared(); EXPECT_CALL(*stream, IsDisconnected()) .WillRepeatedly(::testing::Return(false)); @@ -831,10 +832,11 @@ TEST_F(MetricFlowTest, RestV3Unary) { std::string request = R"({"model": "dummy_gpt", "prompt": "Hello World"})"; std::string response; HttpRequestComponents comps; + comps.headers = {{"content-type", "application/json"}}; auto streamPtr = std::static_pointer_cast(stream); - auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr); + auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); - status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr); + status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::OK) << status.string(); } @@ -849,6 +851,7 @@ TEST_F(MetricFlowTest, RestV3Unary) { TEST_F(MetricFlowTest, RestV3UnaryError) { HttpRestApiHandler handler(server, 0); std::shared_ptr stream = std::make_shared(); + std::shared_ptr multiPartParser = std::make_shared(); auto streamPtr = std::static_pointer_cast(stream); EXPECT_CALL(*stream, IsDisconnected()) @@ -860,9 +863,10 @@ TEST_F(MetricFlowTest, RestV3UnaryError) { std::string request = R"({"model": "dummy_gpt", "prompt":"ReturnError"})"; std::string response; HttpRequestComponents comps; - auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr); + comps.headers = {{"content-type", "application/json"}}; + auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); - status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr); + status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); } @@ -874,6 +878,7 @@ TEST_F(MetricFlowTest, RestV3UnaryError) { TEST_F(MetricFlowTest, RestV3Stream) { HttpRestApiHandler handler(server, 0); std::shared_ptr stream = std::make_shared(); + std::shared_ptr multiPartParser = std::make_shared(); ON_CALL(*stream, PartialReplyBegin(::testing::_)).WillByDefault(testing::Invoke([](std::function fn) { fn(); })); // make the streaming flow sequential EXPECT_CALL(*stream, IsDisconnected()) @@ -883,10 +888,11 @@ TEST_F(MetricFlowTest, RestV3Stream) { std::string request = R"({"model": "dummy_gpt", "stream": true, "prompt": "Hello World"})"; std::string response; HttpRequestComponents comps; + comps.headers = {{"content-type", "application/json"}}; auto streamPtr = std::static_pointer_cast(stream); - auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr); + auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::PARTIAL_END) << status.string(); - status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr); + status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::PARTIAL_END) << status.string(); } @@ -905,6 +911,7 @@ TEST_F(MetricFlowTest, RestV3Stream) { TEST_F(MetricFlowTest, RestV3StreamError) { HttpRestApiHandler handler(server, 0); std::shared_ptr stream = std::make_shared(); + std::shared_ptr multiPartParser = std::make_shared(); auto streamPtr = std::static_pointer_cast(stream); ON_CALL(*stream, PartialReplyBegin(::testing::_)).WillByDefault(testing::Invoke([](std::function fn) { fn(); })); @@ -917,9 +924,10 @@ TEST_F(MetricFlowTest, RestV3StreamError) { std::string request = R"({"model": "dummy_gpt", "stream": true, "prompt": "ReturnError"})"; std::string response; HttpRequestComponents comps; - auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr); + comps.headers = {{"content-type", "application/json"}}; + auto status = handler.processV3("/v3/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::PARTIAL_END) << status.string(); - status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr); + status = handler.processV3("/v3/v1/completions", comps, response, request, streamPtr, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::PARTIAL_END) << status.string(); } diff --git a/src/test/multipart_calculator_test.cpp b/src/test/multipart_calculator_test.cpp new file mode 100644 index 0000000000..9f4430bd5c --- /dev/null +++ b/src/test/multipart_calculator_test.cpp @@ -0,0 +1,225 @@ +//***************************************************************************** +// Copyright 2025 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 "../http_rest_api_handler.hpp" +#include "../http_payload.hpp" +#include "../module_names.hpp" +#include "../servablemanagermodule.hpp" +#include "../server.hpp" +#include "test_http_utils.hpp" +#include "test_utils.hpp" + +class MultiPartCalculatorTest : public ::testing::Test { +protected: + ovms::Server& server = ovms::Server::instance(); + std::unique_ptr handler; + + std::unique_ptr t; + std::string port = "9173"; + + std::unordered_map headers{{"content-type", "application/json"}}; + ovms::HttpRequestComponents comp; + const std::string endpoint = "/v3/chat/completions"; + std::shared_ptr writer; + std::shared_ptr multiPartParser; + std::string response; + ovms::HttpResponseComponents responseComponents; + + void SetUpServer(const char* configPath) { + ::SetUpServer(this->t, this->server, this->port, configPath); + EnsureServerStartedWithTimeout(this->server, 5); + handler = std::make_unique(server, 5); + } + + void SetUp() { + writer = std::make_shared(); + multiPartParser = std::make_shared(); + SetUpServer(getGenericFullPathForSrcTest("/ovms/src/test/mediapipe/config_mediapipe_multipart_mock.json").c_str()); + ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); + } + + void TearDown() { + handler.reset(); + server.setShutdownRequest(1); + t->join(); + server.setShutdownRequest(0); + } +}; + +TEST_F(MultiPartCalculatorTest, UnaryWithModelField) { // only unary, there is no way to stream + headers["content-type"] = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"; + + comp = ovms::HttpRequestComponents(); + ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); + + std::string requestBody = R"( +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="username" + +john_doe +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="email" + +john@example.com +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="model" + +multipart +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="doc"; filename="notes.txt" +Content-Type: text/plain + +this is file content +It has two lines. +------WebKitFormBoundary7MA4YWxkTrZu0gW--)"; + + EXPECT_CALL(*multiPartParser, parse()).WillOnce(::testing::Return(true)); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("model"))).WillOnce(::testing::Return("multipart")); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("email"))).WillOnce(::testing::Return("john@example.com")); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("username"))).WillOnce(::testing::Return("john_doe")); + EXPECT_CALL(*multiPartParser, getFileContentByName(::testing::Eq("file"))).WillOnce([](const std::string& name) { + static std::string retval{"this is file content\nIt has two lines."}; + return std::string_view(retval); + }); + + ASSERT_EQ( + handler->dispatchToProcessor("/v3/v1/completions/", requestBody, &response, comp, responseComponents, writer, multiPartParser), + ovms::StatusCode::OK); + + std::string expectedResponse = R"(john@example.com+john_doe +this is file content +It has two lines.)"; + ASSERT_EQ(response, expectedResponse); +} + +TEST_F(MultiPartCalculatorTest, UnaryWithMissingModelFieldDefaultRouting) { + headers["content-type"] = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"; + + comp = ovms::HttpRequestComponents(); + ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); + + std::string requestBody = R"( +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="username" + +john_doe +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="email" + +john@example.com +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="doc"; filename="notes.txt" +Content-Type: text/plain + +this is file content +It has two lines. +------WebKitFormBoundary7MA4YWxkTrZu0gW--)"; + + EXPECT_CALL(*multiPartParser, parse()).WillOnce(::testing::Return(true)); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("model"))).WillOnce(::testing::Return("")); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("email"))).WillOnce(::testing::Return("john@example.com")); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("username"))).WillOnce(::testing::Return("john_doe")); + EXPECT_CALL(*multiPartParser, getFileContentByName(::testing::Eq("file"))).WillOnce([](const std::string& name) { + static std::string retval{"this is file content\nIt has two lines."}; + return std::string_view(retval); + }); + + // Default routing uses everything that comes after /v3/ as graph name + const std::string URI = "/v3/multipart"; + + ASSERT_EQ( + handler->dispatchToProcessor(URI, requestBody, &response, comp, responseComponents, writer, multiPartParser), + ovms::StatusCode::OK); + + std::string expectedResponse = R"(john@example.com+john_doe +this is file content +It has two lines.)"; + ASSERT_EQ(response, expectedResponse); +} + +TEST_F(MultiPartCalculatorTest, UnaryWithMissingModelFieldDefaultRoutingWrongGraphName) { + headers["content-type"] = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"; + + comp = ovms::HttpRequestComponents(); + ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); + + std::string requestBody = R"( +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="username" + +john_doe +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="email" + +john@example.com +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="doc"; filename="notes.txt" +Content-Type: text/plain + +this is file content +It has two lines. +------WebKitFormBoundary7MA4YWxkTrZu0gW--)"; + + EXPECT_CALL(*multiPartParser, parse()).WillOnce(::testing::Return(true)); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::_)).Times(0); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("model"))).WillOnce(::testing::Return("")); + EXPECT_CALL(*multiPartParser, getFileContentByName(::testing::_)).Times(0); + + // Default routing uses everything that comes after /v3/ as graph name + const std::string URI = "/v3/NON_EXISTENT"; + + ASSERT_EQ( + handler->dispatchToProcessor(URI, requestBody, &response, comp, responseComponents, writer, multiPartParser), + ovms::StatusCode::MEDIAPIPE_DEFINITION_NAME_MISSING); +} + +TEST_F(MultiPartCalculatorTest, UnaryWithMissingModelFieldDefaultRoutingMissingGraphNameInURI) { + headers["content-type"] = "multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"; + + comp = ovms::HttpRequestComponents(); + ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpoint, headers), ovms::StatusCode::OK); + + std::string requestBody = R"( +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="username" + +john_doe +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="email" + +john@example.com +------WebKitFormBoundary7MA4YWxkTrZu0gW +Content-Disposition: form-data; name="doc"; filename="notes.txt" +Content-Type: text/plain + +this is file content +It has two lines. +------WebKitFormBoundary7MA4YWxkTrZu0gW--)"; + + EXPECT_CALL(*multiPartParser, parse()).WillOnce(::testing::Return(true)); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::_)).Times(0); + EXPECT_CALL(*multiPartParser, getFieldByName(::testing::Eq("model"))).WillOnce(::testing::Return("")); + EXPECT_CALL(*multiPartParser, getFileContentByName(::testing::_)).Times(0); + + // Default routing uses everything that comes after /v3/ as graph name + const std::string URI = "/v3/"; + + ASSERT_EQ( + handler->dispatchToProcessor(URI, requestBody, &response, comp, responseComponents, writer, multiPartParser), + ovms::StatusCode::REST_INVALID_URL); +} diff --git a/src/test/reranknode_test.cpp b/src/test/reranknode_test.cpp index d46bb18417..c2e15128ef 100644 --- a/src/test/reranknode_test.cpp +++ b/src/test/reranknode_test.cpp @@ -33,11 +33,12 @@ class V3HttpTest : public ::testing::Test { public: std::unique_ptr handler; - std::vector> headers; + std::unordered_map headers{{"content-type", "application/json"}}; ovms::HttpRequestComponents comp; const std::string endpointEmbeddings = "/v3/embeddings"; const std::string endpointRerank = "/v3/rerank"; std::shared_ptr writer; + std::shared_ptr multiPartParser; std::string response; ovms::HttpResponseComponents responseComponents; @@ -50,6 +51,7 @@ class V3HttpTest : public ::testing::Test { void SetUp() { writer = std::make_shared(); + multiPartParser = std::make_shared(); ovms::Server& server = ovms::Server::instance(); handler = std::make_unique(server, 5); ASSERT_EQ(handler->parseRequestComponents(comp, "POST", endpointEmbeddings, headers), ovms::StatusCode::OK); @@ -97,7 +99,7 @@ TEST_F(RerankHttpTest, simplePositive) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -129,7 +131,7 @@ TEST_F(RerankHttpTest, positiveTopN) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -161,7 +163,7 @@ TEST_F(RerankHttpTest, positiveReturnDocuments) { } )"; ASSERT_EQ( - handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); rapidjson::Document d; rapidjson::ParseResult ok = d.Parse(response.c_str()); @@ -237,7 +239,7 @@ TEST_F(RerankWithParamsHttpTest, PositiveMaxAllowedChunksNotExceeded) { std::string requestBody = buffer.GetString(); ASSERT_EQ( - handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer), + handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser), ovms::StatusCode::OK); } @@ -265,7 +267,7 @@ TEST_F(RerankWithParamsHttpTest, MaxAllowedChunksExceededByDocumentsBeforeChunki document.Accept(wr); std::string requestBody = buffer.GetString(); - auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); ASSERT_THAT(status.string(), ::testing::HasSubstr("Number of documents exceeds max_allowed_chunks")); // 5 because we prepared 1 document more than allowed } @@ -297,7 +299,7 @@ TEST_F(RerankWithParamsHttpTest, MaxAllowedChunksExceededAfterChunking) { document.Accept(wr); std::string requestBody = buffer.GetString(); - auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR) << status.string(); ASSERT_THAT(status.string(), ::testing::HasSubstr("Chunking failed: exceeding max_allowed_chunks after chunking limit: 4; actual: 8")); // 8 because of the last document which was chunked to 5 documents, 3 + 5 = 8 } @@ -352,7 +354,7 @@ TEST_F(RerankWithInvalidParamsHttpTest, AnyRequestNegativeWithInvalidSetup) { document.Accept(wr); std::string requestBody = buffer.GetString(); - auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer); + auto status = handler->dispatchToProcessor(endpointRerank, requestBody, &response, comp, responseComponents, writer, multiPartParser); ASSERT_EQ(status, ovms::StatusCode::MEDIAPIPE_EXECUTION_ERROR); ASSERT_THAT(status.string(), ::testing::HasSubstr("max_position_embeddings should be larger than 2 * NUMBER_OF_SPECIAL_TOKENS")); } diff --git a/src/test/test_http_utils.hpp b/src/test/test_http_utils.hpp index c78c40b2f3..aace1f2c11 100644 --- a/src/test/test_http_utils.hpp +++ b/src/test/test_http_utils.hpp @@ -16,6 +16,7 @@ #pragma once #include #include +#include #include #include @@ -24,6 +25,7 @@ #include "../http_async_writer_interface.hpp" #include "../http_server.hpp" #include "../http_status_code.hpp" +#include "../multi_part_parser.hpp" class MockedServerRequestInterface final : public ovms::HttpAsyncWriter { public: @@ -35,3 +37,11 @@ class MockedServerRequestInterface final : public ovms::HttpAsyncWriter { MOCK_METHOD(void, RegisterDisconnectionCallback, (std::function), (override)); MOCK_METHOD(void, PartialReplyBegin, (std::function), (override)); }; + +class MockedMultiPartParser final : public ovms::MultiPartParser { +public: + MOCK_METHOD(bool, parse, (), (override)); + MOCK_METHOD(bool, hasParseError, (), (const override)); + MOCK_METHOD(std::string, getFieldByName, (const std::string&), (const override)); + MOCK_METHOD(std::string_view, getFileContentByName, (const std::string&), (const override)); +};