@@ -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
119119void 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-
390301namespace {
391302
392303template <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+
440381void 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
506425void APIService::Embeddings (::google::protobuf::RpcController* controller,
@@ -733,53 +652,6 @@ void APIService::ModelVersionsHttp(
733652
734653namespace {
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-
783655void 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 ;
0 commit comments