Skip to content
Merged
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
14 changes: 9 additions & 5 deletions src/llm/apis/openai_completions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,7 @@ absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::stri
return absl::InvalidArgumentError("Invalid message structure - content array is empty");
}
jsonChanged = true;
Value contentText(rapidjson::kStringType);
contentText.SetString("", doc.GetAllocator());
std::string combinedText;
for (auto& v : member->value.GetArray()) {
if (!v.IsObject()) {
return absl::InvalidArgumentError("Invalid message structure - content array should contain objects");
Comment thread
mzegla marked this conversation as resolved.
Expand All @@ -216,7 +215,10 @@ absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::stri
if (!entry.HasMember("text") || !entry["text"].IsString()) {
return absl::InvalidArgumentError("Invalid message structure - content text missing");
}
contentText = entry["text"];
if (!combinedText.empty()) {
combinedText += "\n";
}
combinedText.append(entry["text"].GetString(), entry["text"].GetStringLength());
continue;
} else if (entryType == std::string("image_url")) {
if (!entry.HasMember("image_url") || !entry["image_url"].IsObject()) {
Expand All @@ -236,8 +238,10 @@ absl::Status OpenAIChatCompletionsHandler::parseMessages(std::optional<std::stri
return absl::InvalidArgumentError("Unsupported content type");
}
}
// Pulling out text from nested structure to the "content" field for text and replace whole "content" value for image data
// with empty string, since images are stored separately in request.images
// Flatten all text parts (joined with newlines) into the "content" field.
// Images are stored separately in request.imageHistory.
Value contentText(rapidjson::kStringType);
contentText.SetString(combinedText.c_str(), combinedText.length(), doc.GetAllocator());
member->value = contentText;
// Add new field to the last message in history if content is text
if (member->value.IsString()) {
Expand Down
69 changes: 69 additions & 0 deletions src/test/http_openai_handler_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2278,6 +2278,75 @@ TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesEmptyContentArrayFails) {
EXPECT_EQ(apiHandler->parseMessages(), absl::InvalidArgumentError("Invalid message structure - content array is empty"));
}

TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesMultipleTextItemsConcatenatesWithNewline) {
std::string json = R"({
"model": "llama",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "First part."
},
{
"type": "text",
"text": "Second part."
}
]
}
]
})";
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());
// Non-Python path: chatHistory content is the concatenated string
auto& chatHistory = apiHandler->getChatHistory();
ASSERT_EQ(chatHistory.size(), 1);
EXPECT_EQ(chatHistory[0]["content"], "First part.\nSecond part.");
// Python Jinja path: processedJson carries the same flattened content for applyChatTemplate
EXPECT_EQ(apiHandler->getProcessedJson(), R"({"model":"llama","messages":[{"role":"user","content":"First part.\nSecond part."}]})");
}

TEST_F(HttpOpenAIHandlerParsingTest, ParsingMessagesTextBeforeAndAfterImageConcatenatesAllText) {
std::string json = R"({
"model": "llama",
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Before image."
},
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAEElEQVR4nGLK27oAEAAA//8DYAHGgEvy5AAAAABJRU5ErkJggg=="
}
},
{
"type": "text",
"text": "After image."
}
]
}
]
})";
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());
// Non-Python path: chatHistory content is the concatenated string
auto& chatHistory = apiHandler->getChatHistory();
ASSERT_EQ(chatHistory.size(), 1);
EXPECT_EQ(chatHistory[0]["content"], "Before image.\nAfter image.");
EXPECT_EQ(apiHandler->getImageHistory().size(), 1);
// Python Jinja path: processedJson carries the same flattened content for applyChatTemplate
EXPECT_EQ(apiHandler->getProcessedJson(), R"({"model":"llama","messages":[{"role":"user","content":"Before image.\nAfter image."}]})");
}

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