mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-07-26 06:16:02 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c71854292f | |||
| bf2c86ddc0 | |||
| 6e52db5b72 | |||
| 236ab574e0 | |||
| dfba90db63 | |||
| 00e79f6fb1 | |||
| 17a05e451f |
+47
-3
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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([¶ms, 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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
+4
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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>
|
||||
|
||||
+1
-1
@@ -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}?
|
||||
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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1334,7 +1334,7 @@ class ChatStore {
|
||||
}
|
||||
};
|
||||
|
||||
const perChatOverrides = conversationsStore.activeConversation?.mcpServerOverrides;
|
||||
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
|
||||
|
||||
{
|
||||
const agenticResult = await agenticStore.runAgenticFlow({
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Vendored
-1
@@ -15,7 +15,6 @@ import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from '.
|
||||
export interface AgenticConfig {
|
||||
enabled: boolean;
|
||||
maxTurns: number;
|
||||
maxToolPreviewLines: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
-2
@@ -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;
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user