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:
co-authored by
Claude Opus 5
parent
f2b92e96ca
commit
d67f5e396e
+139
-16
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user