pastilhasandClaude Opus 5 d7f3223b6a 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
2026-09-21 13:00:05 +01:00
2026-08-28 22:36:07 +01:00

Atelier

Image generation for an Officer server: a provider-shaped API over OpenAI's image endpoints, storing what it makes so every image has a durable reference — a gallery you can browse, collections you can organise, and a job an agent can poll rather than block on.

Atelier was a standalone service before this plugin existed (~/officer-dev/dockers/officer-atelier, still running as officer-atelier, still what the mobile app talks to). This plugin is a fresh extraction into Officer, not a replacement for it yet — see PLUGIN.md before assuming the two are interchangeable.

What you get

  • Generate — a prompt, a model, size/quality/background, and a job you watch settle. n images become n independently-tracked rows, so one failure out of four does not discard the three that worked.
  • Gallery — everything generated, newest first, with a lightbox: pan, zoom about the cursor, checkerboard/dark/light backdrops for telling a transparent background from an opaque one that only looks it.
  • Collections — named sets of images. A generation request can name one by name and it is created on first use; the gallery and the lightbox can file an image into one after the fact.
  • A provider-shaped backend. "OpenAI" is code, written once; a provider is a row — which model, whose key, on or off — and several can share one adapter. Today OpenAI is the only adapter; the point of the shape is that a second one is a new file, not a redesign.
  • An OpenAI-compatible job API, for agents. POST /api/atelier/images/generations accepts OpenAI's request body verbatim and answers 202 with a job id rather than blocking for the length of the generation — the caller polls GET /api/atelier/images/generations/:id, optionally with ?wait= to long-poll instead of retrying in a loop.

Requirements

  • An Officer server, version 1.0.0 or later.
  • A separate Postgres database, and its own environment variable: OFFICERDEV_POSTGRES_URL. Atelier's tables (generations, collections, collection_items, providers) do not live in officer_db — see PLUGIN.md for why, and set this before installing. There is no default.
  • An OpenAI API key, set with bun run secret:set atelier-openai-key, or configured per-provider in Settings → Integrations → Atelier, or (for an install that predates the secret store) as OPENAI_API_KEY in .env.

Install

From the Officer marketplace: find Atelier, install. That mounts /api/atelier and /atelier, starts the officer-atelier sidecar under PM2, and connects it to whatever OFFICERDEV_POSTGRES_URL points at — which has to already have the schema (the standalone repo's own bun db:push creates it; this plugin does not run migrations against a database it does not own the schema of).

Uninstalling stops the sidecar and unmounts the route. It never touches your database — Atelier's tables and rows are not this plugin's to drop, since it does not own the schema push for them either.

Permissions

One permission, atelier, grantable per role at read or write.

  • Read lets an account use Generate, browse the Gallery and Collections, and see which providers are configured (never a key, in any form — not even masked).
  • Write additionally lets an account add, edit or delete providers, including their API keys. A provider's key authorises real spend on someone's OpenAI account, so treat write here the way you would treat write on any billing-adjacent surface.

Providers is not a panel — it is a platform Settings screen (Settings → Integrations → Atelier), because it is owner-level configuration rather than a working view. See PLUGIN.md for why.

Limitations, honestly

  • Two Ateliers can exist at once. This plugin and the standalone service are independent — installing this one does not migrate the standalone one's data, stop it, or repoint the mobile app. See PLUGIN.md.
  • No per-user data. Every account with read sees the same gallery and the same collections — there is no ownership model yet, the same simple shape offscale shipped with first.
  • The database is separate from officer_db, which means Atelier's tables are not covered by bun db:push, are not part of the platform's usual backup story unless you back up the second database too, and need their own connection string. Migrating them in is real, tracked follow-up work — see PLUGIN.md.
S
Description
Image generation as a service, as an Officer plugin
Readme
1.8 MiB
Languages
TypeScript 100%