Skip to main content

Janus — Project Charter

Status: draft for review · Owner: Ris Adams · Last updated: 2026-10-11

1. One-line definition​

Janus is a local proxy, written in Go, that sits between Claude Code and the Anthropic API and uses a local model running in LM Studio to do as much of the work as it safely can, so that Claude is only paid for the thinking that needs Claude.

The name is the design: one face toward the local model, one toward the upstream model, and a single doorway that every request passes through.

2. Problem​

Claude Code sends everything upstream: the hard reasoning, but also the session titles, the summaries, the 4,000-line test logs, the boilerplate subagent work, and whatever secrets happen to sit in the files it reads. A capable local model is idle on the same machine. Nothing connects the two.

blush proved the proxy position works (Claude Code runs unmodified through ANTHROPIC_BASE_URL), but blush only watches. Janus acts.

3. Operating model​

Claude is the brain. The local model is the workhorse. Janus decides which one a piece of work needs, local-first.

┌──────────────────────── Janus ────────────────────────┐
│ │
Claude Code ──────▶│ classify ─▶ guard ─▶ route ─┬─▶ compress ─▶ upstream ─┼──▶ Anthropic API
(Messages API) │ │ │
▲ │ └─▶ local ─▶ validate ────┼──▶ LM Studio
│ │ │ fail │
│ │ └──▶ escalate ─────┼──▶ Anthropic API
│ │ │
│ │ ledger: every decision, token, and dollar recorded │
│ └───────────────────────────────────────────────────────┘
│
└── janus mcp: worker tools Claude calls to hand subtasks to the local model

Four mechanisms, combined:

MechanismWhat it doesWhere the saving comes from
RouteWhole request classes (utility calls, pinned subagents) go to the local model and never reach Claude.Requests avoided entirely.
EscalateEligible requests are tried locally first; Janus validates the answer and falls back to Claude before the client sees anything.Requests avoided when local is good enough.
DelegateClaude, as orchestrator, calls Janus worker tools (digest these files, search and summarize, draft this) that run on the local model.Bulk content never enters Claude's context.
CompressBefore an upstream call, the local model shrinks what Claude doesn't need verbatim (large tool results, stale context).Fewer input tokens per upstream request.

4. Goals and success measures​

Targets are proposals. They get replaced with real numbers once the baseline (roadmap M1) exists.

#GoalMeasured byProposed target
G1Cut upstream spendCache-weighted upstream cost per working session vs. the measured baseline≥ 30% lower, with no drop in task success
G2Keep private data localCount of protected items found in upstream request bodies, over a test corpus and in real-use auditZero. Fails closed.
G3Keep working when upstream doesn'tA session continues through a simulated outage / 429 storm / no networkDegraded but usable, clearly labelled
G4LearnEvery request has a decision record explaining what Janus did and why; local and upstream answers can be compared offline100% of requests explained
G5Never be worse than no JanusWith every feature off: byte-equivalent pass-through. Added latency on pass-through.Equivalent; p50 overhead < 10 ms

5. Design principles​

  1. Precedence when goals conflict: privacy → correctness → availability → savings. A protected request is never sent upstream to keep a session alive. A saving is never taken at the cost of a broken turn.
  2. Measure before optimizing. Every feature ships behind a switch and reports its own effect in the ledger. A feature that can't show a net saving stays off.
  3. Cache-aware or not at all. Claude Code leans on prompt caching, where cached input is billed at a tenth of the base rate. A transform that rewrites an already-cached prefix can raise the bill. Savings are always counted in cache-weighted cost, never raw tokens.
  4. Fail open to pass-through. If Janus, the local model, or any stage misbehaves, the request goes upstream untouched (unless principle 1 forbids it).
  5. Transparent to the client. Claude Code is not patched or wrapped. It is pointed at Janus with environment variables and sees a normal Anthropic API.
  6. Boring to run. One static binary, one config file, loopback only, standard library first.

6. Scope​

In scope

  • Inbound: Anthropic Messages API, as Claude Code speaks it (streaming, tool use, thinking, prompt caching, token counting).
  • Backends: Anthropic API upstream; LM Studio locally.
  • Single user, single machine, macOS first; Linux and Windows builds.
  • Routing, local-first escalation, delegation tools, context compression, redaction and local-only policy, fallback, capture logs, cost ledger, shadow comparison and replay.

Out of scope (for 1.0)

  • Multi-user gateway, hosted service, API keys of its own, billing.
  • Inbound OpenAI-format clients. (A backend interface keeps the door open; nothing is built for it.)
  • Other local runtimes (Ollama, llama.cpp server, vLLM) and other upstream providers (Bedrock, Vertex).
  • Loading, downloading, or tuning local models. Janus uses what LM Studio has loaded.
  • A GUI. The interface is the CLI, the config file, and the logs.

7. Relationship to blush​

Janus is a new codebase that ports from blush rather than forking it.

From blushDisposition
Streaming pass-through, hop-by-hop header handling, auth pass-throughPort into internal/proxy
Markdown capture logs with redacted headersPort; add 0600 permissions and retention
Config file + flag + env layering, first-run starter configPort the pattern; new multi-backend schema
Startup banner that prints the right env varsPort; grows into janus doctor
CI (gofmt, vet, build, test matrix), stdlib-only, httptest stylePort as-is
Single upstream, flat package main, body treated as opaque bytesReplace: multiple backends, staged pipeline, typed Messages/SSE model

8. Risks​

#RiskWhy it mattersMitigation
R1Transforms break prompt cachingCould make Janus cost more than it savesDeterministic, memoized transforms; net-savings guard; cache-weighted ledger (M5)
R2Mixed-origin conversations are rejected upstreamThinking blocks carry signatures; a locally produced turn inside a Claude conversation may not replaySpike in M0; route whole request classes and whole subagent conversations rather than individual turns; integrity guard (M3)
R3Local models can't drive Claude Code's tool loopDozens of tool schemas and a very large system prompt strain local context, speed, and tool-call accuracySpike in M0 on real hardware; start with tool-free request classes; validators + escalation
R4Triage costs more time than it saves moneyA local classification pass on every request adds latencyDeterministic rules first; model triage only for ambiguous cases, under a latency budget
R5Savings are smaller than hopedMost of Claude Code's input is already cached at a tenth of the priceBaseline first (M1); pick targets by measured share of cost, not intuition
R6Claude Code's traffic changes shapeClassification by fingerprint is brittle across releasesClassify on stable signals where possible; unknown classes pass through; fixture suite refreshed per release
R7Rehydrating redacted values corrupts a tool callA placeholder split across stream chunks, or altered by the model, yields a wrong editStreaming-aware rehydration; unresolved placeholders block the turn rather than pass silently

9. Open decisions​

These change details, not direction. Defaults are assumed until decided.

#DecisionDefault assumed
D1Claude Code auth: API key or subscription (OAuth)? Decides whether "spend" is dollars or rate-limit headroom.Support both by passing auth through untouched; ledger reports tokens and estimated dollars
D2Hardware and candidate local modelsDecided by the M0 benchmark spike: Apple M5 Max, 36 GB; qwen/qwen3.8-27b (MLX, 4-bit) for subagent conversations only (models)
D3Keep blush's zero-dependency rule?Stdlib first; each exception recorded as an ADR
D4LicenseMIT, matching blush
D5Module path casing (risadams/Janus vs janus)Lowercase github.com/risadams/janus