Add OpenRouter chat provider; fix Windows load errors (0.17.0).

- OpenRouter as an alternative chat provider: server-side API key
  (settings.json or OPENROUTER_API_KEY), model list with vision marks and
  filter, SSE streaming, images as data URLs. Memory embeddings stay on Ollama;
  park/warm LLM are no-ops for the remote provider.
- AssistentSaveKnowledgeAttach: JArray param is not supported by SwarmUI API
  reflection and aborted registration of every later API call; use string[].
- SQLite bootstrap: preload native e_sqlite3 for the current OS/arch
  (win/osx/linux) and resolve DllImport to it.
- settings.json: shared-read + atomic write with retry to avoid Windows
  sharing violations.
- csproj: fix MSB4012 in the native SQLite copy target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nid
2026-09-17 06:18:47 +03:00
co-authored by Claude Opus 5
parent f2b92e96ca
commit d67f5e396e
13 changed files with 740 additions and 56 deletions
+139 -16
View File
@@ -1733,6 +1733,12 @@
waitImageTimer: null,
lastImageDataUrl: null,
preferredModel: null,
/** Chat provider mirrored from server settings: 'ollama' | 'openrouter'. */
provider: "ollama",
chatModels: [],
chatModelsPreferred: "",
visionModels: [],
openrouterModelSaved: "",
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
inventoryFetchedAt: 0,
streamEl: null,
@@ -3479,7 +3485,7 @@ ${patch.prompt}`;
}
const model = $("sa_model")?.value;
if (!model) {
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0447\u0430\u0442\u0430 \u0432 \u2699");
return false;
}
const { prompt, foldCount } = buildCompressUserPrompt();
@@ -3642,7 +3648,7 @@ ${patch.prompt}`;
}
const model = $("sa_model")?.value;
if (!model) {
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0447\u0430\u0442\u0430 \u0432 \u2699");
return;
}
state.busy = true;
@@ -7459,6 +7465,7 @@ ${patch.prompt}`;
localStorage.setItem(LS_AUTO_CRITIQUE, $("sa_auto_critique")?.checked ? "1" : "0");
localStorage.setItem(LS_AUTO_DOWNLOAD, $("sa_auto_download")?.checked ? "1" : "0");
localStorage.setItem(LS_PARK_LLM, $("sa_park_llm")?.checked ? "1" : "0");
persistOpenRouterModel();
persistServerSettings();
saveUiStateToDisk();
}
@@ -8023,22 +8030,44 @@ ${data.ui.help_extra}`.trim();
if (!list.length) {
return "";
}
const preferred = apiPreferred && list.includes(apiPreferred) ? apiPreferred : pickSeniorChatModel(list);
const ls = state.preferredModel || localStorage.getItem(LS_MODEL) || "";
if (isOpenRouter()) {
if (apiPreferred && list.includes(apiPreferred)) {
return apiPreferred;
}
return ls && list.includes(ls) ? ls : list[0];
}
const preferred = apiPreferred && list.includes(apiPreferred) ? apiPreferred : pickSeniorChatModel(list);
if (ls && list.includes(ls)) {
return ls;
}
return preferred || list[0];
}
function isOpenRouter() {
return state.provider === "openrouter";
}
function providerLabel() {
return isOpenRouter() ? "OpenRouter" : "Ollama";
}
function renderChatModelOptions() {
const current = $("sa_model")?.value || "";
const filter = isOpenRouter() ? String($("sa_openrouter_filter")?.value || "").trim().toLowerCase() : "";
let names = state.chatModels;
if (filter) {
names = names.filter((n) => n.toLowerCase().includes(filter) || n === current);
}
setModelOptions(names, { preferred: current || state.chatModelsPreferred });
}
function setModelOptions(models, { error, preferred } = {}) {
const sel = $("sa_model");
const sel2 = $("sa_settings_chat_model");
const vision = new Set(state.visionModels || []);
const apply = (target) => {
if (!target) {
return;
}
let names = (models || []).map((n) => String(n || "").trim()).filter(Boolean);
names = [...names].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b));
names = isOpenRouter() ? [...names].sort((a, b) => a.localeCompare(b)) : [...names].sort((a, b) => chatModelSeniority(b) - chatModelSeniority(a) || a.localeCompare(b));
target.innerHTML = "";
if (error) {
const opt = document.createElement("option");
@@ -8052,14 +8081,14 @@ ${data.ui.help_extra}`.trim();
if (!names.length) {
const opt = document.createElement("option");
opt.value = "";
opt.textContent = "No Ollama models \u2014 pull / Refresh";
opt.textContent = isOpenRouter() ? "\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 OpenRouter \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043A\u043B\u044E\u0447 / \u0444\u0438\u043B\u044C\u0442\u0440" : "No Ollama models \u2014 pull / Refresh";
target.appendChild(opt);
return;
}
for (const name of names) {
const opt = document.createElement("option");
opt.value = name;
opt.textContent = name;
opt.textContent = vision.has(name) ? `${name} \u{1F441}` : name;
target.appendChild(opt);
}
const pick = resolveChatModel(names, preferred);
@@ -8113,10 +8142,17 @@ ${data.ui.help_extra}`.trim();
"AssistentListModels",
{ baseUrl },
(data) => {
if (data.provider) {
setProviderUi(data.provider);
}
const models = data.models || [];
const memoryModels = data.memory_models || [];
const preferred = (data.preferred || "").trim();
state.chatModels = models;
state.visionModels = data.vision_models || [];
state.chatModelsPreferred = preferred;
setModelOptions(models, { preferred });
renderChatModelOptions();
setEmbedModelOptions(memoryModels);
const pick = resolveChatModel(models, preferred);
if (pick && $("sa_model")) {
@@ -8127,24 +8163,91 @@ ${data.ui.help_extra}`.trim();
state.preferredModel = pick;
localStorage.setItem(LS_MODEL, pick);
}
setStatus(models.length ? `${models.length} chat \xB7 ${memoryModels.length} memory` : "No Ollama models (gpu-rent: ollama pull)");
const label = providerLabel();
setStatus(models.length ? `${label}: ${models.length} chat \xB7 ${memoryModels.length} memory` : isOpenRouter() ? "OpenRouter: \u043D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439" : "No Ollama models (gpu-rent: ollama pull)");
if (models.length) {
setOllamaHealth("ok", `Ollama \xB7 ${models.length}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${models.length}, \u043F\u0430\u043C\u044F\u0442\u044C: ${memoryModels.length}`);
setOllamaHealth("ok", `${label} \xB7 ${models.length}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${models.length}, \u043F\u0430\u043C\u044F\u0442\u044C: ${memoryModels.length}${data.memory_error ? ` (Ollama: ${data.memory_error})` : ""}`);
} else {
setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
setOllamaHealth("warn", `${label} \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439`, isOpenRouter() ? "\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043A\u043B\u044E\u0447" : "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
}
saveSettings();
},
0,
(err) => {
const msg = String(err || "Ollama unreachable");
const msg = String(err || `${providerLabel()} unreachable`);
state.chatModels = [];
setStatus(msg);
setModelOptions([], { error: msg });
setOllamaHealth("down", "Ollama \u2715", msg);
setOllamaHealth("down", `${providerLabel()} \u2715`, msg);
appendMessage("error", msg);
}
);
}
function setProviderUi(provider) {
state.provider = provider === "openrouter" ? "openrouter" : "ollama";
if ($("sa_provider")) {
$("sa_provider").value = state.provider;
}
const box = $("sa_openrouter_box");
if (box) {
box.hidden = !isOpenRouter();
}
}
function applyProviderInfo(data) {
if (!data || data.error) {
return;
}
setProviderUi(data.provider);
state.openrouterModelSaved = data.openrouter_model || "";
const status = $("sa_openrouter_key_status");
if (status) {
status.textContent = data.openrouter_key_set ? `\u041A\u043B\u044E\u0447 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 ${data.openrouter_key_hint || ""}`.trim() : "\u041A\u043B\u044E\u0447 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D";
}
}
function providerRequest(name, body) {
return new Promise((resolve) => {
if (typeof genericRequest !== "function") {
resolve(null);
return;
}
genericRequest(name, body || {}, (data) => resolve(data), 0, (err) => {
setStatus(String(err || `${name} failed`));
resolve(null);
});
});
}
async function loadProviderState() {
applyProviderInfo(await providerRequest("AssistentGetProvider", {}));
}
async function setChatProvider(provider) {
const data = await providerRequest("AssistentSetProvider", { provider });
applyProviderInfo(data);
state.chatModels = [];
refreshModels();
probeOllamaHealth();
}
async function saveOpenRouterKey(key) {
const data = await providerRequest("AssistentSetProvider", { api_key: key });
if (!data) {
return;
}
applyProviderInfo(data);
if ($("sa_openrouter_key")) {
$("sa_openrouter_key").value = "";
}
setStatus(key ? "\u041A\u043B\u044E\u0447 OpenRouter \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D" : "\u041A\u043B\u044E\u0447 OpenRouter \u0443\u0434\u0430\u043B\u0451\u043D");
if (isOpenRouter()) {
refreshModels();
}
}
function persistOpenRouterModel() {
const model = $("sa_model")?.value || "";
if (!isOpenRouter() || !model || model === state.openrouterModelSaved) {
return;
}
state.openrouterModelSaved = model;
providerRequest("AssistentSetProvider", { model });
}
function refreshInventory(done, opts = {}) {
if (typeof genericRequest !== "function") {
if (done) {
@@ -8932,19 +9035,19 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
{ baseUrl },
(data) => {
if (data?.error) {
setOllamaHealth("down", "Ollama \u2715", String(data.error));
setOllamaHealth("down", `${providerLabel()} \u2715`, String(data.error));
return;
}
const chat = (data.models || []).length;
const mem = (data.memory_models || []).length;
if (!chat) {
setOllamaHealth("warn", "Ollama \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439", "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
setOllamaHealth("warn", `${providerLabel()} \xB7 0 \u043C\u043E\u0434\u0435\u043B\u0435\u0439`, isOpenRouter() ? "\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043A\u043B\u044E\u0447" : "\u041D\u0435\u0442 \u0447\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u2014 \u0441\u0434\u0435\u043B\u0430\u0439 ollama pull");
return;
}
setOllamaHealth("ok", `Ollama \xB7 ${chat}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${chat}, \u043F\u0430\u043C\u044F\u0442\u044C: ${mem} \xB7 ${baseUrl}`);
setOllamaHealth("ok", `${providerLabel()} \xB7 ${chat}`, `\u0427\u0430\u0442-\u043C\u043E\u0434\u0435\u043B\u0435\u0439: ${chat}, \u043F\u0430\u043C\u044F\u0442\u044C: ${mem} \xB7 ${data.base_url || baseUrl}`);
},
0,
(err) => setOllamaHealth("down", "Ollama \u2715", `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")} \xB7 ${baseUrl}`)
(err) => setOllamaHealth("down", `${providerLabel()} \u2715`, `\u041D\u0435\u0442 \u0441\u0432\u044F\u0437\u0438: ${String(err || "")}`)
);
}
function setView(view) {
@@ -9963,7 +10066,7 @@ ${HELP_TEXT}`);
const persona = $("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral";
const model = $("sa_model")?.value;
if (!model) {
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C Ollama \u0432 \u2699");
setStatus("\u0412\u044B\u0431\u0435\u0440\u0438 \u043C\u043E\u0434\u0435\u043B\u044C \u0447\u0430\u0442\u0430 \u0432 \u2699");
refreshModels();
return;
}
@@ -10509,6 +10612,7 @@ ${HELP_TEXT}`);
} catch (e) {
console.warn("Assistent: chat sessions failed", e);
}
await loadProviderState();
loadConfig(localStorage.getItem(LS_PERSONA) || "neutral", () => {
refreshModels();
refreshInventory(() => {
@@ -10736,6 +10840,25 @@ ${HELP_TEXT}`);
document.getElementById(TAB_BUTTON_ID)?.addEventListener("click", () => {
setTimeout(() => $("sa_input")?.focus(), 80);
});
$("sa_provider")?.addEventListener("change", () => {
setChatProvider($("sa_provider")?.value || "ollama");
});
$("sa_btn_openrouter_key_save")?.addEventListener("click", () => {
const key = String($("sa_openrouter_key")?.value || "").trim();
if (!key) {
setStatus("\u0412\u0441\u0442\u0430\u0432\u044C \u043A\u043B\u044E\u0447 OpenRouter");
return;
}
saveOpenRouterKey(key);
});
$("sa_openrouter_key")?.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
$("sa_btn_openrouter_key_save")?.click();
}
});
$("sa_btn_openrouter_key_clear")?.addEventListener("click", () => saveOpenRouterKey(""));
$("sa_openrouter_filter")?.addEventListener("input", () => renderChatModelOptions());
$("sa_btn_refresh_models")?.addEventListener("click", () => {
saveSettings();
refreshModels();
+9
View File
@@ -1110,6 +1110,15 @@
opacity: 0.8;
}
.sa-openrouter-box {
display: grid;
gap: 0.45rem;
}
.sa-openrouter-box[hidden] {
display: none;
}
.sa-danger-btn {
color: #e06c75 !important;
}