# 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.