---
id: M003
title: "Domain + DNS + Per-Stage LLM Model Routing"
status: complete
completed_at: 2026-03-30T02:13:37.009Z
key_decisions:
  - D015: Subnet 172.32.0.0/24 for Chrysopedia network
  - D016: Ollama container for embedding model
  - nginx reverse proxy on nuc01 following XPLTD pattern
  - Thinking modality appends JSON instructions to system prompt instead of response_format
key_files:
  - backend/config.py
  - backend/pipeline/llm_client.py
  - backend/pipeline/stages.py
  - backend/tests/test_pipeline.py
  - .env.example
lessons_learned:
  - XPLTD domain setup flow: AdGuard rewrite (10.0.0.9) → nginx on nuc01 (proxy to ub01:port) → certbot SSL
  - AD DNS at 10.0.0.15/.16 may already have CNAME entries for domains — check before assuming manual setup needed
  - AdGuard API for DNS rewrites: POST /control/rewrite/add with {domain, answer} JSON body
  - Per-stage LLM config with getattr-based helper is cleaner than separate config classes
---

# M003: Domain + DNS + Per-Stage LLM Model Routing

**chrysopedia.com live with SSL, plus per-stage LLM model routing with thinking modality support — 59 tests pass.**

## What Happened

M003 delivered two independent workstreams across 2 slices.\n\n**S01 — Domain Setup** configured chrysopedia.com end-to-end: added DNS rewrites in AdGuard (chrysopedia.com + www → 10.0.0.9), created nginx reverse proxy config on nuc01 (proxy_pass to 10.0.0.10:8096 with security headers and rate limiting), and obtained a Certbot SSL cert. The AD DNS already had a CNAME for chrysopedia.com → dyndns.xpltd.co → public IP. https://chrysopedia.com is live with HTTP/2.\n\n**S02 — Per-Stage LLM Model Routing** added independent model/modality configuration for each pipeline stage (2-5). The LLMClient now supports 'chat' modality (standard JSON mode) and 'thinking' modality (appends JSON instructions to system prompt, strips <think>...</think> tags from output). A _get_stage_config helper reads per-stage settings. All 59 tests pass including a new think-tag stripping test with 7 cases. Changes deployed to ub01.

## Success Criteria Results

### 1. https://chrysopedia.com loads with valid SSL ✅\ncurl -sI returns HTTP/2 200 with valid cert (expires 2026-06-28)\n\n### 2. Internal DNS resolves chrysopedia.com ✅\nnslookup chrysopedia.com 10.0.0.10 returns 10.0.0.9\n\n### 3. Per-stage model/modality independently configurable ✅\n8 Settings fields, _get_stage_config helper, all stages wired\n\n### 4. Think-tag stripping handles <think> blocks ✅\nstrip_think_tags() tested with 7 cases: single, multiline, multiple, passthrough, empty, special chars, think-only\n\n### 5. All tests pass with updated mocks + new test ✅\n59/59 pass in 140s

## Definition of Done Results

| # | Item | Met | Evidence |\n|---|------|-----|----------|\n| 1 | chrysopedia.com resolves via AdGuard | ✅ | nslookup chrysopedia.com 10.0.0.10 returns 10.0.0.9 |\n| 2 | nginx proxies to ub01:8096 with SSL | ✅ | curl -sI https://chrysopedia.com returns HTTP/2 200 |\n| 3 | https://chrysopedia.com loads web UI | ✅ | Returns valid HTML |\n| 4 | Per-stage config fields in Settings | ✅ | 8 fields: llm_stage{2-5}_model and llm_stage{2-5}_modality |\n| 5 | LLMClient modality-aware | ✅ | complete() accepts modality and model_override |\n| 6 | Think-tag stripping | ✅ | strip_think_tags() tested with 7 cases |\n| 7 | Stages pass per-stage config | ✅ | All 4 stages call _get_stage_config |\n| 8 | .env.example documents vars | ✅ | Per-stage model/modality vars documented |\n| 9 | All tests pass | ✅ | 59/59 pass |

## Requirement Outcomes

No requirement status changes. Infrastructure and pipeline improvement milestone.

## Deviations

www.chrysopedia.com not covered by SSL cert — missing external CNAME record. AD DNS already had chrysopedia.com → dyndns.xpltd.co CNAME pre-configured.

## Follow-ups

Add CNAME record for www.chrysopedia.com at Namecheap and re-run certbot. Consider adding per-stage model settings to the .env on ub01 once the thinking model availability is confirmed.
