# 1.1 REST Interfaces
## Protocol Design
The REST API provides a single multipart/form-data ingress point for image analysis. Adherence to HTTP semantics is strict: `POST /api/v1/vision` returns `202 Accepted` to signal asynchronous enqueueing rather than synchronous completion. All results are delivered retroactively via Socket.IO, decoupling the HTTP lifecycle from long-running GPU inference.
## Endpoint Specification
### `POST /api/v1/vision`
**Required Headers**
| Header | Value | Semantics |
|--------|-------|-----------|
| `x-vision-llm` | Model tag (e.g., `llama3.2-vision`) | Target Ollama model; validated by header extraction in `ClassicController` |
**Query Parameters**
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `requestId` | `string` | Yes | Client-generated correlation identifier; used for job tracking, room naming, and cancellation |
| `stream` | `boolean` | No | Enable streaming via `ollama.chat` with streaming callback; default `false` |
| `roomId` | `string` | No | Socket.IO room; derived from `requestId` if omitted |
| `event` | `string` | No | Event name emitted by Socket.IO; default read from `SOCKET_IO_EVENT` config |
| `numCtx` | `number` | No | Model context window size (overrides default) |
**Multipart Body Fields**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `images` | `File[]` | Yes | One or more images (PNG, JPG, JPEG, WEBP) |
| `task` | `string` | Yes | `describe`, `compare`, or `ocr` |
| `prompt` | `json` | No | Array of message objects `[{"role":"user","content":"..."}]` |
| `preprocessing` | `json` | No | Preprocessing options; see [1.5 Image Preprocessing Pipeline](1.5-image-preprocessing.md) |
### `POST /api/v1/vision/cancel`
| Parameter | Type | Required |
|-----------|------|----------|
| `requestId` | `string` | Yes |
Sets an atomic flag in `JobTrackingService`. Active jobs detect cancellation via `isCanceled(requestId)` during the streaming loop and raise `UnrecoverableError`, preventing retries.
### `GET /api/v1/vision/models`
Returns an array of strings representing locally available Ollama models, hydrating the dashboard model selector.
## Request Flow
```mermaid
sequenceDiagram
participant C as REST Client
participant Ctrl as ClassicController
participant Svc as AnalyzeImageService
participant Queue as BullMQ Queue
participant W as Vision Worker
participant Preproc as ImagePreprocessingService
participant O as Ollama
participant Socket as Socket.IO
C->>Ctrl: POST /api/v1/vision
multipart/form-data
Ctrl->>Svc: toFilePayloads(images) + emit()
Svc->>Queue: addJob({ buffers, meta, filters })
Queue-->>Ctrl: job.id
Ctrl-->>C: 202 Accepted
{ realtime: { event, roomId, requestId } }
Queue->>W: process job
alt preprocessing enabled
W->>Preproc: preprocessImages(buffers, meta, preprocessing)
Preproc-->>W: PreprocessedImage[] (original, grayscale, clahe, ...)
end
W->>O: chat(messages, stream)
O-->>W: chunk
W->>Socket: emitToSocket(roomId, event, data)
Socket-->>C: vision event (fragmented or complete)
W->>Queue: moveToCompleted
```
## Controller Code Structure
```typescript
// classic.controller.ts (conceptual)
async visionStream(
@Query(REQUEST_ID) requestId: string,
@Headers(X_VISION_LLM) vLLM: string,
@MultiPartValue(TASK) task: MultipartValue,
@MultiPartFiles() images?: MultipartFile[],
): Promise {
const buffers = await this.analyzeImageService.toFilePayloads(requestId, images);
const meta = extractMeta(images);
const filters = buildFilters({ requestId, vLLM, task, stream, numCtx, preprocessing });
void this.analyzeImageService.emit({ buffers, meta, filters });
return { realtime: { event, roomId: filters.roomId, requestId } };
}
```
The `void` prefix on `emit()` ensures the enqueueing operation is fire-and-forget from the HTTP response lifecycle, preventing the controller from awaiting potentially slow queue insertion. Preprocessing configuration is passed through `filters.preprocessing` and applied later, inside the BullMQ worker, not during the HTTP request.
## Response Contracts
### HTTP 202 Response
```json
{
"realtime": {
"event": "vision",
"roomId": "room-123",
"requestId": "1234"
}
}
```
### Socket.IO Event Payload (`vision`)
```json
{
"meta": [
{
"name": "photo.jpg",
"type": "image/jpeg",
"hash": "abc123...",
"requestId": "1234",
"variant": "original"
}
],
"task": "describe",
"message": {
"role": "assistant",
"content": "The image shows a cat sitting on a windowsill..."
},
"done": false
}
```
| Field | Type | Semantics |
|-------|------|-----------|
| `meta` | `ImageMeta[]` | One entry per preprocessed variant (not per original file) |
| `task` | `string` | Echo of the requested task type |
| `message` | `Message` | LLM assistant role and token content |
| `done` | `boolean?` | Streaming sentinel; omitted in non-streaming mode |
## REST vs MCP Comparison
| Aspect | REST | MCP |
|--------|------|-----|
| Endpoint | `/api/v1/vision` | `/api/v1/mcp` |
| Content-Type | `multipart/form-data` | `application/json` |
| Task specification | `task` form field directly | `arguments.task` inside JSON-RPC payload |
| Prompt delivery | `prompt` form field | `arguments.prompt` inside JSON-RPC |
| Images | `images` multipart files | `arguments.images[]` base64 objects |
| Preprocessing toggle | `preprocessing` form field | `arguments.preprocessing` nested object |
| Parameter prefix | `pproc_*` query params | `arguments.preprocessing.*` nested object |
| Model selection | `x-vision-llm` header | `arguments.model` field |
| Response envelope | `{ realtime: {...} }` | JSON-RPC 2.0 `{ result: { content, isError, realtime } }` |
| Error format | HTTP status + JSON body | JSON-RPC error object with `code` and `message` |
## Preprocessing Query Parameter Mapping
On REST, preprocessing keys are flattened as individual query parameters prefixed with `pproc_`. This design decision accommodates HTTP client limitations that struggle with nested JSON in query strings while preserving type safety via `ImagePreprocessingOptionsDto`.
| REST Query Param | JSON-RPC Path | Type | Default |
|------------------|---------------|------|---------|
| `pproc_enabled` | `arguments.preprocessing.enabled` | `boolean` | `false` |
| `pproc_resize_maxWidth` | `arguments.preprocessing.resize.maxWidth` | `number` | `768` |
| `pproc_resize_maxHeight` | `arguments.preprocessing.resize.maxHeight` | `number \| null` | `null` |
| `pproc_resize_withoutEnlargement` | `arguments.preprocessing.resize.withoutEnlargement` | `boolean` | `true` |
| `pproc_original` | `arguments.preprocessing.variants.original` | `boolean` | `true` |
| `pproc_grayscale` | `arguments.preprocessing.variants.grayscale` | `boolean` | `true` |
| `pproc_denoised` | `arguments.preprocessing.variants.denoised` | `boolean` | `true` |
| `pproc_sharpened` | `arguments.preprocessing.variants.sharpened` | `boolean` | `false` |
| `pproc_clahe` | `arguments.preprocessing.variants.clahe` | `boolean` | `true` |
| `pproc_blurSigma` | `arguments.preprocessing.parameters.blurSigma` | `number` | `0.5` |
| `pproc_sharpenSigma` | `arguments.preprocessing.parameters.sharpenSigma` | `number` | `1` |
| `pproc_sharpenM1` | `arguments.preprocessing.parameters.sharpenM1` | `number` | `1` |
| `pproc_sharpenM2` | `arguments.preprocessing.parameters.sharpenM2` | `number` | `2` |
| `pproc_brightnessLevel` | `arguments.preprocessing.parameters.brightnessLevel` | `number` | `1.2` |
| `pproc_claheWidth` | `arguments.preprocessing.parameters.claheWidth` | `number` | `8` |
| `pproc_claheHeight` | `arguments.preprocessing.parameters.claheHeight` | `number` | `8` |
| `pproc_claheMaxSlope` | `arguments.preprocessing.parameters.claheMaxSlope` | `number` | `3` |
| `pproc_normalizeLower` | `arguments.preprocessing.parameters.normalizeLower` | `number` | `1` |
| `pproc_normalizeUpper` | `arguments.preprocessing.parameters.normalizeUpper` | `number` | `99` |
## Swagger / OpenAPI
Auto-generated Swagger documentation is served at `http://{host}:{port}/docs`. Decorators on `ClassicController` enumerate each field, content type, and response code, enabling type-safe client generation. The `fastify-compress` middleware explicitly skips compressing `text/html` (Swagger UI) to mitigate rendering issues.