Skip to content

Commit 983ca89

Browse files
tha80Ralph Paßgang
andauthored
server: (router) Forward form-data to model server (Fixes #22044) (#22118)
* 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 #22118 (comment) * Added sanitize_field lambda in build_multipart_body for key, filename and content_type as suggested by ngxson in #22118 (comment) * explicitly checking if value/item is string before calling value/item.get<std::string>() as requested by ngxson in #22118 (comment) * Added double quote to the sanitize lambda and throw on json parse failure --------- Co-authored-by: Ralph Paßgang <ralph@trust-it.de>
1 parent 665abc6 commit 983ca89

7 files changed

Lines changed: 135 additions & 10 deletions

File tree

tools/server/server-chat.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -575,14 +575,14 @@ json server_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff) {
575575
json convert_transcriptions_to_chatcmpl(
576576
const json & inp_body,
577577
const common_chat_templates * tmpls,
578-
const std::map<std::string, raw_buffer> & in_files,
578+
const std::map<std::string, uploaded_file> & in_files,
579579
std::vector<raw_buffer> & out_files) {
580580
// TODO @ngxson : this function may need to be improved in the future
581581
// handle input files
582582
out_files.clear();
583583
auto it = in_files.find("file");
584584
if (it != in_files.end()) {
585-
out_files.push_back(it->second);
585+
out_files.push_back(it->second.data);
586586
} else {
587587
throw std::invalid_argument("No input file found for transcription");
588588
}

tools/server/server-chat.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "chat.h"
66
#include "server-common.h"
7+
#include "server-http.h"
78

89
#include <nlohmann/json_fwd.hpp>
910

@@ -19,7 +20,7 @@ json server_chat_convert_anthropic_to_oai(const json & body);
1920
json convert_transcriptions_to_chatcmpl(
2021
const json & body,
2122
const common_chat_templates * tmpls,
22-
const std::map<std::string, raw_buffer> & in_files,
23+
const std::map<std::string, uploaded_file> & in_files,
2324
std::vector<raw_buffer> & out_files);
2425

2526
json server_chat_msg_diff_to_json_oaicompat(const common_chat_msg_diff & diff);

tools/server/server-cors-proxy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ static server_http_res_ptr proxy_request(const server_http_req & req, std::strin
4949
parsed_url.path,
5050
headers,
5151
req.body,
52+
req.files,
5253
req.should_stop,
5354
600, // timeout_read (default to 10 minutes)
5455
600 // timeout_write (default to 10 minutes)

tools/server/server-http.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ void server_http_context::get(const std::string & path, const server_http_contex
438438
void server_http_context::post(const std::string & path, const server_http_context::handler_t & handler) const {
439439
pimpl->srv->Post(path_prefix + path, [handler](const httplib::Request & req, httplib::Response & res) {
440440
std::string body = req.body;
441-
std::map<std::string, raw_buffer> files;
441+
std::map<std::string, uploaded_file> files;
442442

443443
if (req.is_multipart_form_data()) {
444444
// 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
459459

460460
// populate files from multipart form
461461
for (const auto & [key, file] : req.form.files) {
462-
files[key] = raw_buffer(file.content.begin(), file.content.end());
462+
files[key] = uploaded_file{
463+
raw_buffer(file.content.begin(), file.content.end()),
464+
file.filename,
465+
file.content_type,
466+
};
463467
}
464468
}
465469

tools/server/server-http.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,19 @@ struct server_http_res {
3636
using server_http_res_ptr = std::unique_ptr<server_http_res>;
3737
using raw_buffer = std::vector<uint8_t>;
3838

39+
struct uploaded_file {
40+
raw_buffer data;
41+
std::string filename;
42+
std::string content_type;
43+
};
44+
3945
struct server_http_req {
4046
std::map<std::string, std::string> params; // path_params + query_params
4147
std::map<std::string, std::string> headers; // used by MCP proxy
4248
std::string path;
4349
std::string query_string; // query parameters string (e.g. "action=save")
4450
std::string body;
45-
std::map<std::string, raw_buffer> files; // used for file uploads (form data)
51+
std::map<std::string, uploaded_file> files; // used for file uploads (form data)
4652
const std::function<bool()> & should_stop;
4753

4854
std::string get_param(const std::string & key, const std::string & def = "") const {

tools/server/server-models.cpp

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <chrono>
1919
#include <queue>
2020
#include <filesystem>
21+
#include <random>
22+
#include <sstream>
2123
#include <cstring>
2224

2325
#ifdef _WIN32
@@ -823,6 +825,7 @@ server_http_res_ptr server_models::proxy_request(const server_http_req & req, co
823825
proxy_path,
824826
req.headers,
825827
req.body,
828+
req.files,
826829
req.should_stop,
827830
base_params.timeout_read,
828831
base_params.timeout_write
@@ -1126,6 +1129,77 @@ static bool should_strip_proxy_header(const std::string & header_name) {
11261129
return false;
11271130
}
11281131

1132+
static std::string generate_multipart_boundary() {
1133+
thread_local std::mt19937 gen(std::random_device{}());
1134+
static const char chars[] = "0123456789abcdefghijklmnopqrstuvwxyz";
1135+
std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
1136+
std::string boundary = "----llama-cpp-proxy-";
1137+
for (int i = 0; i < 16; i++) {
1138+
boundary += chars[dis(gen)];
1139+
}
1140+
return boundary;
1141+
}
1142+
1143+
static std::string build_multipart_body(
1144+
const json & form_fields,
1145+
const std::map<std::string, uploaded_file> & files,
1146+
const std::string & boundary) {
1147+
static auto sanitize_field = [](const std::string & text) {
1148+
std::string result;
1149+
result.reserve(text.size());
1150+
for (char c : text) {
1151+
if (c != '\n' && c != '\r' && c != '"') {
1152+
result += c;
1153+
}
1154+
}
1155+
return result;
1156+
};
1157+
1158+
std::ostringstream body;
1159+
1160+
for (const auto & [key, value] : form_fields.items()) {
1161+
if (value.is_array()) {
1162+
for (const auto & item : value) {
1163+
body << "--" << boundary << "\r\n";
1164+
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"\r\n";
1165+
body << "\r\n";
1166+
if (!item.is_string()) {
1167+
throw std::invalid_argument("expected string");
1168+
}
1169+
body << item.get<std::string>() << "\r\n";
1170+
}
1171+
} else {
1172+
body << "--" << boundary << "\r\n";
1173+
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"\r\n";
1174+
body << "\r\n";
1175+
if (!value.is_string()) {
1176+
throw std::invalid_argument("expected string");
1177+
}
1178+
body << value.get<std::string>() << "\r\n";
1179+
}
1180+
}
1181+
1182+
for (const auto & [key, file] : files) {
1183+
body << "--" << boundary << "\r\n";
1184+
body << "Content-Disposition: form-data; name=\"" << sanitize_field(key) << "\"";
1185+
if (!file.filename.empty()) {
1186+
body << "; filename=\"" << sanitize_field(file.filename) << "\"";
1187+
}
1188+
body << "\r\n";
1189+
if (!file.content_type.empty()) {
1190+
body << "Content-Type: " << sanitize_field(file.content_type) << "\r\n";
1191+
} else {
1192+
body << "Content-Type: application/octet-stream\r\n";
1193+
}
1194+
body << "\r\n";
1195+
body.write(reinterpret_cast<const char*>(file.data.data()), file.data.size());
1196+
body << "\r\n";
1197+
}
1198+
1199+
body << "--" << boundary << "--\r\n";
1200+
return body.str();
1201+
}
1202+
11291203
server_http_proxy::server_http_proxy(
11301204
const std::string & method,
11311205
const std::string & scheme,
@@ -1134,6 +1208,7 @@ server_http_proxy::server_http_proxy(
11341208
const std::string & path,
11351209
const std::map<std::string, std::string> & headers,
11361210
const std::string & body,
1211+
const std::map<std::string, uploaded_file> & files,
11371212
const std::function<bool()> should_stop,
11381213
int32_t timeout_read,
11391214
int32_t timeout_write
@@ -1195,28 +1270,65 @@ server_http_proxy::server_http_proxy(
11951270
return pipe->write({{}, 0, std::string(data, data_length), ""});
11961271
};
11971272

1273+
// when files are present, the body was converted from multipart form data to JSON
1274+
// we need to reconstruct the multipart body for the downstream server
1275+
std::string effective_body = body;
1276+
std::string override_content_type;
1277+
bool has_files = !files.empty();
1278+
1279+
if (has_files) {
1280+
json form_fields = json::parse(body, nullptr, false);
1281+
if (!form_fields.is_discarded()) {
1282+
auto boundary = generate_multipart_boundary();
1283+
effective_body = build_multipart_body(form_fields, files, boundary);
1284+
override_content_type = "multipart/form-data; boundary=" + boundary;
1285+
} else {
1286+
throw std::runtime_error("failed to parse multipart form fields JSON");
1287+
}
1288+
}
1289+
11981290
// prepare the request to destination server
11991291
httplib::Request req;
12001292
{
12011293
req.method = method;
12021294
req.path = path;
12031295
for (const auto & [key, value] : headers) {
1204-
if (key == "Accept-Encoding") {
1296+
const auto lowered = to_lower_copy(key);
1297+
if (lowered == "accept-encoding") {
12051298
// disable Accept-Encoding to avoid compressed responses
12061299
continue;
12071300
}
1208-
if (key == "Transfer-Encoding") {
1301+
if (lowered == "transfer-encoding") {
12091302
// the body is already decoded
12101303
continue;
12111304
}
1212-
if (key == "Host" || key == "host") {
1305+
if (lowered == "content-length") {
1306+
// let httplib calculate Content-Length from the actual body
1307+
continue;
1308+
}
1309+
if (lowered == "content-type") {
1310+
if (has_files) {
1311+
// we set our own Content-Type with the new boundary
1312+
continue;
1313+
}
1314+
// when no files but the original request was multipart,
1315+
// the body is now JSON, so correct the Content-Type
1316+
if (value.find("multipart/form-data") != std::string::npos) {
1317+
override_content_type = "application/json; charset=utf-8";
1318+
continue;
1319+
}
1320+
}
1321+
if (lowered == "host") {
12131322
bool is_default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
12141323
req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port));
12151324
} else {
12161325
req.set_header(key, value);
12171326
}
12181327
}
1219-
req.body = body;
1328+
req.body = effective_body;
1329+
if (!override_content_type.empty()) {
1330+
req.set_header("Content-Type", override_content_type);
1331+
}
12201332
req.response_handler = response_handler;
12211333
req.content_receiver = content_receiver;
12221334
}

tools/server/server-models.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ struct server_http_proxy : server_http_res {
202202
const std::string & path,
203203
const std::map<std::string, std::string> & headers,
204204
const std::string & body,
205+
const std::map<std::string, uploaded_file> & files,
205206
const std::function<bool()> should_stop,
206207
int32_t timeout_read,
207208
int32_t timeout_write

0 commit comments

Comments
 (0)