qwen-local: the first provider that runs on our own GPU

sd-server (stable-diffusion.cpp, Vulkan) serving Qwen-Image 2.1 on this
machine. It already speaks OpenAI's image API, which is the vocabulary
`ImageRequest` is written in, so there is no translation layer — only the parts
that are true of a local GPU and not of a hosted one.

── keyless is a category, not an exception ──

`credentials.ts` decided a provider was usable by asking whether a key existed,
so a service that takes none read as unconfigured forever. The fix is an
adapter declaring `keyless`, not a dummy secret: a dummy would record a
credential that does not exist and make the secret store lie about what is
configured, and that store is the one place meant to be the truth about them.

A keyless adapter never reaches `keyFor` at all. That function's own comment
says why it uses `getSecret` rather than `getKey` — so a missing FOREIGN
credential is never minted — and a provider with no credential by design has no
business in that path.

whisper, OCR and TTS already run on this machine, so this will not be the last.

── measured, not assumed ──

The integration doc said "tens of seconds, not two". The server's own log says
512x512 completes in ~185s and 1024x1024 samples at ~41.4s/step over 20 steps,
about 14 minutes. And it serialises strictly: a second caller waits for the
first to finish entirely, so a queued 512 can wait 17 minutes before a byte
comes back.

So: 512 is the default and the substitute for `auto`, 1024 is offered with the
cost in its label, and `timeout: false` is a requirement rather than caution —
every default anyone would reach for is far under 17 minutes. The caller's
`signal` still aborts, because a user cancelling is not a client giving up.

── the model id moved, because only it could ──

`GET /v1/models` returns `sd-cpp-local`, hardcoded in sd-server with no
override. The catalogue now agrees with the server and carries "Qwen-Image 2.1
(local)" in the label, so requests name a model the server knows while the UI
still says something meaningful.

── not shipped: edit() ──

Same GPU, same queue. It doubles the ways generation can be blocked while the
generate path is unproven in anger, and `ImageAdapter` makes absence the way to
say so — the route answers 400 instead of the queue finding out late.

Verified: /v1/models answers, provider statuses report qwen-local as
configured/keyless/usable, and the shipped entry survives the merge with the
stored catalogue.json. NOT verified end to end — a generation is occupying the
GPU and I left it alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: de3ed4e6-f88a-429c-94e6-a760336ba83b
This commit is contained in:
2026-09-21 13:00:05 +01:00
co-authored by Claude Opus 5
parent bc11b3ea9d
commit d7f3223b6a
8 changed files with 253 additions and 16 deletions
+30 -6
View File
@@ -23,6 +23,19 @@ import type { ImageAdapter } from './types';
* The catalogue says which providers exist; the secret store says which of them you can use.
*/
/**
* Does this provider authenticate with anything at all?
*
* Asked of the ADAPTER, because it is a fact about the code rather than about configuration: a local
* service of ours takes no key and never will, so there is nothing a user could set or forget. Every
* "is it configured?" test below has to consult this FIRST — otherwise a keyless provider reads as
* unconfigured forever and can never be selected.
*/
export const isKeyless = (provider: string): boolean => adapterByName(provider)?.keyless === true;
/** Usable means: no key needed, or a key we hold. The single test the resolver and the UI both want. */
export const isUsable = (provider: string): boolean => isKeyless(provider) || Boolean(keyFor(provider));
/** `openai` -> `atelier-openai-key`. The store creates purposes on first use, so nothing is provisioned. */
export const secretPurposeFor = (provider: string) => `atelier-${provider}-key`;
@@ -47,7 +60,8 @@ export function keyFor(provider: string): string | undefined {
}
/** Where a key came from, for a UI that should say "from the environment" rather than show an empty field. */
export function keySource(provider: string): 'store' | 'environment' | null {
export function keySource(provider: string): 'store' | 'environment' | 'keyless' | null {
if (isKeyless(provider)) return 'keyless';
try {
if (hasKey(secretPurposeFor(provider))) return 'store';
} catch {
@@ -101,16 +115,21 @@ export async function resolveForRequest(options: {
// A model nobody in the catalogue claims is not refused: the catalogue is advisory, and a model
// released today should work before we have shipped a file that mentions it. The first provider we
// hold a key for gets it, and the vendor gives the real answer.
if (!name) name = catalogue.providers.find((entry) => keyFor(entry.name))?.name;
if (!name) name = catalogue.providers.find((entry) => isUsable(entry.name))?.name;
} else {
name = catalogue.providers.find((entry) => keyFor(entry.name))?.name;
name = catalogue.providers.find((entry) => isUsable(entry.name))?.name;
}
if (!name) return { error: 'No provider has an API key yet. Add one in Atelier.', status: 503 };
if (!name) return { error: 'No provider is usable yet. Add an API key in Atelier.', status: 503 };
const adapter = adapterByName(name);
if (!adapter) return { error: `Provider "${name}" has no adapter in this build.`, status: 400 };
// A keyless adapter must not reach `keyFor` at all. The comment on it is the reason: it uses
// `getSecret` rather than `getKey` precisely so a missing FOREIGN credential is never minted, and a
// provider that has no credential by design has no business in that code path.
if (adapter.keyless) return { providerName: name, adapter, apiKey: '' };
const apiKey = keyFor(name);
if (!apiKey) return { error: `Provider "${name}" has no API key.`, status: 503 };
@@ -133,12 +152,17 @@ export async function providerStatuses() {
const models = provider.models.filter((model) => model.status !== 'deprecated');
const source = keySource(provider.name);
const keyless = isKeyless(provider.name);
return {
name: provider.name,
label: provider.label,
configured: Boolean(keyFor(provider.name)),
/** Keyless is configured by existing. There is nothing to add, so nothing can be missing. */
configured: keyless || Boolean(keyFor(provider.name)),
keyless,
key_source: source,
env_var: envVarFor(provider.name),
/** Absent for a keyless provider: naming a variable nobody can usefully set would be a wrong hint. */
env_var: keyless ? null : envVarFor(provider.name),
/** False for a catalogue entry we have no code for — listed, but not yet usable. */
usable: Boolean(adapter),
supports_edit: typeof adapter?.edit === 'function',