Skip to content

Commit 8b8400a

Browse files
authored
Support for multipart requests, new ways of routing the request to MP graph (#3250)
CVS-163721 If request content-type is application-json - route based on model JSON field If request content-type s multipart - route based on mode multipart field In other cases try to deduce graph name from URI: /v3/GRAPH-NAME - multipart field/file access for calculators - unit tests
1 parent 40a4662 commit 8b8400a

38 files changed

+1164
-361
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,4 @@ out
3636
*.errors.txt
3737
tmp/
3838
*.zip
39+
*.tar.gz

common_settings.bzl

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,8 +123,7 @@ def create_config_settings():
123123
###############################
124124
# compilation settings
125125
###############################
126-
COMMON_STATIC_LIBS_COPTS = select({
127-
"//conditions:default": [
126+
LINUX_COMMON_STATIC_LIBS_COPTS = [
128127
"-Wall",
129128
# TODO: was in ovms bin "-Wconversion",
130129
"-Wno-unknown-pragmas",
@@ -133,8 +132,9 @@ COMMON_STATIC_LIBS_COPTS = select({
133132
"-Werror",
134133
# ov::Tensor::data method call results in deprecated warning and we use it in multiple places
135134
"-Wno-deprecated-declarations",
136-
],
137-
"//src:windows" : [
135+
]
136+
137+
WINDOWS_COMMON_STATIC_LIBS_COPTS = [
138138
"/W4",
139139
"/WX",
140140
"/external:anglebrackets",
@@ -157,7 +157,11 @@ COMMON_STATIC_LIBS_COPTS = select({
157157
"/wd4702",
158158
"/wd4267",
159159
"/wd4996",
160-
],
160+
]
161+
162+
COMMON_STATIC_LIBS_COPTS = select({
163+
"//conditions:default": LINUX_COMMON_STATIC_LIBS_COPTS,
164+
"//src:windows" : WINDOWS_COMMON_STATIC_LIBS_COPTS,
161165
})
162166

163167
COMMON_STATIC_TEST_COPTS = select({

src/BUILD

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -615,8 +615,14 @@ cc_library(
615615
"//src/embeddings:embeddingscalculator",
616616
"//src/rerank:rerankcalculator",],
617617
}) + select({
618-
"//:enable_drogon": ["libdrogon_http_server"],
619-
"//conditions:default" : ["libnet_http_server"],
618+
"//:enable_drogon": [
619+
"libdrogon_http_server",
620+
"libmultipart_parser_drogon_impl",
621+
],
622+
"//conditions:default" : [
623+
"libnet_http_server",
624+
# Add net-http implementation here if we ever want one
625+
],
620626
}) + [
621627
"cpp_headers",
622628
"ovms_header",
@@ -811,6 +817,7 @@ cc_library(
811817
deps = [
812818
"@com_github_tencent_rapidjson//:rapidjson",
813819
"libovmsclient_connection",
820+
"libmultipart_parser",
814821
],
815822
visibility = ["//visibility:public"],
816823
copts = COPTS_ADJUSTED,
@@ -829,6 +836,7 @@ cc_library(
829836
linkopts = LINKOPTS_ADJUSTED,
830837
alwayslink = 1,
831838
)
839+
832840
cc_library(
833841
name = "libhttpclientconnection",
834842
hdrs = ["http_frontend/http_client_connection.hpp"],
@@ -843,6 +851,31 @@ cc_library(
843851
alwayslink = 1,
844852
)
845853

854+
cc_library(
855+
name = "libmultipart_parser",
856+
hdrs = ["multi_part_parser.hpp"],
857+
deps = [
858+
],
859+
visibility = ["//visibility:public",],
860+
local_defines = COMMON_LOCAL_DEFINES,
861+
copts = COPTS_ADJUSTED,
862+
linkopts = LINKOPTS_ADJUSTED,
863+
)
864+
865+
cc_library(
866+
name = "libmultipart_parser_drogon_impl",
867+
hdrs = ["http_frontend/multi_part_parser_drogon_impl.hpp"],
868+
srcs = ["http_frontend/multi_part_parser_drogon_impl.cpp"],
869+
deps = [
870+
"libmultipart_parser",
871+
"@drogon//:drogon_cmake",
872+
],
873+
visibility = ["//visibility:public",],
874+
local_defines = COMMON_LOCAL_DEFINES,
875+
copts = COPTS_ADJUSTED,
876+
linkopts = LINKOPTS_ADJUSTED,
877+
)
878+
846879
cc_library(
847880
name = "libovms_module",
848881
hdrs = ["module.hpp"],
@@ -2757,6 +2790,7 @@ cc_test(
27572790
"test/get_mediapipe_graph_metadata_response_test.cpp",
27582791
"test/mediapipe_framework_test.cpp",
27592792
"test/http_openai_handler_test.cpp",
2793+
"test/multipart_calculator_test.cpp",
27602794
],
27612795
"//:disable_mediapipe" : [
27622796
"test/disabled_mediapipe_test.cpp",
@@ -2776,6 +2810,12 @@ cc_test(
27762810
"test/llm/visual_language_model/initialization_test.cpp",
27772811
],
27782812
"//:disable_python" : [],
2813+
}) + select({
2814+
"//:enable_drogon": [
2815+
"test/multi_part_parser_drogon_test.cpp",
2816+
],
2817+
"//conditions:default" : [
2818+
],
27792819
}),
27802820
data = [
27812821
"test/add_two_inputs_model/1/add.xml",
@@ -2827,6 +2867,7 @@ cc_test(
28272867
"test/mediapipe/config_mediapipe_graph_with_side_packets.json",
28282868
"test/mediapipe/config_mediapipe_two_inputs.json",
28292869
"test/mediapipe/config_mediapipe_two_outputs_dag.json",
2870+
"test/mediapipe/config_mediapipe_multipart_mock.json",
28302871
"test/mediapipe/config_mp_tf_passthrough.json",
28312872
"test/mediapipe/config_standard_add.json",
28322873
"test/mediapipe/config_standard_dummy.json",
@@ -2845,6 +2886,7 @@ cc_test(
28452886
"test/mediapipe/graphdummyadapterfull_dummyinputnames.pbtxt",
28462887
"test/mediapipe/graphadapterfull_two_outputs_dag.pbtxt",
28472888
"test/mediapipe/graphdummyadapterfull_two_outputs.pbtxt",
2889+
"test/mediapipe/graph_multipart.pbtxt",
28482890
"test/mediapipe/negative/config_exception_during_process.json",
28492891
"test/mediapipe/negative/config_no_calc_output_stream.json",
28502892
"test/mediapipe/negative/graph_exception_during_process.pbtxt",
@@ -2927,6 +2969,7 @@ cc_test(
29272969
"//conditions:default": [
29282970
"//src/llm:genai_servables",
29292971
"//src/test/mediapipe/calculators:mediapipe_test_calculators",
2972+
"//src/test/mediapipe/calculators:dependency_free_http_test_calculators",
29302973
"@mediapipe//mediapipe/calculators/ovms:ovms_calculator",
29312974
"@mediapipe//mediapipe/framework:calculator_runner",
29322975
],

src/embeddings/BUILD

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ cc_library(
6464
name = "embeddings_api",
6565
hdrs = ["embeddings_api.hpp"],
6666
srcs = ["embeddings_api.cpp"],
67-
deps = ["@mediapipe//mediapipe/framework:calculator_framework",
67+
deps = ["//src:libovmslogging",
68+
"@mediapipe//mediapipe/framework:calculator_framework",
6869
"//third_party:openvino",
6970
"@com_github_tencent_rapidjson//:rapidjson",],
7071
visibility = ["//visibility:public"],

src/embeddings/embeddings_api.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include <utility>
2121
#include <variant>
2222

23+
#include "../logging.hpp"
24+
2325
#pragma warning(push)
2426
#pragma warning(disable : 4005 4309 6001 6386 6011 6246)
2527
#pragma GCC diagnostic push
@@ -128,6 +130,12 @@ std::variant<EmbeddingsRequest, std::string> EmbeddingsRequest::fromJson(rapidjs
128130
}
129131

130132
absl::Status EmbeddingsHandler::parseRequest() {
133+
// Parsed JSON is not guaranteed to be valid, we may reach this point via multipart content type request with no valid JSON parser
134+
if (this->doc.HasParseError()) {
135+
SPDLOG_LOGGER_DEBUG(embeddings_calculator_logger, "Non-json request received in embeddings calculator");
136+
return absl::InvalidArgumentError("Non-json request received in embeddings calculator");
137+
}
138+
131139
auto parsed = EmbeddingsRequest::fromJson(&(this->doc));
132140

133141
if (auto error = std::get_if<std::string>(&parsed)) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//*****************************************************************************
2+
// Copyright 2025 Intel Corporation
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//*****************************************************************************
16+
#include "multi_part_parser_drogon_impl.hpp"
17+
18+
namespace ovms {
19+
20+
bool DrogonMultiPartParser::parse() {
21+
this->hasParseError_ = this->parser->parse(request) != 0;
22+
return !this->hasParseError_;
23+
}
24+
25+
bool DrogonMultiPartParser::hasParseError() const {
26+
return this->hasParseError_;
27+
}
28+
29+
std::string DrogonMultiPartParser::getFieldByName(const std::string& name) const {
30+
return this->parser->getParameter<std::string>(name);
31+
}
32+
33+
std::string_view DrogonMultiPartParser::getFileContentByFieldName(const std::string& name) const {
34+
auto fileMap = this->parser->getFilesMap();
35+
36+
auto it = fileMap.find(name);
37+
if (it == fileMap.end()) {
38+
return "";
39+
}
40+
return it->second.fileContent();
41+
}
42+
43+
} // namespace ovms
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//*****************************************************************************
2+
// Copyright 2025 Intel Corporation
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//*****************************************************************************
16+
#pragma once
17+
18+
#include "../multi_part_parser.hpp"
19+
20+
#pragma warning(push)
21+
#pragma warning(disable : 6326)
22+
#include <drogon/drogon.h>
23+
#pragma warning(pop)
24+
25+
#include <string>
26+
#include <string_view>
27+
#include <memory>
28+
29+
namespace ovms {
30+
31+
class DrogonMultiPartParser : public MultiPartParser {
32+
bool hasParseError_{true};
33+
const drogon::HttpRequestPtr request{nullptr};
34+
const std::shared_ptr<drogon::MultiPartParser> parser{nullptr};
35+
36+
public:
37+
DrogonMultiPartParser(const drogon::HttpRequestPtr& request) :
38+
request(request),
39+
parser(std::make_shared<drogon::MultiPartParser>()) {}
40+
41+
bool parse() override;
42+
43+
bool hasParseError() const override;
44+
45+
std::string getFieldByName(const std::string& name) const override;
46+
std::string_view getFileContentByFieldName(const std::string& name) const override;
47+
};
48+
49+
} // namespace ovms

src/http_payload.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <memory>
1919
#include <string>
20+
#include <unordered_map>
2021
#include <utility>
2122
#include <vector>
2223
#pragma warning(push)
@@ -25,15 +26,17 @@
2526
#pragma warning(pop)
2627

2728
#include "client_connection.hpp"
29+
#include "multi_part_parser.hpp"
2830

2931
namespace ovms {
3032

3133
struct HttpPayload {
3234
std::string uri;
33-
std::vector<std::pair<std::string, std::string>> headers;
35+
std::unordered_map<std::string, std::string> headers;
3436
std::string body; // always
3537
std::shared_ptr<rapidjson::Document> parsedJson; // pre-parsed body = null
3638
std::shared_ptr<ClientConnection> client;
39+
std::shared_ptr<MultiPartParser> multipartParser;
3740
};
3841

3942
} // namespace ovms

0 commit comments

Comments
 (0)