Compare commits

...

7 Commits

Author SHA1 Message Date
Chyan c71854292f hexagon: fix hmx-queue signal enum-narrowing problem (#25677) 2026-07-14 12:27:09 -07:00
Georgi Gerganov bf2c86ddc0 server : refactor prompt cache state ownership (#25649)
* server : clear checkpoints upon prompt clear

* server : move the prompt state data to the server_prompt_cache

Assisted-by: pi:llama.cpp/Qwen3.6-27B

* server : handle batched slot being cleared
2026-07-14 18:25:52 +03:00
Xuan-Son Nguyen 6e52db5b72 server: add --cors-* options (#25655)
* server: add --cors-* options

* add special "localhost" value

* add tests

* fix test

* add link to PR
2026-07-14 17:23:44 +02:00
Bill Sideris 236ab574e0 ui: Fix spacing in tool-call request (#25634) 2026-07-14 17:23:11 +02:00
Emanuil Rusev dfba90db63 webui: parse effective-parameter sizes (E2B, E4B) as params (#25529) 2026-07-14 17:12:22 +02:00
Hongqiang Wang 00e79f6fb1 opencl: fix a dp4a bug for devices where cl_khr_integer_dot_product is unavailable (#25639)
* opencl: do not fail backend init on devices without cl_khr_integer_dot_product

* opencl: do not call dp4 kernels when dp is unavailable

---------

Co-authored-by: Li He <lih@qti.qualcomm.com>
2026-07-14 08:08:13 -07:00
Pascal 17a05e451f ui: fix mcp panel for toggle + timeout + proxy + ON/OFF state (#25631)
* ui: fix MCP panel regressions after settings rework

Restore the llama-server proxy switch in the Add New Server dialog.
The dialog never passed useProxy/onUseProxyChange to McpServerForm,
which only renders the proxy switch when the handler is provided.
The flag is now wired, persisted on addServer, and reset on close.

Bound the MCP connection handshake with the configured timeout.
handshakeTimeoutMs was set in the server config but never consumed.
The SDK timeout only covers the initialize request, not
transport.start(), which can hang forever on an unreachable host.
The whole handshake now races against the timeout and closes the
transport on expiry so the underlying fetch or socket is aborted.

Keep disabled MCP servers visible in management and chat-add UIs.
Collapsing mcpDefaultServerOverrides into mcpServers[i].enabled turned
the visibleMcpServers enabled filter into a visibility trap: toggling
a server off outside a conversation hid it from every surface with no
way to re-enable it. The filter is dropped, tools derived from health
checks still skip disabled servers, and the settings page and server
card render the real card instead of a skeleton for disabled servers
that never receive a startup health check.

* ui: clarify MCP server list semantics and add regression test

Remove the visibleMcpServers getter, a filterless alias of getServers
whose name invites the next refactor to put a filter back. Call sites
read getServers directly, the duplicate list in the chat submenu is
merged, and the misleading local variable in the sheet is renamed.

A parser unit test pins the invariant: enabled is an on/off state,
never a visibility filter, so disabled servers stay listed and
toggleable.

* ui: apply the MCP request timeout setting live to all servers

The per-server requestTimeoutSeconds field was never editable in any
UI and froze the global setting at server creation time, so changing
the timeout in Settings was a no-op for existing servers. The field
is removed from the data model and parsers, the timeout is read live
from the global setting wherever a request config is built, and the
misleading "Can be overridden per server" help text is dropped. A
parser unit test guards against reintroducing the stored field.

* ui: move the MCP request timeout into the Agentic settings section

The MCP section held a single setting. The timeout is a global tool
execution parameter like the other Agentic entries, so it moves there
and the section is removed. Same settings key, no migration needed.

* ui: remove the dead tool preview lines setting

The agenticMaxToolPreviewLines setting was read into AgenticConfig
and consumed by nothing: the agentic loop only uses enabled and
maxTurns. Its help text described a previous architecture where only
truncated previews and the final response survived the loop; tool
results and intermediate turns now persist as full DB messages, so
the setting had no effect at any value. Stale keys in localStorage
or a server ui-config are ignored.

* ui: resolve absent MCP per-chat overrides to the server enabled flag

New conversations started with every MCP server off: the settings
rework stopped seeding a per-conversation override list, assuming
the enabled check would fall back to mcpServers[i].enabled, but it
fell back to false, and the send path passed the raw stored list
with no fallback at all. The per-conversation list is now sparse by
contract, holding only explicit toggles, and every access point
resolves a missing entry to the server's own enabled flag: the
toggle display, the resolved list handed to the agentic flow, and
the enabled check itself.
2026-07-14 16:50:44 +02:00
33 changed files with 457 additions and 283 deletions
+47 -3
View File
@@ -697,7 +697,7 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
}
};
// parse the first time to get -hf option (used for remote preset)
// parse all CLI args now, so that -hf is available below for remote preset resolution
parse_cli_args();
postprocess_cpu_params(params.cpuparams, nullptr);
@@ -748,6 +748,11 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
params.kv_overrides.back().key[0] = 0;
}
if (!params.server_tools.empty() && !params.cors_origins_explicit) {
LOG_WRN("server tools are enabled, using localhost as default CORS origin (change via --cors-origins)\n");
params.cors_origins = "localhost";
}
// pad tensor_buft_overrides for llama_params_fit:
const size_t ntbo = llama_max_tensor_buft_overrides();
while (params.tensor_buft_overrides.size() < ntbo) {
@@ -3047,6 +3052,42 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.public_path = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_STATIC_PATH"));
add_opt(common_arg(
{"--cors-origins"}, "ORIGINS",
string_format(
"comma-separated list of allowed origins for CORS (default: %s)\n"
"if set to special value 'localhost', reflect the Origin header only if it is localhost",
params.cors_origins.c_str()),
[](common_params & params, const std::string & value) {
params.cors_origins = value;
params.cors_origins_explicit = true;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CORS_ORIGINS"));
add_opt(common_arg(
{"--cors-methods"}, "METHODS",
string_format("comma-separated list of allowed methods for CORS (default: %s)", params.cors_methods.c_str()),
[](common_params & params, const std::string & value) {
params.cors_methods = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CORS_METHODS"));
add_opt(common_arg(
{"--cors-headers"}, "HEADERS",
string_format("comma-separated list of allowed headers for CORS (default: %s)", params.cors_headers.c_str()),
[](common_params & params, const std::string & value) {
params.cors_headers = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CORS_HEADERS"));
add_opt(common_arg(
{"--cors-credentials"},
{"--no-cors-credentials"},
string_format(
"whether to allow credentials for CORS (default: %s)\n"
"note: if this is enabled and --cors-origins is set to * (default), the Origin header will be echoed back, and credentials will always be allowed",
params.cors_credentials ? "enabled" : "disabled"),
[](common_params & params, bool value) {
params.cors_credentials = value;
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CORS_CREDENTIALS"));
add_opt(common_arg(
{"--api-prefix"}, "PREFIX",
string_format("prefix path the server serves from, without the trailing slash (default: %s)", params.api_prefix.c_str()),
@@ -3080,7 +3121,8 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
{"--tools"}, "TOOL1,TOOL2,...",
"experimental: whether to enable built-in tools for AI agents - do not enable in untrusted environments (default: no tools)\n"
"specify \"all\" to enable all tools\n"
"available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime",
"available tools: read_file, file_glob_search, grep_search, exec_shell_command, write_file, edit_file, get_datetime\n"
"note: for security reasons, this will limit --cors-origins to localhost by default",
[](common_params & params, const std::string & value) {
params.server_tools = parse_csv_row(value);
}
@@ -3088,7 +3130,8 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
add_opt(common_arg(
{"-ag", "--agent"},
{"-no-ag", "--no-agent"},
"whether to enable CORS proxy and all built-in tools - do not enable in untrusted environments (default: disabled)",
"whether to enable CORS proxy and all built-in tools - do not enable in untrusted environments (default: disabled)\n"
"note: for security reasons, this will limit --cors-origins to localhost by default",
[](common_params & params, bool value) {
if (value) {
params.server_tools = {"all"};
@@ -3097,6 +3140,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
params.server_tools.clear();
params.ui_mcp_proxy = false;
}
// note: do not modify cors_origins here, as the options are not evaluated in order (user may explicitly set --cors-origins before --agent)
}
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_AGENT"));
add_opt(common_arg(
+8
View File
@@ -631,6 +631,14 @@ struct common_params {
std::string api_prefix = ""; // NOLINT
std::string chat_template = ""; // NOLINT
bool use_jinja = true; // NOLINT
// server CORS params
std::string cors_origins = "*";
std::string cors_methods = "GET, POST, DELETE, OPTIONS";
std::string cors_headers = "*";
bool cors_credentials = true;
bool cors_origins_explicit = false; // for --agent option
bool enable_chat_template = true;
bool force_pure_content_parser = false;
common_reasoning_format reasoning_format = COMMON_REASONING_FORMAT_DEEPSEEK;
+1 -1
View File
@@ -38,7 +38,7 @@ static inline void hmx_queue_process(struct hmx_queue *q, bool* killed) {
if (!d->done) {
FARF(HIGH, "hmx-queue-process: ir %u func %p data %p", ir, d->func, d->data);
enum hmx_queue_signal sig = (enum hmx_queue_signal) (unsigned int) d->func;
uintptr_t sig = (uintptr_t) d->func;
switch (sig) {
case HMX_QUEUE_NOOP: /* noop */; break;
case HMX_QUEUE_KILL: *killed = true; break;
+68 -58
View File
@@ -532,6 +532,7 @@ struct ggml_backend_opencl_context {
bool fp16_support;
bool has_vector_subgroup_broadcast;
bool has_subgroup_shuffle = false; // cl_khr_subgroup_shuffle or cl_qcom_subgroup_shuffle
bool has_integer_dot = false; // cl_khr_integer_dot_product or cl_qcom_dot_product8
bool has_qcom_subgroup_shuffle = false; // specifically cl_qcom_subgroup_shuffle
bool disable_fusion;
@@ -834,7 +835,7 @@ struct ggml_backend_opencl_context {
cl_kernel kernel_gemv_moe_q5_1_f32_ns, kernel_gemm_moe_q5_1_f32_ns;
cl_kernel kernel_gemv_moe_q4_k_f32_ns, kernel_gemm_moe_q4_k_f32_ns, kernel_gemm_moe_q4_k_f32_ns_bin;
cl_kernel kernel_gemv_moe_q4_k_f32_ns_wimg = nullptr; // weight-as-texture MoE decode GEMV (opt-in)
cl_kernel kernel_gemm_moe_q4_k_q8_1_dp4a; // dp4a (int8) prefill GEMM variant
cl_kernel kernel_gemm_moe_q4_k_q8_1_dp4a = nullptr; // dp4a (int8) prefill GEMM variant
cl_kernel kernel_moe_reorder_quant_a_q8_1; // fused reorder + q8_1 quant for the dp4a GEMM
cl_kernel kernel_gemm_moe_q8_1_dp4a_q80 = nullptr; // generic dp4a MoE GEMM (MOE_QT=80), opt-in
cl_kernel kernel_moe_expand_scale_q8_0 = nullptr; // q8_0 per-block d -> uniform scale[16]
@@ -844,12 +845,12 @@ struct ggml_backend_opencl_context {
cl_kernel kernel_moe_expand_scale_q5_K = nullptr; // q5_K 6-bit s[] -> uniform scale[16]/min[8]
cl_kernel kernel_gemv_moe_q5_k_f32_ns, kernel_gemm_moe_q5_k_f32_ns;
cl_kernel kernel_gemv_moe_q6_k_f32_ns, kernel_gemm_moe_q6_k_f32_ns;
cl_kernel kernel_gemm_moe_q6_k_q8_1_dp4a; // dp4a (int8) q6_K MoE prefill GEMM
cl_kernel kernel_gemm_moe_q6_k_q8_1_dp4a = nullptr; // dp4a (int8) q6_K MoE prefill GEMM
cl_kernel kernel_gemv_moe_mxfp4_f32, kernel_gemm_moe_mxfp4_f32;
cl_kernel kernel_gemv_moe_mxfp4_f32_ns, kernel_gemm_moe_mxfp4_f32_ns, kernel_gemm_moe_mxfp4_f32_ns_bin;
cl_kernel kernel_gemv_moe_mxfp4_f32_ns_wimg = nullptr; // weight-as-texture MoE decode GEMV
cl_kernel kernel_gemm_moe_mxfp4_q8_1_dp4a; // dp4a (int8) mxfp4 MoE prefill GEMM
cl_kernel kernel_gemm_moe_q4_0_q8_1_dp4a; // dp4a (int8) q4_0 MoE prefill GEMM
cl_kernel kernel_gemm_moe_mxfp4_q8_1_dp4a = nullptr; // dp4a (int8) mxfp4 MoE prefill GEMM
cl_kernel kernel_gemm_moe_q4_0_q8_1_dp4a = nullptr; // dp4a (int8) q4_0 MoE prefill GEMM
cl_kernel kernel_moe_reorder_b;
cl_kernel kernel_moe_histogram, kernel_moe_scan, kernel_moe_fill, kernel_moe_scatter;
cl_kernel kernel_moe_combine_f32 = nullptr; // fused router-weight mul + cross-expert sum
@@ -1037,10 +1038,10 @@ struct ggml_backend_opencl_context {
cl_kernel kernel_gemv_noshuffle_q1_0_f32;
cl_kernel kernel_gemv_noshuffle_q4_k_f32;
cl_kernel kernel_gemm_noshuffle_q4_k_f32;
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a; // dp4a (int8) dense prefill GEMM
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg; // dp4a dense prefill GEMM, weights via texture (X1 opt-in)
cl_kernel kernel_gemm_noshuffle_q5_k_q8_1_dp4a; // dp4a (int8) dense q5_K prefill GEMM
cl_kernel kernel_gemm_noshuffle_q6_k_q8_1_dp4a; // dp4a (int8) dense q6_K prefill GEMM
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a = nullptr; // dp4a (int8) dense prefill GEMM
cl_kernel kernel_gemm_noshuffle_q4_k_q8_1_dp4a_wimg = nullptr; // dp4a dense prefill GEMM, weights via texture (X1 opt-in)
cl_kernel kernel_gemm_noshuffle_q5_k_q8_1_dp4a = nullptr; // dp4a (int8) dense q5_K prefill GEMM
cl_kernel kernel_gemm_noshuffle_q6_k_q8_1_dp4a = nullptr; // dp4a (int8) dense q6_K prefill GEMM
cl_kernel kernel_quant_a_q8_1; // plain activation q8_1 pre-pass
cl_kernel kernel_gemv_noshuffle_q6_K_f32;
cl_kernel kernel_gemm_noshuffle_q6_K_f32;
@@ -3490,7 +3491,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q5_0_q8_1_dp4a (dp4a dense q5_0 prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q5_0_q8_1_dp4a.cl.h"
@@ -3580,7 +3581,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_iq4_nl_q8_1_dp4a (dp4a dense IQ4_NL prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_iq4_nl_q8_1_dp4a.cl.h"
@@ -3595,7 +3596,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q4_0_q8_1_dp4a (dp4a dense q4_0 prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q4_0_q8_1_dp4a.cl.h"
@@ -3708,7 +3709,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q4_k_q8_1_dp4a (dp4a dense prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q4_k_q8_1_dp4a.cl.h"
@@ -3730,7 +3731,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q8_0_q8_1_dp4a (dp4a dense q8_0 prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q8_0_q8_1_dp4a.cl.h"
@@ -3746,7 +3747,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q5_k_q8_1_dp4a (dp4a dense prefill GEMM for q5_K)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q5_k_q8_1_dp4a.cl.h"
@@ -3761,7 +3762,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_noshuffle_q6_k_q8_1_dp4a (dp4a dense prefill GEMM for q6_K ffn_down/output)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_noshuffle_q6_k_q8_1_dp4a.cl.h"
@@ -4091,7 +4092,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_moe_q4_k_q8_1_dp4a (dp4a prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_moe_q4_k_q8_1_dp4a.cl.h"
@@ -4108,7 +4109,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_moe_mxfp4_q8_1_dp4a (dp4a prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_moe_mxfp4_q8_1_dp4a.cl.h"
@@ -4125,7 +4126,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_moe_q4_0_q8_1_dp4a (dp4a prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_moe_q4_0_q8_1_dp4a.cl.h"
@@ -4142,7 +4143,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_moe_q8_1_dp4a (generic dp4a MoE GEMM; MOE_QT=80 -> q8_0 expert variant)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_moe_q8_1_dp4a.cl.h"
@@ -4256,7 +4257,7 @@ static void load_cl_kernels(ggml_backend_opencl_context *backend_ctx) {
}
// gemm_moe_q6_k_q8_1_dp4a (dp4a q6_K MoE prefill GEMM)
{
if (backend_ctx->has_integer_dot) {
#ifdef GGML_OPENCL_EMBED_KERNELS
const std::string kernel_src {
#include "gemm_moe_q6_k_q8_1_dp4a.cl.h"
@@ -5602,6 +5603,8 @@ static void ggml_opencl_print_backend_info(ggml_backend_opencl_device_context *
backend_ctx->has_subgroup_shuffle ? "true" : "false");
GGML_LOG_INFO("ggml_opencl: device FP16 support: %s\n",
backend_ctx->fp16_support ? "true" : "false");
GGML_LOG_INFO("ggml_opencl: khr dot product support: %s\n",
backend_ctx->has_integer_dot ? "true" : "false");
GGML_LOG_INFO("ggml_opencl: mem base addr align: %u\n",
backend_ctx->alignment);
GGML_LOG_INFO("ggml_opencl: global mem size: %zu MB\n",
@@ -5810,6 +5813,12 @@ static ggml_backend_opencl_context * ggml_cl_init(ggml_backend_dev_t dev) {
strstr(ext_buffer, "cl_khr_subgroup_shuffle") != NULL ||
backend_ctx->has_qcom_subgroup_shuffle;
// check for cl_khr_integer_dot_product
// cl_qcom_dot_product8 uses signed * unsigned
// while cl_khr_integer_dot_product uses signed * signed -- we stick with khr for now
backend_ctx->has_integer_dot =
strstr(ext_buffer, "cl_khr_integer_dot_product") != NULL;
cl_uint base_align_in_bits;
CL_CHECK(clGetDeviceInfo(device, CL_DEVICE_MEM_BASE_ADDR_ALIGN, sizeof(cl_uint), &base_align_in_bits, NULL));
GGML_ASSERT(base_align_in_bits % 8u == 0);
@@ -15819,18 +15828,14 @@ static void ggml_cl_mul_mat_q4_0_f32_adreno(ggml_backend_t backend, const ggml_t
CL_CHECK(clReleaseMemObject(b_sub_buf));
CL_CHECK(clReleaseMemObject(b_img));
} else {
// dp4a (int8) dense prefill GEMM: quant activations to q8_1, then the int8
// dp4a inner-loop GEMM, in place of the transpose + f16 half-dot kernel.
// q4_0 = d*(q-8); mirrors the IQ4_NL/q8_0 dense dp4a paths (+ the sum term).
// OPT-IN / DEFAULT OFF: correct, but neutral on X2E. q4_0's dequant
// ((q-8)*scale) is already trivial so the f16 GEMM is weight-BW-bound and the
// int8 ALU win has nothing to beat -- same as q5_0 dense (unlike IQ4_NL, whose
// codebook dequant is expensive enough for dp4a to help). Kept for A/B; force
// on with GGML_OPENCL_Q4_0_DENSE_DP4A=1. Needs N>8, K%32==0, M%64==0.
// dp4a (int8) dense prefill GEMM, default off
static const char * q4_0_dense_dp4a_env = getenv("GGML_OPENCL_Q4_0_DENSE_DP4A");
const bool q4_0_dense_dp4a_on = q4_0_dense_dp4a_env
bool q4_0_dense_dp4a_on = q4_0_dense_dp4a_env
? (atoi(q4_0_dense_dp4a_env) != 0)
: false;
// dot prod has to be available
q4_0_dense_dp4a_on = backend_ctx->has_integer_dot && q4_0_dense_dp4a_on;
if (q4_0_dense_dp4a_on && backend_ctx->kernel_gemm_noshuffle_q4_0_q8_1_dp4a
&& N > 8 && (K % 32 == 0) && (M % 64 == 0)) {
cl_mem a_sub = nullptr;
@@ -16253,27 +16258,16 @@ static void ggml_cl_mul_mat_q5_0_f32_adreno(ggml_backend_t backend, const ggml_t
CL_CHECK(clReleaseMemObject(b_sub_buf));
CL_CHECK(clReleaseMemObject(b_img));
} else {
// dp4a (int8) dense q5_0 prefill GEMM. Quantizes the [N,K] activations to
// q8_1 and runs the int8 dot instead of the f16 half-dot. Large-batch
// (ne1>8) only. q5_0 weight = (x-16)*d (x = nibble | hi<<4); x packed as a
// 0..31 byte (dp4a), the -16 centering folded into a single min term
// (d*16) via the q8_1 block sum. Reads the qs/qh/d buffers byte-identically
// to the f16 kernel (greedy byte-identical, MUL_MAT NMSE-OK).
//
// OPT-IN / DEFAULT OFF. Unlike q8_0/q4_K dense, dp4a is not a win for q5_0 on
// X2E: the q5_0 model is bottlenecked elsewhere, so the dense-GEMM int8 win
// has nothing to surface and the q8_1 prepass slightly hurts. Kept correct +
// opt-in for the X1 A/B (different texture-cache dynamic) and the
// weight-texture variant. Env: GGML_OPENCL_Q5_DENSE_DP4A=1.
// Weight-as-texture variant (X1 lever): routes the dominant qs nibble plane
// through an image1d_buffer (qh stays a buffer). Opt-in
// GGML_OPENCL_Q5_DENSE_DP4A_WIMG; when set it also forces the dp4a path on.
// dp4a (int8) dense q5_0 prefill GEMM, default off
static const char * q5_dense_dp4a_env = getenv("GGML_OPENCL_Q5_DENSE_DP4A");
static const char * q5_dense_wimg_env = getenv("GGML_OPENCL_Q5_DENSE_DP4A_WIMG");
const bool q5_dense_wimg_on = q5_dense_wimg_env && (atoi(q5_dense_wimg_env) != 0);
const bool q5_dense_dp4a_on = q5_dense_wimg_on
bool q5_dense_dp4a_on = q5_dense_wimg_on
? true
: (q5_dense_dp4a_env && (atoi(q5_dense_dp4a_env) != 0));
// dot prod has to be available
q5_dense_dp4a_on = backend_ctx->has_integer_dot && q5_dense_dp4a_on;
if (q5_dense_dp4a_on && backend_ctx->kernel_gemm_noshuffle_q5_0_q8_1_dp4a
&& N > 8 && (K % 32 == 0) && (M % 64 == 0)) {
cl_mem a_sub = nullptr;
@@ -16708,15 +16702,14 @@ static void ggml_cl_mul_mat_iq4_nl_f32_adreno(ggml_backend_t backend, const ggml
} else {
// dp4a (int8) dense IQ4_NL prefill GEMM. Quantizes the [N,K] activations to
// q8_1 and runs the int8 dot instead of the f16 half-dot. Large-batch
// (ne1>8) only. IQ4_NL weight = kvalues[nibble]*d; the codebook value IS the
// int8 (no min term), so this is the q8_0 dense case plus a nibble->int8 LUT
// unpack. Reads the q/d buffers byte-identically to the f16 kernel. No bin
// kernel for IQ4_NL -> baseline is f16, default ON for X2E (like q4_K/q6_K
// dense dp4a). X1 stays on f16. Env: GGML_OPENCL_IQ4NL_DENSE_DP4A.
// (ne1>8) only
static const char * iq4nl_dense_dp4a_env = getenv("GGML_OPENCL_IQ4NL_DENSE_DP4A");
const bool iq4nl_dense_dp4a_on = iq4nl_dense_dp4a_env
bool iq4nl_dense_dp4a_on = iq4nl_dense_dp4a_env
? (atoi(iq4nl_dense_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dot prod has to be available
iq4nl_dense_dp4a_on = backend_ctx->has_integer_dot && iq4nl_dense_dp4a_on;
if (iq4nl_dense_dp4a_on && backend_ctx->kernel_gemm_noshuffle_iq4_nl_q8_1_dp4a
&& N > 8 && (K % 32 == 0) && (M % 64 == 0)) {
cl_mem a_sub = nullptr;
@@ -16966,13 +16959,15 @@ static void ggml_cl_mul_mat_q8_0_f32_adreno(ggml_backend_t backend, const ggml_t
static const char * q8_dense_wimg_env = getenv("GGML_OPENCL_Q8_DENSE_DP4A_WIMG");
const bool q8_dense_wimg_on = q8_dense_wimg_env && (atoi(q8_dense_wimg_env) != 0);
const bool q8_bin_loaded = (backend_ctx->kernel_gemm_noshuffle_q8_0_f32_bin != nullptr);
const bool q8_bin_loaded = (backend_ctx->kernel_gemm_noshuffle_q8_0_f32_bin != nullptr);
// bin kernel takes precedence
const bool q8_dense_dp4a_on = q8_dense_wimg_on
bool q8_dense_dp4a_on = q8_dense_wimg_on
? true
: q8_dense_dp4a_env
? (atoi(q8_dense_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E && !q8_bin_loaded);
// dot prod has to be available
q8_dense_dp4a_on = backend_ctx->has_integer_dot && q8_dense_dp4a_on;
if (q8_dense_dp4a_on && backend_ctx->kernel_gemm_noshuffle_q8_0_q8_1_dp4a
&& N > 8 && (K % 32 == 0) && (M % 64 == 0)) {
@@ -17379,13 +17374,16 @@ static void ggml_cl_mul_mat_q4_k_f32_adreno(ggml_backend_t backend, const ggml_t
static const char * q4k_dense_dp4a_env = getenv("GGML_OPENCL_Q4K_DENSE_DP4A");
static const char * q4k_dense_wimg_env = getenv("GGML_OPENCL_Q4K_DENSE_DP4A_WIMG");
const bool q4k_dense_wimg_on = q4k_dense_wimg_env && (atoi(q4k_dense_wimg_env) != 0);
const bool q4k_dense_dp4a_on = q4k_dense_wimg_on
const bool q4k_dense_wimg_on = q4k_dense_wimg_env && (atoi(q4k_dense_wimg_env) != 0);
bool q4k_dense_dp4a_on = q4k_dense_wimg_on
? true
: q4k_dense_dp4a_env
? (atoi(q4k_dense_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dp4 has to be available
q4k_dense_dp4a_on = backend_ctx->has_integer_dot && q4k_dense_dp4a_on;
// Min N for the dp4a prefill GEMM, default 9, i.e., ne1 > 8
static const char * q4k_dp4a_minn_env = getenv("GGML_OPENCL_Q4K_DP4A_MINN");
const int q4k_dp4a_minn = q4k_dp4a_minn_env ? atoi(q4k_dp4a_minn_env) : 9;
@@ -17608,9 +17606,11 @@ static void ggml_cl_mul_mat_q6_K_f32_adreno(ggml_backend_t backend, const ggml_t
// dp4a (int8) dense q6_K prefill GEMM
static const char * q6k_dense_dp4a_env = getenv("GGML_OPENCL_Q6K_DENSE_DP4A");
static const bool q6k_dense_dp4a_on = (q6k_dense_dp4a_env != nullptr)
bool q6k_dense_dp4a_on = (q6k_dense_dp4a_env != nullptr)
? (atoi(q6k_dense_dp4a_env) != 0)
: (backend_ctx->adreno_gen != ADRENO_GPU_GEN::X1E);
// dot prod has to be available
q6k_dense_dp4a_on = backend_ctx->has_integer_dot && q6k_dense_dp4a_on;
const bool is_output_w_dp4a = strncmp(src0->name, "output", 6) == 0 ||
strncmp(src0->name, "token_embd", 10) == 0;
@@ -17901,9 +17901,11 @@ static void ggml_cl_mul_mat_q5_K_f32_adreno(ggml_backend_t backend, const ggml_t
// dp4a (int8) dense q5_K prefill GEMM
static const char * q5k_dense_dp4a_env = getenv("GGML_OPENCL_Q5K_DENSE_DP4A");
const bool q5k_dense_dp4a_on = q5k_dense_dp4a_env
bool q5k_dense_dp4a_on = q5k_dense_dp4a_env
? (atoi(q5k_dense_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dot prod has to be available
q5k_dense_dp4a_on = backend_ctx->has_integer_dot && q5k_dense_dp4a_on;
if (q5k_dense_dp4a_on && ne1 > 8 && (ne00 % 32 == 0) && (ne01 % 64 == 0)) {
const int Mm = ne01, Nn = ne1, Kk = ne00;
@@ -20640,6 +20642,8 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
bool use_moe_dp4a = q4_0_moe_dp4a_env
? (atoi(q4_0_moe_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
// bin kernel takes precedence
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_q4_0_f32_ns_bin == nullptr;
@@ -21815,6 +21819,8 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
bool use_moe_dp4a = (q4k_moe_dp4a_env != nullptr)
? (atoi(q4k_moe_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E || backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E);
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
// bin kernel takes precedence
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_q4_k_f32_ns_bin == nullptr;
@@ -22316,10 +22322,12 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
// dp4a (int8) q6_K MoE prefill GEMM
static const char * q6k_moe_dp4a_env = getenv("GGML_OPENCL_Q6K_MOE_DP4A");
static const bool use_moe_dp4a = (q6k_moe_dp4a_env != nullptr)
bool use_moe_dp4a = (q6k_moe_dp4a_env != nullptr)
? (atoi(q6k_moe_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E
|| backend_ctx->adreno_gen == ADRENO_GPU_GEN::X1E);
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
cl_buffer_region region;
region.origin = 0;
@@ -22569,6 +22577,8 @@ static void ggml_cl_mul_mat_id(ggml_backend_t backend, const ggml_tensor * src0,
bool use_moe_dp4a = mxfp4_moe_dp4a_env
? (atoi(mxfp4_moe_dp4a_env) != 0)
: (backend_ctx->adreno_gen == ADRENO_GPU_GEN::X2E);
// dot prod has to be available
use_moe_dp4a = backend_ctx->has_integer_dot && use_moe_dp4a;
// bin kernel takes precedence
use_moe_dp4a = use_moe_dp4a && backend_ctx->kernel_gemm_moe_mxfp4_f32_ns_bin == nullptr;
+33 -38
View File
@@ -220,8 +220,6 @@ struct server_slot {
return false;
}
GGML_ASSERT(prompt.data.size() == 0);
const size_t cur_size_tgt = llama_state_seq_get_size_ext(ctx_tgt, id, LLAMA_STATE_SEQ_FLAGS_NONE);
const size_t cur_size_dft = ctx_dft ? llama_state_seq_get_size_ext(ctx_dft, id, LLAMA_STATE_SEQ_FLAGS_NONE) : 0;
@@ -252,11 +250,7 @@ struct server_slot {
return res;
}
void prompt_clear(bool allow_processing) {
if (!allow_processing) {
GGML_ASSERT(!is_processing());
}
void prompt_clear() {
SLT_TRC(*this, "clearing prompt with %zu tokens\n", prompt.tokens.size());
common_context_seq_rm(ctx_tgt, id, -1, -1);
@@ -264,7 +258,7 @@ struct server_slot {
common_context_seq_rm(ctx_dft, id, -1, -1);
}
prompt.tokens.clear();
prompt.clear();
}
std::vector<common_adapter_lora_info> lora;
@@ -493,7 +487,7 @@ struct server_slot {
// do not keep context of the child slots - the parent's context is enough
if (task->is_child()) {
prompt_clear(false);
prompt_clear();
}
reset();
@@ -1626,7 +1620,7 @@ private:
ret->prompt_save(*prompt_cache);
if (!ret->prompt_load(*prompt_cache, task.tokens)) {
ret->prompt_clear(false);
ret->prompt_clear();
}
prompt_cache->update();
@@ -1658,7 +1652,7 @@ private:
if (slot.prompt.n_tokens() > 0) {
SRV_WRN("purging slot %d with %zu tokens\n", slot.id, slot.prompt.tokens.size());
slot.prompt_clear(false);
slot.prompt_clear();
res = true;
@@ -1691,7 +1685,7 @@ private:
// if lora has changed, check to see if the cache should be cleared
if (lora_should_clear_cache(slot.lora, task_loras)) {
SLT_TRC(slot, "clearing cache for lora change. %zu loras -> %zu loras\n", slot.lora.size(), task.params.lora.size());
slot.prompt.tokens.clear();
slot.prompt.clear();
} else {
SLT_TRC(slot, "keeping cache for alora. %zu target loras\n", task_loras.size());
}
@@ -2405,7 +2399,7 @@ private:
if (params_base.kv_unified) {
// [TAG_IDLE_SLOT_CLEAR]
slot.prompt_clear(false);
slot.prompt_clear();
}
}
}
@@ -2573,12 +2567,12 @@ private:
size_t token_count = 0;
size_t nread = llama_state_seq_load_file(ctx_tgt, filepath.c_str(), slot->id, tokens.data(), tokens.size(), &token_count);
if (nread == 0) {
slot->prompt.tokens.clear(); // KV may already been invalidated?
slot->prompt.clear(); // KV may already been invalidated?
send_error(task, "Unable to restore slot, no available space in KV cache or invalid slot save file", ERROR_TYPE_INVALID_REQUEST);
break;
}
tokens.resize(token_count);
slot->prompt.tokens.clear();
slot->prompt.clear();
slot->prompt.tokens.insert(tokens);
const int64_t t_end = ggml_time_us();
@@ -2615,7 +2609,7 @@ private:
// Erase token cache
const size_t n_erased = slot->prompt.tokens.size();
slot->prompt_clear(false);
slot->prompt_clear();
auto res = std::make_unique<server_task_result_slot_erase>();
res->id = task.id;
@@ -2775,6 +2769,27 @@ private:
abort_all_slots("pre_decode() failed: " + std::string(e.what()));
}
GGML_ASSERT(batch.slot_batched || batch.size() == 0);
if (batch.slot_batched) {
auto & slot_batched = batch.slot_batched;
auto & alora_scale = batch.alora_scale;
auto & alora_disabled_id = batch.alora_disabled_id;
// TODO @ngxson : alora handling is too messy, need to refactor it to be more clear and maintainable
// apply lora, only need to do it once per batch
common_set_adapter_lora(ctx_tgt, slot_batched->lora);
// if the lora is temporarily disabled for an alora, re-enable it
// for next time
if (alora_scale > 0.0f) {
SRV_DBG("re-enabling alora with scale %f\n", alora_scale);
slot_batched->lora[alora_disabled_id].scale = alora_scale;
}
llama_set_embeddings(ctx_tgt, slot_batched->need_embd());
}
llama_batch batch_view;
int32_t off_next = 0;
int32_t n_batch = llama_n_batch(ctx_tgt);
@@ -2814,7 +2829,6 @@ private:
abort_all_slots("post_decode() failed: " + std::string(e.what()));
break; // stop any further processing
}
}
}
@@ -2880,7 +2894,7 @@ private:
new_tokens.resize(slot.prompt.tokens.size() - n_discard);
slot.prompt.tokens.clear();
slot.prompt.clear();
slot.prompt.tokens.insert(new_tokens);
}
@@ -3556,25 +3570,6 @@ private:
bool decode(int32_t & n_batch, int32_t off, llama_batch & batch_view) {
SRV_DBG("n_batch (effective) = %d, off = %d\n", n_batch, off);
auto & slot_batched = batch.slot_batched;
auto & alora_scale = batch.alora_scale;
auto & alora_disabled_id = batch.alora_disabled_id;
// TODO @ngxson : alora handling is too messy, need to refactor it to be more clear and maintainable
if (slot_batched) {
// apply lora, only need to do it once per batch
common_set_adapter_lora(ctx_tgt, slot_batched->lora);
// if the lora is temporarily disabled for an alora, re-enable it
// for next time
if (alora_scale > 0.0f) {
SRV_DBG("re-enabling alora with scale %f\n", alora_scale);
slot_batched->lora[alora_disabled_id].scale = alora_scale;
}
llama_set_embeddings(ctx_tgt, slot_batched->need_embd());
}
if (batch.size() == 0) {
SRV_WRN("%s", "no tokens to decode\n");
@@ -3622,7 +3617,7 @@ private:
// note: it's complicated to keep track of how much of the current batch has been
// processed before the error occurred, so we simply clear the entire context
slot.prompt_clear(false);
slot.prompt_clear();
}
}
+28 -5
View File
@@ -47,6 +47,16 @@ static void log_server_request(const httplib::Request & req, const httplib::Resp
SRV_DBG("response: %s\n", res.body.c_str());
}
// returns true if the Origin header value's host is localhost / 127.0.0.1 / ::1 (any port)
static bool origin_is_localhost(const std::string & origin) {
try {
const std::string host = common_http_parse_url(origin).host;
return host == "localhost" || host == "127.0.0.1" || host == "::1";
} catch (const std::exception &) {
return false;
}
}
// For Google Cloud Platform deployment compatibility
struct gcp_params {
bool enabled;
@@ -266,13 +276,26 @@ bool server_http_context::init(const common_params & params) {
};
// register server middlewares
srv->set_pre_routing_handler([middleware_validate_api_key, middleware_server_state](const httplib::Request & req, httplib::Response & res) {
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
srv->set_pre_routing_handler([&params, middleware_validate_api_key, middleware_server_state](const httplib::Request & req, httplib::Response & res) {
if (params.cors_credentials && params.cors_origins == "*") {
// special case: echo back the Origin header to allow any origin to access the server with credentials
res.set_header("Access-Control-Allow-Origin", req.get_header_value("Origin"));
} else if (params.cors_origins == "localhost") {
// special case: only reflect the Origin header if it is a localhost origin
std::string origin = req.get_header_value("Origin");
if (origin_is_localhost(origin)) {
res.set_header("Access-Control-Allow-Origin", origin);
} else {
SRV_WRN("(CORS) skip non-localhost origin: %s\n", origin.c_str());
}
} else {
res.set_header("Access-Control-Allow-Origin", params.cors_origins);
}
// If this is OPTIONS request, skip validation because browsers don't include Authorization header
if (req.method == "OPTIONS") {
res.set_header("Access-Control-Allow-Credentials", "true");
res.set_header("Access-Control-Allow-Methods", "GET, POST");
res.set_header("Access-Control-Allow-Headers", "*");
res.set_header("Access-Control-Allow-Credentials", params.cors_credentials ? "true" : "false");
res.set_header("Access-Control-Allow-Methods", params.cors_methods);
res.set_header("Access-Control-Allow-Headers", params.cors_headers);
res.set_content("", "text/html"); // blank response, no data
return httplib::Server::HandlerResponse::Handled; // skip further processing
}
+14 -12
View File
@@ -1646,16 +1646,16 @@ size_t server_prompt_cache::n_tokens() const {
size_t res = 0;
for (const auto & state : states) {
res += state.n_tokens();
res += state.prompt.n_tokens();
}
return res;
}
server_prompt * server_prompt_cache::alloc(const server_prompt & prompt, size_t state_size_tgt, size_t state_size_dft) {
server_prompt_cache_state * server_prompt_cache::alloc(const server_prompt & prompt, size_t state_size_tgt, size_t state_size_dft) {
// first check if the current state is contained fully in the cache
for (auto it = states.begin(); it != states.end(); ++it) {
const int cur_lcp_len = it->tokens.get_common_prefix(prompt.tokens);
const int cur_lcp_len = it->prompt.tokens.get_common_prefix(prompt.tokens);
if (cur_lcp_len == (int) prompt.tokens.size()) {
SRV_TRC("%s", " - prompt is already in the cache, skipping\n");
@@ -1680,9 +1680,9 @@ server_prompt * server_prompt_cache::alloc(const server_prompt & prompt, size_t
// remove any cached prompts that are fully contained in the current prompt
for (auto it = states.begin(); it != states.end();) {
const int len = it->tokens.get_common_prefix(prompt.tokens);
const int len = it->prompt.tokens.get_common_prefix(prompt.tokens);
if (len == (int) it->tokens.size()) {
if (len == (int) it->prompt.tokens.size()) {
SRV_TRC(" - removing obsolete cached prompt with length %d\n", len);
it = states.erase(it);
@@ -1721,12 +1721,14 @@ server_prompt * server_prompt_cache::alloc(const server_prompt & prompt, size_t
}
states.push_back({
/*.tokens =*/ prompt.tokens.clone(),
/*.data =*/ {
/*.prompt =*/ {
/*.tokens =*/ prompt.tokens.clone(),
/*.checkpoints =*/ prompt.checkpoints,
},
/*.data =*/ {
/*.main =*/ std::move(state_data_tgt),
/*.drft =*/ std::move(state_data_dft),
},
/*.checkpoints =*/ prompt.checkpoints,
});
return &states.back();
@@ -1744,9 +1746,9 @@ bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tok
// find the most similar cached prompt, that would also preserve the most context
for (auto it = states.begin(); it != states.end(); ++it) {
const int lcp_cur = it->tokens.get_common_prefix(tokens_new);
const int lcp_cur = it->prompt.tokens.get_common_prefix(tokens_new);
const float f_keep_cur = float(lcp_cur) / it->tokens.size();
const float f_keep_cur = float(lcp_cur) / it->prompt.tokens.size();
const float sim_cur = float(lcp_cur) / tokens_new.size();
// don't trash large prompts
@@ -1799,7 +1801,7 @@ bool server_prompt_cache::load(server_prompt & prompt, const server_tokens & tok
}
}
prompt = std::move(*it_best);
prompt = std::move(it_best->prompt);
states.erase(it_best);
}
@@ -1836,6 +1838,6 @@ void server_prompt_cache::update() {
for (const auto & state : states) {
SRV_TRC(" - prompt %p: %7d tokens, checkpoints: %2zu, %9.3f MiB\n",
(const void *)&state, state.n_tokens(), state.checkpoints.size(), state.size() / (1024.0 * 1024.0));
(const void *)&state, state.prompt.n_tokens(), state.prompt.checkpoints.size(), state.size() / (1024.0 * 1024.0));
}
}
+29 -24
View File
@@ -584,32 +584,14 @@ struct server_task_result_apply_lora : server_task_result {
virtual json to_json() override;
};
struct server_prompt_data {
std::vector<uint8_t> main;
std::vector<uint8_t> drft;
size_t size() const {
return main.size() + drft.size();
}
};
struct server_prompt {
server_tokens tokens;
server_prompt_data data;
std::list<common_prompt_checkpoint> checkpoints;
size_t size() const {
size_t res = 0;
res += data.size();
for (const auto & ckpt : checkpoints) {
res += ckpt.size();
}
return res;
void clear() {
tokens.clear();
checkpoints.clear();
}
int n_tokens() const {
@@ -619,19 +601,42 @@ struct server_prompt {
server_prompt clone() const {
return server_prompt {
tokens.clone(),
data,
checkpoints,
};
}
};
struct server_prompt_data {
std::vector<uint8_t> main;
std::vector<uint8_t> drft;
size_t size() const {
return main.size() + drft.size();
}
};
struct server_prompt_cache_state {
server_prompt prompt;
server_prompt_data data;
size_t size() const {
size_t res = data.size();
for (const auto & ckpt : prompt.checkpoints) {
res += ckpt.size();
}
return res;
}
};
struct server_prompt_cache {
server_prompt_cache(int32_t limit_size_mib, size_t limit_tokens) {
this->limit_size = 1024ull*1024ull*(limit_size_mib < 0 ? 0 : limit_size_mib);
this->limit_tokens = limit_tokens;
}
std::list<server_prompt> states;
std::list<server_prompt_cache_state> states;
// in bytes, 0 = no limit
size_t limit_size = 0;
@@ -643,7 +648,7 @@ struct server_prompt_cache {
size_t n_tokens() const;
server_prompt * alloc(const server_prompt & prompt, size_t state_size_main, size_t state_size_drft);
server_prompt_cache_state * alloc(const server_prompt & prompt, size_t state_size_main, size_t state_size_drft);
bool load(server_prompt & prompt, const server_tokens & tokens_new, llama_context * ctx_main, llama_context * ctx_drft, int32_t id_slot);
+25 -11
View File
@@ -303,14 +303,24 @@ int llama_server(common_params & params, int argc, char ** argv) {
return res;
};
if (params.cors_origins == "*" && params.api_keys.empty()) {
SRV_WRN("%s", "-----------------\n");
SRV_WRN("%s", "CORS is set to allow all origins ('*') and no API key is set\n");
SRV_WRN("%s", "this can be a security risk (cross-origin attacks)\n");
SRV_WRN("%s", "more info: https://github.com/ggml-org/llama.cpp/pull/25655\n");
SRV_WRN("%s", "-----------------\n");
}
// CORS proxy (EXPERIMENTAL, only used by the Web UI for MCP)
std::vector<std::string> warn_names;
if (is_router_server) {
warn_names.push_back("router mode");
}
if (params.ui_mcp_proxy) {
SRV_WRN("%s", "-----------------\n");
SRV_WRN("%s", "CORS proxy is enabled, do not expose server to untrusted environments\n");
SRV_WRN("%s", "This feature is EXPERIMENTAL and may be removed or changed in future versions\n");
SRV_WRN("%s", "-----------------\n");
ctx_http.get ("/cors-proxy", ex_wrapper(proxy_handler_get));
ctx_http.post("/cors-proxy", ex_wrapper(proxy_handler_post));
warn_names.push_back("MCP proxy (experimental)");
} else {
ctx_http.get ("/cors-proxy", ex_wrapper(res_403));
ctx_http.post("/cors-proxy", ex_wrapper(res_403));
@@ -324,17 +334,24 @@ int llama_server(common_params & params, int argc, char ** argv) {
SRV_ERR("tools setup failed: %s\n", e.what());
return 1;
}
SRV_WRN("%s", "-----------------\n");
SRV_WRN("%s", "Built-in tools are enabled, do not expose server to untrusted environments\n");
SRV_WRN("%s", "This feature is EXPERIMENTAL and may be changed in the future\n");
SRV_WRN("%s", "-----------------\n");
ctx_http.get ("/tools", ex_wrapper(tools.handle_get));
ctx_http.post("/tools", ex_wrapper(tools.handle_post));
warn_names.push_back("built-in tools (experimental)");
} else {
ctx_http.get ("/tools", ex_wrapper(res_403));
ctx_http.post("/tools", ex_wrapper(res_403));
}
if (warn_names.size() > 0) {
SRV_WRN("%s", "-----------------\n");
SRV_WRN("%s", "the following feature(s) are enabled:\n");
for (const auto & name : warn_names) {
SRV_WRN(" %s\n", name.c_str());
}
SRV_WRN("%s", "do not expose the server to untrusted environments\n");
SRV_WRN("%s", "-----------------\n");
}
//
// Handle downloading model
//
@@ -452,9 +469,6 @@ int llama_server(common_params & params, int argc, char ** argv) {
SRV_INF("listening on %s\n", ctx_http.listening_address.c_str());
if (is_router_server) {
SRV_WRN("%s", "NOTE: router mode is experimental\n");
SRV_WRN("%s", " it is not recommended to use this mode in untrusted environments\n");
if (!params.models_preset_hf.empty()) {
SRV_WRN( "NOTE: using preset.ini from HF repo '%s'\n", params.models_preset_hf.c_str());
SRV_WRN("%s", " please only use presets that you can trust! Unknown presets may be unsafe\n");
+65 -1
View File
@@ -91,7 +91,7 @@ def test_openai_library_correct_api_key():
("localhost", "Access-Control-Allow-Origin", "localhost"),
("web.mydomain.fr", "Access-Control-Allow-Origin", "web.mydomain.fr"),
("origin", "Access-Control-Allow-Credentials", "true"),
("web.mydomain.fr", "Access-Control-Allow-Methods", "GET, POST"),
("web.mydomain.fr", "Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"),
("web.mydomain.fr", "Access-Control-Allow-Headers", "*"),
])
def test_cors_options(origin: str, cors_header: str, cors_header_value: str):
@@ -107,6 +107,70 @@ def test_cors_options(origin: str, cors_header: str, cors_header_value: str):
assert res.headers[cors_header] == cors_header_value
@pytest.mark.parametrize("origin", [
"http://localhost",
"http://localhost:8080",
"http://127.0.0.1",
"http://127.0.0.1:3000",
"http://[::1]",
"http://[::1]:3000",
])
def test_cors_origins_localhost_reflects(origin: str):
global server
server = ServerPreset.router()
server.cors_origins = "localhost"
server.start()
res = server.make_request("OPTIONS", "/completions", headers={
"Origin": origin,
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Authorization",
})
assert res.status_code == 200
assert res.headers["Access-Control-Allow-Origin"] == origin
@pytest.mark.parametrize("origin", [
"http://web.mydomain.fr",
"http://evil.com",
"http://notlocalhost",
"http://localhost.evil.com",
])
def test_cors_origins_localhost_rejects(origin: str):
global server
server = ServerPreset.router()
server.cors_origins = "localhost"
server.start()
res = server.make_request("OPTIONS", "/completions", headers={
"Origin": origin,
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Authorization",
})
assert res.status_code == 200
assert "Access-Control-Allow-Origin" not in res.headers
def test_cors_origins_defaults_to_localhost_with_tools_enabled():
global server
server = ServerPreset.router()
server.server_tools = "all"
server.start()
res = server.make_request("OPTIONS", "/completions", headers={
"Origin": "http://localhost:8080",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Authorization",
})
assert res.status_code == 200
assert res.headers["Access-Control-Allow-Origin"] == "http://localhost:8080"
res = server.make_request("OPTIONS", "/completions", headers={
"Origin": "http://evil.com",
"Access-Control-Request-Method": "POST",
"Access-Control-Request-Headers": "Authorization",
})
assert res.status_code == 200
assert "Access-Control-Allow-Origin" not in res.headers
def test_cors_proxy_only_forwards_explicit_proxy_headers():
class CaptureHeadersHandler(BaseHTTPRequestHandler):
def do_GET(self):
+4 -1
View File
@@ -114,6 +114,7 @@ class ServerProcess:
backend_sampling: bool = False
gcp_compat: bool = False
server_tools: str | None = None
cors_origins: str | None = None
# session variables
process: subprocess.Popen | None = None
@@ -170,6 +171,8 @@ class ServerProcess:
server_args.extend(["--models-max", self.models_max])
if self.models_preset:
server_args.extend(["--models-preset", self.models_preset])
if self.cors_origins:
server_args.extend(["--cors-origins", self.cors_origins])
if self.n_batch:
server_args.extend(["--batch-size", self.n_batch])
if self.n_ubatch:
@@ -359,7 +362,7 @@ class ServerProcess:
if parse_body:
try:
result.body = response.json()
except JSONDecodeError:
except (JSONDecodeError, requests.exceptions.JSONDecodeError):
result.body = response.text
else:
result.body = None
@@ -17,10 +17,10 @@
let { onMcpSettingsClick }: Props = $props();
let mcpSearchQuery = $state('');
let allMcpServers = $derived(mcpStore.getServers());
let mcpServers = $derived(mcpStore.visibleMcpServers);
// Every configured server is listed; `enabled` is an on/off state,
// not a visibility filter, so a disabled server stays toggleable.
let mcpServers = $derived(mcpStore.getServers());
let hasMcpServers = $derived(mcpServers.length > 0);
// let hasAnyMcpServers = $derived(allMcpServers.length > 0);
let filteredMcpServers = $derived.by(() => {
const query = mcpSearchQuery.toLowerCase().trim();
if (!query) return mcpServers;
@@ -46,7 +46,7 @@
function handleMcpSubMenuOpen(open: boolean) {
if (open) {
mcpSearchQuery = '';
mcpStore.runHealthChecksForServers(allMcpServers);
mcpStore.runHealthChecksForServers(mcpServers);
}
}
@@ -84,7 +84,7 @@
const sheetItemRowClass =
'flex w-full items-center justify-between gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent';
let visibleMcpServers = $derived(mcpStore.visibleMcpServers);
let mcpServers = $derived(mcpStore.getServers());
</script>
<div class="flex items-center gap-1 {className}">
@@ -218,13 +218,13 @@
<span class="flex-1">MCP Servers</span>
<span class="text-xs text-muted-foreground">
{visibleMcpServers.length} server{visibleMcpServers.length !== 1 ? 's' : ''}
{mcpServers.length} server{mcpServers.length !== 1 ? 's' : ''}
</span>
</Collapsible.Trigger>
<Collapsible.Content>
<div class="flex flex-col gap-0.5 pl-4">
{#each visibleMcpServers as server (server.id)}
{#each mcpServers as server (server.id)}
{@const healthState = mcpStore.getHealthCheckState(server.id)}
{@const hasError = healthState.status === HealthCheckStatus.ERROR}
{@const displayName = mcpStore.getServerLabel(server)}
@@ -267,7 +267,7 @@
</button>
{/each}
{#if visibleMcpServers.length === 0}
{#if mcpServers.length === 0}
<div class="px-3 py-2 text-center text-sm text-muted-foreground">
No MCP servers configured
</div>
@@ -21,7 +21,7 @@
<ChatMessageActionCard icon={ShieldQuestion}>
{#snippet message()}
Allow use of <span class="font-semibold">{toolName}</span>{#if serverLabel}
from <span class="font-semibold">{serverLabel}</span>{/if}?
&nbsp;from <span class="font-semibold">{serverLabel}</span>{/if}?
{/snippet}
{#snippet actions()}
@@ -16,6 +16,7 @@
let newServerUrl = $state('');
let newServerHeaders = $state('');
let newServerUseProxy = $state(false);
let newServerUrlError = $derived.by(() => {
if (!newServerUrl.trim()) return 'URL is required';
try {
@@ -35,6 +36,7 @@
if (!value) {
newServerUrl = '';
newServerHeaders = '';
newServerUseProxy = false;
}
open = value;
onOpenChange?.(value);
@@ -49,7 +51,8 @@
id: newServerId,
enabled: true,
url: newServerUrl.trim(),
headers: newServerHeaders.trim() || undefined
headers: newServerHeaders.trim() || undefined,
useProxy: newServerUseProxy
});
conversationsStore.setMcpServerOverride(newServerId, true);
@@ -74,8 +77,10 @@
<McpServerForm
url={newServerUrl}
headers={newServerHeaders}
useProxy={newServerUseProxy}
onUrlChange={(v) => (newServerUrl = v)}
onHeadersChange={(v) => (newServerHeaders = v)}
onUseProxyChange={(v) => (newServerUseProxy = v)}
urlError={newServerUrl ? newServerUrlError : null}
id="new-server"
/>
@@ -32,7 +32,9 @@
let isHealthChecking = $derived(healthState.status === HealthCheckStatus.CONNECTING);
let isConnected = $derived(healthState.status === HealthCheckStatus.SUCCESS);
let isError = $derived(healthState.status === HealthCheckStatus.ERROR);
let showSkeleton = $derived(isIdle || isHealthChecking);
// Disabled servers stay IDLE (no startup health check), so the body
// skeleton only applies while a check is running or expected to run.
let showSkeleton = $derived(isHealthChecking || (isIdle && server.enabled));
let errorMessage = $derived(
healthState.status === HealthCheckStatus.ERROR ? healthState.message : undefined
);
@@ -22,7 +22,9 @@
let { class: className }: Props = $props();
let servers = $derived(mcpStore.visibleMcpServers);
// Every configured server is listed; `enabled` is an on/off state,
// not a visibility filter, so a disabled server stays toggleable.
let servers = $derived(mcpStore.getServers());
let isAddingServer = $state(false);
@@ -58,9 +60,14 @@
// Each card decides for itself whether to render based on its own
// health-check state, so adding a server only flashes the new card
// (not every other already-loaded card) until its health check resolves.
function isServerPending(serverId: string): boolean {
// Disabled servers never receive a startup health check, so IDLE only
// counts as pending when the server is enabled; otherwise the real card
// renders and keeps the enable toggle reachable.
function isServerPending(serverId: string, enabled: boolean): boolean {
const status = mcpStore.getHealthCheckState(serverId).status;
return status === HealthCheckStatus.IDLE || status === HealthCheckStatus.CONNECTING;
return (
status === HealthCheckStatus.CONNECTING || (status === HealthCheckStatus.IDLE && enabled)
);
}
</script>
@@ -109,7 +116,7 @@
style="grid-template-columns: repeat(auto-fill, minmax(min(32rem, calc(100dvw - 2rem)), 1fr));"
>
{#each servers as server (server.id)}
{#if isServerPending(server.id)}
{#if isServerPending(server.id, server.enabled)}
<McpServerCardSkeleton />
{:else}
<McpServerCard
+1 -2
View File
@@ -6,8 +6,7 @@ export const NEWLINE_SEPARATOR = '\n';
export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
enabled: true,
maxTurns: 100,
maxToolPreviewLines: 25
maxTurns: 100
} as const;
export const REASONING_TAGS = {
+3 -1
View File
@@ -24,8 +24,10 @@ export const MODEL_CUSTOM_QUANTIZATION_PREFIX_RE = /^UD$/i;
/**
* Matches a parameter-count segment, e.g. `7B`, `1.5b`, `120M`.
* The optional leading `E` covers effective-parameter sizes, e.g. Gemma's
* `E2B`/`E4B` (MatFormer models sized by resident params).
*/
export const MODEL_PARAMS_RE = /^\d+(\.\d+)?[BbMmKkTt]$/;
export const MODEL_PARAMS_RE = /^[Ee]?\d+(\.\d+)?[BbMmKkTt]$/;
/**
* Matches an activated-parameter-count segment, e.g. `A10B`, `a2.4b`.
-1
View File
@@ -8,7 +8,6 @@ export const SETTINGS_SECTION_SLUGS = {
PENALTIES: 'penalties',
AGENTIC: 'agentic',
DEVELOPER: 'developer',
MCP: 'mcp',
TOOLS: 'tools',
IMPORT_EXPORT: 'import-export'
} as const;
@@ -59,7 +59,6 @@ export const SETTINGS_KEYS = {
MCP_SERVERS: 'mcpServers',
MCP_REQUEST_TIMEOUT_SECONDS: 'mcpRequestTimeoutSeconds',
AGENTIC_MAX_TURNS: 'agenticMaxTurns',
AGENTIC_MAX_TOOL_PREVIEW_LINES: 'agenticMaxToolPreviewLines',
SHOW_TOOL_CALL_IN_PROGRESS: 'showToolCallInProgress',
// Performance
PRE_ENCODE_CONVERSATION: 'preEncodeConversation',
@@ -24,7 +24,6 @@ import type {
SettingsSection
} from '$lib/types';
import { CLI_FLAGS, DEFAULT_MCP_CONFIG } from '$lib/constants';
import McpLogo from '$lib/components/app/mcp/McpLogo.svelte';
import { SETTINGS_KEYS } from './settings-keys';
import { ROUTES, SETTINGS_SECTION_SLUGS } from './routes';
import { TITLE_GENERATION } from './title-generation';
@@ -36,7 +35,6 @@ export const SETTINGS_SECTION_TITLES = {
PENALTIES: 'Penalties',
AGENTIC: 'Agentic',
TOOLS: 'Tools',
MCP: 'MCP',
IMPORT_EXPORT: 'Import/Export',
DEVELOPER: 'Developer'
} as const;
@@ -635,15 +633,15 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
}
},
{
key: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
label: 'Max lines per tool preview',
help: 'Number of lines shown in tool output previews (last N lines). Only these previews and the final LLM response persist after the agentic loop completes.',
defaultValue: 25,
key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
label: 'MCP request timeout (seconds)',
help: 'Timeout for individual MCP tool calls.',
defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.AGENTIC,
isPositiveInteger: true,
sync: {
serverKey: SETTINGS_KEYS.AGENTIC_MAX_TOOL_PREVIEW_LINES,
serverKey: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
paramType: SyncableParameterType.NUMBER
}
}
@@ -735,26 +733,6 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
}
}
]
},
[SETTINGS_SECTION_SLUGS.MCP]: {
title: SETTINGS_SECTION_TITLES.MCP,
slug: SETTINGS_SECTION_SLUGS.MCP,
icon: McpLogo,
settings: [
{
key: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
label: 'Request timeout (seconds)',
help: 'Default timeout for individual MCP tool calls. Can be overridden per server.',
defaultValue: DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
type: SettingsFieldType.INPUT,
section: SETTINGS_SECTION_SLUGS.MCP,
isPositiveInteger: true,
sync: {
serverKey: SETTINGS_KEYS.MCP_REQUEST_TIMEOUT_SECONDS,
paramType: SyncableParameterType.NUMBER
}
}
]
}
} as const;
+24 -1
View File
@@ -692,8 +692,31 @@ export class MCPService {
this.createLog(MCPConnectionPhase.INITIALIZING, 'Sending initialize request...')
);
// The SDK timeout only covers the initialize request, not transport.start(),
// which can hang forever on an unreachable host (SSE endpoint wait, WebSocket
// handshake, proxied fetch). This race bounds the whole handshake and closes
// the transport on expiry so the underlying fetch or socket is aborted.
const handshakeTimeoutMs =
serverConfig.handshakeTimeoutMs ?? DEFAULT_MCP_CONFIG.connectionTimeoutMs;
try {
await client.connect(transport);
let handshakeTimer: ReturnType<typeof setTimeout> | undefined;
const handshakeDeadline = new Promise<never>((_, reject) => {
handshakeTimer = setTimeout(() => {
void transport.close().catch(() => {});
reject(new Error(`Connection timed out after ${Math.round(handshakeTimeoutMs / 1000)}s`));
}, handshakeTimeoutMs);
});
try {
await Promise.race([
client.connect(transport, { timeout: handshakeTimeoutMs }),
handshakeDeadline
]);
} finally {
clearTimeout(handshakeTimer);
}
// Transport diagnostics are only for the initial handshake, not long-lived traffic.
stopPhaseLogging();
client.onerror = runtimeErrorHandler;
+1 -4
View File
@@ -280,16 +280,13 @@ class AgenticStore {
getConfig(settings: SettingsConfigType, perChatOverrides?: McpServerOverride[]): AgenticConfig {
const maxTurns = Number(settings.agenticMaxTurns) || DEFAULT_AGENTIC_CONFIG.maxTurns;
const maxToolPreviewLines =
Number(settings.agenticMaxToolPreviewLines) || DEFAULT_AGENTIC_CONFIG.maxToolPreviewLines;
const hasTools =
mcpStore.hasEnabledServers(perChatOverrides) ||
toolsStore.builtinTools.length > 0 ||
toolsStore.customTools.length > 0;
return {
enabled: hasTools && DEFAULT_AGENTIC_CONFIG.enabled,
maxTurns,
maxToolPreviewLines
maxTurns
};
}
+1 -1
View File
@@ -1334,7 +1334,7 @@ class ChatStore {
}
};
const perChatOverrides = conversationsStore.activeConversation?.mcpServerOverrides;
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
{
const agenticResult = await agenticStore.runAgenticFlow({
+23 -30
View File
@@ -243,9 +243,9 @@ class ConversationsStore {
const conversationName = name || `Chat ${new Date().toLocaleString()}`;
const conversation = await DatabaseService.createConversation(conversationName);
// New conversations inherit per-server enabled defaults directly from
// `mcpServers[i].enabled` (see #checkServerEnabled). No per-conversation
// override list needs to be seeded.
// No MCP override list is seeded: getAllMcpServerOverrides resolves
// servers without a per-conversation override to `mcpServers[i].enabled`,
// and only explicit toggles are stored on the conversation.
// Inherit global thinking/reasoning defaults into the new conversation
const thinkingEnabled = this.getThinkingEnabled();
@@ -601,48 +601,41 @@ class ConversationsStore {
*/
/**
/**
* Resolve the per-server enabled value when no active conversation exists.
* The default for new chats is the server's own `enabled` flag in `mcpServers`.
* Resolve the default enabled value for a server: its own `enabled`
* flag in `mcpServers`, so the global on/off state lives in one place.
*/
#getDefaultOverrideForNoConversation(serverId: string): McpServerOverride | undefined {
#getDefaultOverride(serverId: string): McpServerOverride | undefined {
const server = mcpStore.getServers().find((s) => s.id === serverId);
if (!server) return undefined;
return { serverId, enabled: server.enabled };
}
/**
* Default overrides for new chats are derived from `mcpServers[i].enabled`,
* so the global on/off state lives in one place.
*/
#getAllDefaultOverridesForNoConversation(): McpServerOverride[] {
return mcpStore.getServers().map((s) => ({ serverId: s.id, enabled: s.enabled }));
}
/**
* Gets MCP server override for a specific server in the active conversation.
* Falls back to `mcpServers[i].enabled` if no active conversation exists.
* Gets the effective MCP server override for a specific server.
* A per-conversation override wins when present; a server without one
* resolves to its `mcpServers[i].enabled` default.
* @param serverId - The server ID to check
* @returns The override if set, undefined if no matching server
* @returns The effective override, undefined if no matching server
*/
getMcpServerOverride(serverId: string): McpServerOverride | undefined {
if (this.activeConversation) {
return this.activeConversation.mcpServerOverrides?.find(
(o: McpServerOverride) => o.serverId === serverId
);
}
return this.#getDefaultOverrideForNoConversation(serverId);
const override = this.activeConversation?.mcpServerOverrides?.find(
(o: McpServerOverride) => o.serverId === serverId
);
if (override) return override;
return this.#getDefaultOverride(serverId);
}
/**
* Get all MCP server overrides for the current conversation.
* When no active conversation, derives from `mcpServers[i].enabled`.
* Gets the effective override list for the current conversation:
* one entry per configured server, resolved per server. The stored
* per-conversation list is sparse and only holds explicit toggles.
*/
getAllMcpServerOverrides(): McpServerOverride[] {
if (this.activeConversation?.mcpServerOverrides) {
return this.activeConversation.mcpServerOverrides;
}
return this.#getAllDefaultOverridesForNoConversation();
const overrides = this.activeConversation?.mcpServerOverrides;
return mcpStore.getServers().map((s) => {
const override = overrides?.find((o: McpServerOverride) => o.serverId === s.id);
return { serverId: s.id, enabled: override?.enabled ?? s.enabled };
});
}
/**
+17 -21
View File
@@ -148,15 +148,22 @@ class MCPStore {
enabled: Boolean((entry as { enabled?: unknown })?.enabled),
url,
name: (entry as { name?: string })?.name,
requestTimeoutSeconds:
(entry as { requestTimeoutSeconds?: number })?.requestTimeoutSeconds ??
DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
headers: headers || undefined,
useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)
} satisfies MCPServerSettingsEntry;
});
}
/**
* Request timeout in milliseconds, read live from the global setting
* so a change in Settings applies to every server immediately.
*/
#requestTimeoutMs(): number {
const seconds =
Number(config().mcpRequestTimeoutSeconds) || DEFAULT_MCP_CONFIG.requestTimeoutSeconds;
return Math.round(seconds * 1000);
}
/**
* Builds server configuration from a settings entry.
*/
@@ -183,7 +190,7 @@ class MCPStore {
url: entry.url,
transport: detectMcpTransportFromUrl(entry.url),
handshakeTimeoutMs: connectionTimeoutMs,
requestTimeoutMs: Math.round(entry.requestTimeoutSeconds * 1000),
requestTimeoutMs: this.#requestTimeoutMs(),
headers,
useProxy: entry.useProxy
};
@@ -191,15 +198,15 @@ class MCPStore {
/**
* Checks if a server is enabled for a given chat.
* Only per-chat overrides (persisted in localStorage for new chats,
* or in IndexedDB for existing conversations) control enabled state.
* A per-chat override wins when present; a server without one resolves
* to its own `enabled` flag in `mcpServers`.
*/
#checkServerEnabled(
server: MCPServerSettingsEntry,
perChatOverrides?: McpServerOverride[]
): boolean {
const override = perChatOverrides?.find((o) => o.serverId === server.id);
return override?.enabled ?? false;
return override?.enabled ?? server.enabled;
}
/**
@@ -230,7 +237,7 @@ class MCPStore {
protocolVersion: DEFAULT_MCP_CONFIG.protocolVersion,
capabilities: DEFAULT_MCP_CONFIG.capabilities,
clientInfo: DEFAULT_MCP_CONFIG.clientInfo,
requestTimeoutMs: Math.round(DEFAULT_MCP_CONFIG.requestTimeoutSeconds * 1000),
requestTimeoutMs: this.#requestTimeoutMs(),
servers
};
}
@@ -500,7 +507,7 @@ class MCPStore {
}
addServer(
serverData: Omit<MCPServerSettingsEntry, 'id' | 'requestTimeoutSeconds'> & { id?: string }
serverData: Omit<MCPServerSettingsEntry, 'id'> & { id?: string }
): MCPServerSettingsEntry {
const servers = this.getServers();
const newServer: MCPServerSettingsEntry = {
@@ -509,8 +516,6 @@ class MCPStore {
url: serverData.url.trim(),
name: serverData.name,
headers: serverData.headers?.trim() || undefined,
requestTimeoutSeconds:
Number(config().mcpRequestTimeoutSeconds) || DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
useProxy: serverData.useProxy
};
settingsStore.updateConfig(SETTINGS_KEYS.MCP_SERVERS, JSON.stringify([...servers, newServer]));
@@ -551,14 +556,6 @@ class MCPStore {
});
}
/**
* MCP servers selectable in chat-add UIs and the settings page,
* in the order they were added to the config.
*/
get visibleMcpServers(): MCPServerSettingsEntry[] {
return this.getServers().filter((server) => server.enabled);
}
async ensureInitialized(perChatOverrides?: McpServerOverride[]): Promise<boolean> {
if (!browser) {
return false;
@@ -1226,7 +1223,6 @@ class MCPStore {
id: string;
enabled: boolean;
url: string;
requestTimeoutSeconds: number;
headers?: string;
}[],
skipIfChecked = true,
@@ -1317,7 +1313,7 @@ class MCPStore {
logs: []
});
const timeoutMs = Math.round(server.requestTimeoutSeconds * 1000);
const timeoutMs = this.#requestTimeoutMs();
const headers = this.parseHeaders(server.headers);
try {
+2 -1
View File
@@ -412,7 +412,8 @@ class ToolsStore {
tools: { name: string; description?: string }[];
}[] {
const result: ReturnType<ToolsStore['getMcpToolsFromHealthChecks']> = [];
for (const server of mcpStore.visibleMcpServers) {
for (const server of mcpStore.getServers()) {
if (!server.enabled) continue;
const health = mcpStore.getHealthCheckState(server.id);
if (health.status === HealthCheckStatus.SUCCESS && health.tools.length > 0) {
result.push({
-1
View File
@@ -15,7 +15,6 @@ import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from '.
export interface AgenticConfig {
enabled: boolean;
maxTurns: number;
maxToolPreviewLines: number;
}
/**
-2
View File
@@ -174,7 +174,6 @@ export interface HealthCheckParams {
id: string;
enabled: boolean;
url: string;
requestTimeoutSeconds: number;
headers?: string;
useProxy?: boolean;
}
@@ -220,7 +219,6 @@ export interface MCPServerDisplayInfo {
export type MCPServerSettingsEntry = MCPServerDisplayInfo & {
enabled: boolean;
requestTimeoutSeconds: number;
headers?: string;
iconUrl?: string;
useProxy?: boolean;
-5
View File
@@ -9,7 +9,6 @@ import {
MimeTypeText
} from '$lib/enums';
import {
DEFAULT_MCP_CONFIG,
MCP_SERVER_ID_PREFIX,
IMAGE_FILE_EXTENSION_REGEX,
CODE_FILE_EXTENSION_REGEX,
@@ -64,7 +63,6 @@ export function detectMcpTransportFromUrl(url: string): MCPTransportType {
/**
* Parses MCP server settings from a JSON string or array.
* Preserves per-server requestTimeoutSeconds if stored, otherwise falls back to the global default.
* @param rawServers - The raw servers to parse
* @returns An empty array if the input is invalid.
*/
@@ -103,9 +101,6 @@ export function parseMcpServerSettings(rawServers: unknown): MCPServerSettingsEn
enabled: Boolean((entry as { enabled?: unknown })?.enabled),
url,
name: (entry as { name?: string })?.name,
requestTimeoutSeconds:
(entry as { requestTimeoutSeconds?: number })?.requestTimeoutSeconds ??
DEFAULT_MCP_CONFIG.requestTimeoutSeconds,
headers: headers || undefined,
useProxy: Boolean((entry as { useProxy?: unknown })?.useProxy)
} satisfies MCPServerSettingsEntry;
@@ -36,6 +36,11 @@ describe('parseModelId', () => {
expect(parseModelId('model-100b:q4_k_m')).toMatchObject({ params: '100B' });
});
it('extracts effective parameters correctly', () => {
expect(parseModelId('model-E4B-BF16')).toMatchObject({ params: 'E4B' });
expect(parseModelId('model-e2b:q4_k_m')).toMatchObject({ params: 'E2B' });
});
it('extracts activated parameters correctly', () => {
expect(parseModelId('model-100B-A10B-BF16')).toMatchObject({ activatedParams: 'A10B' });
expect(parseModelId('model-100B-A10B:Q4_K_M')).toMatchObject({ activatedParams: 'A10B' });
@@ -1,6 +1,6 @@
import { describe, expect, it, vi } from 'vitest';
import { parseMcpServerSettings } from '$lib/utils/mcp';
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
import { MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
/**
* Tests for the mcpServers settings parser.
@@ -58,24 +58,16 @@ describe('parseMcpServerSettings', () => {
expect(parsed[2]?.id).toBe('custom-3');
});
it('falls back to the configured default requestTimeoutSeconds only for nullish values', () => {
const fallback = DEFAULT_MCP_CONFIG.requestTimeoutSeconds;
it('does not emit a per-server timeout, the request timeout is a live global setting', () => {
// A stored per-server requestTimeoutSeconds was never editable in
// any UI and froze the global setting at server creation time,
// making the Settings value a no-op for existing servers. The
// parser drops the field so the global applies live everywhere.
const parsed = parseMcpServerSettings(
JSON.stringify([
{ id: 'a', url: 'https://a.test' },
{ id: 'b', url: 'https://b.test', requestTimeoutSeconds: undefined },
{ id: 'c', url: 'https://c.test', requestTimeoutSeconds: 0 },
{ id: 'd', url: 'https://d.test', requestTimeoutSeconds: 45 }
])
JSON.stringify([{ id: 'a', url: 'https://a.test', requestTimeoutSeconds: 45 }])
);
// The parser uses ?? for timeout fallback, which only triggers on
// null/undefined. Explicit 0 is preserved at face value.
expect(parsed[0]?.requestTimeoutSeconds).toBe(fallback);
expect(parsed[1]?.requestTimeoutSeconds).toBe(fallback);
expect(parsed[2]?.requestTimeoutSeconds).toBe(0);
expect(parsed[3]?.requestTimeoutSeconds).toBe(45);
expect(parsed[0]).not.toHaveProperty('requestTimeoutSeconds');
});
it('treats whitespace-only headers strings as undefined', () => {
@@ -108,6 +100,22 @@ describe('parseMcpServerSettings', () => {
expect(parsed[3]?.useProxy).toBe(true);
});
it('keeps disabled entries in the list, enabled is state and never a visibility filter', () => {
// Regression guard for issue #25625: filtering the server list on
// `enabled` hides a toggled-off server from every UI surface with
// no way to re-enable it. Any list derived from this parser must
// contain disabled entries.
const parsed = parseMcpServerSettings(
JSON.stringify([
{ id: 'on', url: 'https://on.test', enabled: true },
{ id: 'off', url: 'https://off.test', enabled: false }
])
);
expect(parsed.map((entry) => entry.id)).toEqual(['on', 'off']);
expect(parsed[1]?.enabled).toBe(false);
});
it('preserves input order when mapping entries', () => {
const source = [
{ id: 'gamma', url: 'https://c.test' },