Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ test:linux --test_env LD_LIBRARY_PATH=/opt/opencv/lib/:/opt/intel/openvino/runti
test:linux --test_env OPENVINO_TOKENIZERS_PATH_GENAI=/opt/intel/openvino/runtime/lib/intel64/libopenvino_tokenizers.so
test:linux --test_env PYTHONPATH=/opt/intel/openvino/python:/ovms/bazel-bin/src/python/binding
test:linux --test_env no_proxy=localhost
test:linux --test_env "OVMS_MEDIA_URL_ALLOW_REDIRECTS=1"

# Bazelrc imports ############################################################################################################################
# file below should contain sth like
Expand Down
1 change: 1 addition & 0 deletions src/capi_frontend/server_settings.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ struct ServerSettingsImpl {
std::string metricsList;
std::string cpuExtensionLibraryPath;
std::optional<std::string> allowedLocalMediaPath;
std::optional<std::vector<std::string>> allowedMediaDomains;
std::string logLevel = "INFO";
std::string logPath;
bool allowCredentials = false;
Expand Down
7 changes: 7 additions & 0 deletions src/cli_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ std::variant<bool, std::pair<int, std::string>> CLIParser::parse(int argc, char*
"A path to shared library containing custom CPU layer implementation. Default: empty.",
cxxopts::value<std::string>()->default_value(""),
"CPU_EXTENSION")
("allowed_media_domains",
"Path to directory that contains multimedia files that can be used as input for LLMs.",
cxxopts::value<std::vector<std::string>>(),
"ALLOWED_MEDIA_DOMAINS")
("allowed_local_media_path",
"Path to directory that contains multimedia files that can be used as input for LLMs.",
cxxopts::value<std::string>(),
Expand Down Expand Up @@ -502,6 +506,9 @@ void CLIParser::prepareServer(ServerSettingsImpl& serverSettings) {
if (result->count("cpu_extension")) {
serverSettings.cpuExtensionLibraryPath = result->operator[]("cpu_extension").as<std::string>();
}
if (result->count("allowed_media_domains")) {
serverSettings.allowedMediaDomains = result->operator[]("allowed_media_domains").as<std::vector<std::string>>();
}
if (result->count("allowed_local_media_path")) {
serverSettings.allowedLocalMediaPath = result->operator[]("allowed_local_media_path").as<std::string>();
}
Expand Down
33 changes: 27 additions & 6 deletions src/llm/apis/openai_completions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "src/port/rapidjson_stringbuffer.hpp"
#include "src/port/rapidjson_writer.hpp"
#include <set>
#include <string.h>

#include "openai_json_response.hpp"

Expand Down Expand Up @@ -113,7 +114,10 @@ static absl::Status downloadImage(const char* url, std::string& image, const int
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, appendChunkCallback))
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, &image))
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA))
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L))
const char* envAllowRedirects = std::getenv("OVMS_MEDIA_URL_ALLOW_REDIRECTS");
if (envAllowRedirects != nullptr && (std::strcmp(envAllowRedirects, "1") == 0)) {
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L))
}
CURL_SETOPT(curl_easy_setopt(curl_handle, CURLOPT_MAXFILESIZE, sizeLimit))

if (status != CURLE_OK) {
Expand Down Expand Up @@ -159,7 +163,7 @@ absl::Status OpenAIChatCompletionsHandler::ensureArgumentsInToolCalls(Value& mes
return absl::OkStatus();
}

absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::string> allowedLocalMediaPath) {
absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::string> allowedLocalMediaPath, std::optional<std::vector<std::string>> allowedMediaDomains) {
auto it = doc.FindMember("messages");
if (it == doc.MemberEnd())
return absl::InvalidArgumentError("Messages missing in request");
Expand Down Expand Up @@ -237,6 +241,23 @@ absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::stri
} else if (std::regex_match(url.c_str(), std::regex("^(http|https|ftp|sftp|)://(.*)"))) {
SPDLOG_LOGGER_TRACE(llm_calculator_logger, "Loading image using curl");
int64_t sizeLimit = 20000000; // restrict single image size to 20MB
bool allowedDomain = false;
if (!allowedMediaDomains.has_value()) {
return absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains");
}
for (auto domain : allowedMediaDomains.value()) {
const auto firstMissmatch = std::mismatch(url.begin(), url.end(), domain.begin(), domain.end());
if (firstMissmatch.second == domain.end()) {
allowedDomain = true;
break;
}
if (domain == "*") {
allowedDomain = true;
}
}
if (!allowedDomain) {
return absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains");
}
auto status = downloadImage(url.c_str(), decoded, sizeLimit);
if (status != absl::OkStatus()) {
return status;
Expand Down Expand Up @@ -469,9 +490,9 @@ std::string convertOpenAIResponseFormatToStructuralTagStringFormat(const rapidjs
return buffer.GetString();
}

absl::Status OpenAIChatCompletionsHandler::parseChatCompletionsPart(std::optional<uint32_t> maxTokensLimit, std::optional<std::string> allowedLocalMediaPath) {
absl::Status OpenAIChatCompletionsHandler::parseChatCompletionsPart(std::optional<uint32_t> maxTokensLimit, std::optional<std::string> allowedLocalMediaPath, std::optional<std::vector<std::string>> allowedMediaDomains) {
// messages: [{role: content}, {role: content}, ...]; required
auto status = parseMessages(allowedLocalMediaPath);
auto status = parseMessages(allowedLocalMediaPath, allowedMediaDomains);
if (status != absl::OkStatus()) {
return status;
}
Expand Down Expand Up @@ -791,14 +812,14 @@ void OpenAIChatCompletionsHandler::incrementProcessedTokens(size_t numTokens) {
usage.completionTokens += numTokens;
}

absl::Status OpenAIChatCompletionsHandler::parseRequest(std::optional<uint32_t> maxTokensLimit, uint32_t bestOfLimit, std::optional<uint32_t> maxModelLength, std::optional<std::string> allowedLocalMediaPath) {
absl::Status OpenAIChatCompletionsHandler::parseRequest(std::optional<uint32_t> maxTokensLimit, uint32_t bestOfLimit, std::optional<uint32_t> maxModelLength, std::optional<std::string> allowedLocalMediaPath, std::optional<std::vector<std::string>> allowedMediaDomains) {
absl::Status status = parseCommonPart(maxTokensLimit, bestOfLimit, maxModelLength);
if (status != absl::OkStatus())
return status;
if (endpoint == Endpoint::COMPLETIONS)
status = parseCompletionsPart();
else
status = parseChatCompletionsPart(maxTokensLimit, allowedLocalMediaPath);
status = parseChatCompletionsPart(maxTokensLimit, allowedLocalMediaPath, allowedMediaDomains);

return status;
}
Expand Down
6 changes: 3 additions & 3 deletions src/llm/apis/openai_completions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class OpenAIChatCompletionsHandler {
std::unique_ptr<OutputParser> outputParser = nullptr;

absl::Status parseCompletionsPart();
absl::Status parseChatCompletionsPart(std::optional<uint32_t> maxTokensLimit, std::optional<std::string> allowedLocalMediaPath);
absl::Status parseChatCompletionsPart(std::optional<uint32_t> maxTokensLimit, std::optional<std::string> allowedLocalMediaPath, std::optional<std::vector<std::string>> allowedMediaDomains);
absl::Status parseCommonPart(std::optional<uint32_t> maxTokensLimit, uint32_t bestOfLimit, std::optional<uint32_t> maxModelLength);

ParsedOutput parseOutputIfNeeded(const std::vector<int64_t>& generatedIds);
Expand Down Expand Up @@ -112,8 +112,8 @@ class OpenAIChatCompletionsHandler {

void incrementProcessedTokens(size_t numTokens = 1);

absl::Status parseRequest(std::optional<uint32_t> maxTokensLimit, uint32_t bestOfLimit, std::optional<uint32_t> maxModelLength, std::optional<std::string> allowedLocalMediaPath = std::nullopt);
absl::Status parseMessages(std::optional<std::string> allowedLocalMediaPath = std::nullopt);
absl::Status parseRequest(std::optional<uint32_t> maxTokensLimit, uint32_t bestOfLimit, std::optional<uint32_t> maxModelLength, std::optional<std::string> allowedLocalMediaPath = std::nullopt, std::optional<std::vector<std::string>> allowedMediaDomains = std::nullopt);
absl::Status parseMessages(std::optional<std::string> allowedLocalMediaPath = std::nullopt, std::optional<std::vector<std::string>> allowedMediaDomains = std::nullopt);
absl::Status parseTools();
const bool areToolsAvailable() const;

Expand Down
2 changes: 1 addition & 1 deletion src/llm/visual_language_model/legacy/servable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ absl::Status VisualLanguageModelLegacyServable::parseRequest(std::shared_ptr<Gen
getProperties()->tokenizer);
auto& config = ovms::Config::instance();

auto status = executionContext->apiHandler->parseRequest(getProperties()->maxTokensLimit, getProperties()->bestOfLimit, getProperties()->maxModelLength, config.getServerSettings().allowedLocalMediaPath);
auto status = executionContext->apiHandler->parseRequest(getProperties()->maxTokensLimit, getProperties()->bestOfLimit, getProperties()->maxModelLength, config.getServerSettings().allowedLocalMediaPath, config.getServerSettings().allowedMediaDomains);
if (!status.ok()) {
SPDLOG_LOGGER_ERROR(llm_calculator_logger, "Failed to parse request: {}", status.message());
return status;
Expand Down
70 changes: 68 additions & 2 deletions src/test/http_openai_handler_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,8 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttp) {
doc.Parse(json.c_str());
ASSERT_FALSE(doc.HasParseError());
std::shared_ptr<ovms::OpenAIChatCompletionsHandler> apiHandler = std::make_shared<ovms::OpenAIChatCompletionsHandler>(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer);
ASSERT_EQ(apiHandler->parseMessages(), absl::OkStatus());
std::vector<std::string> allowedDomains = {"http://raw.githubusercontent.com"};
ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus());
const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory();
ASSERT_EQ(imageHistory.size(), 1);
auto [index, image] = imageHistory[0];
Expand Down Expand Up @@ -499,7 +500,44 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttps) {
doc.Parse(json.c_str());
ASSERT_FALSE(doc.HasParseError());
std::shared_ptr<ovms::OpenAIChatCompletionsHandler> apiHandler = std::make_shared<ovms::OpenAIChatCompletionsHandler>(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer);
ASSERT_EQ(apiHandler->parseMessages(), absl::OkStatus());
std::vector<std::string> allowedDomains = {"https://raw.githubusercontent.com"};
ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus());
const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory();
ASSERT_EQ(imageHistory.size(), 1);
auto [index, image] = imageHistory[0];
EXPECT_EQ(index, 0);
EXPECT_EQ(image.get_element_type(), ov::element::u8);
EXPECT_EQ(image.get_size(), 225792);
json = apiHandler->getProcessedJson();
EXPECT_EQ(json, std::string("{\"model\":\"llama\",\"messages\":[{\"role\":\"user\",\"content\":\"What is in this image?\"}]}"));
}

TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesSucceedsUrlHttpsAllowedDomainWildcard) {
std::string json = R"({
"model": "llama",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?"
},
{
"type": "image_url",
"image_url": {
"url": "https://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg"
}
}
]
}
]
})";
doc.Parse(json.c_str());
ASSERT_FALSE(doc.HasParseError());
std::shared_ptr<ovms::OpenAIChatCompletionsHandler> apiHandler = std::make_shared<ovms::OpenAIChatCompletionsHandler>(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer);
std::vector<std::string> allowedDomains = {"*"};
ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::OkStatus());
const ovms::ImageHistory& imageHistory = apiHandler->getImageHistory();
ASSERT_EQ(imageHistory.size(), 1);
auto [index, image] = imageHistory[0];
Expand Down Expand Up @@ -572,6 +610,34 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesImageStringWithNoPrefixFails
EXPECT_EQ(apiHandler->parseMessages(), absl::InvalidArgumentError("Loading images from local filesystem is disabled."));
}

TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesFailsUrlHttpNotAllowedDomain) {
std::string json = R"({
"model": "llama",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "What is in this image?"
},
{
"type": "image_url",
"image_url": {
"url": "http://raw.githubusercontent.com/openvinotoolkit/model_server/refs/heads/main/demos/common/static/images/zebra.jpeg"
}
}
]
}
]
})";
doc.Parse(json.c_str());
ASSERT_FALSE(doc.HasParseError());
std::shared_ptr<ovms::OpenAIChatCompletionsHandler> apiHandler = std::make_shared<ovms::OpenAIChatCompletionsHandler>(doc, ovms::Endpoint::CHAT_COMPLETIONS, std::chrono::system_clock::now(), *tokenizer);
std::vector<std::string> allowedDomains = {"http://wikipedia.com"};
ASSERT_EQ(apiHandler->parseMessages(std::nullopt, allowedDomains), absl::InvalidArgumentError("Given url does not match any allowed domain from allowed_media_domains"));
}

TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesImageLocalFilesystem) {
std::string json = R"({
"model": "llama",
Expand Down