213 std::string
host =
"localhost";
236 Logger::info(
"EMBER_DETERMINISTIC set: deterministic CUDA mode (run-to-run bit-identical logits; /health "
237 "reports \"deterministic\": true).");
239 std::shared_ptr<tinyllama::TinyLlamaSession>
session;
244 }
catch (
const std::exception&
e) {
255 const char*
v = std::getenv(
"EMBER_PUBLIC");
256 return v && std::string(
v) ==
"1";
260 svr.set_payload_max_length(64 * 1024);
261 svr.set_read_timeout(10, 0);
262 svr.set_write_timeout(30, 0);
267 if (
const char*
scripted = std::getenv(
"EMBER_TEST_SCRIPTED_REPLIES")) {
269 Logger::error(
"EMBER_TEST_SCRIPTED_REPLIES is a test hook: refused under EMBER_PUBLIC=1");
273 const json s = json::parse(
f,
nullptr,
false);
274 if (!
session->is_bpe() ||
s.is_discarded() || !
s.is_array()) {
276 " is not a JSON list of id lists, or the model has no chat mode");
279 session->set_scripted_replies(
s.get<std::vector<std::vector<int>>>());
280 Logger::warning(
"EMBER_TEST_SCRIPTED_REPLIES: " + std::to_string(
s.size()) +
281 " scripted replies stand in for sampling (a test server)");
284 if (std::filesystem::exists(
www_path) &&
285 std::filesystem::is_directory(
www_path)) {
294 ". Web client will not be served.");
304 static std::atomic<int>
waiting{0};
310 httplib::Response&
res) {
312 const auto t_request = std::chrono::steady_clock::now();
317 res.set_header(
"Access-Control-Allow-Origin",
"*");
318 res.set_header(
"Retry-After",
"2");
319 res.set_content(
json_out(
json{{
"error",
"busy: too many people chatting at once, try again in a moment"}}),
325 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
327 res.set_header(
"Access-Control-Allow-Origin",
"*");
328 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
329 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
333 float temperature = 0.1f;
334 int max_new_tokens = 60;
344 if (
req_json.contains(
"user_input")) {
346 }
else if (!tool_turns) {
347 throw std::runtime_error(
"Missing 'user_input' field in request JSON");
350 if (
req_json.contains(
"max_new_tokens"))
351 max_new_tokens =
req_json[
"max_new_tokens"].get<
int>();
352 if (
req_json.contains(
"temperature"))
363 res.set_header(
"Access-Control-Allow-Origin",
"*");
364 res.set_content(
json_out(
json{{
"error",
"message too long (600 characters at most)"}}),
"application/json");
367 max_new_tokens = std::min(std::max(max_new_tokens, 1), 64);
368 temperature = std::min(std::max(temperature, 0.0f), 1.5f);
369 top_k = std::min(std::max(top_k, 0), 200);
370 top_p = std::min(std::max(top_p, 0.05f), 1.0f);
384 hist[
at].value(
"role",
"") !=
"user")
385 hist.erase(
hist.begin() +
static_cast<std::ptrdiff_t
>(
at));
387 for (
const char* key : {
"content",
"user",
"assistant"})
388 if (
h.is_object() &&
h.contains(key) &&
h[key].is_string() &&
389 h[key].get<std::string>().size() >
kMaxText)
410 r.max_new_tokens = max_new_tokens;
411 r.temperature = temperature;
415 r.seed =
r.has_seed ?
req_json[
"seed"].get<
unsigned>() : 0
u;
416 r.want_ids =
req_json.value(
"return_ids",
false);
433 if (!
bp.error.empty()) {
438 if (!
req_json.value(
"stream",
false)) {
447 res.set_header(
"Cache-Control",
"no-cache");
448 res.set_chunked_content_provider(
452 const double waited = std::chrono::duration<double, std::milli>(
453 std::chrono::steady_clock::now() -
t_request).count();
455 const std::string
ev =
"data: " +
json_out(
j) +
"\n\n";
477 if (
config.is_gguf_file_loaded) {
491 Logger::info(std::string(
"WORD_LEVEL model type probed: ") +
495 std::transform(lower.begin(), lower.end(), lower.begin(),
::tolower);
496 bool has_field = lower.find(
"story:") != std::string::npos ||
497 lower.find(
"summary:") != std::string::npos ||
498 lower.find(
"words:") != std::string::npos ||
499 lower.rfind(
"once upon", 0) == 0 ||
500 lower.rfind(
"user:", 0) == 0;
510 Logger::info(
"WORD_LEVEL: wrapped input as dialogue turn.");
514 Logger::info(
"WORD_LEVEL: wrapped free-form input as story request.");
521 Logger::info(
"GGUF Llama 3 model detected (via tokenizer_family). Q&A prompt formatting will be DISABLED for session generate.");
525 "GGUF (Non-Llama 3) model detected. Using Q:A: format via session->generate.");
533 "Safetensors model detected. Applied chat template via "
534 "tokenizer. Prompt: " +
538 "CRITICAL: Tokenizer not available for Safetensors model in "
539 "server. Cannot apply chat template.");
552 size_t cut =
reply.find(
"user:");
553 if (cut != std::string::npos && cut > 0) {
567 auto ms = [](
auto a,
auto b) {
568 return std::chrono::duration<double, std::milli>(
b -
a).count();
573 {
"request_to_first_token_ms",
576 {
"ttft_ms",
tb.ttft_ms},
577 {
"num_prompt_tokens",
tb.num_prompt_tokens},
578 {
"batch_prefill",
tb.batch_prefill},
579 {
"tokenize_ms",
tb.tokenize_ms},
580 {
"kv_reset_ms",
tb.kv_reset_ms},
581 {
"embed_ms",
tb.embed_ms},
582 {
"prefill_wall_ms",
tb.prefill_wall_ms},
583 {
"prefill_gpu_span_ms",
tb.prefill_gpu_span_ms},
584 {
"sample_ms",
tb.sample_ms},
585 {
"prefix_prev_cached_tokens",
tb.prefix_prev_cached_tokens},
586 {
"prefix_reusable_tokens",
tb.prefix_reusable_tokens},
587 {
"prefix_reused_tokens",
tb.prefix_reused_tokens}, {
"spec_drafted",
tb.spec_drafted}, {
"spec_accepted",
tb.spec_accepted}, {
"spec_passes",
tb.spec_passes},
588 {
"prefix_hit_rate",
tb.prefix_hit_rate},
589 {
"op_ms",
tb.op_ms}};
590 if (
tb.spec_draft_calls > 0) {
591 res_json[
"ttft"][
"spec_draft_ms"] =
tb.spec_draft_ms;
592 res_json[
"ttft"][
"spec_draft_calls"] =
tb.spec_draft_calls;
598 }
catch (
const json::parse_error&
e) {
602 err_json[
"error"] =
"Invalid JSON format: " + std::string(
e.what());
604 }
catch (
const std::exception&
e) {
608 err_json[
"error"] =
"Internal server error: " + std::string(
e.what());
619 h[
"tokenizer"] =
session->is_bpe() ?
"bpe" :
"ember";
623 h[
"chat_template"] = {{
"name",
t->name}, {
"version", json::parse(
t->version.dump())}};
625 h[
"chat_template"] =
nullptr;
626 h[
"chat_template_why"] =
session->chat_template_why();
628 h[
"vocab_size"] =
c.vocab_size;
629 h[
"hidden_size"] =
c.hidden_size;
630 h[
"layers"] =
c.num_hidden_layers;
633 res.set_header(
"Access-Control-Allow-Origin",
"*");
650 const std::string
model_id = std::filesystem::path(
model_dir).filename().string();
653 res.set_header(
"Access-Control-Allow-Origin",
"*");
654 res.set_content(
json_out(
json{{
"error", {{
"message",
msg}, {
"type",
"invalid_request_error"}}}}),
657 svr.Get(
"/v1/models", [
model_id](
const httplib::Request&, httplib::Response&
res) {
658 res.set_header(
"Access-Control-Allow-Origin",
"*");
663 {
"owned_by",
"ember.cpp"}}})}}),
666 svr.Options(
"/v1/chat/completions", [](
const httplib::Request&, httplib::Response&
res) {
667 res.set_header(
"Access-Control-Allow-Origin",
"*");
668 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type, Authorization");
669 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
673 httplib::Response&
res) {
674 const auto t_request = std::chrono::steady_clock::now();
677 res.set_header(
"Retry-After",
"2");
678 openai_error(
res, 503,
"busy: too many requests at once, try again in a moment");
682 openai_error(
res, 400,
"this model has no chat template ember can apply; use POST /chat");
686 if (
body.is_discarded()) {
687 openai_error(
res, 400,
"body must be JSON with a non-empty 'messages' array");
694 r.max_new_tokens = 64;
710 if (
body.contains(
"temperature") &&
body[
"temperature"].is_number())
r.temperature =
body[
"temperature"].get<
float>();
711 if (
body.contains(
"top_p") &&
body[
"top_p"].is_number())
r.top_p =
body[
"top_p"].get<
float>();
712 if (
body.contains(
"top_k") &&
body[
"top_k"].is_number())
r.top_k =
body[
"top_k"].get<
int>();
713 for (
const char* key : {
"max_completion_tokens",
"max_tokens"})
714 if (
body.contains(key) &&
body[key].is_number()) {
715 r.max_new_tokens =
body[key].get<
int>();
718 if (
body.contains(
"seed") &&
body[
"seed"].is_number()) {
720 r.seed =
static_cast<unsigned>(
body[
"seed"].get<
long long>());
723 if (
body.contains(
"stream_options") &&
body[
"stream_options"].is_object())
725 }
catch (
const json::exception&
e) {
731 for (
const char* key : {
"frequency_penalty",
"presence_penalty"})
732 if (
body.contains(key) && !
body[key].is_null() && !(
body[key].is_number() &&
body[key].get<
double>() == 0.0)) {
733 openai_error(
res, 400, std::string(key) +
" is not supported (only 0); use repeat_penalty and repeat_last_n");
744 r.max_new_tokens = std::max(
r.max_new_tokens, 1);
748 std::vector<microtorch::chat::Message>&
m =
r.conv.messages;
749 if (
m.back().role ==
"user" &&
m.back().content.size() >
kMaxText) {
753 r.max_new_tokens = std::min(
r.max_new_tokens, 64);
754 r.temperature = std::min(std::max(
r.temperature, 0.0f), 1.5f);
755 r.top_k = std::min(std::max(
r.top_k, 0), 200);
756 r.top_p = std::min(std::max(
r.top_p, 0.05f), 1.0f);
761 m.erase(
m.begin() + (
m[0].role ==
"system" ? 1 : 0),
m.begin() +
static_cast<std::ptrdiff_t
>(
ex[
ex.size() - 9].first));
768 r.max_new_tokens = std::min(
r.max_new_tokens, 64);
769 r.temperature = std::min(std::max(
r.temperature, 0.0f), 1.5f);
770 r.top_k = std::min(std::max(
r.top_k, 0), 200);
771 r.top_p = std::min(std::max(
r.top_p, 0.05f), 1.0f);
773 while (
r.conv.turns.size() > 8)
r.conv.turns.erase(
r.conv.turns.begin());
774 for (
auto&
t :
r.conv.turns) {
781 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
790 if (!
bp.error.empty()) {
794 static std::mt19937_64
id_rng{std::random_device{}()};
796 std::snprintf(
idbuf,
sizeof idbuf,
"chatcmpl-%016llx",
static_cast<unsigned long long>(
id_rng()));
797 const std::string
id =
idbuf;
798 const long long created =
static_cast<long long>(std::time(
nullptr));
799 res.set_header(
"Access-Control-Allow-Origin",
"*");
802 return json{{
"prompt_tokens",
bp.prompt_tokens},
809 if (
result.value(
"stop_reason", std::string(
"eos")) ==
"length")
return "length";
810 return result.contains(
"tool_calls") ?
"tool_calls" :
"stop";
817 {
"object",
"chat.completion"},
820 {
"choices", json::array({
json{{
"index", 0},
822 {
"logprobs",
nullptr},
828 res.set_header(
"Cache-Control",
"no-cache");
829 res.set_chunked_content_provider(
832 size_t, httplib::DataSink&
sink) {
836 std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() -
t_request).count();
838 json c = {{
"id",
id},
839 {
"object",
"chat.completion.chunk"},
842 {
"choices", json::array({
json{{
"index", 0}, {
"delta",
delta}, {
"finish_reason", finish}}})}};
843 const std::string
ev =
"data: " +
json_out(
c) +
"\n\n";
846 chunk(
json{{
"role",
"assistant"}, {
"content",
""}},
nullptr);
850 if (
result.contains(
"tool_calls"))
851 for (
size_t k = 0;
k <
result[
"tool_calls"].size(); ++
k) {
858 json u = {{
"id",
id},
859 {
"object",
"chat.completion.chunk"},
862 {
"choices", json::array()},
864 const std::string
ev =
"data: " +
json_out(
u) +
"\n\n";
867 const std::string
done =
"data: [DONE]\n\n";
880 svr.Post(
"/score", [&
session](
const httplib::Request&
req, httplib::Response&
res) {
881 const auto t_request = std::chrono::steady_clock::now();
883 res.set_header(
"Access-Control-Allow-Origin",
"*");
889 refuse(403,
"/score is off on a public server (EMBER_PUBLIC=1)");
892 const json body = json::parse(
req.body,
nullptr,
false);
893 if (
body.is_discarded()) {
894 refuse(400,
"body must be JSON: {\"requests\": [{\"key\", \"prompt_ids\", \"continuation_ids\"}, ...]}");
899 m.ctx =
static_cast<size_t>(
session->score_context());
902 m.state_k =
static_cast<size_t>(
session->state_k());
904 m.user =
f.turn_ids.at(0);
905 m.assistant =
f.turn_ids.at(1);
907 std::vector<ember::ScoreItem>
items;
915 const auto t_start = std::chrono::steady_clock::now();
922 it.rec[
"over_ctx"] =
true;
927 std::vector<int> ctx =
it.prompt;
928 ctx.insert(ctx.end(),
it.cont.begin(),
it.cont.end() - 1);
930 const std::vector<float>
rows =
939 }
catch (
const std::invalid_argument&
e) {
940 refuse(400, std::string(
"score: ") +
e.what());
942 }
catch (
const std::exception&
e) {
943 refuse(500, std::string(
"score: ") +
e.what());
946 auto ms = [](
auto a,
auto b) {
return std::chrono::duration<double, std::milli>(
b -
a).count(); };
947 const auto t_end = std::chrono::steady_clock::now();
949 {
"stats", {{
"requests",
items.size()},
958 svr.Options(
"/score", [](
const httplib::Request&, httplib::Response&
res) {
959 res.set_header(
"Access-Control-Allow-Origin",
"*");
960 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
961 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
965 svr.Options(
"/chat", [](
const httplib::Request&
req, httplib::Response&
res) {
966 res.set_header(
"Access-Control-Allow-Origin",
"*");
967 res.set_header(
"Access-Control-Allow-Headers",
"Content-Type");
968 res.set_header(
"Access-Control-Allow-Methods",
"POST, OPTIONS");
974 " with " + std::to_string(
num_threads) +
" threads.");