Skip to content

Support for multipart requests, new ways of routing the request to MP graph #3250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from

Conversation

dkalinowski
Copy link
Collaborator

@dkalinowski dkalinowski commented Apr 18, 2025

🛠 Summary

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

🧪 Checklist

  • Unit tests added.
  • Change follows security best practices.

@dkalinowski dkalinowski added the WIP Do not merge until resolved label Apr 18, 2025
@dkalinowski dkalinowski requested review from atobiszei and mzegla April 22, 2025 11:15
src/BUILD Outdated
@@ -810,6 +816,7 @@ cc_library(
deps = [
"@com_github_tencent_rapidjson//:rapidjson",
"libovmsclient_connection",
"libovmsmulti_part_parser",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why split multi and part? How about naming it libovmsmultipart_parser
I can see such split is made in other places as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, also removing ovms from name per @atobiszei request

@@ -0,0 +1,49 @@
//*****************************************************************************
// Copyright 2024 Intel Corporation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2024 Intel Corporation
// Copyright 2025 Intel Corporation

return this->parser->getParameter<std::string>(name);
}

std::string_view DrogonMultiPartParser::getFileContentByName(const std::string& name) const {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work based on extension? I wonder how much can we trust the name in establishing the content type.
Yet I'm not sure if we should dive into this here since we use third party parser.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not file name, but field name. it is not clear from method name, will change that to getFileContentByFieldName

if (isMultiPart) {
OVMS_PROFILE_SCOPE("multipart parse");
if (!multiPartParser->parse()) {
return StatusCode::REST_INVALID_URL;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add logger?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no specific logger for http component, however, I will add specific status codes

SPDLOG_DEBUG("Model name from deduced from MultiPart field: {}", model_name);
}
// Set json parser in invalid state in order to get HasParseError to respond with true
doc->Parse("error");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to rename doc to jsonParser? What do you think about such idea?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is already named parsedJson in HttpPayload class, will name it similarly here

if (uri.size() <= 4) { // nothing after "/v3/..."
return StatusCode::REST_INVALID_URL;
}
model_name = std::string(uri.substr(4));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we assume model name comes right after /v3/.
Don't we want to nest it a bit like /v3/models/<model_name> or something like that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its good idea, with this we wont be able to support OpenAI image variation API:
https://platform.openai.com/docs/api-reference/images/createVariation

@@ -838,11 +879,11 @@ Status HttpRestApiHandler::processRequest(

headers->clear();
response->clear();
headers->push_back({"Content-Type", "application/json"});
(*headers)["content-type"] = "application/json";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this line still be here since we can now support other content types and here we do hard assignment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very good catch. all our tests use dispatchToProcessor directly, therefore we never noticed this overwrite! will remove it

ovms::StatusCode::OK);

std::string expectedResponse = R"(/v3/v1/completions/

content-typeapplication/json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is that correct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the calculator concatenates all headers keys and values so we can test the final output

if (!doc->IsObject()) {
return Status(StatusCode::JSON_INVALID, "JSON body must be an object");
}
std::string model_name;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::string model_name;
std::string modelName;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

});

ASSERT_EQ(
handler->dispatchToProcessor("/v3/v1/completions/", requestBody, &response, comp, responseComponents, writer, multiPartParser),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using /v3/v1/completions here is a bit confusing to me. It's pretty much OpenAI dedicated URI and here it's used for completely different thing. If I understand it correctly you could use any other v3 uri like /v3/test/multipart and it would work since model name is in the content right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only a test and does not matter what URI we pass. i can change it to any but multipart. this test ensures that we route not based on URI, but rather multipart model field

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will change to any URI and comment

src/BUILD Outdated
local_defines = COMMON_LOCAL_DEFINES,
copts = COPTS_ADJUSTED,
linkopts = LINKOPTS_ADJUSTED,
alwayslink = 1,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it required?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it wasnt needed, removed that

bool parse() override;

bool hasParseError() const override;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment that it returns empty in case of error

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added

@@ -49,6 +49,7 @@ cc_library(
"streaming_test_calculator.cpp",
"two_input_calculator.cpp",
"openai_chat_completions_mock_calculator.cpp",
"multipart_accepting_calculator.cpp",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calculator will be rebuild with every change in ovms_lib and its dependencies. We could depend only onhttp payload. Please check if other calculators are easily splittable as well.

request(request),
parser(std::make_shared<drogon::MultiPartParser>()) {}

bool parse() override;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need both parse & hasParseError?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parse is called once, by ovms core in processV3 during route logic, hasParseError is API for calculator users, to check if request was valid multipart content

model_name = std::string(uri.substr(4));
SPDLOG_DEBUG("Model name from deduced from URI: {}", model_name);
// Set json parser in invalid state in order to get HasParseError to respond with true
doc->Parse("error");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make it more explicit what happens here and why is it required

ensureDocInErrorState?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@@ -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<HttpAsyncWriter> 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<HttpAsyncWriter> serverReaderWriter, std::shared_ptr<MultiPartParser> multiPartParser) -> Status {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this need to be shared_ptr?

ovms::StatusCode::OK);

std::string expectedResponse = R"(/v3/v1/completions/

content-typeapplication/json
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
content-typeapplication/json
Key: content-type Value: application/json

???

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix it to be more readable

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WIP Do not merge until resolved
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants