Loom / docs /DESIGN.md
deploy-space action
deploy 6158a00 (c)
b972454
|
Raw
History Blame Contribute Delete
14 kB
# Loom Model Panel: A Privacy-First Gateway for Uncorrelated Frontier-Model Judgment on Free Tiers
*System design document β€” 2026-06. All claims cross-checked against the code at the
commit that ships this file; constants quoted are the env-overridable defaults.*
## 1. Problem & motivation
A single LLM reviewing its own plans inherits its own blind spots. The highest-leverage
use of *other* frontier models is as **uncorrelated second opinions**: a panel of
models with different training lineages (Kimi, DeepSeek, GLM, Nemotron, Llama,
MiniMax…) judging one artifact produces failure-mode diversity a single model cannot.
Three constraints shape the system:
1. **Cost: zero.** Every model must come from recurring-free tiers (NVIDIA NIM free
credits, Cloudflare Workers AI, GitHub Models, OpenRouter `:free`). Free tiers are
throttled, flaky, and quota-capped β€” so reliability must be *engineered around* the
providers, not assumed from them.
2. **Privacy: per-user, verifiable.** The deployment model is **one instance per user,
the user's own provider keys, nothing shared** β€” privacy by isolation rather than by
access control. On top, a request-level **privacy router** refuses providers that
train on or log prompts.
3. **Operations: a single ephemeral container.** The target platform (Hugging Face
Spaces, free Docker tier) gives one container that is recycled on every deploy.
Anything that must survive β€” async jobs, metrics, templates, cache β€” needs explicit
durability machinery.
The result is a stdlib-only Python service (plus `httpx` and optionally
`huggingface_hub`) exposing a token-guarded HTTP API: one-shot panels
(`/api/critique`, `/api/panel`), whole-repo review (`files`/`repo_url` forms), and
multi-stage orchestrated pipelines (`/api/run` + templates).
## 2. Architecture overview
```
client ──HTTP──▢ BoundedThreadingHTTPServer (MAX_WORKERS=48, REQUEST_TIMEOUT_S=30)
β”‚ bearer auth (constant-time), body cap 8MB, 503/429 backpressure
β–Ό
request builders (panel / critique / run)
β”œβ”€ repopack: {files|repo_url} β†’ packed blob + per-judge context budgeting
β”œβ”€ privacy router: strict | fallback | off
└─ prompt cache (exact-match, per-space)
β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
sync (asyncio.run) async JobRunner (one background event loop)
β”‚ β”‚ durable: JobStore (atomic file + HF Dataset mirror)
β–Ό β–Ό reload-on-boot; MAX_INFLIGHT_JOBS=200
route_judge / orchestrator stages
β”‚ per logical model:
β–Ό
SlotScheduler ──▢ provider hosts (NVIDIA / Cloudflare / GitHub / OpenRouter)
rotation Β· pacing Β· cooldown/blacklist Β· cross-provider failover
β”‚
β–Ό every call β†’ per-profile MetricStore (HF-mirrored)
```
Two execution planes share one substrate. The **synchronous plane** runs a request to
completion on a bounded HTTP worker via `asyncio.run`. The **asynchronous plane**
(`{"async": true}`) hands work to a single long-lived background event loop in
`JobRunner`, returns a `job_id` immediately, and the client polls `GET /api/jobs/<id>`
for partial results. Both planes call the same `route_judge`/scheduler/metrics core, so
routing, privacy, caching, and telemetry behave identically regardless of plane.
The canonical request is for a **logical model** (e.g. `kimi-k2.6`); the scheduler maps
it to an ordered list of physical `(provider, model)` hosts and bounces across them on
failure. Catalogs (`providers_catalog.json`, `models_catalog.json`, `benchmarks.json`,
`reasoning_catalog.json`, `panel.json`) are the single source of truth β€” adding a model
or provider is a config edit, not a code change.
## 3. Subsystems
### 3.1 Routing & cross-provider failover (`scheduler.py`)
`SlotScheduler` owns rotation and resilience. `pick_slot_from(candidates, role,
exclude_providers)` returns the best eligible host for a logical model, where eligible =
not blacklisted, not in cooldown, not excluded. Ranking favors the least-recently-attempted
provider (rotation), then healthier model families, then freshness β€” so a flaky provider
drifts to the back instead of being hammered. `route_judge` (`jobs.py`) drives the bounce
loop: on any 429/5xx/empty/timeout it records a penalty (cooldown or blacklist), emits a
metric, and re-picks the next provider hosting the *same logical model*. Retries are
cross-provider by default (`JUDGE_RETRIES=0`): we move hosts rather than re-hit a failing
one. **Pacing** reserves each provider's next allowed call time under a lock
(`wait_for_provider_pacing`), so concurrent callers across both planes stagger to
distinct slots and stay under per-provider RPM.
### 3.2 Privacy router (`jobs.py` `_privacy_tiers` / `_pick_tiered`, `providers.slot_is_privacy_safe`)
Each provider carries a `trains_on_data` posture (`true`/`false`/`"per-model"`); a host is
privacy-safe iff it does not train on or log submissions. OpenRouter `:free` routes require
training consent, so they are `per-model` β†’ unsafe; HF Inference is a proxy whose posture is
the routed backend's. Three request modes: **strict** (default) drops every unsafe host and
returns `privacy_blocked` rather than leak; **fallback** tries safe hosts first and an unsafe
host only as a last resort; **off** uses all. `PRIVACY_MODE` locks an instance to strict;
`no_store` runs a job without persisting its content anywhere.
### 3.3 Prompt cache (`promptcache.py`)
A per-space exact-match cache keyed by `sha256(logical, system_prompt, user_msg, max_tokens,
reasoning, research, privacy)`. **Privacy is part of the key** β€” a result produced under
`privacy=off` can never be served to a `strict` request. In-memory LRU (`CACHE_MAX=512`) +
atomic disk mirror, TTL `CACHE_TTL_S=24h`. Only successful calls are cached; `no_store`
skips writes. A hit returns prior output with zero provider calls and transmits nothing.
### 3.4 Durable async jobs (`jobs.py` `JobRunner`, `jobstore.py`)
Free containers recycle on deploy, which previously killed long async runs. `JobStore`
writes each job to `data/jobs/<id>.json` via atomic tmp-rename on every state transition and
mirrors to a private HF Dataset, so jobs survive both crashes and redeploys. On boot the
runner re-hydrates: panel jobs reschedule any unfinished judge (the `_exec` block carries the
prompt/budgets needed to resume), and mid-flight orchestration runs are marked `interrupted`.
`MAX_INFLIGHT_JOBS=200` caps simultaneously-running jobs (the async plane bypasses the HTTP
worker cap); `JOB_TTL_S=6h` prunes finished jobs.
### 3.5 Codebase ingestion (`repopack.py`)
Turns `{path: content}` or a fetched `repo_url` tarball (GitHub codeload, stdlib only,
`REPO_MAX_BYTES=50MB`) into one annotated blob: binaries/lockfiles/`node_modules` filtered,
per-file `===== FILE: =====` delimiters, token estimate chars/3 for code. Each judge gets a
pack **fitted to its own context window** (`ctx` per host in `models_catalog.json`); when a
repo exceeds a judge's budget, low-value files (generated/bulk) are dropped first, tests and
docs last, and the result reports `coverage` so partial-context reviews are honest. A
`diff_mode {base_ref, head_ref}` packs only the changed files plus a unified diff.
### 3.6 Orchestrator & template library (`orchestrator/`, `orchestrator/templates/`)
Beyond one-shot panels, `/api/run` executes multi-stage **schematics**: stages run
sequentially, a stage may **fan out** (one shard per item in a JSON list a prior stage
emitted, shards spread across providers via shared in-flight exclusion), and a **judge loop**
retries the final body up to `max_rounds` if rules/criteria fail. The built-in power
templates β€” `repo_audit`, `design_doc`, `redteam`, `panel_debate` β€” each map β†’ fan-out β†’
synthesize β†’ judge, and bake in hard-won lessons (e.g. `repo_audit` quarantines
confident-but-unverifiable concurrency claims into a "False-positive watch" section).
File-output mode reconstructs a multi-file artifact tree from marker blocks.
### 3.7 Three-layer judge (`judge/`)
Output hardening runs in order and merges reports: **Layer 1** declarative rules
(`engine.py`: min/max words, required sections, banned phrases, regex); **Layer 2** code
plugins (`citation_integrity`, `url_health`, `json_parses`); **Layer 3** LLM-as-judge
natural-language criteria. Hard fails trigger a retry round; soft flags annotate.
### 3.8 Per-profile metrics (`metrics.py`)
Every call emits a metric tagged with a `profile`, so cost/throttle/latency are tracked per
identity, not globally. Rolling per-provider call/token windows (pruned on write, 24h) feed a
**budget guard** that cools a provider when a profile hits its published daily ceiling.
`HOT_WINDOW=5000` events in memory; flushed every `FLUSH_EVERY=25` and mirrored to the HF
Dataset. Outputs are sampled only if `METRICS_SAMPLE_OUTPUTS=1` (off by default), and never
for `no_store` jobs. Exposed via `GET /api/stats`, `/api/metrics?profile=`, `/api/roster`.
## 4. Guarantees
- **β‰₯2 privacy-safe frontier models, always.** `panel.roster()` joins the benchmark catalog
with live host health and the privacy classification; `/health.frontier_ok` is true iff at
least two *frontier* logical models have a privacy-safe host routable right now. It reports
false honestly rather than silently routing to an unsafe or non-frontier host.
- **Privacy by isolation + verifiable routing.** One instance per user with their own keys;
strict-by-default routing; `no_store` for zero retention. Backpressure responses (503/429)
carry no prompt content.
- **Durability across crash and redeploy.** Atomic local writes + private HF Dataset mirror;
reload-on-boot resumes unfinished panel judges.
## 5. Threat model & security
- **Auth.** Every non-health route is bearer-gated with a constant-time compare
(`hmac.compare_digest`); an unset `CRITIQUE_TOKEN` refuses service (503) rather than
serving open.
- **Resource exhaustion / DoS.** `BoundedThreadingHTTPServer` caps concurrent sync workers
(`MAX_WORKERS=48`) and returns 503+`Retry-After` over capacity without spawning a thread;
`REQUEST_TIMEOUT_S=30` drops slowloris connections; `MAX_INFLIGHT_JOBS=200` caps the async
plane with 429+`Retry-After`; `MAX_BODY_BYTES=8MB` bounds request size. Per-user instances
make these per-tenant by construction; the frontend can tune the env knobs per user.
- **Data at rest.** Job snapshots and the cache live on the instance's local disk and the
user's *private* HF Dataset. `no_store` opts a job out entirely (trading deploy-resume for
zero retention). Metrics store no prompt/output text unless explicitly enabled.
- **Provider trust.** The privacy router is the control that keeps prompts off
training/logging hosts; `:free` and unpinned-proxy routes are classified unsafe.
- **Secret hygiene.** Keys are Space secrets, never committed; `data/` is gitignored.
## 6. Evaluation
- **Offline simulation (zero quota).** A `MOCK_MODE` provider yields deterministic,
fault-injectable responses; nine sims exercise rotation/failover/budget/profile isolation
(`sim_scenarios`), research ReAct (`sim_research`), crash-resume (`sim_persistence`),
orchestration + fanout diversity (`sim_orchestrator`), the privacy router (`sim_privacy`),
the cache incl. the privacy-key (`sim_cache`), ingestion + budget fitting (`sim_repopack`),
the template library (`sim_templates`), and the concurrency bounds β€” 503/slowloris/429
(`sim_server_limits`). All green.
- **Live verification.** Every roster slug is probed (16-token call) before catalog entry
(`docs/ROSTER-2026-06.md`); failures are recorded and excluded, not shipped broken.
- **Whole-repo reality check.** A 163k-token review of this codebase confirmed strong hosts
(Kimi, MiniMax) genuinely reason over the full repo, surfaced two real P1 bugs since fixed
(cache privacy bypass; `privacy_blocked` fallthrough) and several false positives (later
disproved), and exposed reliability variance (qwen3.5-397b returned empty content β†’ demoted
from the default panel).
## 7. Limitations & future work
- **`/api/run` does not context-fit the prompt.** The repo-pack per-judge budgeting lives in
the `/api/panel` `files` form, not the orchestrator path β€” so a large prompt sent through a
template hits small-context hosts (e.g. Cloudflare llama-3.3, 24k) with **413** and only
survives via cross-provider failover to big-context hosts. Fix: have the scheduler skip
hosts whose `ctx` cannot fit the estimated input, or extend ingestion budgeting to `/api/run`.
- **Per-request event loop on the sync plane.** Each sync request runs `asyncio.run` with a
fresh `httpx.AsyncClient`, discarding connection pooling; acceptable under the worker cap,
but a shared loop/client would be more efficient at scale.
- **Benchmarks are indicative.** `benchmarks.json` scores are hand-curated and dated, used
only to order the roster and define "frontier" β€” not an authoritative ranking.
- **Provider breadth.** Current pool is four providers; expanding to Groq, Cerebras, Mistral
La Plateforme, and HF Inference (each with a privacy block + live probe) widens the
frontier-safety margin (planned).
## 8. Reproducibility
Duplicate the Hugging Face Space, set your own provider keys as Space secrets
(`NVIDIA_API_KEY`, `CF_API_TOKEN`+`CF_ACCOUNT_ID`, `GITHUB_TOKEN`, `OPENROUTER_API_KEY`) and
a `CRITIQUE_TOKEN`; optionally `METRICS_HF_REPO`/`JOBS_HF_REPO` for cross-deploy durability.
The service boots with whatever keys are present and reports its live posture at `/health`
and `/api/roster`. Everything is stdlib + `httpx`; no build step. See `README.md` for the
API surface and `PRIVACY.md` for the full privacy/off-switch matrix.