Reverts the MODEL_CHOICES/MODEL_LABELS additions from 2609ab5. The .27 models are
gone from the family console, so the two dropdown entries would have been dead
options that error when selected (and they were offered to every user).
The list is back to exactly the original 10 cloud entries. Existing sessions that
still reference a removed id fall back to the default rather than erroring, since
the server validates the id against MODEL_CHOICES.
docs/dsh-architecture.md: drop the 'Local Ollama models' section, and add a warning
that auto/* are OmniRoute *routers* — when OmniRoute's pool is exhausted they fail
with 'Maximum combo retry limit reached' and DSH shows it as 'Model returned no
response', which is not a DSH bug.
Verified on .13: all four instances report 10 models with no ollama entries, and a
real streamed reply comes back through DSH on an explicit model.
159 lines
7.5 KiB
Markdown
159 lines
7.5 KiB
Markdown
# DeepSeek Harness (DSH) — Architecture & Integration Spec
|
|
|
|
## 1. System Overview
|
|
|
|
**DeepSeek Harness (`dsh`)** is a single-user, multi-tenant streaming chat web application that serves as the AI chat interface for Family Home Lab members. Instead of running a single shared application with multi-user authentication, DSH is deployed as **isolated, per-user container instances** on `.13`.
|
|
|
|
The portal (`console.lab.audasmedia.com.au`) embeds each user's dedicated DSH instance inline via iframe or links directly to their subdomains (`https://dsh-{username}.lab.audasmedia.com.au`).
|
|
|
|
---
|
|
|
|
## 2. Multi-Tenancy & Port Allocation
|
|
|
|
Each family member gets a dedicated DSH container instance running on host `.13`:
|
|
|
|
| User | Subdomain | Host Port | Container Name | Host Workspace Directory |
|
|
| :--- | :--- | :--- | :--- | :--- |
|
|
| **Sam** | `dsh-sam.lab.audasmedia.com.au` | `3081` | `dsh-sam` | `/mnt/data/family-home-lab/dsh/sam` |
|
|
| **Jo** | `dsh-jo.lab.audasmedia.com.au` | `3082` | `dsh-jo` | `/mnt/data/family-home-lab/dsh/jo` |
|
|
| **Harry** | `dsh-harry.lab.audasmedia.com.au` | `3083` | `dsh-harry` | `/mnt/data/family-home-lab/dsh/harry` |
|
|
| **Finn** | `dsh-finn.lab.audasmedia.com.au` | `3084` | `dsh-finn` | `/mnt/data/family-home-lab/dsh/finn` |
|
|
|
|
### Reverse Proxy & Routing
|
|
- Caddy on `.35` terminates HTTPS for `dsh-*.lab.audasmedia.com.au` and proxies traffic to `.13:{port}`.
|
|
- Portal `tools.py` dynamically resolves the user's DSH URL via `_dsh_for_user(user)` based on their authenticated portal session.
|
|
|
|
---
|
|
|
|
## 3. Storage & Session Persistence
|
|
|
|
Chat history and sessions are stored directly as JSON files inside each container's `/workspace/sessions/` directory (mapped to host `/mnt/data/family-home-lab/dsh/{user}`):
|
|
|
|
### Session Directory Structure
|
|
```
|
|
/workspace/
|
|
├── sessions/
|
|
│ ├── main.json
|
|
│ ├── session-172840192.json
|
|
│ └── ...
|
|
└── uploads/
|
|
```
|
|
|
|
### Session File Schema (`sessions/{id}.json`)
|
|
```json
|
|
{
|
|
"id": "session-172840192",
|
|
"name": "Project discussion",
|
|
"updated": "2026-10-08T19:30:00Z",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"content": "Summarise the home lab setup",
|
|
"timestamp": 172840191
|
|
},
|
|
{
|
|
"role": "assistant",
|
|
"content": "The family home lab consists of...",
|
|
"timestamp": 172840192
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Multi-Model Routing & External Integration Table
|
|
|
|
DSH routes user requests across specialized AI services and local house infrastructure:
|
|
|
|
| Function | Backend Endpoint | Model / Engine | Environment Variable |
|
|
| :--- | :--- | :--- | :--- |
|
|
| **Text Chat (SSE Stream)** | `http://192.168.20.13:20129/v1` | `auto/best-chat` (OmniRoute) | `DSH_LLM_BASE`, `DSH_LLM_MODEL` |
|
|
| **Vision (Image Ingest)** | `https://openrouter.ai/api/v1` | `openai/gpt-5` | `DSH_VISION_BASE`, `DSH_VISION_MODEL` |
|
|
| **Speech-to-Text (STT)** | `http://192.168.20.13:5000/transcribe` | `faster-whisper` (`voice_whisper`) | `DSH_WHISPER_URL` |
|
|
| **Voice Agent (Action/TTS)** | `http://192.168.20.13:8501/voice` | Jervis Voice Agent | `DSH_VOICE_AGENT_URL` |
|
|
| **Image Generation** | `https://openrouter.ai/api/v1` | `openai/gpt-image-1` | `DSH_IMAGE_MODEL` |
|
|
| **Video Generation** | `https://openrouter.ai/api/v1` | `z-ai/glm-5v-turbo` | `DSH_VIDEO_MODEL` |
|
|
|
|
---
|
|
|
|
## 5. Model Selection (the chat dropdown)
|
|
|
|
**The model list is hardcoded in `dsh/app.py`** (`MODEL_CHOICES`, ~line 660). DSH does
|
|
**not** discover models from OmniRoute. `GET /api/models` simply returns that list
|
|
plus `MODEL_LABELS` for the friendly names.
|
|
|
|
Consequence: a model can exist in OmniRoute and still be invisible in DSH until it
|
|
is added to `MODEL_CHOICES` **and** `MODEL_LABELS`.
|
|
|
|
A chosen model is validated server-side (`if active_model not in MODEL_CHOICES:
|
|
active_model = LLM_MODEL`), so an unlisted id silently falls back to the default.
|
|
That also means removing an id is safe: sessions that still reference it fall back
|
|
rather than erroring.
|
|
|
|
### Adding a model
|
|
|
|
1. Confirm it works through OmniRoute first — `curl http://127.0.0.1:20129/v1/models`
|
|
and then a real `chat/completions` call with a short timeout.
|
|
2. Add the id to `MODEL_CHOICES` and a friendly string to `MODEL_LABELS` in
|
|
`dsh/app.py`.
|
|
3. Deploy: copy `app.py` to `.13:/home/sam/Docker/Containers/dsh/`, then
|
|
`docker compose build && docker compose up -d` in that directory (the image
|
|
bakes `app.py` via `COPY . .`, so a rebuild is required — restarting is not enough).
|
|
4. Verify: `curl -s http://127.0.0.1:3081/api/models` should list the new id.
|
|
|
|
### The default model is not necessarily a working model
|
|
|
|
`DSH_LLM_MODEL` (set to `auto/best-chat` in `docker-compose.yml`) is the default.
|
|
The `auto/*` entries are **OmniRoute routers**, not providers: if OmniRoute's
|
|
candidate pool is exhausted it returns `service_unavailable` / "Maximum combo
|
|
retry limit reached", and DSH surfaces that as *"Model returned no response"*.
|
|
That is an OmniRoute-side failure, not a DSH bug — check it by calling OmniRoute
|
|
directly, and prefer an explicit model id (e.g. `deepseek/deepseek-v4-flash`)
|
|
when testing whether DSH itself is healthy.
|
|
|
|
---
|
|
|
|
## 6. API Endpoints & Request Flow
|
|
|
|
### Primary API Routes (`dsh/app.py`)
|
|
|
|
| Method | Route | Description |
|
|
| :--- | :--- | :--- |
|
|
| `GET` | `/` | Serves main single-page chat UI (`templates/chat.html`). |
|
|
| `POST` | `/chat/stream` | Primary chat endpoint. Receives `{message, session_id}`,Streams response via Server-Sent Events (SSE). |
|
|
| `POST` | `/transcribe` | Uploads `.wav`/`.mp3`/`.m4a` file; forwards to `voice_whisper` (`:5000`); returns transcribed text. |
|
|
| `POST` | `/upload-image` | Uploads image; forwards to Vision model (`openai/gpt-5`) with user prompt; returns image description. |
|
|
| `POST` | `/voice-agent` | Forwards prompt to Jervis Voice Agent (`:8501`); triggers house audio/action. |
|
|
| `POST` | `/generate-image` | Generates image via OpenRouter API and saves output to workspace. |
|
|
| `POST` | `/generate-video` | Generates video via OpenRouter API and saves output to workspace. |
|
|
| `GET` | `/sessions` | Returns list of all session summaries for the current user. |
|
|
| `POST` | `/sessions/new` | Creates a new conversation session. |
|
|
| `POST` | `/sessions/{id}/delete` | Deletes a conversation session file. |
|
|
|
|
---
|
|
|
|
## 8. Embedding & Security Headers
|
|
|
|
To support seamless inline framing inside the Family Home Lab Console (`/tool/dsh`):
|
|
- **Content Security Policy (CSP)**: `frame-ancestors https://console.lab.audasmedia.com.au`
|
|
- **X-Frame-Options**: Explicitly omitted (not set to `DENY` or `SAMEORIGIN`), allowing Caddy and Portal to frame DSH cleanly.
|
|
|
|
---
|
|
|
|
## 9. Environment Variables Reference
|
|
|
|
| Variable | Default Value | Purpose |
|
|
| :--- | :--- | :--- |
|
|
| `DSH_USER` | `user` | Name of the user (e.g., `sam`, `jo`). |
|
|
| `DSH_SLOT` | `Welcome to DeepSeek Harness` | Subtitle/greeting shown in the header. |
|
|
| `DSH_LLM_BASE` | `http://192.168.20.13:20129/v1` | OmniRoute OpenAI-compatible base URL. |
|
|
| `DSH_LLM_MODEL` | `auto/best-chat` | Default model passed to OmniRoute. |
|
|
| `DSH_LLM_KEY` | `""` | Optional API key for OmniRoute. |
|
|
| `DSH_SYSTEM_PROMPT` | `"You are {USER_NAME}'s helpful DeepSeek assistant..."` | Custom system prompt per user instance. |
|
|
| `DSH_VISION_BASE` | `https://openrouter.ai/api/v1` | OpenRouter base URL for multimodal vision. |
|
|
| `DSH_VISION_MODEL` | `openai/gpt-5` | Multimodal model for image analysis. |
|
|
| `DSH_WHISPER_URL` | `http://192.168.20.13:5000/transcribe` | Local faster-whisper STT service. |
|
|
| `DSH_VOICE_AGENT_URL` | `http://192.168.20.13:8501/voice` | Jervis Voice Agent execution endpoint. |
|
|
| `DSH_HISTORY_DIR` | `/workspace` | Mount point for history and session storage. |
|