From eb85087d1a0874bbccc0bdedc3395433416d6266 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 30 Oct 2026 18:09:38 +0100 Subject: cloud: retry a failed key_cmd on the next models request Keys were read once at startup. At login the smartcard PIN is often not entered within key_cmd's timeout, so that provider was left out for the whole run and its models stayed missing until a restart. send_models now asks again for every provider without a key. It runs off the lock on purpose: route() holds it and must not wait on a PIN prompt. The refusal message says to open the model list to retry, and the self-check covers a key that fails at startup and loads on the next models request. Co-Authored-By: Claude Opus 5.5 --- assistant.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) (limited to 'assistant.py') diff --git a/assistant.py b/assistant.py index 173d767..96b9156 100644 --- a/assistant.py +++ b/assistant.py @@ -749,9 +749,11 @@ def collect_calls(calls, fragments): c["arguments"] += fn.get("arguments") or "" -# Cloud providers, named in [[cloud]]. A key is key_cmd's stdout, read once -# at startup; a provider whose key_cmd fails is left out, so its models are -# not offered and a chat still on one is refused. +# Cloud providers, named in [[cloud]]. A key is key_cmd's stdout, read at +# startup; a provider whose key_cmd fails (at login the smartcard PIN is +# often not entered within the timeout) is left out, so its models are not +# offered and a chat still on one is refused, until a models request asks +# for its key again. PROVIDERS = {} # name -> {"url", "key", "models"} @@ -796,7 +798,7 @@ def route(model): p = PROVIDERS[name] return p["url"], p["key"], rest, name if sep and any(p["name"] == name for p in CFG["cloud"]): - raise Refused(f"cloud {name}: no key (key_cmd failed at startup)") + raise Refused(f"cloud {name}: no key (key_cmd failed; open the model list to retry)") return CFG["llm"], None, model, None @@ -850,7 +852,9 @@ def cloud_models(name): def send_models(): # The canvas's model dropdown: the router's models, then every cloud - # model whose provider's key loaded. + # model whose provider's key loaded. Keys that failed are asked again + # here, off the lock: route() runs under it and must not wait on a PIN. + PROVIDERS.update(load_providers([p for p in CFG["cloud"] if p["name"] not in PROVIDERS])) try: with urllib.request.urlopen(CFG["llm"] + "/models", timeout=10) as r: local = [{"id": m["id"], "status": m["status"]["value"], "cloud": False} @@ -1619,6 +1623,12 @@ def test(): {"name": "broken", "url": base + "/v1", "key_cmd": "false", "models": ["y"]}] PROVIDERS.update(load_providers(CFG["cloud"])) assert set(PROVIDERS) == {"fake"} and PROVIDERS["fake"]["key"] == "sk-test" + # A key that failed at startup loads on the next models request. + CFG["cloud"][1]["key_cmd"] = "echo sk-late" + send_models() + assert PROVIDERS["broken"]["key"] == "sk-late" + del PROVIDERS["broken"] + CFG["cloud"][1]["key_cmd"] = "false" assert route("fake:vendor/x:free") == (base + "/cloud/v1", "sk-test", "vendor/x:free", "fake") assert route("Gemma:q4") == (CFG["llm"], None, "Gemma:q4", None) # no such provider try: -- cgit v1.2.3