## Summary
Every image backend now generates through the AI SDK's `generateImage`
(`sdkImage.ts`), with one request path for debug logging, abort and 429
retry. Before, Google went through the SDK while the rest used
hand-written requests, and ComfyUI ran on its own SDK plus a Tauri
WebSocket plugin.
- OpenAI, OpenAI Compatible, OpenRouter and Google use their SDK
providers. Pollinations and Z.AI reuse the OpenAI Compatible helper.
- NanoGPT, A1111 and ComfyUI have no usable SDK provider, so each gets a
small custom `ImageModelV4` (`nanogptImage.ts`, `a1111Image.ts`,
`comfyImage.ts`).
- ComfyUI now submits a workflow and polls `/api/jobs/{id}` over plain
HTTP, and sends a cancel for the job if the user aborts or the app gives
up. It needs ComfyUI 0.26.0 or later (`MIN_COMFYUI_VERSION`); older
servers are refused with a message, and a server that can't report its
version is let through.
- `@saintno/comfyui-sdk`, `@tauri-apps/plugin-websocket`,
`tauri-plugin-websocket` and `buffer` are removed.
## User-facing changes
- **Chutes image provider removed.** Chutes text generation is
unchanged.
- **New OpenAI Compatible image provider.** Base URL is required; API
key is optional.
- **Zhipu is now Z.AI** and defaults to `api.z.ai`.
- **Model discovery fails loudly.** A failed model list throws and the
settings form shows the error and clears the old list. Before, it
silently fell back to a built-in list.
- **Missing credentials are reported by kind** (profile, API key, base
URL) in portrait generation and in the image tools, instead of always
saying "API key required".
- **ComfyUI needs 0.26.0+.**
## Migration 042 (`042_image_profiles.sql`)
Runs in SQL and replaces `migrateImageProfileDefaults` in the settings
store. It does four things:
- Pins `zhipu` API profiles that have no base URL to `open.bigmodel.cn`,
the host they were already using.
- Builds image profiles from legacy image settings that name an API
profile and a model, only when no image profile exists yet.
- Moves `openai` profiles that point at a proxy to OpenAI Compatible.
- Moves every `chutes` image profile to OpenAI Compatible.
`src-tauri/src/image_profiles.rs` holds the Rust tests for it.
## Changes
**Transport**
- `createSdkFetch` and `createPollSdkFetch` (`sdkFetch.ts`): the Tauri
transport for SDK image models, with binary and multipart bodies, the
request guard, debug logging, and redirects returned to the SDK for it
to check. The poll variant keeps a ComfyUI generation's requests in one
debug entry.
- A1111 and ComfyUI send all requests through `createSdkFetch`.
- 429 retry for image models (`retryOn429ImageMiddleware`), with
`Retry-After` capped at `IMAGE_TIMEOUT_DEFAULT`.
- Header redaction (including the Gemini key) and base64 scrubbing in
debug logs.
**Providers and registry**
- `registry.ts` declares labels, API key mode and base URL mode once per
provider (`IMAGE_PROVIDER_LABELS`, `PROVIDER_API_KEY`,
`PROVIDER_BASE_URL`). The images settings tab and `missingCredential`
read them.
- NanoGPT sends the model the closest aspect ratio it lists.
- Model lists are validated with zod (`parseModelList.ts`).
**Docs**
- The Images section of `docs/architecture/overview.md` is rewritten for
the SDK path.
- `docs/development/testing.md` and `ios-build-target.md` are updated.
## Changes outside the main purpose
These touch code that was already on `master`:
- `fetch.ts` (`createTimeoutFetch`): the debug-log helpers move to
`sdk/providers/debugLog.ts` and are typed instead of `any`. Behaviour is
unchanged, and `debugLog` and the body-merge/usage-patch logic gain
tests.
- `sleep` moves from `retryMiddleware.ts` to `utils/async.ts`.
- The debug window replaces an entry in place when the store re-emits
it, instead of appending a duplicate.
- `supportsImageGeneration` uses `Object.hasOwn`, so an inherited key
like `constructor` is not a supported provider.
- `formatImageSize` returns a template-literal type, and the unused
`base64ImageMime` is removed.
## Test plan
- [x] `aubr check`, `aube test`, `aubr lint`
- [x] `cargo test` in `src-tauri` (migration 042 tests)
- [x] Generate an image with each provider you have access to, including
a reference image where the provider supports it.
- [ ] ComfyUI: generate, cancel mid-job and confirm the job is cancelled
on the server, and check that a pre-0.26.0 server is refused.
- [ ] Upgrade a database that has `openai` proxy, `chutes` and `zhipu`
image profiles and confirm each lands as described above.
- [ ] Android release build (not yet checked).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Kz2WsAnqEzPZv9Sj8u9VG4
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **New Features**
* Added OpenAI-compatible image generation and expanded image-generation
support for ComfyUI, A1111, NanoGPT, and OpenRouter.
* Existing image-generation settings are migrated to image profiles,
with provider-specific defaults applied where needed.
* Image settings show the credential fields required by each provider,
and errors identify missing credentials.
* **Improvements**
* ComfyUI image generation requires version 0.26.0 or later; image
requests can run for up to five minutes.
* Debug logs update existing entries instead of duplicating them.
* **Changes**
* Chutes is no longer available for image generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
6.2 KiB
Testing
Vitest (vitest.config.ts), covering targeted units under src/lib/ — not a full-coverage suite. Run
with npm test.
Rune modules cannot be imported by tests. vitest.config.ts deliberately omits the SvelteKit plugin
to keep the suite fast and stable, so any *.svelte.ts file fails at import with $state is not defined.
Services reach the stores through vi.mock('#lib/stores/….svelte.ts'), a path TypeScript does not check
but local/lib-import-exists does (why); logic that needs testing on its own is
extracted into a plain .ts module instead (settingsMigrations.ts, advancedPanelView.ts,
stickiness.ts, recentTail.ts are all this pattern). Those modules are production code with real
callers, not test scaffolding.
The default environment is node, with no DOM, and no test renders a component. A Svelte-level mistake —
a bind: to an undefined value, for instance — passes check, lint and the whole suite, and only
fails when the app runs.
A test whose code under test parses HTML with the DOM (utils/narrationClean.ts) opts in per file with
// @vitest-environment jsdom as its first line. jsdom is a devDependency only, and every other file
stays on node.
Discovery provider tests
The discovery provider unit tests mock corsFetch with representative upstream payloads. They are the
CI contract for request construction, result mapping, and card downloads; they do not depend on remote
services remaining available or preserving their current catalog.
An opt-in smoke suite exercises public search and download paths against every registered discovery provider:
$env:AVENTURAS_LIVE_DISCOVERY='1'; npx vitest run src/lib/services/discovery/providers/providers.live.test.ts
It is skipped unless AVENTURAS_LIVE_DISCOVERY=1 and must not be enabled in required CI. The suite is
intentionally comprehensive: failures from Cloudflare, volatile application protocols, upstream
catalog changes, or provider outages are useful signals during an explicit live run, but would make
required CI flaky.
Fake provider
scripts/fake-provider.mjs stands in for a provider, so the running app can be driven into
failures by hand: rate limits, server errors, hangs, empty and malformed answers, cut streams. It
answers as an OpenAI-compatible chat API and as the OpenAI image API, with no dependencies, and logs
one line per request. What each request does is chosen by the model name it asks for, so every
service can be pointed at its own failure from one profile.
npm run fake-provider
It listens on http://localhost:4010/v1; npm run fake-provider -- 4020 picks another port.
Setup in the app
- Settings → API Connection: add a profile, provider OpenAI Compatible, base URL
http://localhost:4010/v1, any API key. Make it the main narrative profile, modelok. - Settings → Generation → Agent Profiles: profiles on that API profile, all with Structured Output
on — the fake builds structured answers from the request's JSON schema, which it cannot see when
the schema travels inside the prompt. One on
okfor every task; one onok-fillfor Image Gen and BG Image generation analyzer, so scenes are found and the background is judged changed; one whose model is set to the failure under test, to move a single task onto. - Settings → Images: a profile with provider OpenAI Compatible, the same base URL and model
img-ok. No API key is needed; without a profile every story's image settings stay on Text Only.
Chat models. Append -slow to any of them for a 4 s delay before the answer starts.
| Model | Behaviour |
|---|---|
ok |
a valid answer; structured output is built from the request's schema |
ok-fill |
as ok, with one item in every list and flags true |
partial |
as ok-fill, plus one list element missing its required fields |
malformed |
JSON that does not parse; a plain-text call gets x |
empty |
200 with no content |
fail-400 … fail-404 |
400 context length, 401 bad key, 403 no access, 404 no such model |
fail-429 |
429 on every request |
fail-429x2 |
429 twice, then a valid answer |
fail-429-ra3 |
429 once with Retry-After: 3, then a valid answer |
fail-429-ra900 |
429 with Retry-After: 900, beyond the request timeout |
fail-500, fail-503 |
500 or 503 on every request |
fail-500x1 |
500 once, then a valid answer |
fail-502-html |
502 with an HTML page as the body |
fail-reset |
the connection closed with no response |
fail-hang |
never answers |
stream-connection-lost |
streaming only: some text, then the connection drops |
stream-cut-error |
streaming only: some text, then an error event in the stream, as OpenRouter sends |
stream-stall |
streaming only: some text, then silence with the connection open |
The narration is always The dragon fell from the sky, and the valley was quiet again., and every scene's quote is The dragon fell from the sky, so images land in the text — including the part a cut stream keeps.
Image models: img-ok, img-fail-400 (content policy), img-fail-401, img-fail-429,
img-fail-500, img-empty (200 without an image), img-hang.