Changes
diff --git a/designs/design-3-api.md b/designs/design-3-api.md
new file mode 100644
index 0000000..5c86b4d
--- /dev/null
+++ b/designs/design-3-api.md
@@ -0,0 +1,1161 @@
+# API Profile Server Design
+
+## Purpose
+
+This document describes how `diffusion-cli` should expose HTTP image
+generation APIs for external clients.
+
+The immediate target client is SillyTavern. SillyTavern has a provider
+option named `stable-diffusion.cpp`, but the pinned implementation at
+commit `51ad27fb86d39a3daca3adaa970375c9670c12df` does not call the
+native stable-diffusion.cpp API for image generation. It calls a small
+mix of OpenAI-style and WebUI-style endpoints:
+
+- `OPTIONS /v1/images/generations`
+- `GET /v1/models`
+- `POST /sdapi/v1/txt2img`
+
+The first server profile should therefore be named
+`sillytavern-sdcpp`. The name is intentionally specific. It describes
+the behavior needed to satisfy SillyTavern's `sdcpp` adapter, not the
+real stable-diffusion.cpp native API.
+
+Later profiles can expose other contracts, including a real `sdcpp`
+profile that implements stable-diffusion.cpp's native `/sdcpp/v1/...`
+job API.
+
+## Goals
+
+The feature must:
+
+- Add a long-running HTTP server mode to the CLI.
+- Require a command line option that selects one API profile.
+- Implement the first profile, `sillytavern-sdcpp`.
+- Keep API-profile behavior isolated from generation internals.
+- Reuse the existing generation configuration and model loading path.
+- Return response payloads shaped exactly enough for SillyTavern.
+- Provide clear errors for unsupported profile features.
+- Avoid pretending that the SillyTavern profile is the native sdcpp API.
+
+The feature should not:
+
+- Add a web UI.
+- Add ComfyUI workflow support.
+- Add image-to-image support.
+- Add LoRA support.
+- Add true async job cancellation.
+- Add the native `/sdcpp/v1/...` API in the first milestone.
+- Add every A1111 endpoint just because `/sdapi/v1/txt2img` is used.
+
+The purpose of the first milestone is interoperability, not broad API
+coverage.
+
+## External References
+
+The implementation should be checked against these references:
+
+- SillyTavern's pinned stable diffusion endpoint:
+ [stable-diffusion.js at 51ad27f][sillytavern-stable-diffusion].
+- stable-diffusion.cpp's current server API documentation:
+ [examples/server/api.md][sdcpp-api-doc].
+- Python's built-in HTTP concepts can be implemented with a framework,
+ but request and response behavior should still follow standard HTTP
+ status code semantics from
+ [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html).
+- Use Flask for HTTP routing and request handling. Refer to the
+ official documentation:
+ [Flask documentation](https://flask.palletsprojects.com/).
+
+[sillytavern-stable-diffusion]: https://github.com/SillyTavern/SillyTavern/raw/51ad27fb86d39a3daca3adaa970375c9670c12df/src/endpoints/stable-diffusion.js
+[sdcpp-api-doc]: https://github.com/leejet/stable-diffusion.cpp/blob/master/examples/server/api.md
+
+## Terminology
+
+### API Profile
+
+An API profile is a named HTTP contract.
+
+The profile decides:
+
+- Which routes exist.
+- Which HTTP methods those routes accept.
+- Which request fields are read.
+- Which request fields are ignored.
+- Which response shape is returned.
+- Which compatibility endpoints are exposed.
+
+The profile does not decide how Z-Image inference works. It translates
+client requests into the existing generation configuration.
+
+This separation matters because API compatibility and model inference
+change at different speeds. A client such as SillyTavern can require a
+quirky endpoint mix, while the local generator still needs one clean
+internal generation path.
+
+### Native API
+
+A native API is the API that a backend project intentionally documents
+as its own first-class contract.
+
+For stable-diffusion.cpp, the native API is currently documented under
+`/sdcpp/v1/...`, including:
+
+- `GET /sdcpp/v1/capabilities`
+- `POST /sdcpp/v1/img_gen`
+- `GET /sdcpp/v1/jobs/{id}`
+- `POST /sdcpp/v1/jobs/{id}/cancel`
+
+The first profile in this project is not that API.
+
+### Compatibility API
+
+A compatibility API exists so another program can talk to this server
+without modification.
+
+The `sillytavern-sdcpp` profile is a compatibility API. It exists
+because SillyTavern's provider adapter has a specific endpoint pattern.
+The profile should be maintained against that adapter's behavior.
+
+## User Interface
+
+### Server Command
+
+Add a server mode to the CLI.
+
+Recommended command shape:
+
+```bash
+uv run diffusion-cli serve \
+ --api-profile sillytavern-sdcpp \
+ --host 127.0.0.1 \
+ --port 7860
+```
+
+`serve`
+
+: Starts the HTTP server instead of running one CLI generation.
+
+`--api-profile`
+
+: Required in server mode. Selects the API profile. The first supported
+ value is `sillytavern-sdcpp`.
+
+`--host`
+
+: Bind host. The default should be `127.0.0.1` so the server is local
+ only unless the user explicitly exposes it.
+
+`--port`
+
+: Bind port. The default can be `7860`, because many Stable Diffusion
+ clients already expect local image generation servers there.
+
+`--model-residency`
+
+: Controls how server mode keeps model components loaded between
+ requests. Allowed values should be `staged` and `cpu-cache`. The
+ server default should be `cpu-cache`.
+
+The profile flag should be required for two reasons.
+
+First, it prevents accidental compatibility promises. If the server
+defaults to a profile, users may assume that profile is the project's
+general API. Requiring a profile makes the contract explicit.
+
+Second, it leaves room for incompatible future profiles. A native
+`sdcpp` profile and a WebUI profile will have different routes and
+different response formats.
+
+### SillyTavern Configuration
+
+For the first profile, the user should configure SillyTavern's
+stable-diffusion.cpp provider to point at:
+
+```text
+http://127.0.0.1:7860
+```
+
+The user should select the SillyTavern provider that reaches its
+`sdcpp` server adapter.
+
+The project documentation must avoid saying "select sdcpp because this
+server is stable-diffusion.cpp." A more accurate instruction is:
+
+```text
+Start diffusion-cli with --api-profile sillytavern-sdcpp, then point
+SillyTavern's stable-diffusion.cpp provider at the local server URL.
+```
+
+## Server Architecture
+
+The server should have four layers.
+
+### CLI Layer
+
+The CLI layer parses command line arguments and chooses between:
+
+- one-shot generation mode
+- server mode
+- model inspection mode
+
+The parser should accept `serve` as a subcommand. This avoids mixing
+server-only options with one-shot generation options.
+
+Conceptual parser layout:
+
+```text
+diffusion-cli [generation options]
+diffusion-cli serve --api-profile PROFILE [server options]
+diffusion-cli --inspect-models
+```
+
+The existing one-shot command behavior should keep working. The server
+feature should not force users to use a subcommand for the current
+generation path unless a broader CLI redesign is intentionally done
+later.
+
+### Profile Registry Layer
+
+The profile registry maps profile names to profile objects.
+
+Conceptual structure:
+
+```python
+API_PROFILES = {
+ "sillytavern-sdcpp": SillyTavernSdcppProfile,
+}
+```
+
+The registry should be the only place that knows all profile names.
+This gives the CLI a single source of truth for validation and help
+text.
+
+Each profile object should provide:
+
+- a stable profile name
+- a short description
+- a route registration function
+- optional profile-specific defaults
+
+### HTTP Adapter Layer
+
+The HTTP adapter layer owns request parsing and response formatting.
+
+For `sillytavern-sdcpp`, this layer reads WebUI-style request fields
+from `/sdapi/v1/txt2img` and creates an internal generation request.
+
+The adapter should not load models directly. It should call the
+generation service layer.
+
+### Generation Service Layer
+
+The generation service layer owns the bridge to existing inference code.
+
+It should expose a function that accepts a validated internal request
+object and returns generated image paths or image bytes.
+
+The current `generate(args, user_config)` function is tied to argparse
+objects and file output. Server code should not construct fake argparse
+objects as a long-term design. Instead, generation should gradually be
+factored around an internal request model.
+
+The first implementation can be incremental:
+
+1. Add a small internal request dataclass.
+2. Add a helper that builds `GenerationConfig` from that dataclass plus
+ `UserConfig`.
+3. Keep the existing CLI path by converting CLI args into the same
+ internal shape.
+
+This refactor makes the server and CLI share validation behavior without
+making HTTP code depend on argparse details.
+
+Server mode should use a long-lived generation service object. That
+object should own model residency state across requests. Keeping this
+state in the generation service, instead of in profile route handlers,
+keeps API compatibility separate from model lifecycle decisions.
+
+## Model Residency
+
+### Purpose
+
+Server mode should avoid repeated disk I/O.
+
+The one-shot CLI can load model files, generate images, and exit. Server
+mode is different because it handles many requests in one process. If it
+reads the same safetensors files from disk for every request, prompt
+iteration will feel slow and the operating system will repeatedly move
+large weight files through the I/O path.
+
+The server should therefore support model residency policies.
+
+### Policy Values
+
+`staged`
+
+: Load each component from disk when needed, use it, then release it.
+ This matches the current low-VRAM CLI behavior. It is reliable but
+ slow for repeated server requests.
+
+`cpu-cache`
+
+: Load model components from disk once into system RAM, keep the Python
+ model objects alive on CPU between requests, and move one component at
+ a time to the selected generation device when needed. After each
+ stage, move that component back to CPU and clear CUDA cache. This is
+ the default server policy.
+
+### Default Policy
+
+Server mode should default to `cpu-cache`.
+
+This policy is the best first server default because it avoids repeated
+disk reads without assuming that the user's GPU can hold the text
+encoder, diffusion model, and VAE at the same time.
+
+Z-Image is a staged pipeline:
+
+1. The text encoder turns prompt text into conditioning.
+2. The diffusion model samples latents.
+3. The VAE decodes latents into images.
+
+Only one of those components is actively needed for each stage. Keeping
+all components in system RAM and moving only the active component to GPU
+preserves that staged memory profile while removing repeated safetensors
+reads.
+
+### CPU Cache Lifecycle
+
+Under `cpu-cache`, the first request should:
+
+1. Load the text encoder weights from disk into CPU memory.
+2. Move the text encoder to the generation device.
+3. Encode the prompt.
+4. Move the text encoder back to CPU.
+5. Release CUDA cache.
+6. Load the diffusion model weights from disk into CPU memory.
+7. Move the diffusion model to the generation device.
+8. Sample latents.
+9. Move the diffusion model back to CPU.
+10. Release CUDA cache.
+11. Load the VAE weights from disk into CPU memory.
+12. Move the VAE to the generation device.
+13. Decode the image.
+14. Move the VAE back to CPU.
+15. Release CUDA cache.
+
+Later requests should reuse the CPU-resident objects:
+
+1. Move the cached text encoder to the generation device.
+2. Encode the prompt.
+3. Move the text encoder back to CPU.
+4. Move the cached diffusion model to the generation device.
+5. Sample latents.
+6. Move the diffusion model back to CPU.
+7. Move the cached VAE to the generation device.
+8. Decode the image.
+9. Move the VAE back to CPU.
+
+No safetensors model file should be read again unless the server is
+restarted or the model source configuration changes in a future feature.
+
+### Required Component API
+
+The model wrapper classes should expose explicit lifecycle operations.
+
+Conceptual methods:
+
+```python
+class ZImageTextEncoder:
+ def toDevice(self, device, dtype) -> None: ...
+ def toCpu(self) -> None: ...
+
+class ZImageModel:
+ def toDevice(self, device, dtype) -> None: ...
+ def toCpu(self) -> None: ...
+
+class ZImageVae:
+ def toDevice(self, device, dtype) -> None: ...
+ def toCpu(self) -> None: ...
+```
+
+The method names should follow the project's existing Python style if
+the implementation has a more specific local convention. The important
+requirement is explicit ownership: the generation service must be able
+to move a component onto the generation device for a stage and move it
+back to CPU afterward.
+
+The wrappers must not keep hidden CUDA tensors after `toCpu()` returns.
+If they maintain caches, those caches must either move to CPU or be
+cleared. Otherwise `cpu-cache` will still leak VRAM across requests.
+
+### Interaction With Concurrency
+
+`cpu-cache` requires serialized generation.
+
+The generation service owns mutable model objects and moves them between
+CPU and GPU. Two requests cannot safely move the same shared object at
+the same time. The generation lock described in the concurrency section
+must cover the whole staged generation process.
+
+### Fallback Behavior
+
+If `cpu-cache` fails because system RAM is insufficient, the server
+should return a clear `500` error and log the underlying exception.
+
+It should not silently fall back to `staged` after a partial load. A
+silent fallback would make latency unpredictable and would hide the fact
+that the configured residency policy is not viable on the machine.
+
+GPU-resident caching is intentionally out of scope for the first
+milestone. A later enhancement can add a `vram-cache` or `auto` policy
+after the safer `cpu-cache` path is working.
+
+## Data Model
+
+### `ApiProfile`
+
+Use a small protocol or dataclass for profile metadata.
+
+Conceptual fields:
+
+```python
+@dataclass(frozen=True)
+class ApiProfile:
+ """HTTP API contract exposed by server mode."""
+
+ name: str
+ description: str
+ register_routes: Callable[[ServerApp, ServerContext], None]
+```
+
+`name`
+
+: The exact value accepted by `--api-profile`.
+
+`description`
+
+: Human-readable help text.
+
+`register_routes`
+
+: Function that adds the profile's routes to the HTTP app.
+
+`ServerApp` is a Flask app object.
+
+### `ServerConfig`
+
+Server configuration should be separate from generation configuration.
+
+Conceptual fields:
+
+```python
+@dataclass(frozen=True)
+class ServerConfig:
+ """Validated settings for HTTP server mode."""
+
+ api_profile: str
+ host: str
+ port: int
+ model_residency: str
+```
+
+Validation rules:
+
+- `api_profile` must exist in the profile registry.
+- `host` must be a non-empty string.
+- `port` must be from `1` through `65535`.
+- `model_residency` must be `staged` or `cpu-cache`.
+
+The server config should not contain prompt, seed, width, or model
+paths. Those belong to generation configuration.
+
+### `ImageGenerationRequest`
+
+Create an internal request model that is independent of HTTP and
+argparse.
+
+Conceptual fields:
+
+```python
+@dataclass(frozen=True)
+class ImageGenerationRequest:
+ """Validated user intent for one text-to-image request."""
+
+ prompt: str
+ negative_prompt: str | None = None
+ seed: int | None = None
+ width: int | None = None
+ height: int | None = None
+ batch_size: int | None = None
+ steps: int | None = None
+ cfg: float | None = None
+ output: Path | None = None
+ device: str | None = None
+ dtype: str | None = None
+```
+
+Fields are optional when they can fall back to config or built-in
+defaults. `prompt` is required because generation without a prompt is
+not useful and already fails in the CLI.
+
+Server requests must use the same precedence model as the CLI for
+generation values:
+
+1. HTTP request value.
+2. `~/.config/diffusion.toml` value.
+3. Built-in default.
+
+This means a SillyTavern request can omit values such as
+`negative_prompt`, `width`, `height`, `steps`, `batch_size`, `cfg`, and
+`device`, and the server will still use the user's configured defaults
+when those defaults exist. The server must not use one-shot CLI
+generation arguments as defaults, because server mode has its own
+command line surface.
+
+The request should not include unsupported compatibility fields such as
+`sampler_name` or `scheduler`. Those belong to the profile adapter. The
+adapter can decide whether to ignore them, reject them, or map them to
+future internal fields.
+
+### `GeneratedImage`
+
+The server should return image bytes, not only file paths.
+
+Conceptual fields:
+
+```python
+@dataclass(frozen=True)
+class GeneratedImage:
+ """One generated image encoded for transport or file output."""
+
+ data: bytes
+ format: str
+ seed: int
+```
+
+`data`
+
+: Encoded image bytes kept in memory. For the first server profile, this
+ should be PNG.
+
+`format`
+
+: Lowercase image format string, such as `png`.
+
+`seed`
+
+: Effective seed for this image.
+
+The current CLI writes files through `saveImages`. The server needs
+encoded bytes so it can base64 encode them directly. The implementation
+must add a byte-oriented image encoding helper. Server mode should not
+write generated images to disk in the first milestone. Avoiding disk
+writes keeps server behavior simple, prevents filename collisions, and
+avoids leaving behind generated files that the user did not explicitly
+ask to save.
+
+## `sillytavern-sdcpp` Profile
+
+### Profile Purpose
+
+This profile satisfies SillyTavern's stable-diffusion.cpp provider
+adapter as observed in the pinned file.
+
+It is named `sillytavern-sdcpp` because the client calls it `sdcpp`,
+while the actual route set is not the native stable-diffusion.cpp API.
+
+### Endpoint Summary
+
+The profile should expose exactly these first-milestone endpoints:
+
+| Method | Path | Purpose |
+| --- | --- | --- |
+| `OPTIONS` | `/v1/images/generations` | SillyTavern ping check |
+| `GET` | `/v1/models` | SillyTavern model list |
+| `POST` | `/sdapi/v1/txt2img` | Image generation |
+
+It may also expose:
+
+| Method | Path | Purpose |
+| --- | --- | --- |
+| `GET` | `/health` | Manual local health check |
+
+`/health` is not part of SillyTavern compatibility. It is useful for
+debugging and can return a tiny project-owned shape.
+
+### `OPTIONS /v1/images/generations`
+
+SillyTavern's adapter pings this endpoint with `OPTIONS`.
+
+Response:
+
+```http
+204 No Content
+Allow: OPTIONS, POST
+```
+
+The implementation does not need to support `POST /v1/images/generations`
+in this profile. Returning `Allow: OPTIONS, POST` is acceptable because
+the endpoint is modelled after an OpenAI-compatible path, but only the
+`OPTIONS` method is needed by SillyTavern's ping logic.
+
+If the framework automatically returns `200 OK` for `OPTIONS`, that is
+also likely to satisfy SillyTavern because the adapter only checks that
+the response is successful. Prefer `204` because there is no body.
+
+### `GET /v1/models`
+
+SillyTavern's adapter forwards this response directly.
+
+The response should use the OpenAI-compatible model list shape:
+
+```json
+{
+ "data": [
+ {
+ "id": "z-image-local",
+ "object": "model",
+ "owned_by": "local"
+ }
+ ]
+}
+```
+
+The model id can be static in the first milestone. The local generator
+does not support runtime model switching yet, so a single model is
+accurate.
+
+Later, if the config can expose a friendly model name, `id` can become
+that name. Until then, `z-image-local` is explicit and stable.
+
+### `POST /sdapi/v1/txt2img`
+
+This is the generation endpoint SillyTavern uses for the profile.
+
+Request body fields to support:
+
+| Field | Type | Mapping |
+| --- | --- | --- |
+| `prompt` | string | required internal prompt |
+| `negative_prompt` | string | internal negative prompt |
+| `width` | integer | internal width |
+| `height` | integer | internal height |
+| `steps` | integer | internal steps |
+| `cfg_scale` | number | internal `cfg` |
+| `seed` | integer | internal seed, random if missing or `-1` |
+| `batch_size` | integer | internal batch size |
+| `sampler_name` | string | accepted, ignored |
+| `scheduler` | string | accepted, ignored |
+| `clip_skip` | integer | accepted, ignored |
+| `model` | string | accepted, ignored |
+
+The ignored fields are accepted because SillyTavern sends them in the
+`sdcpp` adapter payload. Rejecting them would make compatibility brittle
+without improving generation correctness.
+
+The adapter should reject fields only when they create an unsupported
+mode. For example, if a future SillyTavern payload sends `init_images`,
+the first implementation should return a clear `400` because that means
+image-to-image, which is out of scope.
+
+Request validation:
+
+- `prompt` must be a non-empty string.
+- `width`, if present, must be a positive multiple of 8.
+- `height`, if present, must be a positive multiple of 8.
+- `steps`, if present, must be at least 1.
+- `batch_size`, if present, must be at least 1.
+- `cfg_scale`, if present, must be non-negative.
+- `seed` of `-1`, missing, or `null` means random seed.
+
+Response body:
+
+```json
+{
+ "images": ["<base64 png bytes>"],
+ "parameters": {
+ "prompt": "a ceramic mug",
+ "width": 832,
+ "height": 1248
+ },
+ "info": "{\"prompt\":\"a ceramic mug\",\"seed\":1234}"
+}
+```
+
+`images`
+
+: Array of raw base64 strings. Do not include a `data:image/png;base64,`
+ prefix, because WebUI-style clients expect the bare base64 payload.
+
+`parameters`
+
+: Echo of the parsed outer request body. Echoing the request helps
+ WebUI-compatible clients display generation settings.
+
+`info`
+
+: JSON encoded as a string. This odd shape matches WebUI-style
+ responses. It should include at least `prompt`, `negative_prompt`,
+ `seed`, `width`, `height`, `steps`, and `cfg_scale`.
+
+The first milestone can return an empty JSON object string, `"{}"`, if
+building complete metadata would slow implementation. However, including
+the core fields is better because it helps diagnose SillyTavern requests.
+
+## Error Handling
+
+Every incoming server request should be logged at debug level before it
+is handled.
+
+The log should include:
+
+- HTTP method.
+- Path.
+- Query string, if present.
+- JSON body for JSON requests.
+
+The log should not include binary payloads in full. The first profile is
+text-to-image only, so this mostly affects future profiles. If a request
+contains image-like fields such as `init_images`, `mask`, or
+`extra_images`, log the field name and payload length instead of the
+full base64 value.
+
+Debug logging every request matters because API profile compatibility is
+mostly about exact client behavior. When SillyTavern changes a payload,
+the server logs should make the difference visible without requiring a
+packet capture.
+
+### Client Errors
+
+Use `400 Bad Request` when the request is syntactically valid HTTP but
+cannot be used for generation.
+
+Examples:
+
+- Empty JSON body.
+- Invalid JSON body.
+- Missing prompt.
+- Width or height is not valid.
+- Unsupported image-to-image field is present.
+
+Response shape:
+
+```json
+{
+ "error": "bad_request",
+ "message": "prompt required"
+}
+```
+
+The `message` should be short and concrete. Avoid stack traces in client
+responses.
+
+### Server Errors
+
+Use `500 Internal Server Error` when generation fails after validation.
+
+Examples:
+
+- Model loading fails.
+- CUDA runs out of memory.
+- The sampler produces invalid tensors.
+- Image encoding fails.
+
+Response shape:
+
+```json
+{
+ "error": "generation_failed",
+ "message": "model loading failed"
+}
+```
+
+Log the full exception server-side. Return a shorter message to the
+client.
+
+### Unsupported Routes
+
+Routes that are not part of the selected profile should return the
+framework's normal `404 Not Found`.
+
+Do not register placeholder routes for future profiles. A placeholder
+route can make clients believe a feature exists.
+
+## Concurrency
+
+The first implementation should serialize generation requests.
+
+Z-Image generation loads and unloads large model components. Running
+multiple generations concurrently can exhaust VRAM and produce failures
+that look random to the caller.
+
+Use a process-local lock around the generation service:
+
+```text
+request arrives
+validate request
+acquire generation lock
+run generation with configured model residency policy
+release generation lock
+return response
+```
+
+This does not create a queue with job status. It simply lets HTTP
+requests wait for the lock. That is acceptable for the first
+SillyTavern profile because its adapter expects synchronous generation.
+
+If a second request waits too long, the client may time out. That is a
+client-side limitation of synchronous APIs. The future native `sdcpp`
+profile can add async job submission and polling.
+
+## Cancellation
+
+The first milestone does not need generation cancellation.
+
+SillyTavern's A1111 path has interrupt support, but the pinned `sdcpp`
+adapter does not call a native job cancellation endpoint for generation.
+Adding real cancellation would require model and sampler code to check a
+shared cancellation token between denoising steps. That is useful, but
+it is separate from initial compatibility.
+
+If the HTTP client disconnects during generation, the server may still
+finish the image and discard the response. This is acceptable for the
+first milestone.
+
+## Security
+
+Default binding must be local:
+
+```text
+127.0.0.1
+```
+
+The server performs expensive local computation and may expose model
+paths or generation behavior through metadata. It should not listen on
+all interfaces unless the user explicitly passes a different host.
+
+The server should not implement authentication in the first milestone.
+Local-only binding is the safety boundary.
+
+If remote access becomes necessary later, add authentication as a
+separate design. Do not silently expose an unauthenticated generation
+server on `0.0.0.0`.
+
+## File Organization
+
+Recommended new files:
+
+```text
+diffusion_cli/server.py
+diffusion_cli/api_profiles.py
+diffusion_cli/generation_service.py
+tests/test_server.py
+tests/test_api_profiles.py
+```
+
+`server.py`
+
+: Owns server configuration, app creation, and running the development
+ server process.
+
+`api_profiles.py`
+
+: Owns profile metadata and route registration for each profile.
+
+`generation_service.py`
+
+: Owns HTTP-independent generation request and response objects, plus
+ server-mode model residency state.
+
+`tests/test_server.py`
+
+: Tests CLI server argument parsing and app creation.
+
+`tests/test_api_profiles.py`
+
+: Tests request and response behavior for `sillytavern-sdcpp`.
+
+If the chosen HTTP framework encourages a different file split, keep the
+same ownership boundaries even if the filenames differ.
+
+## Dependency Choice
+
+Use Flask for the HTTP server.
+
+Flask is the right fit for the first server milestone because the route
+surface is intentionally small:
+
+- `OPTIONS /v1/images/generations`
+- `GET /v1/models`
+- `POST /sdapi/v1/txt2img`
+
+Flask also keeps the server synchronous, which matches the first
+profile's synchronous generation contract. The future native `sdcpp`
+profile may need an async job model, but that can still be implemented
+with explicit job state and polling routes instead of requiring the
+entire HTTP stack to be async.
+
+The implementation should use Flask's test client for route tests. The
+test client can exercise the full request and response path without
+binding a real TCP port.
+
+Do not use the Python standard library `http.server` for this feature.
+It is useful for simple file serving, but JSON APIs, method routing,
+testing, and error handling become unnecessary hand-written framework
+work.
+
+## Testing Strategy
+
+### Unit Tests
+
+Test profile registration:
+
+- `sillytavern-sdcpp` is listed in the registry.
+- Unknown profile names fail validation.
+- The CLI requires `--api-profile` in server mode.
+- The server defaults to `cpu-cache` model residency.
+- Invalid model residency values fail before the server starts.
+
+Test request parsing:
+
+- Minimal request with only `prompt` succeeds.
+- Full SillyTavern-like request maps `cfg_scale` to `cfg`.
+- `seed = -1` becomes random seed behavior.
+- Empty prompt returns `400`.
+- Unsupported `init_images` returns `400`.
+
+Test response formatting:
+
+- `/v1/models` returns a `data` array.
+- `OPTIONS /v1/images/generations` returns a success status.
+- `/sdapi/v1/txt2img` returns an `images` array.
+- Returned image strings are bare base64 strings.
+- `info` is a string containing valid JSON.
+
+Generation should be mocked in API profile tests. These tests should not
+load model files or require CUDA.
+
+Test model residency with mocked component wrappers:
+
+- Under `cpu-cache`, each component is loaded from disk once across two
+ requests.
+- Under `cpu-cache`, each request moves each component to the generation
+ device before use.
+- Under `cpu-cache`, each request moves each component back to CPU after
+ use.
+- Under `staged`, each request reloads components.
+
+### Integration Tests
+
+Add one integration test that starts the app in-process and calls the
+three SillyTavern endpoints with a mocked generation service.
+
+The test should simulate the exact endpoint sequence:
+
+1. `OPTIONS /v1/images/generations`
+2. `GET /v1/models`
+3. `POST /sdapi/v1/txt2img`
+
+This catches route registration mistakes and response-shape mistakes.
+
+### Manual Test
+
+Manual validation with SillyTavern:
+
+1. Start the server:
+
+ ```bash
+ uv run diffusion-cli serve \
+ --api-profile sillytavern-sdcpp \
+ --host 127.0.0.1 \
+ --port 7860
+ ```
+
+2. Configure SillyTavern's stable-diffusion.cpp provider URL:
+
+ ```text
+ http://127.0.0.1:7860
+ ```
+
+3. Use SillyTavern's ping or connection test.
+4. Generate one image from a simple prompt.
+5. Confirm the server logs show one `/sdapi/v1/txt2img` request.
+
+## Implementation Plan
+
+### Step 1: Add Server CLI Shape
+
+Add a `serve` subcommand with:
+
+- `--api-profile`
+- `--host`
+- `--port`
+
+Keep existing one-shot generation behavior unchanged.
+
+### Step 2: Add Profile Registry
+
+Create the profile registry with one profile:
+
+```text
+sillytavern-sdcpp
+```
+
+Unknown profiles should fail before the server starts.
+
+### Step 3: Add HTTP App Factory
+
+Create an app factory that accepts:
+
+- `ServerConfig`
+- `UserConfig`
+- generation service dependency
+
+The app factory registers only the selected profile's routes.
+
+### Step 4: Add Model Residency Policy
+
+Add `--model-residency` for server mode.
+
+Allowed values:
+
+```text
+staged
+cpu-cache
+```
+
+The default should be `cpu-cache`.
+
+The first implementation must support `staged` and `cpu-cache`.
+
+Do not accept `vram-cache` in the first implementation. A command line
+option that accepts a value should have working runtime behavior.
+
+### Step 5: Factor Generation Service
+
+Introduce `ImageGenerationRequest` and `GeneratedImage`.
+
+Move enough logic out of `cli.generate` so both CLI and server can call
+the same generation path without constructing fake argparse objects.
+
+The server generation service should own cached CPU components when
+`model_residency` is `cpu-cache`.
+
+### Step 6: Implement SillyTavern Routes With Mocked Generation
+
+Implement:
+
+- `OPTIONS /v1/images/generations`
+- `GET /v1/models`
+- `POST /sdapi/v1/txt2img`
+
+Use mocked generation in route tests. This proves the HTTP contract
+before expensive model loading is connected.
+
+### Step 7: Connect Routes To Real Generation
+
+Wire `/sdapi/v1/txt2img` to the generation service.
+
+The route should translate WebUI-style fields into
+`ImageGenerationRequest`, then call the service. It should not know
+whether the service is using `staged` or `cpu-cache` residency.
+
+### Step 8: Encode PNG Bytes
+
+Add a helper that converts generated PIL images or tensors into PNG
+bytes for the server response.
+
+The CLI can keep writing files. The server should keep generated image
+bytes in memory and base64 encode those bytes. It should not write
+server-generated images to disk in the first milestone.
+
+### Step 9: Add Documentation
+
+Update `README.md` with:
+
+- server command example
+- SillyTavern configuration URL
+- the meaning of `sillytavern-sdcpp`
+- a warning that it is not the native stable-diffusion.cpp API
+- the default `cpu-cache` model residency behavior
+
+## Future Profiles
+
+### `sdcpp`
+
+The future native stable-diffusion.cpp profile should implement:
+
+- `GET /sdcpp/v1/capabilities`
+- `POST /sdcpp/v1/img_gen`
+- `GET /sdcpp/v1/jobs/{id}`
+- `POST /sdcpp/v1/jobs/{id}/cancel`
+
+That profile should use an async job model. It should not reuse the
+synchronous `/sdapi/v1/txt2img` response shape.
+
+### `webui`
+
+A future WebUI profile could implement broader A1111 compatibility:
+
+- `GET /sdapi/v1/options`
+- `GET /sdapi/v1/samplers`
+- `GET /sdapi/v1/schedulers`
+- `GET /sdapi/v1/sd-models`
+- `POST /sdapi/v1/txt2img`
+- `POST /sdapi/v1/interrupt`
+
+This should be a separate profile because A1111 clients expect a larger
+surface area than SillyTavern's pinned `sdcpp` adapter.
+
+### `openai`
+
+A future OpenAI-compatible profile could implement:
+
+- `GET /v1/models`
+- `POST /v1/images/generations`
+
+This profile would return:
+
+```json
+{
+ "created": 1234567890,
+ "data": [
+ {
+ "b64_json": "<base64>"
+ }
+ ]
+}
+```
+
+It should not return WebUI's `images`, `parameters`, and `info` fields.
+
+## Resolved Decisions
+
+The following design decisions are fixed for the first milestone:
+
+- Use Flask for HTTP routing and tests.
+- Do not write server-generated images to disk.
+- Debug-log every incoming request.
+- Use config-file generation defaults when the HTTP request omits a
+ supported generation value.
+- Use `cpu-cache` as the default server model residency policy to avoid
+ repeated disk I/O while keeping VRAM usage staged.
+
+## Acceptance Criteria
+
+The first milestone is complete when:
+
+- `diffusion-cli serve --api-profile sillytavern-sdcpp` starts a local
+ HTTP server.
+- `OPTIONS /v1/images/generations` returns a successful response.
+- `GET /v1/models` returns an OpenAI-style model list.
+- `POST /sdapi/v1/txt2img` accepts a SillyTavern-like payload.
+- The generation endpoint returns WebUI-style `images`, `parameters`,
+ and `info` fields.
+- API tests pass without loading real model files.
+- Server-mode residency tests prove `cpu-cache` avoids repeated model
+ file loads across requests.
+- One manual SillyTavern generation works against the local server.
+- Documentation clearly says this is SillyTavern compatibility, not the
+ native stable-diffusion.cpp API.