AI features bill you per token, per user, forever, while the hardware already in your users' hands sits idle. byollm lets your app run its LLM jobs there instead: their Ollama box, their Mac running MLX, their claude CLI. No tunnels. No inbound ports. No shared keys.
Open source · MIT · built by Of Tomorrow
The shape
Your web app stays hosted. The user runs a daemon that connects out to your backend, claims only their own jobs, and runs them on whatever model they've got. There's nothing to open on their network — and jobs are typed data, never code.
your web app ──enqueue──▶ your backend (@byollm/server) │ │ your job queue — Supabase, Postgres, memory… ▼ user's device ──outbound poll──▶ claim ─▶ run locally ─▶ result (byollm) │ Ollama · MLX · claude CLI
For developers
Mount one route, point it at a store, and enqueue. You never see the user's model or credentials — you get a result back.
// app/api/byollm/[...route]/route.ts import { createHandler } from "@byollm/server/next"; import { store } from "@/lib/byollm"; export const { POST } = createHandler({ store, verificationUrl: "https://your-app.com/settings/runners", });
// enqueue from anywhere in your app const job = await app.enqueue({ kind: "llm.generate", audience: "private", // their device only owner: userId, payload: { prompt }, }); const { outcome } = await job.result({ onNoRunner: promptUserToConnect, // never hangs });
A server is byollm-compatible when the conformance kit passes against it. That's the whole versioning story — no framework version to chase.
For users
You decide what runs, and where. A local model means the prompt never leaves your machine; a service you already pay for means the job goes where you already send it, on your account. And byollm never holds your key.
npx byollm@latest connect https://the-app.com — opens your browser to pair with one click. Nothing to configure on your network.
Name a service for whatever you’ve got installed, and say which job kinds it answers: "ollama-local": { "type": "ollama", "model": "gemma3:12b", "kinds": ["llm.generate"] }. The daemon only advertises models you approve. And only ones it can actually run.
byollm log shows every prompt that ever ran on your device. byollm stop stops it. The daemon is your meter — that's the point.
Providers
A provider is where a request goes; a model is which weights answer it. Providers are registry entries over one audited transport — adding one is a single line — and every one arrives with a cost class your config can't override.
| Provider | What it is | Cost |
|---|---|---|
| Your hardware — local compute, costs electricity · shareable up to team | ||
| Ollama | The easiest way to run models locally | free |
| MLX | Apple-silicon native, via mlx_lm.server | free |
| llama.cpp | The C++ engine behind much of local AI | free |
| vLLM | Production-grade GPU serving | free |
| LM Studio | Local models with a desktop app | free |
| Jan | Open-source desktop runner | free |
| LocalAI | Self-hosted OpenAI-compatible server | free |
| Your API key — per-token billing on your account · shareable with an acknowledgment and a daily ceiling | ||
| Anthropic | Claude models on your platform key | metered |
| OpenAI | GPT models on your platform key | metered |
| Gemini | Google's models on your API key | metered |
| Grok | xAI's models on your API key | metered |
| Groq | Open models served fast on custom silicon | metered |
| OpenRouter | One key, many providers | metered |
| Together | Open-model cloud | metered |
| DeepSeek | DeepSeek's models on your API key | metered |
| Mistral | Mistral's models on your API key | metered |
| Your subscription — a personal plan · never shared, enforced by the protocol | ||
| claude CLI | Your Claude plan, on your computer, yours alone | subscription |
| codex CLI | Your ChatGPT plan, on your computer, yours alone | subscription |
Anthropic appears twice, in two cost classes — a platform key bills per token, a Claude plan covers one person's work. The axis asks who pays and under what terms, not which company answers.
Not listed? openai-http reaches anything OpenAI-compatible — its cost class is detected from where it points, and remote is never free.
Security
The daemon runs prompts on the owner's computer, so every payload is treated as hostile input. The design makes escape structurally impossible rather than trying to spot it.
HTTP backends receive the prompt as a request body; process backends (the claude CLI) receive it on stdin with a fixed argv. Either way, --flags, $(…) and shell metacharacters are just characters the model reads — no code path turns a payload into a command.
Model, backend, and flags come from the owner's config. A job can never name a model, a path, a URL, or a flag. Unknown job kind is refused, not guessed.
Process backends spawn with a stripped environment (no API key), an empty scratch directory, no inherited file descriptors, and hard timeout and output caps. A job cannot become a process of its own choosing.
A named corpus of command-injection, argv-injection, path-traversal and exfiltration payloads runs as a blocking CI check. Every backend ships its own hostile-payload suite before it can be added.
We're precise about the boundary: BYOLLM makes breakout impossible; prompt injection — steering the model's words — is the model's problem, bounded here because the model has no tools and the output is inert. We don't promise more than we can keep.
Packages
The wire contract — types, schemas, and the normative spec. Six endpoints, lease semantics, typed job kinds.
What users run. Pluggable backends, model routing, and the append-only trust log.
Drop-in handlers plus a first-party Supabase adapter for your backend.
The compatibility contract. Certify any server implementation with one command.
The reference relay. Routes sealed jobs between sites and devices, holding no key that opens either.
The reference control plane. Decides which device service answers and signs one grant per job.