2,553 prompts from top AI collections
← Back to Market
[rderaison/bromure] Local Model Inference for Bromure Agentic Coding — Integration Plan
ChatGPT API Leak/ChatGPT
23,406 characters
# Local Model Inference for Bromure Agentic Coding — Integration Plan Status: draft / design agreed, implementation not started. Goal: let Bromure run coding models **locally on the Mac host** and expose them to the agents running **inside the disposable Linux VMs**, with an MLX-based engine, easy no-conversion model downloads, and a cloud↔local routing layer. --- ## 0. TL;DR - Inference runs as a **host (macOS) subprocess** — the Linux guests have no GPU/Metal/MLX access, so this is the only option. This matches the "sub-process + proxy" instinct. - Engine: **`vllm-mlx`** (MLX backend, OpenAI **and Anthropic** compatible, agent-grade tool-calling, prefix caching, continuous batching). This is the engine that actually satisfies "only MLX" *and* fits agentic coding. - One shared engine, reachable through **two front doors**: 1. **Env-var pin** inside the VM (`ANTHROPIC_BASE_URL` / `OPENAI_BASE_URL`) — explicit, always-local, no interception. 2. **MITM interception** — transparent; the agent targets `api.anthropic.com` and we re-route. - **Routing is a top-level, per-VM axis** (`Cloud | Local | Hybrid`), independent of the existing **Fusion** (host-credential) axis. - **Curated MLX model catalog** (bundled + remote-refreshable JSON) with RAM-fit gating and tool-calling verification, plus a "paste any HF MLX repo" escape hatch. - Users **never convert or self-distribute models** — pre-converted MLX weights are pulled straight from Hugging Face (`mlx-community/...` and friends). --- ## 1. Why these choices (context) ### 1.1 llama.cpp ≠ MLX llama.cpp has **no MLX backend** — on Apple Silicon it uses its own **Metal** backend and the **GGUF** format. "Integrate llama.cpp for MLX" is not achievable; llama.cpp would give Metal, not MLX. The MLX runtimes are `mlx-lm`, Ollama's new MLX engine (preview, narrow architecture support), and vLLM's MLX path. ### 1.2 vLLM on Apple Silicon is now an MLX engine Two flavors exist: - **`vllm-metal`** — official community plugin (Docker + vLLM project) running vLLM with **MLX as the primary compute backend**. v0.2.0 (Apr 2026) added a unified paged Metal attention kernel (~83× TTFT, 3.6× throughput vs v0.1.0). arm64 Python 3.12 only. - **`vllm-mlx`** (waybarrios) — community vLLM-style server, MLX backend, ~400+ tok/s, continuous batching, paged KV cache, **prefix caching**, SSD-tiered cache, MCP tool calling, exposing **both OpenAI `/v1/*` and Anthropic `/v1/messages`** from one process. Explicitly "works with Claude Code" and handles structured tool calls that break on other quantized frameworks. **We standardize on `vllm-mlx`** because, for agentic coding: 1. It's MLX (honors the requirement; llama.cpp can't). 2. **Anthropic-native API** → drop-in for Claude-shaped agents, no OpenAI↔Anthropic shim. This is the decisive property for the MITM/hybrid path (no protocol translation = tool-calling stays intact). 3. **Reliable quantized tool-calling + prefix caching** — the two things coding agents need (tools that don't break; cheap re-sends of huge system prompts). 4. Continuous batching → multiple VMs can share one engine. Tradeoffs to accept: younger community project (two near-identical forks: `waybarrios`, `raullenchai`), Python-based (vendor a venv or require install), batching edge only matters under concurrency. **Fallback engine:** keep the engine *pluggable* and offer **Ollama** as the "I just want to pull any model, easiest UX" path (GGUF/Metal, broadest coverage). `mlx-lm` (Apple-official, OpenAI-only) and a bundled `llama-server` (Metal, not MLX) are lower-priority alternatives. ### 1.3 Inference must run on the host Virtualization.framework gives the Linux guest **no GPU / Metal / MLX**. The engine runs on macOS; guests reach it over the network. The agents live *inside* the guest (via the `vm exec` path), so the entire integration is about wiring guest → host engine. --- ## 2. Architecture ``` macOS host ┌─────────────────────────────────────────────────────────┐ │ InferenceService (Swift) │ │ └─ supervises vllm-mlx subprocess │ │ bound to 127.0.0.1:<engine-port> (loopback only) │ │ serves /v1/* and /v1/messages │ │ │ │ MITM engine (per-profile) Catalog + downloads │ │ └─ Routing: Cloud|Local|Hybrid (HF hub) │ │ └─ Fusion: host-cred swap │ └───────▲───────────────────────────▲──────────────────────┘ │ vsock 8446 (new) │ on-host 127.0.0.1 call │ (bridge → guest TCP) │ ┌───────┼───────────────────────────┼──────────────────────┐ │ Linux guest │ │ │ bromure-vm-bridge.py │ │ │ 127.0.0.1:11434 ──────────────┘ (Path 1 door) │ │ │ │ coding agent │ │ Path 1: ANTHROPIC_BASE_URL=http://127.0.0.1:11434 │ │ Path 2: targets api.anthropic.com → MITM re-routes │ └───────────────────────────────────────────────────────────┘ ``` **One engine, two doors, shared model load.** Path 1 reaches the engine via the vsock bridge; Path 2 (MITM) reaches it on-host at `127.0.0.1:<engine-port>`. ### 2.1 Why the vsock bridge (not the NAT gateway IP) The guest *could* reach a host server at the NAT gateway `192.168.64.1` (`VMNetSwitch.swift`), but that exposes the engine to every VM on the subnet and shifts if the subnet walks. The AC variant already pumps vsock → guest-TCP for the mitm/ssh/aws services (ports 8443/8444/8445 in `MitmEngine.register()`, forwarded by `bromure-vm-bridge.py`). **Adding one more vsock port (8446) is a copy-paste of that pattern** and gives a per-VM, loopback-only endpoint. Recommended. --- ## 3. Component plan (host) ### 3.1 `InferenceService` (new Swift module) Lives alongside `HostServices`, or in the AC target where the agent lives. - **Process supervisor** — spawn/supervise the `vllm-mlx` server subprocess. - Lazy-start on first request; readiness via `/v1/models` (or `/health`). - Auto-restart on crash; graceful teardown with the app. - **Bind `127.0.0.1:<engine-port>` only** — never `0.0.0.0`. - **Decided: a single shared instance serves all VMs** — model weights load once (decisive for a ~100 GB GLM-5.2), continuous batching + prefix caching are shared. Tradeoff accepted: VMs share one model/KV namespace, no per-VM isolation. - **Binary/runtime acquisition** — `vllm-mlx` is Python (arm64 3.12). **Decided: vendor a pinned arm64 Python 3.12 venv inside the app bundle.** Reproducible, zero user setup, works offline. Implications to handle: - **Codesign the embedded interpreter + all `.so`/`.dylib`** for the virtualization-entitlement build (`build.sh` must walk the venv and sign each Mach-O); hardened-runtime exceptions as needed for the Python runtime. - Pin exact versions (`vllm-mlx`, `mlx`, `mlx-lm`, transitive wheels) in a lockfile; bundle wheels so the build is hermetic (no network at build time). - Bundle size grows materially — keep the venv out of the SPM resource bundle hot path; copy it in `build.sh` like the other `Bundle.module` resources. - **Active-model handle** — knows which catalog model is loaded; exposes switch. ### 3.2 vsock proxy (Path 1 plumbing) - Register a new vsock listener on **port 8446** in `MitmEngine.register()` (alongside 8443/8444/8445) that pumps to `127.0.0.1:<engine-port>`. - Add the matching forward in `bromure-vm-bridge.py` so the guest sees the engine at `http://127.0.0.1:11434` (raw TCP pump, same shape as the existing bridges). ### 3.3 Guest env injection (Path 1) - Push into the agent's environment via the existing `proxy.env` / `config-agent` mechanism: - `ANTHROPIC_BASE_URL=http://127.0.0.1:11434` - `OPENAI_BASE_URL=http://127.0.0.1:11434/v1` - dummy `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` (engine ignores them) - Then any explicitly-configured agent "just works" against local inference with no code change. ### 3.4 Model catalog + downloads See §5. ### 3.5 Routing layer (Path 2 / MITM) See §4. --- ## 4. Access paths & routing (the two-axis model) ### 4.1 Two paths, different jobs | | **Path 1 — env var in VM** | **Path 2 — MITM / interception** | |-------------------|-----------------------------------|----------------------------------------| | Mechanism | `ANTHROPIC_BASE_URL` → local | agent targets `api.anthropic.com`; mitm re-routes | | Agent awareness | Explicit (knows it's local) | Transparent | | Routing | **Static pin — always local** | **Dynamic — cloud / local / hybrid** | | Works for | Agents whose config you control | *Any* tool in the VM hitting Anthropic | | Hybrid possible? | ❌ never sees the cloud | ✅ **only place hybrid can live** | Both hit the **same** host engine. Two ways to get local: explicit (Path 1, no interception) and transparent (Path 2 routing=Local). Pick by whether you control the agent's config. ### 4.2 Two **independent** axes (Routing is top-level, NOT a Fusion mode) Fusion and Routing are orthogonal: Fusion is an **identity** concern (whose creds serve the cloud leg); Routing is a **backend-selection** concern. Nesting Hybrid inside Fusion makes valid combinations inexpressible — e.g. *"agent uses its own Anthropic key (Fusion off) but still falls back to local when Anthropic is slow."* ``` Substrate: MITM interception (engaged when Routing ≠ Cloud, or Fusion on) Axis 1 — Routing (TOP-LEVEL): Cloud | Local | Hybrid Axis 2 — Fusion / identity: host-cred swap on/off (cloud leg only) Separate mechanism — Path 1: ANTHROPIC_BASE_URL pin (explicit local, no mitm) ``` - Selecting **Local** or **Hybrid** routing **auto-engages** interception — the user does not flip a separate "mitm on" switch. - Falling back to local is **identity-agnostic** → Hybrid composes with either Fusion state with no special-casing. - Surface as a sibling of `vm fusion`: **`vm routing cloud|local|hybrid`**, reading the same per-profile config the mitm already consumes. Example grid: - `Hybrid + Fusion-on` → "use my host Claude sub, fall back to local on slowdown." - `Hybrid + Fusion-off` → "agent's own key, fall back to local." - `Local` → "always local, transparently." - `Cloud` (today's behavior) → pass-through, optionally host-cred swapped. ### 4.3 Hybrid fallback — the three traps **Trap 1 — you can't un-send a request. Fallback must fire *before the first token*.** Once the mitm forwards one SSE event downstream, it's committed. Triggers: - **Hard:** connection refused / timeout / `429` / `529` / `5xx` → route local immediately (covers real Anthropic outages). - **Soft:** no first token within **5 s** (decided — cloud-tolerant default) → cancel upstream, replay same request local. Configurable. - **Health-gating (conservative — decided):** maintain an EWMA of recent TTFT + error-rate per upstream. Flip to "unhealthy" only on a clear signal — **≥3 failures or TTFT EWMA > 8 s over the last ~10 requests** — and **recover after a few clean probes**. While unhealthy, send new requests straight to local *without* paying the per-request timeout penalty. Conservative thresholds match the cloud-tolerant 5 s soft default and avoid churning to local on transient jitter. - **No speculative hedging** (decided) — fallback only fires *after* a hard or soft trigger; we never race both backends. Simpler, no double spend; worst case is the 5 s soft-timeout latency before failover. **Trap 2 — swapping models mid-conversation can wreck a coding trajectory.** Different tool-call style / prompt adherence between turns derails multi-step agents. Mitigation: **sticky at session granularity** — once a conversation switches to local, keep it local for the rest of that session (detect session via message-history hash / continuity header). Stickiness is the *coherence guard* that sits under the policy knobs below: whenever a knob decides to switch, the switch is applied at a session boundary, not mid-trajectory. ### 4.3.1 Hybrid is a small policy engine (not one switch) Hybrid routing is controlled by a few independent, per-profile toggles rather than a single stickiness mode: - **Cloud token budget** — a cap on tokens served by the cloud leg over a **rolling wall-clock window** (decided — e.g. "max N cloud tokens per 24 h", resets continuously). Once exceeded, route to local until the window slides back under cap. Requires persisted usage counters; pairs with the trace marker (§4.4) for accounting. - **Performance thresholds** — the trigger set above: hard errors (`429/529/5xx`/timeout) + the 5 s soft TTFT + health-gating EWMA. Tunable per profile. - **Local↔cloud split** — an explicit ratio that proactively sends a configurable share of *new sessions* to local even when cloud is healthy (e.g. 30% local). Lets the user blend cost/latency/privacy instead of only falling back on failure. **Applied at session granularity** so it never swaps models within a trajectory. Precedence (first match wins): hard error → over-budget → unhealthy (EWMA) → soft-timeout → split-ratio assignment → otherwise cloud. All switches respect the sticky-session guard. **Trap 3 — protocol translation is the silent killer; `vllm-mlx` avoids it.** The intercepted request is Anthropic `/v1/messages` with Anthropic tool blocks + SSE. Because `vllm-mlx` speaks `/v1/messages` natively, hybrid is **near pass-through** (rewrite host, drop real key) and tool-calling stays intact. An OpenAI-only engine would force on-the-fly Anthropic↔OpenAI translation of messages/tools/streaming — exactly where quantized tool-calling breaks. **Reason #1 to keep the engine Anthropic-native for the MITM path.** ### 4.4 Observability Inject a response marker (e.g. `x-bromure-served-by: local-glm-5.2` vs `cloud`) and log it into the existing trace CLI, so the user can see *which model answered each turn* under transparent routing. --- ## 5. Curated MLX model catalog The catalog's real job for agentic coding is a **quality gate**, not just a menu: the dominant failure mode is quantized models that silently break tool-calling. ### 5.1 Shipped-but-updatable manifest - **Bundle a baseline `catalog.json`** (works offline day one) — same JSON-in- Application-Support pattern as profiles. - **Fetch an updated `catalog.json` from a Bromure-hosted URL** at launch / on refresh; merge over baseline. Lets new MLX quants be added the *same day* they hit HF without an app release. - **Baseline seed (decided): 3–4 tool-calling-verified models across RAM tiers** so every machine sees something usable offline on day one: - small — fits ~32 GB (e.g. a Qwen3.5-Coder-class small model) - mid — ~64 GB - large — GLM-5.2 (~128 GB) Remote refresh layers newer/larger options on top. ### 5.2 Entry schema (draft) ```json { "id": "glm-5.2-mlx-3bit", "repo": "mlx-community/GLM-5.2-mxfp4", "engine": "vllm-mlx", "name": "GLM-5.2 (3-bit MLX)", "publisher": "Z.ai", "license": "MIT", "params_total_b": 744, "params_active_b": 40, "quant": "mxfp4", "download_gb": 95, "min_unified_mem_gb": 128, "context": 1000000, "tags": ["coding", "tools", "reasoning"], "tool_calling": "verified", "min_chip": "M3 Max", "recommended": true } ``` ### 5.3 RAM-fit gating Detect host unified memory once; label each entry **Fits / Tight / Won't fit** against `min_unified_mem_gb`. Picker defaults to "Fits," with a toggle to reveal the rest. Prevents a 32 GB MacBook user from pulling 95 GB of GLM-5.2 and OOMing. ### 5.4 Curation criteria (what earns a slot) 1. **MLX format** (not GGUF) — validated. 2. **Tool-calling verified** — actually smoke-tested against that quant (`verified` / `untested` / `broken`). Headline filter for agentic use. 3. **Trusted quant publisher** (`mlx-community`, model author's own MLX repo). 4. **Coding-relevant** tags so the picker can default to coding models. ### 5.5 Escape hatch (power users) Keep "paste any HF repo," but **validate before download**: query the HF API, confirm MLX safetensors + config (reject GGUF with a clear "that's an ollama model" message), then pull with progress. Custom pulls get `tool_calling: "untested"` and no RAM-fit guarantee. ### 5.6 No conversion, ever `vllm-mlx` loads **any HF repo already in MLX format** — `mlx-community` is just the canonical/biggest source, not a lock-in. Give `org/repo`, it downloads to `~/.cache/huggingface/...` on first run and serves it. Users never touch `mlx_lm.convert` or distribute anything. (Must be an **MLX** repo, not GGUF — `unsloth/GLM-5.2-GGUF` will *not* load here; that's the Ollama path.) --- ## 6. CLI / UI surface (docker-style, matches existing CLI) ``` bromure-ac model catalog # curated list + Fits/Won't-fit badges bromure-ac model pull glm-5.2-mlx-3bit # by catalog id … bromure-ac model pull mlx-community/… # … or any MLX repo (validated) bromure-ac model ls # installed + disk used bromure-ac model use <id> # set active engine model bromure-ac model rm <id> bromure-ac vm routing cloud|local|hybrid # top-level routing, sibling of `vm fusion` bromure-ac vm fusion on|off # existing identity axis # hybrid policy knobs (per profile; only meaningful when routing=hybrid) bromure-ac vm hybrid budget <tokens> # cloud token cap per window (0 = unlimited) bromure-ac vm hybrid ttft <seconds> # soft fallback threshold (default 5) bromure-ac vm hybrid split <0..100> # % of new sessions pinned local ``` Settings UI mirrors `model catalog` as cards with RAM badges + tool-calling check. --- ## 7. GLM-5.2 specifics - ~744B-param MoE (~40B active), 1M context, MIT license, coding-focused. - **MLX:** `mlx-community/GLM-5.2-mxfp4`, `pipenetwork/GLM-5.2-MLX-mixed-3_6bit`. - **Hardware reality (good news):** a **3-bit MLX build ran ~26 tok/s at ~100 GB peak on an M3 Max 128 GB**. So full GLM-5.2 is reachable on a **128 GB Mac** via aggressive quant — not the 512 GB the raw param count implies. Still out of reach on 16–32 GB; for those, seed the catalog with smaller coding models (Qwen3.5-Coder-class) across RAM tiers. --- ## 8. Decisions Resolved: 1. **Engine packaging — vendor a pinned arm64 Python 3.12 venv in the app bundle.** Reproducible/offline; requires codesigning the embedded interpreter + dylibs in `build.sh` and a hermetic pinned wheel set (§3.1). 2. **Hybrid control = toggle-based policy engine**, not a single stickiness mode: **cloud token budget**, **performance thresholds**, **local↔cloud split** (§4.3.1). Sticky-per-session is the coherence guard under all three; switches happen at session boundaries only. 3. **No speculative hedging** — fallback only after a hard/soft trigger; never race both backends (no double spend). 4. **Soft-timeout = 5 s** (cloud-tolerant) + hard triggers (`429/529/5xx`/timeout) + health-gating EWMA. All per-profile tunable. 5. **Single shared engine instance** for all VMs — model loaded once; no per-VM isolation (§3.1). 6. **Catalog: 3–4 tool-calling-verified models across RAM tiers** bundled as baseline (small ~32 GB / mid ~64 GB / GLM-5.2 ~128 GB) **+ remote `catalog.json` refresh** from a Bromure-hosted URL (§5.1). 7. **Cloud token budget = rolling wall-clock window** (e.g. per-24 h), persisted counters (§4.3.1). 8. **Health-gate = conservative** — flip unhealthy on ≥3 failures or TTFT EWMA > 8 s over last ~10 requests; recover after a few clean probes (§4.3). Still open (content/tuning, not blocking): 9. **Catalog refresh URL host** + the exact baseline model picks (editorial). 10. **Exact budget default** (N tokens / window length) and EWMA decay factor — ship sensible defaults, tune empirically once running. --- ## 9. Suggested build order 1. `InferenceService` supervisor + `vllm-mlx` launch, bound to loopback. 2. vsock listener (8446) in `MitmEngine` + `bromure-vm-bridge.py` forward. 3. Guest env injection (Path 1) → prove a configured agent reaches local engine. 4. `catalog.json` schema + loader (bundled baseline) + RAM-fit detector. 5. `model` CLI verbs (`catalog`, `pull`, `use`, `ls`, `rm`) + HF download w/ progress. 6. Routing layer in MITM: `Cloud | Local | Hybrid`, top-level, + `vm routing` CLI. 7. Hybrid policy engine: precedence chain (hard error → over-budget → unhealthy → 5 s soft TTFT → split-ratio → cloud), sticky-per-session guard, + `vm hybrid` knobs. 8. Observability marker into the trace CLI. 9. Remote catalog refresh + tool-calling verification harness. --- ## 10. Key existing files to touch | Purpose | Path | |---|---| | NAT/network setup (gateway IP fallback option) | `Sources/SandboxEngine/VMNetSwitch.swift` | | vsock listener registration (add 8446) | `Sources/AgentCoding/Mitm/MitmEngine.swift` | | Guest TCP↔vsock bridge (add LLM forward) | `Sources/AgentCoding/Resources/vm-setup/bromure-vm-bridge.py` | | MITM request routing (Cloud/Local/Hybrid) | `Sources/AgentCoding/Mitm/HTTPProxy.swift` | | CLI subcommands (`model`, `vm routing`) | `Sources/AgentCoding/CLICommands.swift` | | HTTP automation server | `Sources/AgentCoding/AutomationServer.swift` | | Guest env / proxy.env injection | `Sources/SandboxEngine/Resources/vm-setup/scripts/config-agent.py` | | New: `InferenceService`, catalog loader | `Sources/AgentCoding/...` (new) | --- ## Sources - MLX vs llama.cpp benchmarks — https://yage.ai/share/mlx-apple-silicon-en-20260331.html - Ollama MLX engine (preview) — https://ollama.com/blog/mlx - mlx-lm server — https://github.com/ml-explore/mlx-lm/blob/main/mlx_lm/SERVER.md - mlx-community (pre-converted MLX weights) — https://huggingface.co/mlx-community - vllm-metal (official MLX plugin) — https://github.com/vllm-project/vllm-metal - vllm-mlx (OpenAI+Anthropic server) — https://github.com/waybarrios/vllm-mlx - GLM-5.2 model card — https://huggingface.co/zai-org/GLM-5.2 - GLM-5.2 GGUF (Ollama path) — https://huggingface.co/unsloth/GLM-5.2-GGUF - GLM-5.2 MLX — https://huggingface.co/mlx-community/GLM-5.2-mxfp4 - GLM-5.2 local hardware/quants — https://codersera.com/blog/how-to-run-glm-5-2-locally-2026/
Download .txt