* This commit enables the router to forward form-data to model server. Fixes #22044 (enabling to use the /v1/audio/transcriptions in router mode) * * Applied the suggestion from Copilots first comment: using the non-throwing json::parse overload. * Addressed Copilots third comment by extending the files representation to also include filename and content-type * Addressed Copilots fourth comment by making the RNG thread_local * Changed variable body from std::string to std::ostringstream in build_multipart_body as suggested by ngxson in https://github.com/ggml-org/llama.cpp/pull/22118#discussion_r3127099053 * Added sanitize_field lambda in build_multipart_body for key, filename and content_type as suggested by ngxson in https://github.com/ggml-org/llama.cpp/pull/22118#discussion_r3127104647 * explicitly checking if value/item is string before calling value/item.get<std::string>() as requested by ngxson in https://github.com/ggml-org/llama.cpp/pull/22118#discussion_r3127111279 * Added double quote to the sanitize lambda and throw on json parse failure --------- Co-authored-by: Ralph Paßgang <ralph@trust-it.de>
This commit is contained in:
@@ -575,14 +575,14 @@ json server_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff) {
|
|||||||
json convert_transcriptions_to_chatcmpl(
|
json convert_transcriptions_to_chatcmpl(
|
||||||
const json & inp_body,
|
const json & inp_body,
|
||||||
const common_chat_templates * tmpls,
|
const common_chat_templates * tmpls,
|
||||||
const std::map<std::string, raw_buffer> & in_files,
|
const std::map<std::string, uploaded_file> & in_files,
|
||||||
std::vector<raw_buffer> & out_files) {
|
std::vector<raw_buffer> & out_files) {
|
||||||
// TODO @ngxson : this function may need to be improved in the future
|
// TODO @ngxson : this function may need to be improved in the future
|
||||||
// handle input files
|
// handle input files
|
||||||
out_files.clear();
|
out_files.clear();
|
||||||
auto it = in_files.find("file");
|
auto it = in_files.find("file");
|
||||||
if (it != in_files.end()) {
|
if (it != in_files.end()) {
|
||||||
out_files.push_back(it->second);
|
out_files.push_back(it->second.data);
|
||||||
} else {
|
} else {
|
||||||
throw std::invalid_argument("No input file found for transcription");
|
throw std::invalid_argument("No input file found for transcription");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "chat.h"
|
#include "chat.h"
|
||||||
#include "server-common.h"
|
#include "server-common.h"
|
||||||
|
#include "server-http.h"
|
||||||
|
|
||||||
#include <nlohmann/json_fwd.hpp>
|
#include <nlohmann/json_fwd.hpp>
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ json server_chat_convert_anthropic_to_oai(const json & body);
|
|||||||
json convert_transcriptions_to_chatcmpl(
|
json convert_transcriptions_to_chatcmpl(
|
||||||
const json & body,
|
const json & body,
|
||||||
const common_chat_templates * tmpls,
|
const common_chat_templates * tmpls,
|
||||||
const std::map<std::string, raw_buffer> & in_files,
|
const std::map<std::string, uploaded_file> & in_files,
|
||||||
std::vector<raw_buffer> & out_files);
|
std::vector<raw_buffer> & out_files);
|
||||||
|
|
||||||
json server_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);
|
json server_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ static server_http_res_ptr proxy_request(const server_http_req & req, std::strin
|
|||||||
parsed_url.path,
|
parsed_url.path,
|
||||||
headers,
|
headers,
|
||||||
req.body,
|
req.body,
|
||||||
|
req.files,
|
||||||
req.should_stop,
|
req.should_stop,
|
||||||
600, // timeout_read (default to 10 minutes)
|
600, // timeout_read (default to 10 minutes)
|
||||||
600 // timeout_write (default to 10 minutes)
|
600 // timeout_write (default to 10 minutes)
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ void server_http_context::get(const std::string & path, const server_http_contex
|
|||||||
void server_http_context::post(const std::string & path, const server_http_context::handler_t & handler) const {
|
void server_http_context::post(const std::string & path, const server_http_context::handler_t & handler) const {
|
||||||
pimpl->srv->Post(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {
|
pimpl->srv->Post(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {
|
||||||
std::string body = req.body;
|
std::string body = req.body;
|
||||||
std::map<std::string, raw_buffer> files;
|
std::map<std::string, uploaded_file> files;
|
||||||
|
|
||||||
if (req.is_multipart_form_data()) {
|
if (req.is_multipart_form_data()) {
|
||||||
// translate text fields to a JSON object and use it as the body
|
// translate text fields to a JSON object and use it as the body
|
||||||
@@ -459,7 +459,11 @@ void server_http_context::post(const std::string & path, const server_http_conte
|
|||||||
|
|
||||||
// populate files from multipart form
|
// populate files from multipart form
|
||||||
for (const auto & [key, file] : req.form.files) {
|
for (const auto & [key, file] : req.form.files) {
|
||||||
files[key] = raw_buffer(file.content.begin(), file.content.end());
|
files[key] = uploaded_file{
|
||||||
|
raw_buffer(file.content.begin(), file.content.end()),
|
||||||
|
file.filename,
|
||||||
|
file.content_type,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,13 +36,19 @@ struct server_http_res {
|
|||||||
using server_http_res_ptr = std::unique_ptr<server_http_res>;
|
using server_http_res_ptr = std::unique_ptr<server_http_res>;
|
||||||
using raw_buffer = std::vector<uint8_t>;
|
using raw_buffer = std::vector<uint8_t>;
|
||||||
|
|
||||||
|
struct uploaded_file {
|
||||||
|
raw_buffer data;
|
||||||
|
std::string filename;
|
||||||
|
std::string content_type;
|
||||||
|
};
|
||||||
|
|
||||||
struct server_http_req {
|
struct server_http_req {
|
||||||
std::map<std::string, std::string> params; // path_params + query_params
|
std::map<std::string, std::string> params; // path_params + query_params
|
||||||
std::map<std::string, std::string> headers; // used by MCP proxy
|
std::map<std::string, std::string> headers; // used by MCP proxy
|
||||||
std::string path;
|
std::string path;
|
||||||
std::string query_string; // query parameters string (e.g. "action=save")
|
std::string query_string; // query parameters string (e.g. "action=save")
|
||||||
std::string body;
|
std::string body;
|
||||||
std::map<std::string, raw_buffer> files; // used for file uploads (form data)
|
std::map<std::string, uploaded_file> files; // used for file uploads (form data)
|
||||||
const std::function<bool()> & should_stop;
|
const std::function<bool()> & should_stop;
|
||||||
|
|
||||||
std::string get_param(const std::string & key, const std::string & def = "") const {
|
std::string get_param(const std::string & key, const std::string & def = "") const {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <queue>
|
#include <queue>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <random>
|
||||||
|
#include <sstream>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
@@ -823,6 +825,7 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
|
|||||||
proxy_path,
|
proxy_path,
|
||||||
req.headers,
|
req.headers,
|
||||||
req.body,
|
req.body,
|
||||||
|
req.files,
|
||||||
req.should_stop,
|
req.should_stop,
|
||||||
base_params.timeout_read,
|
base_params.timeout_read,
|
||||||
base_params.timeout_write
|
base_params.timeout_write
|
||||||
@@ -1126,6 +1129,77 @@ static bool should_strip_proxy_header(const std::string & header_name) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string generate_multipart_boundary() {
|
||||||
|
thread_local std::mt19937 gen(std::random_device{}());
|
||||||
|
static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||||
|
std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
|
||||||
|
std::string boundary = "----llama-cpp-proxy-";
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
boundary += chars[dis(gen)];
|
||||||
|
}
|
||||||
|
return boundary;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string build_multipart_body(
|
||||||
|
const json & form_fields,
|
||||||
|
const std::map<std::string, uploaded_file> & files,
|
||||||
|
const std::string & boundary) {
|
||||||
|
static auto sanitize_field = [](const std::string & text) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(text.size());
|
||||||
|
for (char c : text) {
|
||||||
|
if (c != '\n' && c != '\r' && c != '"') {
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::ostringstream body;
|
||||||
|
|
||||||
|
for (const auto & [key, value] : form_fields.items()) {
|
||||||
|
if (value.is_array()) {
|
||||||
|
for (const auto & item : value) {
|
||||||
|
body << "--" << boundary << "\r\n";
|
||||||
|
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"\r\n";
|
||||||
|
body << "\r\n";
|
||||||
|
if (!item.is_string()) {
|
||||||
|
throw std::invalid_argument("expected string");
|
||||||
|
}
|
||||||
|
body << item.get<std::string>() << "\r\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body << "--" << boundary << "\r\n";
|
||||||
|
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"\r\n";
|
||||||
|
body << "\r\n";
|
||||||
|
if (!value.is_string()) {
|
||||||
|
throw std::invalid_argument("expected string");
|
||||||
|
}
|
||||||
|
body << value.get<std::string>() << "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto & [key, file] : files) {
|
||||||
|
body << "--" << boundary << "\r\n";
|
||||||
|
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"";
|
||||||
|
if (!file.filename.empty()) {
|
||||||
|
body << "; filename=\"" << sanitize_field(file.filename) << "\"";
|
||||||
|
}
|
||||||
|
body << "\r\n";
|
||||||
|
if (!file.content_type.empty()) {
|
||||||
|
body << "Content-Type: " << sanitize_field(file.content_type) << "\r\n";
|
||||||
|
} else {
|
||||||
|
body << "Content-Type: application/octet-stream\r\n";
|
||||||
|
}
|
||||||
|
body << "\r\n";
|
||||||
|
body.write(reinterpret_cast<const char*>(file.data.data()), file.data.size());
|
||||||
|
body << "\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
body << "--" << boundary << "--\r\n";
|
||||||
|
return body.str();
|
||||||
|
}
|
||||||
|
|
||||||
server_http_proxy::server_http_proxy(
|
server_http_proxy::server_http_proxy(
|
||||||
const std::string & method,
|
const std::string & method,
|
||||||
const std::string & scheme,
|
const std::string & scheme,
|
||||||
@@ -1134,6 +1208,7 @@ server_http_proxy::server_http_proxy(
|
|||||||
const std::string & path,
|
const std::string & path,
|
||||||
const std::map<std::string, std::string> & headers,
|
const std::map<std::string, std::string> & headers,
|
||||||
const std::string & body,
|
const std::string & body,
|
||||||
|
const std::map<std::string, uploaded_file> & files,
|
||||||
const std::function<bool()> should_stop,
|
const std::function<bool()> should_stop,
|
||||||
int32_t timeout_read,
|
int32_t timeout_read,
|
||||||
int32_t timeout_write
|
int32_t timeout_write
|
||||||
@@ -1195,28 +1270,65 @@ server_http_proxy::server_http_proxy(
|
|||||||
return pipe->write({{}, 0, std::string(data, data_length), ""});
|
return pipe->write({{}, 0, std::string(data, data_length), ""});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// when files are present, the body was converted from multipart form data to JSON
|
||||||
|
// we need to reconstruct the multipart body for the downstream server
|
||||||
|
std::string effective_body = body;
|
||||||
|
std::string override_content_type;
|
||||||
|
bool has_files = !files.empty();
|
||||||
|
|
||||||
|
if (has_files) {
|
||||||
|
json form_fields = json::parse(body, nullptr, false);
|
||||||
|
if (!form_fields.is_discarded()) {
|
||||||
|
auto boundary = generate_multipart_boundary();
|
||||||
|
effective_body = build_multipart_body(form_fields, files, boundary);
|
||||||
|
override_content_type = "multipart/form-data; boundary=" + boundary;
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("failed to parse multipart form fields JSON");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// prepare the request to destination server
|
// prepare the request to destination server
|
||||||
httplib::Request req;
|
httplib::Request req;
|
||||||
{
|
{
|
||||||
req.method = method;
|
req.method = method;
|
||||||
req.path = path;
|
req.path = path;
|
||||||
for (const auto & [key, value] : headers) {
|
for (const auto & [key, value] : headers) {
|
||||||
if (key == "Accept-Encoding") {
|
const auto lowered = to_lower_copy(key);
|
||||||
|
if (lowered == "accept-encoding") {
|
||||||
// disable Accept-Encoding to avoid compressed responses
|
// disable Accept-Encoding to avoid compressed responses
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key == "Transfer-Encoding") {
|
if (lowered == "transfer-encoding") {
|
||||||
// the body is already decoded
|
// the body is already decoded
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (key == "Host" || key == "host") {
|
if (lowered == "content-length") {
|
||||||
|
// let httplib calculate Content-Length from the actual body
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lowered == "content-type") {
|
||||||
|
if (has_files) {
|
||||||
|
// we set our own Content-Type with the new boundary
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// when no files but the original request was multipart,
|
||||||
|
// the body is now JSON, so correct the Content-Type
|
||||||
|
if (value.find("multipart/form-data") != std::string::npos) {
|
||||||
|
override_content_type = "application/json; charset=utf-8";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lowered == "host") {
|
||||||
bool is_default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
|
bool is_default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
|
||||||
req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port));
|
req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port));
|
||||||
} else {
|
} else {
|
||||||
req.set_header(key, value);
|
req.set_header(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.body = body;
|
req.body = effective_body;
|
||||||
|
if (!override_content_type.empty()) {
|
||||||
|
req.set_header("Content-Type", override_content_type);
|
||||||
|
}
|
||||||
req.response_handler = response_handler;
|
req.response_handler = response_handler;
|
||||||
req.content_receiver = content_receiver;
|
req.content_receiver = content_receiver;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ public:
|
|||||||
const std::string & path,
|
const std::string & path,
|
||||||
const std::map<std::string, std::string> & headers,
|
const std::map<std::string, std::string> & headers,
|
||||||
const std::string & body,
|
const std::string & body,
|
||||||
|
const std::map<std::string, uploaded_file> & files,
|
||||||
const std::function<bool()> should_stop,
|
const std::function<bool()> should_stop,
|
||||||
int32_t timeout_read,
|
int32_t timeout_read,
|
||||||
int32_t timeout_write
|
int32_t timeout_write
|
||||||
|
|||||||
Reference in New Issue
Block a user