The dropdown is built from the hardcoded MODEL_CHOICES list in app.py, not from OmniRoute, so 'ollama/qwen2.5*' existed upstream but could never appear in DSH. - add ollama/qwen2.5:14b and ollama/qwen2.5-coder:14b to MODEL_CHOICES + MODEL_LABELS - labels note that Ollama must be started first, since it now boots off on .27 - docs/dsh-architecture.md: new section on model selection, incl. the rebuild requirement (app.py is baked into the image via COPY . .) and the ordering trap that a model can be live in OmniRoute yet invisible in DSH Verified: /api/models lists both ids on 3081-3084, and real streamed replies come back from both local models through DSH.
7.2 KiB
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
.35terminates HTTPS fordsh-*.lab.audasmedia.com.auand proxies traffic to.13:{port}. - Portal
tools.pydynamically 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)
{
"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.
Adding a model
- Confirm it works through OmniRoute first —
curl http://127.0.0.1:20129/v1/modelsand then a realchat/completionscall with a short timeout. - Add the id to
MODEL_CHOICESand a friendly string toMODEL_LABELSindsh/app.py. - Deploy: copy
app.pyto.13:/home/sam/Docker/Containers/dsh/, thendocker compose build && docker compose up -din that directory (the image bakesapp.pyviaCOPY . ., so a rebuild is required — restarting is not enough). - Verify:
curl -s http://127.0.0.1:3081/api/modelsshould list the new id.
Local Ollama models
The .27 models are exposed by OmniRoute as ollama/qwen2.5:14b and
ollama/qwen2.5-coder:14b (ollama-local/… also exists but is duplicated).
Because Ollama now starts off at boot, these fail until it is started from the
family console; hence the needs Ollama on label. Inference is CPU-only on .27,
so expect it to be slower than the cloud routes.
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
DENYorSAMEORIGIN), 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. |