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'