Brunobkr/llama.cpp_AlgMor24_github
ΩFFFΣLLIa • llama.cpp • AlgMor24 ██████╗ ███████╗███████╗███████╗██╗ ██╗ ██╗ █████╗ ██╔═══██╗██╔════╝██╔════╝██╔════╝██║ ██║ ██║██╔══██╗ ██║ ██║█████╗ █████╗ █████╗ ██║ ██║ ██║███████║ ██║ ██║██╔══╝ ██╔══╝ ██╔══╝ ██║ ██║ ██║██╔══██║ ╚██████╔╝██║ ██║ ███████╗███████╗███████╗██║██║ ██║ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚═╝╚═╝ ╚═╝ High-Performance LLM / VLM Inference & Autonomous Agentic Ecosystem… See the full description on the dataset page: https://huggingface.co/datasets/Brunobkr/llama.cpp_AlgMor24_github.
03k
1#include "common.h"2#include "http.h"3#include "server-http.h"4#include "server-common.h"5#include "ui.h"6 7#include <cpp-httplib/httplib.h>8 9#include <functional>10#include <future>11#include <memory>12#include <string>13#include <thread>14 15//16// HTTP implementation using cpp-httplib17//18 19class server_http_context::Impl {20public:21 std::unique_ptr<httplib::Server> srv;22};23 24server_http_context::server_http_context()25 : pimpl(std::make_unique<Impl>())26{}27 28server_http_context::~server_http_context() = default;29 30static void log_server_request(const httplib::Request & req, const httplib::Response & res) {31 // skip logging requests that are regularly sent, to avoid log spam32 if (req.path == "/health"33 || req.path == "/v1/health"34 || req.path == "/models"35 || req.path == "/v1/models"36 || req.path == "/props"37 || req.path == "/metrics"38 ) {39 return;40 }41 42 // reminder: this function is not covered by httplib's exception handler; if someone does more complicated stuff, think about wrapping it in try-catch43 44 SRV_TRC("done request: %s %s %s %d\n", req.method.c_str(), req.path.c_str(), req.remote_addr.c_str(), res.status);45 46 SRV_DBG("request: %s\n", req.body.c_str());47 SRV_DBG("response: %s\n", res.body.c_str());48}49 50// returns true if the Origin header value's host is localhost / 127.0.0.1 / ::1 (any port)51static bool origin_is_localhost(const std::string & origin) {52 try {53 const std::string host = common_http_parse_url(origin).host;54 return host == "localhost" || host == "127.0.0.1" || host == "::1";55 } catch (const std::exception &) {56 return false;57 }58}59 60// For Google Cloud Platform deployment compatibility61struct gcp_params {62 bool enabled;63 std::string path_health;64 std::string path_predict;65 int port;66 67 // Ref: https://docs.cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements#aip-variables68 gcp_params() {69 enabled = getenv("AIP_MODE", "") == "PREDICTION";70 path_health = getenv("AIP_HEALTH_ROUTE", "", true); // default: using the route defined in server.cpp71 path_predict = getenv("AIP_PREDICT_ROUTE", "/predict", true);72 port = std::stoi(getenv("AIP_HTTP_PORT", "8080"));73 }74 75 static std::string getenv(const char * name, const std::string & default_value, bool ensure_leading_slash = false) {76 const auto * value = std::getenv(name);77 if (value == nullptr || value[0] == '\0') {78 return default_value;79 }80 std::string val = value;81 if (ensure_leading_slash && !val.empty() && val[0] != '/') {82 val.insert(val.begin(), '/');83 }84 return val;85 }86};87 88bool server_http_context::init(const common_params & params) {89 const gcp_params gcp;90 91 path_prefix = params.api_prefix;92 port = params.port;93 hostname = params.hostname;94 95 if (gcp.enabled) {96 SRV_TRC("Google Cloud Platform compat: health route = %s, predict route = %s, port = %d\n", gcp.path_health.c_str(), gcp.path_predict.c_str(), gcp.port);97 98 if (port != gcp.port) {99 SRV_WRN("Google Cloud Platform compat: overriding server port %d with AIP_HTTP_PORT %d\n", port, gcp.port);100 }101 102 port = gcp.port;103 }104 105 auto & srv = pimpl->srv;106 107#ifdef CPPHTTPLIB_OPENSSL_SUPPORT108 if (!params.ssl_file_key.empty() && !params.ssl_file_cert.empty()) {109 SRV_TRC("running with SSL: key = %s, cert = %s\n", params.ssl_file_key.c_str(), params.ssl_file_cert.c_str());110 srv = std::make_unique<httplib::SSLServer>(111 params.ssl_file_cert.c_str(), params.ssl_file_key.c_str()112 );113 is_ssl = true;114 } else {115 SRV_TRC("%s", "running without SSL\n");116 srv = std::make_unique<httplib::Server>();117 }118#else119 if (params.ssl_file_key != "" && params.ssl_file_cert != "") {120 SRV_ERR("%s", "the server is built without SSL support\n");121 return false;122 }123 srv.reset(new httplib::Server());124#endif125 126 srv->set_default_headers({{"Server", "llama.cpp"}});127 // srv->set_logger(log_server_request); // TODO @ngxson : this is too spamy, no very useful; improve it in the future128 srv->set_exception_handler([](const httplib::Request &, httplib::Response & res, const std::exception_ptr & ep) {129 // this is fail-safe; exceptions should already handled by `ex_wrapper`130 131 std::string message;132 try {133 std::rethrow_exception(ep);134 } catch (const std::exception & e) {135 message = e.what();136 } catch (...) {137 message = "Unknown Exception";138 }139 140 res.status = 500;141 res.set_content(message, "text/plain");142 SRV_ERR("got exception: %s\n", message.c_str());143 });144 145 srv->set_error_handler([](const httplib::Request &, httplib::Response & res) {146 if (res.status == 404) {147 res.set_content(148 safe_json_to_str(json {149 {"error", {150 {"message", "File Not Found"},151 {"type", "not_found_error"},152 {"code", 404}153 }}154 }),155 "application/json; charset=utf-8"156 );157 }158 // for other error codes, we skip processing here because it's already done by res->error()159 });160 161 // set timeouts and change hostname and port162 srv->set_read_timeout (params.timeout_read);163 srv->set_write_timeout(params.timeout_write);164 srv->set_socket_options([reuse_port = params.reuse_port](const socket_t sock) {165 httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);166 if (reuse_port) {167#ifdef SO_REUSEPORT168 httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEPORT, 1);169#else170 SRV_WRN("%s", "SO_REUSEPORT is not supported\n");171#endif172 }173 });174 175 if (params.api_keys.size() == 1) {176 const auto key = params.api_keys[0];177 const std::string substr = key.substr(std::max(static_cast<int>(key.length() - 4), 0));178 SRV_TRC("api_keys: ****%s\n", substr.c_str());179 } else if (params.api_keys.size() > 1) {180 SRV_TRC("api_keys: %zu keys loaded\n", params.api_keys.size());181 }182 183 //184 // Middlewares185 //186 187 // Frontend paths - all embedded UI assets188 static const std::unordered_set<std::string> frontend_paths = []() {189 std::unordered_set<std::string> paths { "/" };190 for (const llama_ui_asset & a : llama_ui_get_assets()) {191 paths.insert("/" + a.name);192 }193 return paths;194 }();195 196 // Public endpoints - API routes plus all embedded UI assets197 static const std::unordered_set<std::string> get_public_endpoints = []() {198 std::unordered_set<std::string> endpoints {199 "/health",200 "/v1/health",201 "/models",202 "/v1/models",203 };204 endpoints.insert(frontend_paths.begin(), frontend_paths.end());205 return endpoints;206 }();207 208 auto middleware_validate_api_key = [api_keys = params.api_keys](const httplib::Request & req, httplib::Response & res) {209 // If API key is not set, skip validation210 if (api_keys.empty()) {211 return true;212 }213 214 // If path is public or a UI asset, skip validation215 if (get_public_endpoints.count(req.path)) {216 return true;217 }218 219 // Check for API key in the Authorization header220 std::string req_api_key = req.get_header_value("Authorization");221 if (req_api_key.empty()) {222 // retry with anthropic header223 req_api_key = req.get_header_value("X-Api-Key");224 }225 226 // remove the "Bearer " prefix if needed227 static std::string prefix = "Bearer ";228 if (req_api_key.substr(0, prefix.size()) == prefix) {229 req_api_key = req_api_key.substr(prefix.size());230 }231 232 // validate the API key233 if (std::find(api_keys.begin(), api_keys.end(), req_api_key) != api_keys.end()) {234 return true; // API key is valid235 }236 237 // API key is invalid or not provided238 res.status = 401;239 res.set_content(240 safe_json_to_str(json {241 {"error", {242 {"message", "Invalid API Key"},243 {"type", "authentication_error"},244 {"code", 401}245 }}246 }),247 "application/json; charset=utf-8"248 );249 250 SRV_WRN("%s", "unauthorized: Invalid API Key\n");251 252 return false;253 };254 255 auto middleware_server_state = [this](const httplib::Request & req, httplib::Response & res) {256 if (!is_ready.load()) {257 if (frontend_paths.count(req.path)) {258 return true; // frontend asset, allow it to load and show "loading"259 }260 // no endpoints are allowed to be accessed when the server is not ready261 // this is to prevent any data races or inconsistent states262 res.status = 503;263 res.set_content(264 safe_json_to_str(json {265 {"error", {266 {"message", "Loading model"},267 {"type", "unavailable_error"},268 {"code", 503}269 }}270 }),271 "application/json; charset=utf-8"272 );273 return false;274 }275 return true;276 };277 278 // register server middlewares279 srv->set_pre_routing_handler([¶ms, middleware_validate_api_key, middleware_server_state](const httplib::Request & req, httplib::Response & res) {280 if (params.cors_credentials && params.cors_origins == "*") {281 // special case: echo back the Origin header to allow any origin to access the server with credentials282 res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));283 } else if (params.cors_origins == "localhost") {284 // special case: only reflect the Origin header if it is a localhost origin285 std::string origin = req.get_header_value("Origin");286 if (!origin.empty() && origin_is_localhost(origin)) {287 res.set_header("Access-Control-Allow-Origin", origin);288 } else if (!origin.empty()) {289 SRV_WRN("(CORS) skip non-localhost origin: %s\n", origin.c_str());290 }291 } else {292 res.set_header("Access-Control-Allow-Origin", params.cors_origins);293 }294 // If this is OPTIONS request, skip validation because browsers don't include Authorization header295 if (req.method == "OPTIONS") {296 res.set_header("Access-Control-Allow-Credentials", params.cors_credentials ? "true" : "false");297 res.set_header("Access-Control-Allow-Methods", params.cors_methods);298 res.set_header("Access-Control-Allow-Headers", params.cors_headers);299 res.set_content("", "text/html"); // blank response, no data300 return httplib::Server::HandlerResponse::Handled; // skip further processing301 }302 if (!middleware_server_state(req, res)) {303 return httplib::Server::HandlerResponse::Handled;304 }305 if (!middleware_validate_api_key(req, res)) {306 return httplib::Server::HandlerResponse::Handled;307 }308 return httplib::Server::HandlerResponse::Unhandled;309 });310 311 auto n_threads_http = params.n_threads_http;312 if (n_threads_http < 1) {313 // +4 threads for monitoring, health and some threads reserved for MCP and other tasks in the future314 n_threads_http = std::max(params.n_parallel + 4, static_cast<int32_t>(std::thread::hardware_concurrency() - 1));315 }316 SRV_TRC("using %d threads for HTTP server\n", n_threads_http);317 srv->new_task_queue = [n_threads_http] {318 // spawn n_threads_http fixed thread (always alive), while allow up to 1024 max possible additional threads319 // when n_threads_http is used, server will create new "dynamic" threads that will be destroyed after processing each request320 // ref: https://github.com/yhirose/cpp-httplib/pull/2368321 const auto max_threads = static_cast<size_t>(n_threads_http + 1024);322 return new httplib::ThreadPool(n_threads_http, max_threads);323 };324 325 //326 // Web UI setup327 //328 329 // Use new `params.ui` field (backed by old `params.webui` for compat)330 if (!params.ui) {331 SRV_INF("%s", "The UI is disabled\n");332 SRV_INF("%s", "Use --ui/--no-ui (or deprecated --webui/--no-webui) to enable/disable\n");333 } else {334 // register static assets routes335 if (!params.public_path.empty()) {336 // Set the base directory for serving static files337 if (const auto is_found = srv->set_mount_point(params.api_prefix + "/", params.public_path); !is_found) {338 SRV_ERR("static assets path not found: %s\n", params.public_path.c_str());339 return false;340 }341 } else {342#if defined(LLAMA_UI_HAS_ASSETS)343 static auto handle_gzip_header = [](const httplib::Request & req, httplib::Response & res) {344 if (!llama_ui_use_gzip()) {345 // no gzip build, skip346 return true;347 }348 if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {349 res.status = 415; // unsupported media type350 res.set_content("Error: gzip is not supported by this browser", "text/plain");351 return false;352 } else {353 res.set_header("Content-Encoding", "gzip");354 }355 return true;356 };357 358 auto serve_asset_cached = [](const std::string & name, bool isolation) {359 return [name, isolation](const httplib::Request & req, httplib::Response & res) {360 if (!handle_gzip_header(req, res)) {361 return true; // returns error message362 }363 const llama_ui_asset * a = llama_ui_find_asset(name);364 if (!a) { res.status = 404; return false; }365 res.set_header("ETag", a->etag);366 if (const std::string & inm = req.get_header_value("If-None-Match");367 !inm.empty() && (inm == a->etag || inm == std::string("W/") + a->etag)) {368 res.status = 304;369 return false;370 }371 if (isolation) {372 res.set_header("Cross-Origin-Embedder-Policy", "require-corp");373 res.set_header("Cross-Origin-Opener-Policy", "same-origin");374 }375 res.set_header("Cache-Control", "public, max-age=31536000, immutable");376 res.set_content(reinterpret_cast<const char*>(a->data), a->size, a->type.c_str());377 return false;378 };379 };380 381 auto serve_asset_nocache = [](const std::string & name) {382 return [name](const httplib::Request & req, httplib::Response & res) {383 if (!handle_gzip_header(req, res)) {384 return true; // returns error message385 }386 const llama_ui_asset * a = llama_ui_find_asset(name);387 if (!a) {388 res.status = 404;389 return false;390 }391 res.set_header("Cache-Control", "no-cache");392 res.set_content(reinterpret_cast<const char*>(a->data), a->size, a->type.c_str());393 return false;394 };395 };396 397 // main index file398 srv->Get(params.api_prefix + "/", serve_asset_cached("index.html", true));399 srv->Get(params.api_prefix + "/index.html", serve_asset_cached("index.html", true));400 401 // All remaining assets registered directly from the embedded asset table.402 // PWA revalidation files (sw.js, manifest, version.json) use no-cache;403 // everything else is immutable.404 static const std::unordered_set<std::string> no_cache_names = {405 "sw.js",406 "manifest.webmanifest",407 "_app/version.json",408 "build.json"409 };410 411 for (const auto & a : llama_ui_get_assets()) {412 if (a.name == "index.html") continue; // served at "/" and "/index.html" above413 if (no_cache_names.count(a.name)) {414 SRV_DBG("serve nocache for %s\n", a.name.c_str());415 srv->Get(params.api_prefix + "/" + a.name, serve_asset_nocache(a.name));416 } else {417 srv->Get(params.api_prefix + "/" + a.name, serve_asset_cached(a.name, false));418 }419 }420 421#endif422 }423 }424 return true;425}426 427bool server_http_context::start() {428 // Bind and listen429 430 const auto & srv = pimpl->srv;431 auto was_bound = false;432 auto is_sock = false;433 if (string_ends_with(std::string(hostname), ".sock")) {434 is_sock = true;435 SRV_TRC("%s", "setting address family to AF_UNIX\n");436 srv->set_address_family(AF_UNIX);437 // bind_to_port requires a second arg, any value other than 0 should438 // simply get ignored439 was_bound = srv->bind_to_port(hostname, 8080);440 } else {441 SRV_TRC("%s", "binding port with default address family\n");442 // bind HTTP listen port443 if (port == 0) {444 const auto bound_port = srv->bind_to_any_port(hostname);445 was_bound = (bound_port >= 0);446 if (was_bound) {447 port = bound_port;448 }449 } else {450 was_bound = srv->bind_to_port(hostname, port);451 }452 }453 454 if (!was_bound) {455 SRV_ERR("couldn't bind HTTP server socket, hostname: %s, port: %d\n", hostname.c_str(), port);456 return false;457 }458 459 // run the HTTP server in a thread460 thread = std::thread([this] { pimpl->srv->listen_after_bind(); });461 srv->wait_until_ready();462 463 listening_address = is_sock ? string_format("unix://%s", hostname.c_str())464 : string_format("%s://%s:%d", is_ssl ? "https" : "http", common_http_format_host(hostname).c_str(), port);465 return true;466}467 468void server_http_context::stop() const {469 if (pimpl->srv) {470 pimpl->srv->stop();471 }472}473 474static void set_headers(httplib::Response & res, const std::map<std::string, std::string> & headers) {475 for (const auto & [key, value] : headers) {476 res.set_header(key, value);477 }478}479 480// percent-decode a path component (%XX). path params arrive raw from httplib, unlike query481// params, so a conv id like "conv::model" sent as "conv%3A%3Amodel" must be decoded here to482// match the value the client put in the X-Conversation-Id header483static std::string decode_path_component(const std::string & in) {484 std::string out;485 out.reserve(in.size());486 for (size_t i = 0; i < in.size(); i++) {487 if (in[i] == '%' && i + 2 < in.size()) {488 auto hex = [](char c) -> int {489 if (c >= '0' && c <= '9') return c - '0';490 if (c >= 'a' && c <= 'f') return c - 'a' + 10;491 if (c >= 'A' && c <= 'F') return c - 'A' + 10;492 return -1;493 };494 int hi = hex(in[i + 1]);495 int lo = hex(in[i + 2]);496 if (hi >= 0 && lo >= 0) {497 out.push_back(char((hi << 4) | lo));498 i += 2;499 continue;500 }501 }502 out.push_back(in[i]);503 }504 return out;505}506 507static std::map<std::string, std::string> get_params(const httplib::Request & req) {508 std::map<std::string, std::string> params;509 for (const auto & [key, value] : req.params) {510 params[key] = value;511 }512 for (const auto & [key, value] : req.path_params) {513 params[key] = decode_path_component(value);514 }515 return params;516}517 518static std::map<std::string, std::string> get_headers(const httplib::Request & req) {519 std::map<std::string, std::string> headers;520 for (const auto & [key, value] : req.headers) {521 headers[key] = value;522 }523 return headers;524}525 526static std::string build_query_string(const httplib::Request & req) {527 std::string qs;528 for (const auto & [key, value] : req.params) {529 if (!qs.empty()) {530 qs += '&';531 }532 qs += httplib::encode_query_component(key) + "=" + httplib::encode_query_component(value);533 }534 return qs;535}536 537// using unique_ptr for request to allow safe capturing in lambdas538using server_http_req_ptr = std::unique_ptr<server_http_req>;539 540static void process_handler_response(server_http_req_ptr && request, server_http_res_ptr & response, httplib::Response & res) {541 if (response->is_stream()) {542 res.status = response->status;543 // Tell Nginx to not buffer any streamed response544 response->headers["X-Accel-Buffering"] = "no";545 set_headers(res, response->headers);546 const std::string content_type = response->content_type;547 // convert to shared_ptr as both chunked_content_provider() and on_complete() need to use it548 std::shared_ptr<server_http_req> q_ptr = std::move(request);549 std::shared_ptr<server_http_res> r_ptr = std::move(response);550 551 const auto chunked_content_provider = [response = r_ptr](size_t, httplib::DataSink & sink) -> bool {552 std::string chunk;553 const bool has_next = response->next(chunk);554 if (!chunk.empty()) {555 if (!sink.write(chunk.data(), chunk.size())) {556 return false;557 }558 SRV_DBG("http: streamed chunk: %s\n", chunk.c_str());559 }560 if (!has_next) {561 sink.done();562 SRV_DBG("%s", "http: stream ended\n");563 }564 return has_next;565 };566 const auto on_complete = [request = q_ptr, response = r_ptr](bool) mutable {567 response->on_complete();568 response.reset();569 request.reset();570 };571 res.set_chunked_content_provider(content_type, chunked_content_provider, on_complete);572 } else {573 res.status = response->status;574 set_headers(res, response->headers);575 res.set_content(response->data, response->content_type);576 response->on_complete();577 }578}579 580void server_http_context::get(const std::string & path, const server_http_context::handler_t & handler) const {581 handlers.emplace(path, handler);582 pimpl->srv->Get(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {583 server_http_req_ptr request = std::make_unique<server_http_req>(server_http_req{584 get_params(req),585 get_headers(req),586 req.path,587 build_query_string(req),588 req.body,589 {},590 req.is_connection_closed591 });592 server_http_res_ptr response = handler(*request);593 process_handler_response(std::move(request), response, res);594 });595}596 597void server_http_context::post(const std::string & path, const server_http_context::handler_t & handler) const {598 handlers.emplace(path, handler);599 pimpl->srv->Post(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {600 std::string body = req.body;601 std::map<std::string, uploaded_file> files;602 603 if (req.is_multipart_form_data()) {604 // translate text fields to a JSON object and use it as the body605 json form_json = json::object();606 for (const auto & [key, field] : req.form.fields) {607 if (form_json.contains(key)) {608 // if the key already exists, convert it to an array609 if (!form_json[key].is_array()) {610 json existing_value = form_json[key];611 form_json[key] = json::array({existing_value});612 }613 form_json[key].push_back(field.content);614 } else {615 form_json[key] = field.content;616 }617 }618 body = form_json.dump();619 620 // populate files from multipart form621 for (const auto & [key, file] : req.form.files) {622 files[key] = uploaded_file{623 raw_buffer(file.content.begin(), file.content.end()),624 file.filename,625 file.content_type,626 };627 }628 }629 630 server_http_req_ptr request = std::make_unique<server_http_req>(server_http_req{631 get_params(req),632 get_headers(req),633 req.path,634 build_query_string(req),635 body,636 std::move(files),637 req.is_connection_closed638 });639 server_http_res_ptr response = handler(*request);640 process_handler_response(std::move(request), response, res);641 });642}643 644void server_http_context::del(const std::string & path, const server_http_context::handler_t & handler) const {645 handlers.emplace(path, handler);646 pimpl->srv->Delete(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {647 server_http_req_ptr request = std::make_unique<server_http_req>(server_http_req{648 get_params(req),649 get_headers(req),650 req.path,651 build_query_string(req),652 req.body,653 {},654 req.is_connection_closed655 });656 server_http_res_ptr response = handler(*request);657 process_handler_response(std::move(request), response, res);658 });659}660 661//662// Vertex AI Prediction protocol (AIP_PREDICT_ROUTE)663// https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements664//665 666// Derives the camelCase @requestFormat alias for a registered path.667// e.g. "/v1/chat/completions" -> "chatCompletions", "/apply-template" -> "applyTemplate"668static std::string path_to_gcp_format(const std::string & path) {669 std::string s = path;670 if (s.size() > 3 && s[0] == '/' && s[1] == 'v' && s[2] == '1') {671 s = s.substr(3);672 }673 if (!s.empty() && s[0] == '/') {674 s = s.substr(1);675 }676 std::string result;677 bool cap = false;678 for (unsigned char c : s) {679 if (c == ':') break; // stop before path parameters680 if (c == '/' || c == '-' || c == '_') {681 cap = true;682 } else {683 result += static_cast<char>(cap ? std::toupper(c) : c);684 cap = false;685 }686 }687 return result;688}689 690static json parse_gcp_predict_response(const server_http_res_ptr & res) {691 if (res == nullptr) {692 throw std::runtime_error("empty response from internal handler");693 }694 if (res->is_stream()) {695 throw std::invalid_argument("predict route does not support streaming responses");696 }697 if (res->data.empty()) {698 return nullptr;699 }700 try {701 return json::parse(res->data);702 } catch (...) {703 return res->data;704 }705}706 707void server_http_context::register_gcp_compat() const {708 const gcp_params gcp;709 710 if (!gcp.enabled) {711 // do nothing712 return;713 }714 715 if (handlers.count(gcp.path_predict)) {716 SRV_ERR("AIP_PREDICT_ROUTE=%s conflicts with an existing llama-server route\n", gcp.path_predict.c_str());717 exit(1);718 }719 720 // camelCase alias -> canonical path (first registration wins on collision)721 // e.g. "chatCompletions" -> "/v1/chat/completions"722 std::unordered_map<std::string, std::string> alias_to_path;723 for (const auto & [path, _] : handlers) {724 alias_to_path.emplace(path_to_gcp_format(path), path);725 }726 727 if (!gcp.path_health.empty()) {728 const auto health_handler = handlers.find("/health");729 GGML_ASSERT(health_handler != handlers.end());730 get(gcp.path_health, health_handler->second);731 }732 733 post(gcp.path_predict, [this, alias_to_path = std::move(alias_to_path)](const server_http_req & req) -> server_http_res_ptr {734 static const auto build_error = [](const std::string & message, error_type type) -> json {735 return json {{"error", format_error_response(message, type)}};736 };737 738 json data;739 try {740 data = json::parse(req.body);741 } catch (const std::exception & e) {742 auto res = std::make_unique<server_http_res>();743 res->status = 400;744 res->data = safe_json_to_str({{"error", format_error_response(e.what(), ERROR_TYPE_INVALID_REQUEST)}});745 return res;746 }747 if (!data.is_object()) {748 auto res = std::make_unique<server_http_res>();749 res->status = 400;750 res->data = safe_json_to_str({{"error", format_error_response("request body must be a JSON object", ERROR_TYPE_INVALID_REQUEST)}});751 return res;752 }753 if (!data.contains("instances") || !data.at("instances").is_array()) {754 auto res = std::make_unique<server_http_res>();755 res->status = 400;756 res->data = safe_json_to_str({{"error", format_error_response("request body must include an array field named instances", ERROR_TYPE_INVALID_REQUEST)}});757 return res;758 }759 760 const json & instances = data.at("instances");761 static const size_t MAX_INSTANCES = 128;762 if (instances.size() > MAX_INSTANCES) {763 auto res = std::make_unique<server_http_res>();764 res->status = 400;765 res->data = safe_json_to_str({{"error", format_error_response("instances array exceeds maximum size of " + std::to_string(MAX_INSTANCES), ERROR_TYPE_INVALID_REQUEST)}});766 return res;767 }768 769 std::vector<std::future<json>> futures;770 futures.reserve(instances.size());771 772 for (const auto & instance : instances) {773 futures.push_back(std::async(std::launch::async, [this, &req, &alias_to_path, instance]() -> json {774 if (!instance.is_object()) {775 return build_error("each instance must be a JSON object", ERROR_TYPE_INVALID_REQUEST);776 }777 if (!instance.contains("@requestFormat") || !instance.at("@requestFormat").is_string()) {778 return build_error("each instance must include a string @requestFormat", ERROR_TYPE_INVALID_REQUEST);779 }780 781 try {782 json payload = instance;783 const std::string format = payload.at("@requestFormat").get<std::string>();784 payload.erase("@requestFormat");785 786 if (payload.contains("stream")) {787 SRV_WRN("%s", "ignoring client-provided stream field in instance, streaming is not supported in predict route\n");788 payload["stream"] = false;789 }790 791 // accept both camelCase aliases (e.g. "chatCompletions") and direct paths792 std::string dispatch_path;793 auto it_alias = alias_to_path.find(format);794 if (it_alias != alias_to_path.end()) {795 dispatch_path = it_alias->second;796 } else if (handlers.count(format)) {797 dispatch_path = format;798 } else {799 return build_error("no handler registered for @requestFormat: " + format, ERROR_TYPE_INVALID_REQUEST);800 }801 802 const server_http_req internal_req {803 req.params,804 req.headers,805 path_prefix + dispatch_path,806 req.query_string,807 payload.dump(),808 {},809 req.should_stop,810 };811 812 server_http_res_ptr internal_res = handlers.at(dispatch_path)(internal_req);813 return parse_gcp_predict_response(internal_res);814 } catch (const std::invalid_argument & e) {815 return build_error(e.what(), ERROR_TYPE_INVALID_REQUEST);816 } catch (const std::exception & e) {817 return build_error(e.what(), ERROR_TYPE_SERVER);818 } catch (...) {819 return build_error("unknown error", ERROR_TYPE_SERVER);820 }821 }));822 }823 824 json predictions = json::array();825 for (auto & future : futures) {826 predictions.push_back(future.get());827 }828 829 auto res = std::make_unique<server_http_res>();830 res->data = safe_json_to_str({{"predictions", predictions}});831 return res;832 });833}834 