Skip to content

Commit f9e58db

Browse files
authored
refactor: extract chat json preprocessing into ChatJsonParser class hierarchy. (#1191)
1 parent 8eb6391 commit f9e58db

7 files changed

Lines changed: 438 additions & 246 deletions

File tree

xllm/api_service/CMakeLists.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ cc_library(
88
api_service.h
99
api_service_impl.h
1010
call.h
11-
chat_json_utils.h
11+
chat_json_parser.h
1212
completion_service_impl.h
1313
rec_completion_service_impl.h
1414
chat_service_impl.h
@@ -28,6 +28,7 @@ cc_library(
2828
utils.h
2929
SRCS
3030
api_service.cpp
31+
chat_json_parser.cpp
3132
call.cpp
3233
completion_service_impl.cpp
3334
rec_completion_service_impl.cpp
@@ -89,7 +90,7 @@ cc_test(
8990
NAME
9091
api_service_test
9192
SRCS
92-
chat_json_utils_test.cpp
93+
chat_json_parser_test.cpp
9394
DEPS
9495
api_service
9596
GTest::gtest_main

xllm/api_service/api_service.cpp

Lines changed: 49 additions & 172 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,10 @@ limitations under the License.
2121
#include <json2pb/pb_to_json.h>
2222

2323
#include <filesystem>
24-
#include <nlohmann/json.hpp>
2524

25+
#include "api_service/chat_json_parser.h"
2626
#include "call.h"
2727
#include "chat.pb.h"
28-
#include "chat_json_utils.h"
2928
#include "common.pb.h"
3029
#include "completion.pb.h"
3130
#include "core/common/constants.h"
@@ -114,6 +113,7 @@ APIService::APIService(Master* master,
114113
models_service_impl_ =
115114
ServiceImplFactory<ModelsServiceImpl>::create_service_impl(
116115
model_names, model_versions);
116+
register_chat_completions_handler();
117117
}
118118

119119
void APIService::set_model_master(const std::string& model_id, Master* master) {
@@ -298,95 +298,6 @@ size_t get_json_content_length(const brpc::Controller* ctrl) {
298298

299299
} // namespace
300300

301-
// Preprocess chat JSON to normalize array content to string.
302-
// For text-only backends, combines text array items into a single string.
303-
// For multimodal backends, passes through unchanged without parsing.
304-
// Returns Status with processed JSON on success, or error status on failure.
305-
std::pair<Status, std::string> preprocess_chat_json(std::string json_str,
306-
bool is_multimodal) {
307-
// Multimodal backends handle array content natively, skip parsing
308-
if (is_multimodal) {
309-
return {Status(), std::move(json_str)};
310-
}
311-
312-
try {
313-
auto json = nlohmann::json::parse(json_str);
314-
if (!json.contains("messages") || !json["messages"].is_array()) {
315-
return {Status(), std::move(json_str)};
316-
}
317-
318-
bool modified = false;
319-
for (auto& msg : json["messages"]) {
320-
if (!msg.is_object()) {
321-
return {Status(StatusCode::INVALID_ARGUMENT,
322-
"Message in 'messages' array must be an object."),
323-
""};
324-
}
325-
if (msg.contains("content") && msg["content"].is_array()) {
326-
// Validate all items are text-only with proper text field
327-
for (const auto& item : msg["content"]) {
328-
if (!item.is_object()) {
329-
return {Status(StatusCode::INVALID_ARGUMENT,
330-
"Content array item must be an object."),
331-
""};
332-
}
333-
if (!item.contains("type") || item["type"] != "text") {
334-
// Non-text content on text-only backend is an error
335-
return {Status(StatusCode::INVALID_ARGUMENT,
336-
"Non-text content (e.g., image_url) requires "
337-
"multimodal backend (-backend vlm)"),
338-
""};
339-
}
340-
// Validate text items have proper text field
341-
if (!item.contains("text") || !item["text"].is_string()) {
342-
return {Status(StatusCode::INVALID_ARGUMENT,
343-
"Missing or invalid 'text' field in content item."),
344-
""};
345-
}
346-
}
347-
348-
// All items are text-only; combine into single string.
349-
// Pre-calculate total size to avoid reallocations.
350-
size_t total_size = 0;
351-
size_t num_items = msg["content"].size();
352-
for (const auto& item : msg["content"]) {
353-
// Already validated above
354-
total_size += item["text"].get_ref<const std::string&>().size();
355-
}
356-
// Add space for newline separators
357-
if (num_items > 1) {
358-
total_size += num_items - 1;
359-
}
360-
361-
// Reserve capacity once to avoid reallocations
362-
std::string combined_text;
363-
combined_text.reserve(total_size);
364-
bool first = true;
365-
for (const auto& item : msg["content"]) {
366-
if (!first) {
367-
combined_text += '\n';
368-
}
369-
combined_text += item["text"].get_ref<const std::string&>();
370-
first = false;
371-
}
372-
msg["content"] = combined_text;
373-
modified = true;
374-
}
375-
}
376-
return modified ? std::make_pair(Status(), json.dump())
377-
: std::make_pair(Status(), std::move(json_str));
378-
} catch (const nlohmann::json::exception& e) {
379-
return {Status(StatusCode::INVALID_ARGUMENT,
380-
"Invalid JSON format: " + std::string(e.what())),
381-
""};
382-
} catch (const std::exception& e) {
383-
LOG(ERROR) << "Exception during JSON preprocessing: " << e.what();
384-
return {Status(StatusCode::UNKNOWN,
385-
"Internal server error during JSON processing."),
386-
""};
387-
}
388-
}
389-
390301
namespace {
391302

392303
template <typename ChatCall, typename Service>
@@ -395,7 +306,7 @@ void chat_completions_http_impl(std::unique_ptr<Service>& service,
395306
brpc::Controller* ctrl,
396307
const proto::HttpRequest* request,
397308
proto::HttpResponse* response,
398-
bool is_multimodal) {
309+
const ChatJsonParser& chat_json_parser) {
399310
auto arena = GetArenaWithCheck<ChatCall>(response);
400311
auto req_pb =
401312
google::protobuf::Arena::CreateMessage<typename ChatCall::ReqType>(arena);
@@ -412,7 +323,7 @@ void chat_completions_http_impl(std::unique_ptr<Service>& service,
412323
ctrl->request_attachment().copy_to(&attachment, content_len, 0);
413324

414325
auto [preprocess_status, processed_json] =
415-
preprocess_chat_json(std::move(attachment), is_multimodal);
326+
chat_json_parser.preprocess(std::move(attachment));
416327
if (!preprocess_status.ok()) {
417328
ctrl->SetFailed(preprocess_status.message());
418329
LOG(ERROR) << "Complex message preprocessing failed: "
@@ -437,6 +348,36 @@ void chat_completions_http_impl(std::unique_ptr<Service>& service,
437348

438349
} // namespace
439350

351+
void APIService::register_chat_completions_handler() {
352+
if (mm_chat_service_impl_) {
353+
chat_completions_handler_ = [this](ClosureGuard& guard,
354+
brpc::Controller* ctrl,
355+
const proto::HttpRequest* request,
356+
proto::HttpResponse* response) {
357+
chat_completions_http_impl<MMChatCall, MMChatServiceImpl>(
358+
mm_chat_service_impl_,
359+
guard,
360+
ctrl,
361+
request,
362+
response,
363+
ChatJsonParser::get("vlm"));
364+
};
365+
} else if (chat_service_impl_) {
366+
chat_completions_handler_ = [this](ClosureGuard& guard,
367+
brpc::Controller* ctrl,
368+
const proto::HttpRequest* request,
369+
proto::HttpResponse* response) {
370+
chat_completions_http_impl<ChatCall, ChatServiceImpl>(
371+
chat_service_impl_,
372+
guard,
373+
ctrl,
374+
request,
375+
response,
376+
ChatJsonParser::get(FLAGS_backend));
377+
};
378+
}
379+
}
380+
440381
void APIService::ChatCompletions(::google::protobuf::RpcController* controller,
441382
const proto::ChatRequest* request,
442383
proto::ChatResponse* response,
@@ -471,36 +412,14 @@ void APIService::ChatCompletionsHttp(
471412
return;
472413
}
473414

474-
auto ctrl = reinterpret_cast<brpc::Controller*>(controller);
475-
476-
if (FLAGS_backend == "llm") {
477-
CHECK(chat_service_impl_) << " chat service is invalid.";
478-
chat_completions_http_impl<ChatCall, ChatServiceImpl>(
479-
chat_service_impl_,
480-
done_guard,
481-
ctrl,
482-
request,
483-
response,
484-
/*is_multimodal=*/false);
485-
} else if (FLAGS_backend == "vlm") {
486-
CHECK(mm_chat_service_impl_) << " mm chat service is invalid.";
487-
chat_completions_http_impl<MMChatCall, MMChatServiceImpl>(
488-
mm_chat_service_impl_,
489-
done_guard,
490-
ctrl,
491-
request,
492-
response,
493-
/*is_multimodal=*/true);
494-
} else if (FLAGS_backend == "rec") {
495-
CHECK(chat_service_impl_) << " chat service is invalid.";
496-
chat_completions_http_impl<ChatCall, ChatServiceImpl>(
497-
chat_service_impl_,
498-
done_guard,
499-
ctrl,
500-
request,
501-
response,
502-
/*is_multimodal=*/false);
415+
if (!chat_completions_handler_) {
416+
LOG(ERROR) << "No chat completions handler registered for backend '"
417+
<< FLAGS_backend << "'";
418+
return;
503419
}
420+
421+
auto ctrl = reinterpret_cast<brpc::Controller*>(controller);
422+
chat_completions_handler_(done_guard, ctrl, request, response);
504423
}
505424

506425
void APIService::Embeddings(::google::protobuf::RpcController* controller,
@@ -733,53 +652,6 @@ void APIService::ModelVersionsHttp(
733652

734653
namespace {
735654

736-
// Preprocess Anthropic API JSON to convert "content" field to
737-
// protobuf-compatible format Anthropic API uses "content" field which can be
738-
// string or array Our protobuf uses "content_string" for string and
739-
// "content_blocks" for array
740-
std::string preprocess_anthropic_json(const std::string& json_str) {
741-
try {
742-
nlohmann::json j = nlohmann::json::parse(json_str);
743-
744-
if (j.contains("messages") && j["messages"].is_array()) {
745-
for (auto& msg : j["messages"]) {
746-
if (msg.contains("content")) {
747-
auto& content = msg["content"];
748-
if (content.is_string()) {
749-
// Convert "content": "string" to "content_string": "string"
750-
msg["content_string"] = content.get<std::string>();
751-
msg.erase("content");
752-
} else if (content.is_array()) {
753-
// Convert "content": [...] to "content_blocks": {"blocks": [...]}
754-
nlohmann::json content_blocks;
755-
content_blocks["blocks"] = content;
756-
msg["content_blocks"] = content_blocks;
757-
msg.erase("content");
758-
}
759-
}
760-
}
761-
}
762-
763-
if (j.contains("system")) {
764-
auto& system = j["system"];
765-
if (system.is_string()) {
766-
j["system_string"] = system.get<std::string>();
767-
j.erase("system");
768-
} else if (system.is_array()) {
769-
nlohmann::json system_blocks;
770-
system_blocks["blocks"] = system;
771-
j["system_blocks"] = system_blocks;
772-
j.erase("system");
773-
}
774-
}
775-
776-
return j.dump();
777-
} catch (const std::exception& e) {
778-
LOG(ERROR) << "Failed to preprocess Anthropic JSON: " << e.what();
779-
return json_str; // Return original on error
780-
}
781-
}
782-
783655
void handle_anthropic_messages(std::unique_ptr<AnthropicServiceImpl>& service,
784656
xllm::ClosureGuard& guard,
785657
brpc::Controller* ctrl,
@@ -801,9 +673,14 @@ void handle_anthropic_messages(std::unique_ptr<AnthropicServiceImpl>& service,
801673
std::string attachment;
802674
ctrl->request_attachment().copy_to(&attachment, content_len, 0);
803675

804-
// Preprocess JSON to convert Anthropic API format to protobuf-compatible
805-
// format
806-
std::string processed_json = preprocess_anthropic_json(attachment);
676+
auto [preprocess_status, processed_json] =
677+
ChatJsonParser::get("anthropic").preprocess(std::move(attachment));
678+
if (!preprocess_status.ok()) {
679+
ctrl->SetFailed(preprocess_status.message());
680+
LOG(ERROR) << "Anthropic JSON preprocessing failed: "
681+
<< preprocess_status.message();
682+
return;
683+
}
807684

808685
google::protobuf::util::JsonParseOptions options;
809686
options.ignore_unknown_fields = true;

xllm/api_service/api_service.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515

1616
#pragma once
1717

18+
#include <functional>
1819
#include <shared_mutex>
1920
#include <string>
2021
#include <unordered_map>
@@ -33,6 +34,8 @@ limitations under the License.
3334

3435
namespace xllm {
3536

37+
class ClosureGuard;
38+
3639
class APIService : public proto::XllmAPIService {
3740
public:
3841
APIService(Master* master,
@@ -171,6 +174,13 @@ class APIService : public proto::XllmAPIService {
171174
::google::protobuf::Closure* done) override;
172175

173176
private:
177+
using ChatHttpHandler = std::function<void(ClosureGuard&,
178+
brpc::Controller*,
179+
const proto::HttpRequest*,
180+
proto::HttpResponse*)>;
181+
182+
void register_chat_completions_handler();
183+
174184
bool ParseForkMasterRequest(const proto::MasterInfos* request,
175185
Options& options);
176186
void set_model_master(const std::string& model_id, Master* master);
@@ -179,6 +189,7 @@ class APIService : public proto::XllmAPIService {
179189
Master* get_model_master(const std::string& model_id) const;
180190

181191
Master* master_;
192+
ChatHttpHandler chat_completions_handler_;
182193
mutable std::shared_mutex masters_mutex_;
183194
std::unordered_map<std::string, Master*> masters_;
184195
std::unique_ptr<AnthropicServiceImpl> anthropic_service_impl_;

0 commit comments

Comments
 (0)