From 5aab279b98a76a44afc48f6173c2d22c5c4120d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 21 Sep 2026 13:21:24 +0100 Subject: [PATCH] the runner demanded a key the provider says it does not need MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported against qwen-local: /providers answers `keyless: true, configured: true, usable: true`, the POST is accepted, and the row then fails with `No API key for provider "qwen-local"` and `duration_ms: 0` — refused at dispatch, before inference, on a credential that cannot exist. `queue.ts` read the key unconditionally at run time. I taught `resolveForRequest` about keyless in the previous commit and never checked whether anything else asked the same question. Something did. The defect is not that one check was missed, it is that there were TWO of them. The catalogue path and the execution path each decided independently whether a provider was usable, from the same secret store, and disagreed the moment "usable" stopped meaning "has a key". That is why the answer now lives on the adapter: `adapter.keyless` is a fact about the code, so every site asks the same question of the same source instead of re-deriving it. The run-time read itself stays, and its reasoning is untouched: a key rotated between queueing and running should be the new one. It just no longer applies to a provider that has nothing to rotate. `undefined` is now the failure, not falsiness — a keyless adapter passes an empty string deliberately. Verified end to end, which the previous commit explicitly had not been: a 512x512 through the real queue returned `status: succeeded`, `duration_ms: 186199`, `provider: qwen-local`, and a valid 512x512 RGBA PNG on disk. The server's own log agrees at 186.13s. Found by the owner from the API's behaviour alone, without the source. Co-Authored-By: Claude Opus 5 Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b --- sidecar/generations/queue.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sidecar/generations/queue.ts b/sidecar/generations/queue.ts index 90021db..6beaebf 100644 --- a/sidecar/generations/queue.ts +++ b/sidecar/generations/queue.ts @@ -62,8 +62,18 @@ async function run(row: Generation): Promise { // The adapter and model come from the ROW's own columns, and the key is read at RUN time: a key // rotated between queueing and running should be the new one, and a row queued before a key existed // at all runs the moment there is one. - const apiKey = keyFor(row.provider); - if (!apiKey) throw new Error(`No API key for provider "${row.provider}"`); + // + // Unless the adapter is KEYLESS, in which case there is nothing to read and never will be. This + // check used to be unconditional, and it is why a local model advertising `keyless: true, + // configured: true, usable: true` still failed at dispatch with `duration_ms: 0` — refused on a + // credential that cannot exist, before inference ever started. + // + // The defect was not the check, it was that there were TWO of them. `resolveForRequest` learned + // about keyless and this one did not, so the catalogue said yes and the runner said no. Anything + // that decides whether a provider is usable has to ask the same question, which is why the answer + // now lives on the adapter rather than being re-derived from the secret store at each site. + const apiKey = adapter.keyless ? '' : keyFor(row.provider); + if (apiKey === undefined) throw new Error(`No API key for provider "${row.provider}"`); const result = row.operation === 'edit'