* 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(
|
||||
const json & inp_body,
|
||||
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) {
|
||||
// TODO @ngxson : this function may need to be improved in the future
|
||||
// handle input files
|
||||
out_files.clear();
|
||||
auto it = in_files.find("file");
|
||||
if (it != in_files.end()) {
|
||||
out_files.push_back(it->second);
|
||||
out_files.push_back(it->second.data);
|
||||
} else {
|
||||
throw std::invalid_argument("No input file found for transcription");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "chat.h"
|
||||
#include "server-common.h"
|
||||
#include "server-http.h"
|
||||
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
@@ -19,7 +20,7 @@ json server_chat_convert_anthropic_to_oai(const json & body);
|
||||
json convert_transcriptions_to_chatcmpl(
|
||||
const json & body,
|
||||
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);
|
||||
|
||||
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,
|
||||
headers,
|
||||
req.body,
|
||||
req.files,
|
||||
req.should_stop,
|
||||
600, // timeout_read (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 {
|
||||
pimpl->srv->Post(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {
|
||||
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()) {
|
||||
// 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
|
||||
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 raw_buffer = std::vector<uint8_t>;
|
||||
|
||||
struct uploaded_file {
|
||||
raw_buffer data;
|
||||
std::string filename;
|
||||
std::string content_type;
|
||||
};
|
||||
|
||||
struct server_http_req {
|
||||
std::map<std::string, std::string> params; // path_params + query_params
|
||||
std::map<std::string, std::string> headers; // used by MCP proxy
|
||||
std::string path;
|
||||
std::string query_string; // query parameters string (e.g. "action=save")
|
||||
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;
|
||||
|
||||
std::string get_param(const std::string & key, const std::string & def = "") const {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include <chrono>
|
||||
#include <queue>
|
||||
#include <filesystem>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -823,6 +825,7 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
|
||||
proxy_path,
|
||||
req.headers,
|
||||
req.body,
|
||||
req.files,
|
||||
req.should_stop,
|
||||
base_params.timeout_read,
|
||||
base_params.timeout_write
|
||||
@@ -1126,6 +1129,77 @@ static bool should_strip_proxy_header(const std::string & header_name) {
|
||||
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(
|
||||
const std::string & method,
|
||||
const std::string & scheme,
|
||||
@@ -1134,6 +1208,7 @@ server_http_proxy::server_http_proxy(
|
||||
const std::string & path,
|
||||
const std::map<std::string, std::string> & headers,
|
||||
const std::string & body,
|
||||
const std::map<std::string, uploaded_file> & files,
|
||||
const std::function<bool()> should_stop,
|
||||
int32_t timeout_read,
|
||||
int32_t timeout_write
|
||||
@@ -1195,28 +1270,65 @@ server_http_proxy::server_http_proxy(
|
||||
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
|
||||
httplib::Request req;
|
||||
{
|
||||
req.method = method;
|
||||
req.path = path;
|
||||
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
|
||||
continue;
|
||||
}
|
||||
if (key == "Transfer-Encoding") {
|
||||
if (lowered == "transfer-encoding") {
|
||||
// the body is already decoded
|
||||
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);
|
||||
req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port));
|
||||
} else {
|
||||
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.content_receiver = content_receiver;
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ public:
|
||||
const std::string & path,
|
||||
const std::map<std::string, std::string> & headers,
|
||||
const std::string & body,
|
||||
const std::map<std::string, uploaded_file> & files,
|
||||
const std::function<bool()> should_stop,
|
||||
int32_t timeout_read,
|
||||
int32_t timeout_write
|
||||
|
||||
Reference in New Issue
Block a user