aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/modules/assistant/AssistantModule.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-17 10:50:09 +0200
committerDanilo M. <danix@danix.xyz>2026-10-17 10:50:09 +0200
commit885d65a2bbcde0e8e5094ae4589647875ee5838d (patch)
treeca0397836bdc61085641410eebdb166b97d0999e /desktop/modules/assistant/AssistantModule.qml
parent166d638bce081631705687501020725045920e50 (diff)
downloadquickshell-885d65a2bbcde0e8e5094ae4589647875ee5838d.tar.gz
quickshell-885d65a2bbcde0e8e5094ae4589647875ee5838d.zip
assistant: pick the preferred model, loaded on demand
The daemon answers with whichever model llama-server has loaded and never asks for one, so it never evicts a model another client is using. The Assistant settings page now lists the router's models with their state. Load saves one as the preferred model, in Prefs, and asks the router to load it now, polling /v1/models until it reports loaded; Prefer marks the loaded model as preferred without swapping. The swap happens only on that click, so the daemon's rule holds. AGENTS.md records the router's load endpoint and its two refusals, both measured: it lives on the server root, not under /v1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/modules/assistant/AssistantModule.qml')
-rw-r--r--desktop/modules/assistant/AssistantModule.qml57
1 files changed, 57 insertions, 0 deletions
diff --git a/desktop/modules/assistant/AssistantModule.qml b/desktop/modules/assistant/AssistantModule.qml
index e89fe96..a85b2b3 100644
--- a/desktop/modules/assistant/AssistantModule.qml
+++ b/desktop/modules/assistant/AssistantModule.qml
@@ -52,6 +52,17 @@ Module {
property var tools: []
// A write since the last restart, which the daemon has not read yet.
property bool pending: false
+
+ // The router's models, [{id, status}], status as it reports it:
+ // "loaded", "loading", "unloaded" and so on. Empty when it is down.
+ property var models: []
+ // The id being loaded, "" when none is.
+ property string loading: ""
+ property string modelError: ""
+ // The router answers /models and /models/load on the server's root, and
+ // the OpenAI API under /v1, which is what the daemon's llm points at.
+ readonly property string llm: am.values.llm ?? am.defaults.llm ?? "http://localhost:8181/v1"
+ readonly property string routerRoot: am.llm.replace(/\/v1\/?$/, "")
property bool restarting: false
property string status: ""
property string error: ""
@@ -59,10 +70,12 @@ Module {
searchItems: am.keys.map(k => ({ text: am.labels[k], key: "key:" + k }))
.concat(am.envList.map(e => ({ text: e.key + " = " + e.value, key: "env:" + e.key })))
.concat(am.tools.map(t => ({ text: "tool: " + t.name, key: "tool:" + t.name })))
+ .concat(am.models.map(m => ({ text: "model: " + m.id, key: "model:" + m.id })))
function refresh() {
fv.reload();
am.listTools();
+ am.listModels();
if (Object.keys(am.defaults).length === 0)
am.run([am.python, am.daemonDir + "/assistant.py", "--defaults"], null, (code, out) => {
try { if (code === 0) am.defaults = JSON.parse(out); } catch (e) {}
@@ -156,6 +169,50 @@ Module {
});
}
+ function listModels(done) {
+ am.run(["curl", "-sf", "-m", "3", am.llm + "/models"], null, (code, out) => {
+ try {
+ am.models = code === 0 ? JSON.parse(out).data.map(m => ({ id: m.id, status: m.status?.value ?? "" })) : [];
+ } catch (e) { am.models = []; }
+ if (done) done();
+ });
+ }
+
+ // The router keeps one model resident, so this swaps out whatever is
+ // loaded, for every client: it runs only on a click. The id becomes the
+ // preferred one, and the list is polled until the router reports it
+ // loaded, since the request may return before the load finishes.
+ function loadModel(id) {
+ Prefs.setAssistantModel(id);
+ am.modelError = "";
+ am.loading = id;
+ am.run(["curl", "-s", "-m", "600", "-X", "POST", "-H", "Content-Type: application/json",
+ "-d", JSON.stringify({ model: id }), am.routerRoot + "/models/load"], null, (code, out) => {
+ let err = code === 0 ? "" : "the router did not answer";
+ try {
+ const e = JSON.parse(out).error;
+ // Already running is the outcome asked for, not a failure.
+ if (e && e.message !== "model is already running") err = e.message;
+ } catch (x) {}
+ if (err) { am.modelError = `${id}: ${err}`; am.loading = ""; am.listModels(); }
+ else { poll.left = 300; poll.start(); }
+ });
+ }
+ property Timer poll: Timer {
+ property int left: 0
+ interval: 1000
+ repeat: true
+ onTriggered: am.listModels(() => {
+ const m = am.models.find(x => x.id === am.loading);
+ if (m && m.status === "loaded") { am.loading = ""; am.poll.stop(); }
+ else if (--am.poll.left <= 0) {
+ am.modelError = `${am.loading} did not finish loading`;
+ am.loading = "";
+ am.poll.stop();
+ }
+ })
+ }
+
function restart() {
am.restarting = true;
am.run([am.daemonDir + "/assistant.sh", "restart"], null, (code, out) => {