Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ poetry.toml

# Scripts
!/scripts/install-oneapi.bat
/examples/server/webui_llamacpp/.gitignore
25 changes: 25 additions & 0 deletions common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,20 @@ int32_t cpu_get_num_math() {
return cpu_get_num_physical_cores();
}

common_webui common_webui_from_name(const std::string& format) {
if (format == "none") {
return COMMON_WEBUI_NONE;
}
else if (format == "auto") {
return COMMON_WEBUI_AUTO;
}
else if (format == "llamacpp") {
return COMMON_WEBUI_LLAMACPP;
}
else {
return COMMON_WEBUI_AUTO;
}
}

static std::string read_file(const std::string& fname) {
std::ifstream file(fname);
Expand Down Expand Up @@ -1401,6 +1415,11 @@ bool gpt_params_find_arg(int argc, char ** argv, const std::string & arg, gpt_pa
params.public_path = argv[i];
return true;
}
if (arg == "--webui") {
CHECK_ARG
params.webui = common_webui_from_name(std::string(argv[i]));
return true;
}
if (arg == "--api-key") {
CHECK_ARG
params.api_keys.push_back(argv[i]);
Expand Down Expand Up @@ -2028,6 +2047,12 @@ void gpt_params_print_usage(int /*argc*/, char ** argv, const gpt_params & param
options.push_back({ "server", " --port PORT", "port to listen (default: %d)", params.port });
options.push_back({ "server", " --path PATH", "path to serve static files from (default: %s)", params.public_path.c_str() });
options.push_back({ "server", " --embedding(s)", "restrict to only support embedding use case; use only with dedicated embedding models (default: %s)", params.embedding ? "enabled" : "disabled" });
options.push_back({ "server", " --webui NAME",
"controls which webui to server:\n"
"- none: disable webui\n"
"- auto: default webui \n"
"- llamacpp: llamacpp webui \n"
"(default: auto)", });
options.push_back({ "server", " --api-key KEY", "API key to use for authentication (default: none)" });
options.push_back({ "server", " --api-key-file FNAME", "path to file containing API keys (default: none)" });
options.push_back({ "server", " --ssl-key-file FNAME", "path to file a PEM-encoded SSL private key" });
Expand Down
12 changes: 10 additions & 2 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ enum common_reasoning_format {
COMMON_REASONING_FORMAT_DEEPSEEK, // Extract thinking tag contents and return as `message.reasoning_content`, including in streaming deltas.
};

enum common_webui {
COMMON_WEBUI_NONE,
COMMON_WEBUI_AUTO,
COMMON_WEBUI_LLAMACPP,
};

common_webui common_webui_from_name(const std::string& format);

struct gpt_params {
uint32_t seed = LLAMA_DEFAULT_SEED; // RNG seed

Expand Down Expand Up @@ -265,8 +273,8 @@ struct gpt_params {
std::map<std::string, std::string> default_template_kwargs;

// "advanced" endpoints are disabled by default for better security
bool webui = true;
bool endpoint_slots = false;
common_webui webui = COMMON_WEBUI_AUTO;
bool endpoint_slots = true;
bool endpoint_props = false; // only control POST requests, not GET
bool endpoint_metrics = false;

Expand Down
24 changes: 23 additions & 1 deletion examples/server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ set(TARGET_SRCS
)
set(PUBLIC_ASSETS
index.html.gz
loading.html

)

foreach(asset ${PUBLIC_ASSETS})
Expand All @@ -29,10 +29,32 @@ foreach(asset ${PUBLIC_ASSETS})
OUTPUT "${output}"
COMMAND "${CMAKE_COMMAND}" "-DINPUT=${input}" "-DOUTPUT=${output}" -P "${PROJECT_SOURCE_DIR}/scripts/xxd.cmake"
)
message("TARGET_SRCS contains: ${input}")
set_source_files_properties(${output} PROPERTIES GENERATED TRUE)

endforeach()

# include new llamacpp webui
set(ALT_PUBLIC_ASSETS
index_llamacpp.html.gz
loading.html
)

foreach(asset ${ALT_PUBLIC_ASSETS})
set(input "${CMAKE_CURRENT_SOURCE_DIR}/public_llamacpp/${asset}")
set(output "${CMAKE_CURRENT_BINARY_DIR}/${asset}.hpp")
list(APPEND TARGET_SRCS ${output})
add_custom_command(
DEPENDS "${input}"
OUTPUT "${output}"
COMMAND "${CMAKE_COMMAND}" "-DINPUT=${input}" "-DOUTPUT=${output}" -P "${PROJECT_SOURCE_DIR}/scripts/xxd.cmake"
)
message("TARGET_SRCS contains: ${input}")
set_source_files_properties(${output} PROPERTIES GENERATED TRUE)

endforeach()


add_executable(${TARGET} ${TARGET_SRCS})
install(TARGETS ${TARGET} RUNTIME)
target_compile_definitions(${TARGET} PRIVATE
Expand Down
417 changes: 0 additions & 417 deletions examples/server/public/index.html

This file was deleted.

1 change: 1 addition & 0 deletions examples/server/public_llamacpp/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1,212 changes: 1,212 additions & 0 deletions examples/server/public_llamacpp/index_llamacpp.html

Large diffs are not rendered by default.

Binary file not shown.
103 changes: 77 additions & 26 deletions examples/server/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#define JSON_ASSERT GGML_ASSERT
#include "json.hpp"
#include "index.html.gz.hpp"
#include "index_llamacpp.html.gz.hpp"
#include "loading.html.hpp"

#include <atomic>
Expand Down Expand Up @@ -4141,6 +4142,7 @@ int main(int argc, char ** argv) {
{ "chat_template", common_chat_templates_source(ctx_server.chat_templates.get()) },
{ "bos_token", llama_token_to_piece(ctx_server.ctx, llama_token_bos(ctx_server.model), /* special= */ true)},
{ "eos_token", llama_token_to_piece(ctx_server.ctx, llama_token_eos(ctx_server.model), /* special= */ true)},
{ "model_path", ctx_server.params.model },
{ "n_ctx", ctx_server.n_ctx }

};
Expand Down Expand Up @@ -4998,38 +5000,51 @@ int main(int argc, char ** argv) {
//
// Router
//

// register static assets routes
if (!params.public_path.empty()) {
// Set the base directory for serving static files
svr->set_base_dir(params.public_path);
if (params.webui == COMMON_WEBUI_NONE) {
LLAMA_LOG_INFO("Web UI is disabled\n");
}

{
else {
// register static assets routes
if (!params.public_path.empty()) {
// Set the base directory for serving static files
bool is_found = svr->set_mount_point("/", params.public_path);
if (!is_found) {
GGML_ABORT("%s: static assets path not found: %s\n", __func__, params.public_path.c_str());
return 1;
}
svr->set_base_dir(params.public_path);
}
else {
// using embedded static index.html
svr->Get("/", [](const httplib::Request& req, httplib::Response& res) {
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
res.set_content("Error: gzip is not supported by this browser", "text/plain");
}
else {
res.set_header("Content-Encoding", "gzip");
// COEP and COOP headers, required by pyodide (python interpreter)
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");

{
// register static assets routes
if (!params.public_path.empty()) {
// Set the base directory for serving static files
bool is_found = svr->set_mount_point("/", params.public_path);
if (!is_found) {
GGML_ABORT("%s: static assets path not found: %s\n", __func__, params.public_path.c_str());
return 1;
}
return false;
});
}
else {

// using embedded static index.html
svr->Get("/", [params](const httplib::Request& req, httplib::Response& res) {
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
res.set_content("Error: gzip is not supported by this browser", "text/plain");
}
else {
res.set_header("Content-Encoding", "gzip");
// COEP and COOP headers, required by pyodide (python interpreter)
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
if (params.webui == COMMON_WEBUI_AUTO) {
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
}
else if (params.webui == COMMON_WEBUI_LLAMACPP) {
res.set_content(reinterpret_cast<const char*>(index_llamacpp_html_gz), index_llamacpp_html_gz_len, "text/html; charset=utf-8");
}
else {
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
}
}
return false;
});
}
}
}
// register API routes
Expand Down Expand Up @@ -5062,6 +5077,42 @@ int main(int argc, char ** argv) {
svr->Post("/rename_prompt", rename_saved_prompt);

}
// SPA fallback route - serve index.html for any route that doesn't match API endpoints
// This enables client-side routing for dynamic routes like /chat/[id]
if (params.webui && params.public_path.empty()) {
// Only add fallback when using embedded static files
svr->Get(".*", [](const httplib::Request& req, httplib::Response& res) {
// Skip API routes - they should have been handled above
if (req.path.find("/v1/") != std::string::npos ||
req.path.find("/health") != std::string::npos ||
req.path.find("/metrics") != std::string::npos ||
req.path.find("/props") != std::string::npos ||
req.path.find("/models") != std::string::npos ||
req.path.find("/api/tags") != std::string::npos ||
req.path.find("/completions") != std::string::npos ||
req.path.find("/chat/completions") != std::string::npos ||
req.path.find("/embeddings") != std::string::npos ||
req.path.find("/tokenize") != std::string::npos ||
req.path.find("/detokenize") != std::string::npos ||
req.path.find("/lora-adapters") != std::string::npos ||
req.path.find("/slots") != std::string::npos) {
return false; // Let other handlers process API routes
}

// Serve index.html for all other routes (SPA fallback)
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
res.set_content("Error: gzip is not supported by this browser", "text/plain");
}
else {
res.set_header("Content-Encoding", "gzip");
// COEP and COOP headers, required by pyodide (python interpreter)
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
}
return false;
});
}
svr->Get ("/version", handle_version);
if (!params.sql_save_file.empty()) {
// these endpoints rely on sql_save_file existing
Expand Down
1 change: 1 addition & 0 deletions examples/server/webui_llamacpp/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
engine-strict=true
9 changes: 9 additions & 0 deletions examples/server/webui_llamacpp/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb

# Miscellaneous
/static/
16 changes: 16 additions & 0 deletions examples/server/webui_llamacpp/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script lang="ts">
import { ModeWatcher } from 'mode-watcher';
import { onMount } from 'svelte';

interface Props {
children?: any;
}

let { children }: Props = $props();

onMount(() => {
const root = document.documentElement;
const theme = localStorage.getItem('mode-watcher-mode') || 'system';

if (theme === 'dark') {
root.classList.add('dark');
} else if (theme === 'light') {
root.classList.remove('dark');
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (prefersDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
}
});
</script>

<ModeWatcher />

{#if children}
{@const Component = children}

<Component />
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import * as Tooltip from '../src/lib/components/ui/tooltip';

interface Props {
children: any;
}

let { children }: Props = $props();
</script>

<Tooltip.Provider>
{@render children()}
</Tooltip.Provider>
17 changes: 17 additions & 0 deletions examples/server/webui_llamacpp/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/sveltekit';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|ts|svelte)'],
addons: [
'@storybook/addon-svelte-csf',
'@chromatic-com/storybook',
'@storybook/addon-docs',
'@storybook/addon-a11y',
'@storybook/addon-vitest'
],
framework: {
name: '@storybook/sveltekit',
options: {}
}
};
export default config;
34 changes: 34 additions & 0 deletions examples/server/webui_llamacpp/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Preview } from '@storybook/sveltekit';
import '../src/app.css';
import ModeWatcherDecorator from './ModeWatcherDecorator.svelte';
import TooltipProviderDecorator from './TooltipProviderDecorator.svelte';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
backgrounds: {
disable: true
}
},
decorators: [
(story) => ({
Component: ModeWatcherDecorator,
props: {
children: story
}
}),
(story) => ({
Component: TooltipProviderDecorator,
props: {
children: story
}
})
]
};

export default preview;
Loading