Merge branch 'upstream' into concedo_experimental

# Conflicts:
#	.devops/openvino.Dockerfile
#	.github/workflows/build-cache.yml
#	.github/workflows/build-openvino.yml
#	.github/workflows/build-self-hosted.yml
#	.github/workflows/release.yml
#	docs/backend/OPENVINO.md
#	docs/backend/SYCL.md
#	docs/ops.md
#	docs/ops/SYCL.csv
#	ggml/src/ggml-opencl/ggml-opencl.cpp
#	ggml/src/ggml-opencl/kernels/mul_mv_f16_f32_l4.cl
#	ggml/src/ggml-openvino/.clang-format
#	ggml/src/ggml-openvino/CMakeLists.txt
#	ggml/src/ggml-openvino/ggml-decoder.cpp
#	ggml/src/ggml-openvino/ggml-decoder.h
#	ggml/src/ggml-openvino/ggml-openvino-extra.cpp
#	ggml/src/ggml-openvino/ggml-openvino-extra.h
#	ggml/src/ggml-openvino/ggml-openvino.cpp
#	ggml/src/ggml-openvino/ggml-quants.cpp
#	ggml/src/ggml-openvino/ggml-quants.h
#	ggml/src/ggml-openvino/openvino/decoder.h
#	ggml/src/ggml-openvino/openvino/frontend.h
#	ggml/src/ggml-openvino/openvino/input_model.h
#	ggml/src/ggml-openvino/openvino/node_context.h
#	ggml/src/ggml-openvino/openvino/op/cont.cpp
#	ggml/src/ggml-openvino/openvino/op/cpy.cpp
#	ggml/src/ggml-openvino/openvino/op/flash_attn_ext.cpp
#	ggml/src/ggml-openvino/openvino/op/get_rows.cpp
#	ggml/src/ggml-openvino/openvino/op/glu_geglu.cpp
#	ggml/src/ggml-openvino/openvino/op/glu_swiglu.cpp
#	ggml/src/ggml-openvino/openvino/op/mulmat.cpp
#	ggml/src/ggml-openvino/openvino/op/permute.cpp
#	ggml/src/ggml-openvino/openvino/op/reshape.cpp
#	ggml/src/ggml-openvino/openvino/op/rms_norm.cpp
#	ggml/src/ggml-openvino/openvino/op/rope.cpp
#	ggml/src/ggml-openvino/openvino/op/set_rows.cpp
#	ggml/src/ggml-openvino/openvino/op/softmax.cpp
#	ggml/src/ggml-openvino/openvino/op/transpose.cpp
#	ggml/src/ggml-openvino/openvino/op/unary_silu.cpp
#	ggml/src/ggml-openvino/openvino/op/view.cpp
#	ggml/src/ggml-openvino/openvino/op_table.cpp
#	ggml/src/ggml-openvino/openvino/op_table.h
#	ggml/src/ggml-openvino/openvino/pass/mark_decompression_convert_constant_folding.h
#	ggml/src/ggml-openvino/openvino/translate_session.cpp
#	ggml/src/ggml-openvino/openvino/translate_session.h
#	ggml/src/ggml-openvino/openvino/utils.cpp
#	ggml/src/ggml-openvino/openvino/utils.h
#	ggml/src/ggml-openvino/utils.cpp
#	ggml/src/ggml-openvino/utils.h
#	ggml/src/ggml-sycl/common.hpp
#	ggml/src/ggml-sycl/ggml-sycl.cpp
This commit is contained in:
Concedo
2026-06-17 16:06:00 +08:00
17 changed files with 424 additions and 151 deletions
+82 -76
View File
@@ -11,8 +11,13 @@
#include <sstream>
#include <thread>
#include <vector>
#include <algorithm>
#if defined(_WIN32)
# define WIN32_LEAN_AND_MEAN
# ifndef NOMINMAX
# define NOMINMAX
# endif
# include <io.h>
# include <windows.h>
# define isatty _isatty
@@ -62,16 +67,15 @@ static const char* g_col[] = {
};
struct common_log_entry {
enum ggml_log_level level;
bool prefix;
int64_t timestamp;
enum ggml_log_level level {GGML_LOG_LEVEL_INFO};
std::vector<char> msg;
// signals the worker thread to stop
bool is_end;
int64_t timestamp { 0 };
bool is_end { false }; // signals the worker thread to stop
bool prefix { false };
common_log_entry(size_t size = 256) : msg(size) { }
void print(FILE * file = nullptr) const {
FILE * fcur = file;
@@ -122,22 +126,15 @@ struct common_log_entry {
};
struct common_log {
// default capacity - will be expanded if needed
common_log() : common_log(256) {}
common_log(size_t capacity) {
file = nullptr;
prefix = false;
// default capacity
common_log(size_t capacity = 512) {
file = nullptr;
prefix = false;
timestamps = false;
running = false;
t_start = t_us();
// initial message size - will be expanded if longer messages arrive
entries.resize(capacity);
for (auto & entry : entries) {
entry.msg.resize(256);
}
running = false;
t_start = t_us();
queue.resize(capacity, common_log_entry(256));
head = 0;
tail = 0;
@@ -152,9 +149,10 @@ struct common_log {
}
private:
std::mutex mtx;
std::thread thrd;
std::condition_variable cv;
std::mutex mtx;
std::thread thrd;
std::condition_variable cv_new; // new entry
std::condition_variable cv_full; // wait on full
FILE * file;
@@ -164,24 +162,53 @@ private:
int64_t t_start;
// ring buffer of entries
std::vector<common_log_entry> entries;
// queue of entries
std::vector<common_log_entry> queue;
size_t head;
size_t tail;
// worker thread copies into this
common_log_entry cur;
bool print_entry(const common_log_entry & e) const {
if (e.is_end) return true;
e.print();
if (file) {
e.print(file);
}
return false;
}
bool flush_queue(size_t start_head, size_t end_tail, size_t & out_head) const {
bool stop = false;
size_t h = start_head;
while (h != end_tail && !stop) {
stop = print_entry(queue[h]);
h = (h + 1) % queue.size();
}
out_head = h;
return stop;
}
public:
bool is_full() const {
return ((tail + 1) % queue.size()) == head;
}
bool is_empty() const {
return head == tail;
}
void add(enum ggml_log_level level, const char * fmt, va_list args) {
std::lock_guard<std::mutex> lock(mtx);
std::unique_lock<std::mutex> lock(mtx);
// block if the queue is full
cv_full.wait(lock, [this]() { return !running || !is_full(); });
if (!running) {
// discard messages while the worker thread is paused
return;
}
auto & entry = entries[tail];
auto & entry = queue[tail];
{
// cannot use args twice, so make a copy in case we need to expand the buffer
@@ -216,38 +243,16 @@ public:
va_end(args_copy);
}
entry.level = level;
entry.prefix = prefix;
entry.is_end = false;
entry.level = level;
entry.prefix = prefix;
entry.timestamp = 0;
if (timestamps) {
entry.timestamp = t_us() - t_start;
}
entry.is_end = false;
tail = (tail + 1) % entries.size();
if (tail == head) {
// expand the buffer
std::vector<common_log_entry> new_entries(2*entries.size());
size_t new_tail = 0;
do {
new_entries[new_tail] = std::move(entries[head]);
head = (head + 1) % entries.size();
new_tail = (new_tail + 1);
} while (head != tail);
head = 0;
tail = new_tail;
for (size_t i = tail; i < new_entries.size(); i++) {
new_entries[i].msg.resize(256);
}
entries = std::move(new_entries);
}
cv.notify_one();
tail = (tail + 1) % queue.size();
cv_new.notify_one();
}
void resume() {
@@ -261,23 +266,24 @@ public:
thrd = std::thread([this]() {
while (true) {
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this]() { return head != tail; });
cur = entries[head];
std::unique_lock<std::mutex> lock(mtx);
cv_new.wait(lock, [this]() { return !is_empty(); });
head = (head + 1) % entries.size();
}
size_t cached_head = head;
size_t cached_tail = tail;
if (cur.is_end) {
lock.unlock(); // drop the lock during flush
size_t next_head;
bool stop = flush_queue(cached_head, cached_tail, next_head);
lock.lock();
head = next_head;
cv_full.notify_all();
if (stop) {
break;
}
cur.print(); // stdout and stderr
if (file) {
cur.print(file);
}
}
});
}
@@ -293,13 +299,13 @@ public:
running = false;
// push an entry to signal the worker thread to stop
{
auto & entry = entries[tail];
entry.is_end = true;
auto & entry = queue[tail];
entry.is_end = true;
tail = (tail + 1) % queue.size();
tail = (tail + 1) % entries.size();
}
cv.notify_one();
// wakeup everyone
cv_new.notify_one();
cv_full.notify_all();
}
thrd.join();
@@ -422,7 +422,7 @@ std::map<std::string, std::string> merge_maps(const std::map<std::string, std::s
return result;
}
static std::vector<std::future<void>> compiles;
static std::deque<std::future<void>> compiles;
void string_to_spv(std::string name, const std::string& source, const std::map<std::string, std::string>& defines, bool fp16 = true, bool coopmat = false, bool coopmat2 = false, bool f16acc = false, const std::string& suffix = "") {
name = name + (f16acc ? "_f16acc" : "") + (coopmat ? "_cm1" : "") + (coopmat2 ? "_cm2" : (fp16 ? "" : "_fp32")) + suffix;
std::string out_path = join_paths(output_dir, name + ".spv");
@@ -452,6 +452,11 @@ void string_to_spv(std::string name, const std::string& source, const std::map<s
string_to_spv_func, name, in_path, out_path, defines, coopmat, generate_dep_file, std::move(slot)));
// Don't write the same dep file from multiple processes
generate_dep_file = false;
// Clean up completed futures - don't accumulate virtual memory for completed threads' stacks.
while (!compiles.empty() && compiles.front().wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
compiles.pop_front();
}
}
void matmul_shaders(bool fp16, MatMulIdType matmul_id_type, bool coopmat, bool coopmat2, bool f16acc, bool dot2 = false) {
+58 -45
View File
@@ -1088,6 +1088,10 @@ ggml_tensor * llm_graph_context::build_lora_mm(
ggml_tensor * w_s) const {
ggml_tensor * res = ggml_mul_mat(ctx0, w, cur);
if (w_s) {
res = ggml_mul(ctx0, res, w_s);
}
for (const auto & lora : *loras) {
llama_adapter_lora_weight * lw = lora.first->get_weight(w);
if (lw == nullptr) {
@@ -1106,18 +1110,24 @@ ggml_tensor * llm_graph_context::build_lora_mm(
res = ggml_add(ctx0, res, ab_cur);
}
if (w_s) {
res = ggml_mul(ctx0, res, w_s);
}
return res;
}
ggml_tensor * llm_graph_context::build_lora_mm_id(
ggml_tensor * w, // ggml_tensor * as
ggml_tensor * cur, // ggml_tensor * b
ggml_tensor * ids) const {
ggml_tensor * ids,
ggml_tensor * w_s) const {
ggml_tensor * res = ggml_mul_mat_id(ctx0, w, cur, ids);
if (w_s) {
const int64_t n_expert = w_s->ne[0];
const int64_t n_tokens = cur->ne[2];
ggml_tensor * s = ggml_reshape_3d(ctx0, w_s, 1, n_expert, 1);
s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1);
s = ggml_get_rows(ctx0, s, ids);
res = ggml_mul(ctx0, res, s);
}
for (const auto & lora : *loras) {
llama_adapter_lora_weight * lw = lora.first->get_weight(w);
if (lw == nullptr) {
@@ -1269,6 +1279,29 @@ ggml_tensor * llm_graph_context::build_ffn(
llm_ffn_op_type type_op,
llm_ffn_gate_type type_gate,
int il) const {
// NVFP4 support is currently restricted to
// 1) LORA absence (*_s would be applied after LORA residual, which is incorrect)
// 2) bias absense (*_s would be applied after bias addition, which is incorrect)
// TODO: disambiguate LLM-architectural scales (which use *_s) from NVFP4 scale_2 (which also uses *_s currently)
auto has_lora = [this](ggml_tensor * w) {
if (!w) {
return false;
}
for (const auto & lora : *loras) {
if (lora.first->get_weight(w) != nullptr) {
return true;
}
}
return false;
};
GGML_ASSERT(!up_s || !up_b || !up || up->type != GGML_TYPE_NVFP4);
GGML_ASSERT(!gate_s || !gate_b || !gate || gate->type != GGML_TYPE_NVFP4);
GGML_ASSERT(!down_s || !down_b || !down || down->type != GGML_TYPE_NVFP4);
GGML_ASSERT(!up_s || !up || up->type != GGML_TYPE_NVFP4 || !has_lora(up));
GGML_ASSERT(!gate_s || !gate || gate->type != GGML_TYPE_NVFP4 || !has_lora(gate));
GGML_ASSERT(!down_s || !down || down->type != GGML_TYPE_NVFP4 || !has_lora(down));
ggml_tensor * tmp = up ? build_lora_mm(up, cur) : cur;
cb(tmp, "ffn_up", il);
@@ -1627,23 +1660,18 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
if (gate_up_exps) {
// merged gate_up path: one mul_mat_id, then split into gate and up views
ggml_tensor * gate_up = build_lora_mm_id(gate_up_exps, cur, selected_experts); // [n_ff*2, n_expert_used, n_tokens]
ggml_tensor * gate_up = build_lora_mm_id(gate_up_exps, cur, selected_experts, up_exps_s); // [n_ff*2, n_expert_used, n_tokens]
cb(gate_up, "ffn_moe_gate_up", il);
if (up_exps_s) {
cb(gate_up, "ffn_moe_gate_up_scaled", il);
}
if (gate_up_exps_b) {
gate_up = ggml_add_id(ctx0, gate_up, gate_up_exps_b, selected_experts);
cb(gate_up, "ffn_moe_gate_up_biased", il);
}
// apply per-expert scale2 to merged gate_up (use up_exps_s since gate and up are fused)
if (up_exps_s) {
ggml_tensor * s = ggml_reshape_3d(ctx0, up_exps_s, 1, n_expert, 1);
s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1);
s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens]
gate_up = ggml_mul(ctx0, gate_up, s);
cb(gate_up, "ffn_moe_gate_up_scaled", il);
}
const int64_t n_ff = gate_up->ne[0] / 2;
cur = ggml_view_3d(ctx0, gate_up, n_ff, gate_up->ne[1], gate_up->ne[2], gate_up->nb[1], gate_up->nb[2], 0);
cb(cur, "ffn_moe_gate", il);
@@ -1651,43 +1679,33 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
cb(up, "ffn_moe_up", il);
} else {
// separate gate and up path
up = build_lora_mm_id(up_exps, cur, selected_experts); // [n_ff, n_expert_used, n_tokens]
up = build_lora_mm_id(up_exps, cur, selected_experts, up_exps_s); // [n_ff, n_expert_used, n_tokens]
cb(up, "ffn_moe_up", il);
if (up_exps_s) {
cb(up, "ffn_moe_up_scaled", il);
}
if (up_exps_b) {
up = ggml_add_id(ctx0, up, up_exps_b, selected_experts);
cb(up, "ffn_moe_up_biased", il);
}
// apply per-expert scale2 to up
if (up_exps_s) {
ggml_tensor * s = ggml_reshape_3d(ctx0, up_exps_s, 1, n_expert, 1);
s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1);
s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens]
up = ggml_mul(ctx0, up, s);
cb(up, "ffn_moe_up_scaled", il);
}
if (gate_exps) {
cur = build_lora_mm_id(gate_exps, cur, selected_experts); // [n_ff, n_expert_used, n_tokens]
cur = build_lora_mm_id(gate_exps, cur, selected_experts, gate_exps_s); // [n_ff, n_expert_used, n_tokens]
cb(cur, "ffn_moe_gate", il);
} else {
cur = up;
}
if (gate_exps_s) {
cb(cur, "ffn_moe_gate_scaled", il);
}
if (gate_exps_b) {
cur = ggml_add_id(ctx0, cur, gate_exps_b, selected_experts);
cb(cur, "ffn_moe_gate_biased", il);
}
// apply per-expert scale2 to gate
if (gate_exps_s) {
ggml_tensor * s = ggml_reshape_3d(ctx0, gate_exps_s, 1, n_expert, 1);
s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1);
s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens]
cur = ggml_mul(ctx0, cur, s);
cb(cur, "ffn_moe_gate_scaled", il);
}
}
const bool has_gate = gate_exps || gate_up_exps;
@@ -1759,23 +1777,18 @@ ggml_tensor * llm_graph_context::build_moe_ffn(
GGML_ABORT("fatal error");
}
experts = build_lora_mm_id(down_exps, cur, selected_experts); // [n_embd, n_expert_used, n_tokens]
experts = build_lora_mm_id(down_exps, cur, selected_experts, down_exps_s); // [n_embd, n_expert_used, n_tokens]
cb(experts, "ffn_moe_down", il);
if (down_exps_s) {
cb(experts, "ffn_moe_down_scaled", il);
}
if (down_exps_b) {
experts = ggml_add_id(ctx0, experts, down_exps_b, selected_experts);
cb(experts, "ffn_moe_down_biased", il);
}
// apply per-expert scale2 to down
if (down_exps_s) {
ggml_tensor * s = ggml_reshape_3d(ctx0, down_exps_s, 1, n_expert, 1);
s = ggml_repeat_4d(ctx0, s, 1, n_expert, n_tokens, 1);
s = ggml_get_rows(ctx0, s, selected_experts); // [1, n_expert_used, n_tokens]
experts = ggml_mul(ctx0, experts, s);
cb(experts, "ffn_moe_down_scaled", il);
}
if (!weight_before_ffn) {
experts = ggml_mul(ctx0, experts, weights);
cb(experts, "ffn_moe_weighted", il);
+3 -2
View File
@@ -853,11 +853,12 @@ struct llm_graph_context {
ggml_tensor * cur,
ggml_tensor * w_s = nullptr) const;
// do mat_mul_id, while optionally apply lora
// do mat_mul_id, while optionally apply lora and per-expert scale
ggml_tensor * build_lora_mm_id(
ggml_tensor * w, // ggml_tensor * as
ggml_tensor * cur, // ggml_tensor * b
ggml_tensor * ids) const;
ggml_tensor * ids,
ggml_tensor * w_s = nullptr) const;
ggml_tensor * build_norm(
ggml_tensor * cur,
@@ -41,6 +41,7 @@
DATA_ERROR_HANDLED_ATTR,
BOOL_TRUE_STRING,
SETTINGS_KEYS,
CODE_BLOCK_HEADER_CLASS,
MERMAID_WRAPPER_CLASS,
MERMAID_BLOCK_CLASS,
MERMAID_LANGUAGE,
@@ -53,7 +54,11 @@
SVG_TAG_PREFIX,
SVG_SOURCE_ATTR,
SVG_RENDERED_ATTR,
SVG_INLINE_SHADOW_STYLE
SVG_INLINE_SHADOW_STYLE,
TOGGLE_SOURCE_BTN_CLASS,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED,
DIAGRAM_VIEW_SOURCE
} from '$lib/constants';
import { ColorMode, UrlProtocol } from '$lib/enums';
import { FileTypeText } from '$lib/enums/files.enums';
@@ -501,6 +506,23 @@
async function handleMermaidClick(event: MouseEvent) {
const target = event.target as HTMLElement;
// Toggle a diagram block between its rendered view and its source view.
// Shared by mermaid and svg, css drives the visibility from the wrapper mode.
const toggleBtn = target.closest(`.${TOGGLE_SOURCE_BTN_CLASS}`);
if (toggleBtn) {
event.preventDefault();
event.stopPropagation();
const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`);
if (!wrapper) return;
const isSource = wrapper.getAttribute(DIAGRAM_VIEW_MODE_ATTR) === DIAGRAM_VIEW_SOURCE;
const next = isSource ? DIAGRAM_VIEW_RENDERED : DIAGRAM_VIEW_SOURCE;
wrapper.setAttribute(DIAGRAM_VIEW_MODE_ATTR, next);
toggleBtn.setAttribute('aria-pressed', String(!isSource));
return;
}
// Check if clicking on copy or preview button in mermaid block
const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`);
const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`);
@@ -573,6 +595,11 @@
}
}
// A click on the header chrome targets the action buttons, never the
// diagram. Guard so a header click can not fall through to the click to
// zoom branches below, whatever the scroll position or stacking.
if (target.closest(`.${CODE_BLOCK_HEADER_CLASS}`)) return;
// Open preview when clicking the svg block itself. A final block carries its
// source, a streaming block does not and is mirrored live into the dialog.
const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`);
@@ -300,7 +300,8 @@ div.markdown-user-content :global(.table-wrapper) {
}
.markdown-content :global(.copy-code-btn),
.markdown-content :global(.preview-code-btn) {
.markdown-content :global(.preview-code-btn),
.markdown-content :global(.toggle-source-btn) {
display: flex;
align-items: center;
justify-content: center;
@@ -312,15 +313,22 @@ div.markdown-user-content :global(.table-wrapper) {
}
.markdown-content :global(.copy-code-btn:hover),
.markdown-content :global(.preview-code-btn:hover) {
.markdown-content :global(.preview-code-btn:hover),
.markdown-content :global(.toggle-source-btn:hover) {
transform: scale(1.05);
}
.markdown-content :global(.copy-code-btn:active),
.markdown-content :global(.preview-code-btn:active) {
.markdown-content :global(.preview-code-btn:active),
.markdown-content :global(.toggle-source-btn:active) {
transform: scale(0.95);
}
/* Pressed state marks the source view as active */
.markdown-content :global(.toggle-source-btn[aria-pressed='true']) {
color: var(--primary);
}
.markdown-content :global(.code-block-wrapper pre) {
background: transparent;
margin: 0;
@@ -629,8 +637,8 @@ div.markdown-user-content :global(.table-wrapper) {
overflow-y: auto;
overflow-x: auto;
display: flex;
align-items: center;
justify-content: center;
align-items: safe center;
justify-content: safe center;
padding: 3rem 1rem 1rem;
}
@@ -645,7 +653,9 @@ div.markdown-user-content :global(.table-wrapper) {
overflow-y: visible;
}
/* Diagram block uses same header styling as code blocks */
/* Diagram block uses same header styling as code blocks. The header floats over
scrollable diagram content and stays transparent, so the overflow shows up to
the box edge. It keeps a z-index so it stays the click target above content. */
.markdown-content :global(.mermaid-block-wrapper .code-block-header),
.markdown-content :global(.svg-block-wrapper .code-block-header) {
display: flex;
@@ -657,6 +667,7 @@ div.markdown-user-content :global(.table-wrapper) {
top: 0;
left: 0;
right: 0;
z-index: 2;
}
.markdown-content :global(.mermaid-block-wrapper .code-block-actions),
@@ -683,6 +694,31 @@ div.markdown-user-content :global(.table-wrapper) {
padding: 3rem 1rem;
}
/* Source view stays hidden while the block renders, css swaps the two views
from the wrapper mode so the click handler only flips one attribute. The view
reuses the code block scroll container, so it matches the app code blocks. */
.markdown-content :global(.diagram-source) {
display: none;
text-align: left;
}
.markdown-content :global(.diagram-source pre) {
background: transparent;
margin: 0;
border-radius: 0;
border: none;
font-size: 0.875rem;
}
.markdown-content :global([data-view-mode='source'] .mermaid-scroll-container),
.markdown-content :global([data-view-mode='source'] .svg-scroll-container) {
display: none;
}
.markdown-content :global([data-view-mode='source'] .diagram-source) {
display: block;
}
/* Streaming mermaid block - empty preview box */
.mermaid-streaming-block {
min-height: 300px;
@@ -7,12 +7,16 @@ import type { Element, ElementContent } from 'hast';
import {
CODE_BLOCK_HEADER_CLASS,
CODE_BLOCK_ACTIONS_CLASS,
CODE_BLOCK_SCROLL_CONTAINER_CLASS,
CODE_LANGUAGE_CLASS,
COPY_CODE_BTN_CLASS,
PREVIEW_CODE_BTN_CLASS,
TOGGLE_SOURCE_BTN_CLASS,
DIAGRAM_SOURCE_CLASS,
RELATIVE_CLASS,
COPY_ICON_SVG,
PREVIEW_ICON_SVG
PREVIEW_ICON_SVG,
CODE_ICON_SVG
} from '$lib/constants';
export interface BlockIdGenerator {
@@ -32,14 +36,16 @@ export function createIconElement(svg: string): Element {
}
/**
* Creates a button element with icon.
* Creates a button element with icon. Extra properties merge onto the button,
* which lets a stateful button carry attributes like aria-pressed.
*/
export function createButton(
className: string,
title: string,
iconSvg: string,
id: string,
idAttribute: string
idAttribute: string,
extraProperties: Record<string, string> = {}
): Element {
return {
type: 'element',
@@ -48,7 +54,8 @@ export function createButton(
className: [className],
[idAttribute]: id,
title,
type: 'button'
type: 'button',
...extraProperties
},
children: [createIconElement(iconSvg)]
};
@@ -72,6 +79,52 @@ export function createPreviewButton(
return createButton(PREVIEW_CODE_BTN_CLASS, title, PREVIEW_ICON_SVG, id, idAttribute);
}
/**
* Creates a button that toggles a diagram block between its rendered view and
* its source view. aria-pressed starts false, the rendered view is the default.
*/
export function createToggleSourceButton(
id: string,
idAttribute: string,
title: string = 'Toggle source'
): Element {
return createButton(TOGGLE_SOURCE_BTN_CLASS, title, CODE_ICON_SVG, id, idAttribute, {
'aria-pressed': 'false'
});
}
/**
* Creates a source view for a diagram block. It reuses the code block scroll
* container so it matches the app code blocks, and wraps the highlighted code
* element captured at transform time. A missing code element falls back to a
* plain code node built from the raw source.
*/
export function createSourceView(
codeElement: Element | undefined,
source: string,
language: string
): Element {
const code: Element = codeElement ?? {
type: 'element',
tagName: 'code',
properties: { className: ['hljs', `language-${language}`] },
children: [{ type: 'text', value: source }]
};
return {
type: 'element',
tagName: 'div',
properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
children: [
{
type: 'element',
tagName: 'pre',
properties: {},
children: [code]
}
]
};
}
/**
* Creates a block header with language label and action buttons.
*/
@@ -116,14 +169,17 @@ export function createScrollContainer(preElement: Element, scrollContainerClass:
}
/**
* Creates a wrapper element with header and scroll container.
* Creates a wrapper element with header and scroll container. Extra children
* append after the scroll container, which lets a block carry a source view
* alongside its rendered output.
*/
export function createWrapper(
header: Element,
preElement: Element,
wrapperClass: string,
scrollContainerClass: string,
additionalAttributes?: Record<string, string>
additionalAttributes?: Record<string, string>,
extraChildren: Element[] = []
): Element {
return {
type: 'element',
@@ -132,7 +188,7 @@ export function createWrapper(
className: [wrapperClass, RELATIVE_CLASS],
...additionalAttributes
} as Element['properties'],
children: [header, createScrollContainer(preElement, scrollContainerClass)]
children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren]
};
}
@@ -19,12 +19,17 @@ import {
MERMAID_BLOCK_CLASS,
MERMAID_LANGUAGE,
MERMAID_SYNTAX_ATTR,
MERMAID_ID_ATTR
MERMAID_ID_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED
} from '$lib/constants';
import type { DiagramPreData } from './pre-transform';
import {
createBlockHeader,
createCopyButton,
createPreviewButton,
createToggleSourceButton,
createSourceView,
createWrapper,
generateBlockId
} from './code-block-utils';
@@ -75,16 +80,23 @@ export const rehypeEnhanceMermaidBlocks: Plugin<[], Root> = () => {
const actions = [
createCopyButton(mermaidId, MERMAID_ID_ATTR, 'Copy mermaid syntax'),
createToggleSourceButton(mermaidId, MERMAID_ID_ATTR, 'Toggle mermaid source'),
createPreviewButton(mermaidId, MERMAID_ID_ATTR, 'Preview diagram')
];
const header = createBlockHeader(MERMAID_LANGUAGE, mermaidId, MERMAID_ID_ATTR, actions);
const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
const sourceView = createSourceView(preservedCode, diagramText, MERMAID_LANGUAGE);
const wrapper = createWrapper(
header,
node,
MERMAID_WRAPPER_CLASS,
MERMAID_SCROLL_CONTAINER_CLASS,
{ [MERMAID_ID_ATTR]: mermaidId }
{
[MERMAID_ID_ATTR]: mermaidId,
[DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED
},
[sourceView]
);
// Replace pre with wrapper in parent
@@ -18,12 +18,17 @@ import {
SVG_BLOCK_CLASS,
SVG_LANGUAGE,
SVG_SOURCE_ATTR,
SVG_ID_ATTR
SVG_ID_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
DIAGRAM_VIEW_RENDERED
} from '$lib/constants';
import type { DiagramPreData } from './pre-transform';
import {
createBlockHeader,
createCopyButton,
createPreviewButton,
createToggleSourceButton,
createSourceView,
createWrapper,
generateBlockId
} from './code-block-utils';
@@ -65,13 +70,24 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
const actions = [
createCopyButton(svgId, SVG_ID_ATTR, 'Copy svg source'),
createToggleSourceButton(svgId, SVG_ID_ATTR, 'Toggle svg source'),
createPreviewButton(svgId, SVG_ID_ATTR, 'Preview svg')
];
const header = createBlockHeader(SVG_LANGUAGE, svgId, SVG_ID_ATTR, actions);
const wrapper = createWrapper(header, node, SVG_WRAPPER_CLASS, SVG_SCROLL_CONTAINER_CLASS, {
[SVG_ID_ATTR]: svgId
});
const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
const sourceView = createSourceView(preservedCode, svgSource, SVG_LANGUAGE);
const wrapper = createWrapper(
header,
node,
SVG_WRAPPER_CLASS,
SVG_SCROLL_CONTAINER_CLASS,
{
[SVG_ID_ATTR]: svgId,
[DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED
},
[sourceView]
);
// Replace pre with wrapper in parent
(parent.children as ElementContent[])[index] = wrapper;
@@ -2,6 +2,15 @@ import type { Plugin } from 'unified';
import type { Root, Element, ElementContent, Text } from 'hast';
import { visit } from 'unist-util-visit';
/**
* Metadata a diagram pre carries on its unist data field. The source code holds
* the highlighted code element captured before the pre became a render target,
* which the enhancer reuses to build a matching source view.
*/
export interface DiagramPreData {
sourceCode: Element;
}
/**
* Recursively extracts all text content from a HAST node.
* Handles nested elements (e.g., span wrappers from syntax highlighting).
@@ -69,7 +78,10 @@ export function createPreTransform(
properties: {
className: [targetClass]
},
children: [{ type: 'text', value: text } as Text]
children: [{ type: 'text', value: text } as Text],
// Keep the highlighted code element so the block can offer a source
// view that matches the app code blocks without re highlighting.
data: { sourceCode: codeElement } satisfies DiagramPreData
};
(parent.children as ElementContent[])[index] = pre;
@@ -0,0 +1,9 @@
// Shared constants for diagram blocks (mermaid and svg) that toggle between a
// rendered view and a source view. The wrapper carries the active mode, css
// drives the visibility, the click handler only flips the attribute.
export const DIAGRAM_VIEW_MODE_ATTR = 'data-view-mode';
export const DIAGRAM_VIEW_RENDERED = 'rendered';
export const DIAGRAM_VIEW_SOURCE = 'source';
export const DIAGRAM_SOURCE_CLASS = 'diagram-source';
export const TOGGLE_SOURCE_BTN_CLASS = 'toggle-source-btn';
+2
View File
@@ -39,3 +39,5 @@ export const MODALITY_LABELS = {
export const COPY_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy-icon lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`;
export const PREVIEW_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye lucide-eye-icon"><path d="M2.062 12.345a1 1 0 0 1 0-.69C3.5 7.73 7.36 5 12 5s8.5 2.73 9.938 6.655a1 1 0 0 1 0 .69C20.5 16.27 16.64 19 12 19s-8.5-2.73-9.938-6.655"/><circle cx="12" cy="12" r="3"/></svg>`;
export const CODE_ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-code lucide-code-icon"><path d="m16 18 6-6-6-6"/><path d="m8 6-6 6 6 6"/></svg>`;
+1
View File
@@ -30,6 +30,7 @@ export * from './literal-html';
export * from './markdown';
export * from './mermaid-blocks';
export * from './svg-blocks';
export * from './diagram-blocks';
export * from './max-bundle-size';
export * from './mcp';
export * from './mcp-form';
+5
View File
@@ -84,3 +84,8 @@ export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
[MCPTransportType.STREAMABLE_HTTP]: Globe,
[MCPTransportType.SSE]: Radio
};
/** Standard SSE endpoint path indicators */
export const MCP_SSE_ENDPOINT = '/sse';
export const MCP_SSE_ENDPOINT_SLASH = '/sse/';
export const MCP_SSE_ENDPOINT_QUERY = '/sse?';
+58 -1
View File
@@ -16,7 +16,8 @@ import {
DEFAULT_MCP_CONFIG,
DEFAULT_CLIENT_VERSION,
DEFAULT_IMAGE_MIME_TYPE,
MCP_PARTIAL_REDACT_HEADERS
MCP_PARTIAL_REDACT_HEADERS,
CORS_PROXY_ENDPOINT
} from '$lib/constants';
import {
MCPConnectionPhase,
@@ -236,6 +237,36 @@ export class MCPService {
return {
fetch: async (input, init) => {
if (useProxy && typeof window !== 'undefined') {
let requestUrlStr = '';
if (typeof input === 'string') {
requestUrlStr = input;
} else if (input instanceof URL) {
requestUrlStr = input.href;
}
if (requestUrlStr) {
const parsedRequestUrl = new URL(requestUrlStr, window.location.origin);
if (
parsedRequestUrl.origin === window.location.origin &&
!parsedRequestUrl.pathname.includes(CORS_PROXY_ENDPOINT)
) {
const originalConfigUrl = new URL(config.url);
const realTargetUrl = new URL(
parsedRequestUrl.pathname + parsedRequestUrl.search,
originalConfigUrl.origin
);
const proxiedUrl = buildProxiedUrl(realTargetUrl.href);
if (typeof input === 'string') {
input = proxiedUrl.href;
} else if (input instanceof URL) {
input = proxiedUrl;
}
}
}
}
const startedAt = performance.now();
const requestHeaders = new Headers(baseInit.headers);
@@ -403,6 +434,32 @@ export class MCPService {
};
}
if (config.transport === MCPTransportType.SSE) {
const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url);
const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch(
serverName,
config,
requestInit,
url,
useProxy,
onLog
);
if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log(`[MCPService] Creating SSE transport for ${url.href}`);
}
return {
transport: new SSEClientTransport(url, {
requestInit,
fetch: diagnosticFetch,
eventSourceInit: { fetch: diagnosticFetch }
}),
type: MCPTransportType.SSE,
stopPhaseLogging
};
}
const url = useProxy ? buildProxiedUrl(config.url) : new URL(config.url);
const { fetch: diagnosticFetch, disable: stopPhaseLogging } = this.createDiagnosticFetch(
serverName,
+19 -4
View File
@@ -19,7 +19,10 @@ import {
DISPLAY_NAME_SEPARATOR_REGEX,
PATH_SEPARATOR,
RESOURCE_TEXT_CONTENT_SEPARATOR,
DEFAULT_RESOURCE_FILENAME
DEFAULT_RESOURCE_FILENAME,
MCP_SSE_ENDPOINT,
MCP_SSE_ENDPOINT_SLASH,
MCP_SSE_ENDPOINT_QUERY
} from '$lib/constants';
import {
Database,
@@ -41,10 +44,22 @@ import type { MimeTypeUnion } from '$lib/types/common';
export function detectMcpTransportFromUrl(url: string): MCPTransportType {
const normalized = url.trim().toLowerCase();
return normalized.startsWith(UrlProtocol.WEBSOCKET) ||
if (
normalized.startsWith(UrlProtocol.WEBSOCKET) ||
normalized.startsWith(UrlProtocol.WEBSOCKET_SECURE)
? MCPTransportType.WEBSOCKET
: MCPTransportType.STREAMABLE_HTTP;
) {
return MCPTransportType.WEBSOCKET;
}
if (
normalized.endsWith(MCP_SSE_ENDPOINT) ||
normalized.endsWith(MCP_SSE_ENDPOINT_SLASH) ||
normalized.includes(MCP_SSE_ENDPOINT_QUERY)
) {
return MCPTransportType.SSE;
}
return MCPTransportType.STREAMABLE_HTTP;
}
/**
+1 -1
View File
@@ -41,7 +41,7 @@ if (LLAMA_BUILD_BORINGSSL)
set(FIPS OFF CACHE BOOL "Enable FIPS (BoringSSL)")
set(BORINGSSL_GIT "https://boringssl.googlesource.com/boringssl" CACHE STRING "BoringSSL git repository")
set(BORINGSSL_VERSION "0.20260526.0" CACHE STRING "BoringSSL version")
set(BORINGSSL_VERSION "0.20260616.0" CACHE STRING "BoringSSL version")
message(STATUS "Fetching BoringSSL version ${BORINGSSL_VERSION}")