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.