From 4754e1a065543893df8c2def37f652d6a232724a Mon Sep 17 00:00:00 2001 From: infosave2007 Date: Tue, 14 Jul 2026 15:55:43 +0300 Subject: [PATCH] =?UTF-8?q?feat(cmf):=20per-model=20reasoning=20switch=20?= =?UTF-8?q?=E2=80=94=20'thoughts:=20off'=20for=20thinking=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Local-models row gains a thoughts selector (auto/off). Off injects enable_thinking=false (cortiq >= 0.2.1 renders the chat template with an empty block) into requests that don't set it themselves — the model answers directly, faster and shorter. A client-supplied enable_thinking always wins. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/config.rs | 7 +++++++ src/pipeline.rs | 8 ++++++++ web/js/i18n/en.js | 3 +++ web/js/i18n/ru.js | 3 +++ web/js/views/settings.js | 8 ++++++-- 7 files changed, 29 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 914da27..b21dd25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -285,7 +285,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cortiq-gateway" -version = "0.2.21" +version = "0.2.22" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 3bffb5b..331637a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cortiq-gateway" -version = "0.2.21" +version = "0.2.22" edition = "2021" rust-version = "1.75" license = "Apache-2.0" diff --git a/src/config.rs b/src/config.rs index b596ea1..f85952b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -97,6 +97,12 @@ pub struct CmfServer { pub top_p: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub max_tokens: Option, + /// Reasoning switch for thinking models (Qwen3/3.5 …): "off" injects + /// enable_thinking=false into requests that don't set it themselves — + /// the model answers directly, skipping the phase. Needs + /// cortiq ≥ 0.2.1. Empty = model/template default. + #[serde(default)] + pub thinking: String, } /// CMF-format model factory: import a HuggingFace model, convert it to a @@ -190,6 +196,7 @@ impl CmfCfg { temperature: None, top_p: None, max_tokens: None, + thinking: String::new(), }]; } Vec::new() diff --git a/src/pipeline.rs b/src/pipeline.rs index 379a85a..ed271f6 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -319,6 +319,14 @@ impl Pipeline { if req.params.max_tokens.is_none() { req.params.max_tokens = s.max_tokens; } + // reasoning switch: "off" → enable_thinking=false (cortiq ≥ 0.2.1 + // renders the chat template with an empty block); a client + // that sets enable_thinking itself wins. + if s.thinking == "off" && !req.params.passthrough.contains_key("enable_thinking") { + req.params + .passthrough + .insert("enable_thinking".into(), serde_json::Value::Bool(false)); + } } } diff --git a/web/js/i18n/en.js b/web/js/i18n/en.js index d9814f6..6634ef3 100644 --- a/web/js/i18n/en.js +++ b/web/js/i18n/en.js @@ -265,6 +265,9 @@ export default { "settings.cmf.models": "Local models", "settings.cmf.modelsHelp": "Each row is one model the gateway runs (its own cortiq serve on its own port) and registers in the pool. Add as many as you have .cmf files and free ports.", "settings.cmf.rowHint": "id · .cmf file · port · threads · GPU · O(1) · T°/top-p/max tok. Applied on save: a removed row stops its server (memory freed), a new row starts one.", + "settings.cmf.thinkAuto": "thoughts: auto", + "settings.cmf.thinkOff": "thoughts: off", + "settings.cmf.thinkHelp": "Reasoning models (Qwen3/3.5) write out a thinking phase before the answer. Off = answer directly without it (faster, shorter; needs cortiq >= 0.2.1). A request that sets enable_thinking itself always wins.", "settings.cmf.tempHelp": "Default temperature for this model (0 = deterministic, 0.7 = balanced, >1 = creative). Used when the request doesn't set one; a request value always wins. Empty = provider default.", "settings.cmf.topPHelp": "Default nucleus sampling (top-p) for this model: only tokens within this cumulative probability are sampled (0.9 typical). Empty = provider default.", "settings.cmf.maxTokHelp": "Default max output tokens for this model when the request doesn't set one. Protects against runaway long generations. Empty = provider default.", diff --git a/web/js/i18n/ru.js b/web/js/i18n/ru.js index 35c114b..aa98cfe 100644 --- a/web/js/i18n/ru.js +++ b/web/js/i18n/ru.js @@ -264,6 +264,9 @@ export default { "settings.cmf.models": "Локальные модели", "settings.cmf.modelsHelp": "Каждая строка — отдельная модель: шлюз поднимает свой cortiq serve на своём порту и регистрирует её в пуле. Добавляйте столько, сколько есть .cmf-файлов и свободных портов.", "settings.cmf.rowHint": "id · файл .cmf · порт · потоки · GPU · O(1) · T°/top-p/max tok. Применяется сразу при сохранении: удалённая строка останавливает сервер (память освобождается), новая — запускает.", + "settings.cmf.thinkAuto": "мысли: авто", + "settings.cmf.thinkOff": "мысли: выкл", + "settings.cmf.thinkHelp": "Рассуждающие модели (Qwen3/3.5) сначала пишут фазу размышлений. Выкл = отвечать сразу, без неё (быстрее и короче; нужен cortiq ≥ 0.2.1). Если запрос сам задал enable_thinking — он важнее.", "settings.cmf.tempHelp": "Температура по умолчанию для этой модели (0 = детерминированно, 0.7 = баланс, >1 = креативно). Подставляется, если запрос её не задал; значение из запроса всегда важнее. Пусто = дефолт провайдера.", "settings.cmf.topPHelp": "Nucleus-сэмплинг (top-p) по умолчанию: выбираются только токены в пределах этой суммарной вероятности (обычно 0.9). Пусто = дефолт провайдера.", "settings.cmf.maxTokHelp": "Максимум выходных токенов по умолчанию, если запрос не задал свой. Защищает от бесконечных генераций. Пусто = дефолт провайдера.", diff --git a/web/js/views/settings.js b/web/js/views/settings.js index d445999..519943d 100644 --- a/web/js/views/settings.js +++ b/web/js/views/settings.js @@ -240,6 +240,10 @@ export async function renderSettings() { ...o1Opts.map((v) => h("option", { value: v, selected: (sv.o1 || "") === v ? true : null }, v === "" ? t("settings.cmf.o1Auto") : v))); if (sv.o1 && !o1Opts.includes(sv.o1)) o1In.appendChild(h("option", { value: sv.o1, selected: true }, sv.o1)); + // reasoning switch for thinking models (off → enable_thinking=false) + const thinkIn = h("select", { style: "width:118px", title: t("settings.cmf.thinkHelp") }, + h("option", { value: "", selected: !sv.thinking ? true : null }, t("settings.cmf.thinkAuto")), + h("option", { value: "off", selected: sv.thinking === "off" ? true : null }, t("settings.cmf.thinkOff"))); // per-model generation defaults (applied when the request doesn't set them) const tempIn = h("input", { type: "number", step: "0.1", min: 0, max: 2, value: sv.temperature ?? "", placeholder: "T° 0.7", style: "width:74px", title: t("settings.cmf.tempHelp") }); const topPIn = h("input", { type: "number", step: "0.05", min: 0, max: 1, value: sv.top_p ?? "", placeholder: "top-p", style: "width:70px", title: t("settings.cmf.topPHelp") }); @@ -268,9 +272,9 @@ export async function renderSettings() { checkBtn.disabled = false; }); const row = h("div", { class: "flex wrap", style: "gap:6px;align-items:center;margin:6px 0" }, - idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), o1In, tempIn, topPIn, maxTokIn, removeBtn, st); + idIn, modelSel, portIn, checkBtn, threadsIn, h("label", { class: "flex", style: "gap:4px" }, gpuIn, "GPU"), o1In, thinkIn, tempIn, topPIn, maxTokIn, removeBtn, st); const entry = { node: row, read: () => { - const v = { id: idIn.value.trim() || autoId(modelSel.value), model: modelSel.value.trim(), port: parseInt(portIn.value) || 8090, threads: parseInt(threadsIn.value) || 0, gpu: gpuIn.checked, o1: o1In.value }; + const v = { id: idIn.value.trim() || autoId(modelSel.value), model: modelSel.value.trim(), port: parseInt(portIn.value) || 8090, threads: parseInt(threadsIn.value) || 0, gpu: gpuIn.checked, o1: o1In.value, thinking: thinkIn.value }; if (tempIn.value !== "") v.temperature = parseFloat(tempIn.value); if (topPIn.value !== "") v.top_p = parseFloat(topPIn.value); if (maxTokIn.value !== "") v.max_tokens = parseInt(maxTokIn.value);