mirror of
https://github.com/ikawrakow/ik_llama.cpp.git
synced 2026-02-02 04:29:53 +00:00
Add mikupad to ik_llama as an alternative WebUI (#558)
* mikupad.html in ik_llama.cpp (functional but WIP) * Remove hardcoded extension and add error handling to extension loading * Update version number and add features array to version * Make version endpoint always accessible * Fix case with empty sql * Add useful error message when launched without sql file * Add sigma sampler * Update sigma step and max based on docs * Remove selectedSessionId and handle it with URL fragment * Export All (code only, no UI) * Add compression to server.cpp * Major UI work (and also add update backend endpoints to accomadate) * Finalize UI * Fix visual bug * fix merge conflict issue * Pull in full sqlite_modern_cpp repo for the license as it is not attached to source files * Make compression not show in sidebar if extension is not loaded * Finalize build, Put support behing LLAMA_SERVER_SQLITE3: command not found build option, and update error message to include the build option is not passed situation * Fix compile without flag on systems without it installed
This commit is contained in:
@@ -38,6 +38,19 @@
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
#include <src/llama-impl.h>
|
||||
#ifdef SQLITE3_MODERN_CPP_SUPPORT
|
||||
#include <sqlite_modern_cpp.h>
|
||||
|
||||
struct DatabaseHandle {
|
||||
sqlite::database db;
|
||||
|
||||
DatabaseHandle(const std::string& path) : db(path) {
|
||||
db << "CREATE TABLE IF NOT EXISTS sessions (key TEXT PRIMARY KEY, data TEXT)";
|
||||
db << "CREATE TABLE IF NOT EXISTS templates (key TEXT PRIMARY KEY, data TEXT)";
|
||||
db << "CREATE TABLE IF NOT EXISTS names (key TEXT PRIMARY KEY, data TEXT)";
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
using json = nlohmann::ordered_json;
|
||||
|
||||
@@ -3441,7 +3454,32 @@ int main(int argc, char ** argv) {
|
||||
|
||||
// Necessary similarity of prompt for slot selection
|
||||
ctx_server.slot_prompt_similarity = params.slot_prompt_similarity;
|
||||
|
||||
#ifdef SQLITE3_MODERN_CPP_SUPPORT
|
||||
auto db_handle = std::make_shared<DatabaseHandle>(params.sql_save_file);
|
||||
bool sqlite_extension_loaded = false;
|
||||
if (!params.sqlite_zstd_ext_file.empty()) {
|
||||
auto* conn = db_handle->db.connection().get();
|
||||
sqlite3_enable_load_extension(conn, 1);
|
||||
char* errmsg = nullptr;
|
||||
const int rc = sqlite3_load_extension(
|
||||
conn,
|
||||
params.sqlite_zstd_ext_file.c_str(),
|
||||
nullptr,
|
||||
&errmsg
|
||||
);
|
||||
if(rc != SQLITE_OK) {
|
||||
const std::string err = errmsg ? errmsg : "Unknown extension error";
|
||||
sqlite3_free(errmsg);
|
||||
LOG_WARNING("Failed to load extension", {{"err", err}});
|
||||
}
|
||||
else {
|
||||
sqlite_extension_loaded = true;
|
||||
}
|
||||
sqlite3_enable_load_extension(conn, 0);
|
||||
}
|
||||
#else
|
||||
auto db_handle = false;
|
||||
#endif
|
||||
// load the model
|
||||
if (!ctx_server.load_model(params)) {
|
||||
state.store(SERVER_STATE_ERROR);
|
||||
@@ -3828,6 +3866,7 @@ int main(int argc, char ** argv) {
|
||||
{ "default_generation_settings", ctx_server.default_generation_settings_for_props },
|
||||
{ "total_slots", ctx_server.params.n_parallel },
|
||||
{ "chat_template", ctx_server.chat_templates.template_default->source() },
|
||||
{ "n_ctx", ctx_server.n_ctx }
|
||||
};
|
||||
if (ctx_server.params.use_jinja && ctx_server.chat_templates.template_tool_use) {
|
||||
data["chat_template_tool_use"] = ctx_server.chat_templates.template_tool_use->source();
|
||||
@@ -4249,9 +4288,28 @@ int main(int argc, char ** argv) {
|
||||
std::vector<llama_token> tokens(n_token_count);
|
||||
file.read(reinterpret_cast<char*>(tokens.data()), tokens.size() * sizeof(llama_token));
|
||||
|
||||
//C++17 is not modern enough to have a nice and portable way to get the mtime of a file
|
||||
//so the following seems to be needed
|
||||
auto ftime = fs::last_write_time(entry.path());
|
||||
auto system_time = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
|
||||
ftime - fs::file_time_type::clock::now() + std::chrono::system_clock::now()
|
||||
);
|
||||
std::time_t c_time = std::chrono::system_clock::to_time_t(system_time);
|
||||
std::tm tm_struct;
|
||||
#if defined(_WIN32)
|
||||
localtime_s(&tm_struct, &c_time);
|
||||
#else
|
||||
localtime_r(&c_time, &tm_struct);
|
||||
#endif
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(&tm_struct, "%Y-%m-%d %H:%M:%S");
|
||||
auto str_time = oss.str();
|
||||
|
||||
|
||||
response.push_back({
|
||||
{"filename", entry.path().filename().string()},
|
||||
{"filesize", entry.file_size()},
|
||||
{"mtime", str_time},
|
||||
{"token_count", n_token_count},
|
||||
{"prompt", tokens_to_str(ctx_server.ctx, tokens.cbegin(), tokens.cend())}
|
||||
});
|
||||
@@ -4263,12 +4321,292 @@ int main(int argc, char ** argv) {
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
};
|
||||
|
||||
const auto list_slot_prompts = [&ctx_server, ¶ms](const httplib::Request& req, httplib::Response& res) {
|
||||
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
|
||||
json response = json::array();
|
||||
for (server_slot & slot : ctx_server.slots) {
|
||||
response.push_back({
|
||||
{"slot_id", slot.id},
|
||||
{"token_count", slot.cache_tokens.size()},
|
||||
{"prompt", tokens_to_str(ctx_server.ctx, slot.cache_tokens.cbegin(), slot.cache_tokens.cend())}
|
||||
});
|
||||
}
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
};
|
||||
|
||||
|
||||
const auto delete_saved_prompt = [&ctx_server, ¶ms](const httplib::Request& req, httplib::Response& res)-> void {
|
||||
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
|
||||
json response;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
try {
|
||||
const json body = json::parse(req.body);
|
||||
const std::string filename_str = body.at("filename");
|
||||
|
||||
// prevent directory traversal attacks
|
||||
if (filename_str.find("..") != std::string::npos || filename_str.find('/') != std::string::npos || filename_str.find('\\') != std::string::npos) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Invalid filename format."}};
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path file_to_delete = fs::path(params.slot_save_path) / fs::path(filename_str);
|
||||
|
||||
if (!fs::exists(file_to_delete) || !fs::is_regular_file(file_to_delete)) {
|
||||
res.status = 404;
|
||||
response = {{"error", "File not found."}};
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs::remove(file_to_delete)) {
|
||||
response = {
|
||||
{"status", "deleted"},
|
||||
{"filename", filename_str}
|
||||
};
|
||||
} else {
|
||||
res.status = 500;
|
||||
response = {{"error", "Failed to delete the file."}};
|
||||
}
|
||||
} catch (const json::parse_error& e) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Invalid JSON request body."}};
|
||||
} catch (const json::out_of_range& e) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Missing 'filename' key in request body."}};
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
response = {{"error", e.what()}};
|
||||
}
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
};
|
||||
|
||||
const auto rename_saved_prompt = [&ctx_server, ¶ms](const httplib::Request& req, httplib::Response& res)-> void {
|
||||
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
|
||||
json response;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
try {
|
||||
const json body = json::parse(req.body);
|
||||
const std::string old_filename_str = body.at("old_filename");
|
||||
const std::string new_filename_str = body.at("new_filename");
|
||||
|
||||
if (old_filename_str.find("..") != std::string::npos || old_filename_str.find_first_of("/\\") != std::string::npos ||
|
||||
new_filename_str.find("..") != std::string::npos || new_filename_str.find_first_of("/\\") != std::string::npos) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Invalid filename format."}};
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
const fs::path old_path = fs::path(params.slot_save_path) / old_filename_str;
|
||||
const fs::path new_path = fs::path(params.slot_save_path) / new_filename_str;
|
||||
|
||||
if (!fs::exists(old_path) || !fs::is_regular_file(old_path)) {
|
||||
res.status = 404;
|
||||
response = {{"error", "Source file not found."}};
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
if (fs::exists(new_path)) {
|
||||
res.status = 409;
|
||||
response = {{"error", "Destination filename already exists."}};
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::rename(old_path, new_path, ec);
|
||||
|
||||
if (ec) {
|
||||
res.status = 500;
|
||||
response = {{"error", "Failed to rename file: " + ec.message()}};
|
||||
} else {
|
||||
response = {
|
||||
{"status", "renamed"},
|
||||
{"old_filename", old_filename_str},
|
||||
{"new_filename", new_filename_str}
|
||||
};
|
||||
}
|
||||
|
||||
} catch (const json::parse_error& e) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Invalid JSON request body."}};
|
||||
} catch (const json::out_of_range& e) {
|
||||
res.status = 400;
|
||||
response = {{"error", "Missing 'old_filename' or 'new_filename' in request body."}};
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
response = {{"error", e.what()}};
|
||||
}
|
||||
|
||||
res.set_content(response.dump(), "application/json; charset=utf-8");
|
||||
};
|
||||
|
||||
auto handle_static_file = [](unsigned char * content, size_t len, const char * mime_type) {
|
||||
return [content, len, mime_type](const httplib::Request &, httplib::Response & res) {
|
||||
res.set_content(reinterpret_cast<const char*>(content), len, mime_type);
|
||||
return false;
|
||||
};
|
||||
};
|
||||
#ifdef SQLITE3_MODERN_CPP_SUPPORT
|
||||
const auto handle_version = [¶ms, sqlite_extension_loaded](const httplib::Request&, httplib::Response& res) {
|
||||
res.set_content(
|
||||
json{{"version", 4},
|
||||
{"features", {{"sql", !params.sql_save_file.empty()}, {"zstd_compression", sqlite_extension_loaded}}}}.dump(),
|
||||
"application/json"
|
||||
);
|
||||
};
|
||||
#else
|
||||
const auto handle_version = [](const httplib::Request&, httplib::Response& res)-> void {
|
||||
res.set_content(
|
||||
json{{"version", 4},
|
||||
{"features", {{"sql", false}, {"zstd_compression", false}}}}.dump(),
|
||||
"application/json"
|
||||
);
|
||||
};
|
||||
#endif
|
||||
|
||||
#ifdef SQLITE3_MODERN_CPP_SUPPORT
|
||||
auto db_handler = [db_handle](auto func) {
|
||||
return [func, db_handle](const httplib::Request& req, httplib::Response& res) {
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
try {
|
||||
const json body = !req.body.empty() ? json::parse(req.body) : json::object();
|
||||
func(*db_handle, body, req, res);
|
||||
} catch(const std::exception& e) {
|
||||
res.status = 500;
|
||||
res.set_content(
|
||||
json{{"ok", false}, {"message", e.what()}}.dump(),
|
||||
"application/json"
|
||||
);
|
||||
}
|
||||
};
|
||||
};
|
||||
#else
|
||||
auto db_handler = [db_handle](auto func) {
|
||||
return [func, db_handle](const httplib::Request& req, httplib::Response& res) {
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
res.status = 500;
|
||||
res.set_content(
|
||||
json{{"ok", false}, {"message", "Sqlite3 support was not enabled. Recompile with '-DLLAMA_SERVER_SQLITE3=ON'"}}.dump(),
|
||||
"application/json"
|
||||
);
|
||||
};
|
||||
};
|
||||
#endif
|
||||
|
||||
const auto normalize_store_name = [](const std::string& storeName) {
|
||||
if(storeName.empty()) return std::string("sessions");
|
||||
|
||||
std::string normalized;
|
||||
normalized.reserve(storeName.size());
|
||||
|
||||
for(char c : storeName) {
|
||||
if(std::isalpha(static_cast<unsigned char>(c))) {
|
||||
normalized.push_back(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
}
|
||||
|
||||
return normalized.empty() ? "sessions" : normalized;
|
||||
};
|
||||
|
||||
const auto get_key_string = [](const json& j) {
|
||||
return j.is_string() ? j.get<std::string>() : j.dump();
|
||||
};
|
||||
|
||||
|
||||
const auto handle_load = db_handler([normalize_store_name, get_key_string](auto& db, const json& body, auto&, auto& res) {
|
||||
std::string data;
|
||||
const std::string store = normalize_store_name(body["storeName"]);
|
||||
db.db << "SELECT data FROM " + store + " WHERE key = ?" << get_key_string(body["key"]) >> data;
|
||||
if(data.empty()) {
|
||||
res.status = 404;
|
||||
res.set_content(json{{"ok", false}, {"message", "Key not found"}}.dump(), "application/json");
|
||||
} else {
|
||||
json response{{"ok", true}};
|
||||
response["result"] = (store == "names") ? json(data) : json::parse(data);
|
||||
res.set_content(response.dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
const auto handle_save = db_handler([normalize_store_name, get_key_string](auto& db, const json& body, auto&, auto& res) {
|
||||
const std::string store = normalize_store_name(body["storeName"]);
|
||||
const std::string data = (store == "names") ? body["data"].get<std::string>() : body["data"].dump();
|
||||
db.db << "INSERT OR REPLACE INTO " + store + " (key, data) VALUES (?, ?)" << get_key_string(body["key"]) << data;
|
||||
res.set_content(json{{"ok", true}, {"result", "Data saved successfully"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_rename = db_handler([get_key_string](auto& db, const json& body, auto&, auto& res) {
|
||||
db.db << "UPDATE names SET data = ? WHERE key = ?"
|
||||
<< body["newName"].get<std::string>()
|
||||
<< get_key_string(body["key"]);
|
||||
res.set_content(json{{"ok", true}, {"result", "Session renamed successfully"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_all = db_handler([normalize_store_name](auto& db, const json& body, auto&, auto& res) {
|
||||
json result = json::object();
|
||||
db.db << "SELECT key, data FROM " + normalize_store_name(body["storeName"]) >>
|
||||
[&](const std::string& key, const std::string& data) {
|
||||
result[key] = json::parse(data);
|
||||
};
|
||||
res.set_content(json{{"ok", true}, {"result", result}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_sessions = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
json result = json::object();
|
||||
db.db << "SELECT key, data FROM names" >> [&](const std::string& key, const std::string& data) {
|
||||
result[key] = data;
|
||||
};
|
||||
res.set_content(json{{"ok", true}, {"result", result}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_delete = db_handler([normalize_store_name, get_key_string](auto& db, const json& body, auto&, auto& res) {
|
||||
db.db << "DELETE FROM " + normalize_store_name(body["storeName"]) + " WHERE key = ?"
|
||||
<< get_key_string(body["key"]);
|
||||
res.set_content(json{{"ok", true}, {"result", "Session deleted successfully"}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_vacuum = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
json result = json::object();
|
||||
db.db << "VACUUM";
|
||||
res.set_content(json{"ok", true}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_zstd_get_configs = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
json result = json::object();
|
||||
db.db << "SELECT id, config FROM _zstd_configs" >> [&](const std::string id, const std::string& config) {
|
||||
result[id] = config;
|
||||
};
|
||||
res.set_content(json{{"ok", true}, {"configs", result}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_zstd_maintenance = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
std::string data;
|
||||
if (body["duration"].is_null()) {
|
||||
db.db << "select zstd_incremental_maintenance(?, ?)" << nullptr << body["db_load"].get<double>() >> data;
|
||||
}
|
||||
else {
|
||||
db.db << "select zstd_incremental_maintenance(?, ?)" << body["duration"].get<double>() << body["db_load"].get<double>() >> data;
|
||||
}
|
||||
json response{{"ok", true}};
|
||||
response["result"] = json::parse(data);
|
||||
res.set_content(response.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_zstd_enable = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
db.db << "select zstd_enable_transparent('{\"table\": \"" + body["table"].get<std::string>() + "\",\"column\": \"" + body["column"].get<std::string>() + "\", \"compression_level\": " + std::to_string(body["compression_level"].get<int>()) + ", \"dict_chooser\": \"''a''\", \"train_dict_samples_ratio\": " + std::to_string(body["train_dict_samples_ratio"].get<int>()) + "}')";
|
||||
res.set_content(json{"ok", true}.dump(), "application/json");
|
||||
});
|
||||
|
||||
const auto handle_zstd_config_update = db_handler([](auto& db, const json& body, auto&, auto& res) {
|
||||
std::string patch_json = "{\"compression_level\": " + std::to_string(body["compression_level"].get<int>()) + ", \"train_dict_samples_ratio\": " + std::to_string(body["train_dict_samples_ratio"].get<int>()) + "}";
|
||||
db.db << "update _zstd_configs set config = json_patch(config, '" + patch_json + "')";
|
||||
res.set_content(json{{"ok", true}}.dump(), "application/json");
|
||||
});
|
||||
|
||||
//
|
||||
// Router
|
||||
@@ -4328,12 +4666,36 @@ int main(int argc, char ** argv) {
|
||||
svr->Post("/lora-adapters", handle_lora_adapters_apply);
|
||||
// Save & load slots
|
||||
svr->Get ("/slots", handle_slots);
|
||||
svr->Get ("/slots/list", list_slot_prompts);
|
||||
if (!params.slot_save_path.empty()) {
|
||||
// these endpoints rely on slot_save_path existing
|
||||
svr->Post("/slots/:id_slot", handle_slots_action);
|
||||
svr->Get ("/list", list_saved_prompts);
|
||||
}
|
||||
svr->Post("/delete_prompt", delete_saved_prompt);
|
||||
svr->Post("/rename_prompt", rename_saved_prompt);
|
||||
|
||||
}
|
||||
svr->Get ("/version", handle_version);
|
||||
if (!params.sql_save_file.empty()) {
|
||||
// these endpoints rely on sql_save_file existing
|
||||
svr->Post("/load", handle_load);
|
||||
svr->Post("/save", handle_save);
|
||||
svr->Post("/rename", handle_rename);
|
||||
svr->Post("/all", handle_all);
|
||||
svr->Post("/sessions", handle_sessions);
|
||||
svr->Get ("/sessions", handle_sessions);
|
||||
svr->Post("/delete", handle_delete);
|
||||
//VACUUM is there for the extension but does not require the extension
|
||||
svr->Get ("/vacuum", handle_vacuum);
|
||||
#ifdef SQLITE3_MODERN_CPP_SUPPORT
|
||||
if (sqlite_extension_loaded) {
|
||||
svr->Get ("/zstd_get_configs", handle_zstd_get_configs);
|
||||
svr->Post("/zstd_incremental_maintenance", handle_zstd_maintenance);
|
||||
svr->Post("/zstd_enable_transparent", handle_zstd_enable);
|
||||
svr->Post("/zstd_update_transparent", handle_zstd_config_update);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
//
|
||||
// Start the server
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user