# How to Install and Configure 9Router and CLIProxyAPI as Local Providers in OmniRoute

> Install and configure 9Router and CLIProxyAPI as local providers in OmniRoute. Learn to manage embedded services and expose them as loopback HTTP endpoints via a local REST API.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: how-to-guide
- Published: 2026-08-22

---

**OmniRoute manages 9Router and CLIProxyAPI as embedded services through the generic `ServiceSupervisor`, installing them via dedicated installer modules and exposing them as loopback-only LLM-compatible HTTP endpoints that you control through a local REST API.**

OmniRoute is an open-source routing framework that lets you run local LLM providers as self-contained **embedded services** directly on your machine. Learning how to install and configure embedded services like 9Router and CLIProxyAPI as local providers in OmniRoute allows you to host compatible inference endpoints without relying on external cloud APIs. The framework automates lifecycle management, health probing, and log capture through its generic `ServiceSupervisor` and dedicated installer modules under `src/lib/services/installers/`.

## Embedded Service Architecture in OmniRoute

OmniRoute treats **9Router** and **CLIProxyAPI** as embedded services — small local daemons that expose an LLM-compatible HTTP endpoint bound strictly to the **loopback** interface. The generic `ServiceSupervisor` in [`src/lib/services/ServiceSupervisor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/ServiceSupervisor.ts) orchestrates the full lifecycle, including installation, start, stop, health-check, and log capture. It treats every embedded service as a managed subprocess with uniform controls.

All service management routes reside under `/api/services/*`. The route guard in [`src/server/authz/routeGuard.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/server/authz/routeGuard.ts) enforces the `isLocalOnlyPath()` check on these routes, ensuring they comply with Hard Rule #17 and are reachable only from `127.0.0.1`.

## Installing 9Router as a Local Provider

### How the 9Router Installer Works

The 9Router installer lives in [`src/lib/services/installers/ninerouter.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/installers/ninerouter.ts). Because 9Router is distributed as a pure-Node package via **npm**, the installer creates a private [`package.json`](https://github.com/diegosouzapw/OmniRoute/blob/main/package.json) inside `$DATA_DIR/services/9router` and runs `npm install 9router@<version>` while omitting dev dependencies.

After installation, the installer records the resulting binary path — typically [`node_modules/9router/app/server.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/node_modules/9router/app/server.js) — in the **`version_manager`** table defined in [`src/lib/db/versionManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/versionManager.ts). This persisted metadata lets OmniRoute resolve the correct executable across restarts.

### Installing 9Router via the REST API

You can trigger installation by posting to the generic service install route. The request is forwarded to the 9Router installer function automatically. The HTTP endpoints that expose these actions follow the Next.js App Router convention used in the repository, such as [`src/app/api/services/9router/install/route.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/app/api/services/9router/install/route.ts):

```bash
curl -X POST http://localhost:20128/api/services/9router/install \
     -H "Content-Type: application/json" \
     -d '{"version":"latest"}'

```

## Installing CLIProxyAPI as a Local Provider

### How the CLIProxyAPI Installer Works

The CLIProxyAPI installer is implemented in [`src/lib/services/installers/cliproxy.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/installers/cliproxy.ts). Unlike 9Router, CLIProxyAPI ships as a compiled binary released on GitHub, so the installer delegates fetching to the **binary manager** in [`src/lib/versionManager/binaryManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/versionManager/binaryManager.ts).

The installer downloads the latest release assets and verifies the checksum. It extracts the archive using `unzip` on POSIX or PowerShell `Expand-Archive` on Windows, creates a version-named symlink (`CLIProxyAPI_<version>`), and writes a minimal **[`config.yaml`](https://github.com/diegosouzapw/OmniRoute/blob/main/config.yaml)** under `$DATA_DIR/services/cliproxy`.

### Installing CLIProxyAPI via the REST API

Call the same install pattern for CLIProxyAPI. The endpoint delegates to [`src/lib/services/installers/cliproxy.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/installers/cliproxy.ts) and returns once the binary is ready:

```bash
curl -X POST http://localhost:20128/api/services/cliproxy/install \
     -H "Content-Type: application/json" \
     -d '{"version":"latest"}'

```

## Configuring Embedded Service Settings

### 9Router Environment and Spawn Arguments

Configuration for 9Router is handled through **environment variables** injected at spawn time. In [`src/lib/services/ServiceSupervisor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/ServiceSupervisor.ts), the `resolveSpawnArgs` and `buildServiceSpawnOptions` functions construct the spawn arguments for the service.

For 9Router, the supervisor sets `command` to **`process.execPath`** and passes the server script path as an argument. It injects several environment variables:

- `API_KEY_SECRET` — generated by OmniRoute and stored in the database
- `DATA_DIR` — the OmniRoute data directory
- `DISABLE_MITM=true`
- `DISABLE_TUNNEL=true`

OmniRoute generates the `API_KEY_SECRET` automatically, retrieves it from the database before starting the process, and injects it into the service environment alongside `DATA_DIR`, `DISABLE_MITM=true`, and `DISABLE_TUNNEL=true`. These variables ensure the Node process starts in the correct context without interfering with system networking.

### CLIProxyAPI File-Based Configuration

For CLIProxyAPI, the installer writes a static configuration file to `$DATA_DIR/services/cliproxy/config.yaml`. When the supervisor starts the service via `ServiceSupervisor.start()`, the spawn arguments point at the symlinked binary and pass **`--config <path>`** to load that file.

## Starting, Stopping, and Monitoring Services

### Starting a Service with ServiceSupervisor

`ServiceSupervisor.start()` handles the runtime phase. Before spawning, the supervisor runs an optional **pre-spawn health probe** to adopt an already-running instance instead of launching a duplicate.

You can start either service through the REST API. The port is optional; each installer defines its own default when omitted:

```bash
curl -X POST http://localhost:20128/api/services/9router/start \
     -H "Content-Type: application/json" \
     -d '{"port":4000}'

```

```bash
curl -X POST http://localhost:20128/api/services/cliproxy/start \
     -H "Content-Type: application/json" \
     -d '{"port":8317}'

```

### Stopping and Checking Service Status

Stop requests terminate the supervised process gracefully:

```bash
curl -X POST http://localhost:20128/api/services/9router/stop

```

```bash
curl -X POST http://localhost:20128/api/services/cliproxy/stop

```

Check the current runtime status with a GET request:

```bash
curl http://localhost:20128/api/services/9router/status

```

```bash
curl http://localhost:20128/api/services/cliproxy/status

```

### Log Capture and Health Monitoring

While running, `ServiceSupervisor` captures the process **stdout** and **stderr** into a ring buffer. It also periodically probes the service's `/health` endpoint. Any errors are sanitized through **`sanitizeErrorMessage()`** before being persisted.

To view recent logs over HTTP:

```bash
curl http://localhost:20128/api/services/9router/logs

```

The dashboard consumes the same data through a WebSocket endpoint; the HTTP fallback returns the recent ring-buffer contents.

## Provider Registration and API Routing

Once installed and started, these embedded services function as standard local providers. CLIProxyAPI is explicitly registered as an **upstream proxy provider** in [`src/shared/constants/providers/upstream-proxy.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/shared/constants/providers/upstream-proxy.ts), which the router layer uses when resolving requests. All provider endpoints ultimately surface through OmniRoute's standard `/v1/*` API routes, letting you treat local and remote providers identically in your client code.

## Summary

- **OmniRoute** manages 9Router and CLIProxyAPI as embedded services via the generic `ServiceSupervisor` in [`src/lib/services/ServiceSupervisor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/ServiceSupervisor.ts).
- **Installation** is service-specific: [`src/lib/services/installers/ninerouter.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/installers/ninerouter.ts) runs `npm install`, while [`src/lib/services/installers/cliproxy.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/services/installers/cliproxy.ts) downloads and symlinks a compiled binary via [`src/lib/versionManager/binaryManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/versionManager/binaryManager.ts).
- **Configuration** is stored per-service under `$DATA_DIR/services/<service>/`, using environment variables for 9Router and a [`config.yaml`](https://github.com/diegosouzapw/OmniRoute/blob/main/config.yaml) file for CLIProxyAPI.
- **Lifecycle control** — install, start, stop, status, and logs — is exposed through loopback-only REST endpoints under `/api/services/*`, protected by `isLocalOnlyPath()` in [`src/server/authz/routeGuard.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/server/authz/routeGuard.ts).
- **Runtime monitoring** includes health probes against `/health`, ring-buffer log capture, and error sanitization via `sanitizeErrorMessage()`.

## Frequently Asked Questions

### Where does OmniRoute store installed versions and binary paths for embedded services?

OmniRoute persists installed versions, binary paths, ports, and runtime status in the `version_manager` table defined in [`src/lib/db/versionManager.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/db/versionManager.ts). For 9Router, this includes the path to [`node_modules/9router/app/server.js`](https://github.com/diegosouzapw/OmniRoute/blob/main/node_modules/9router/app/server.js); for CLIProxyAPI, it tracks the symlinked `CLIProxyAPI_<version>` binary.

### Can I run 9Router or CLIProxyAPI on a custom port?

Yes. When calling the start endpoint, you can pass an optional `port` field in the JSON body, as shown in the `POST /api/services/<service>/start` examples. If you omit the port, the service falls back to the default defined in its installer module.

### How does OmniRoute prevent external access to the embedded service management APIs?

All `/api/services/*` routes are restricted to the loopback interface. The route guard in [`src/server/authz/routeGuard.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/server/authz/routeGuard.ts) enforces `isLocalOnlyPath()` for these routes, ensuring that install, start, stop, and log endpoints comply with Hard Rule #17 and cannot be reached from remote hosts.

### What happens if an embedded service is already running when I try to start it?

`ServiceSupervisor.start()` performs an optional pre-spawn health probe before launching a new process. If the probe detects an existing healthy instance, the supervisor adopts it instead of spawning a duplicate, preventing port conflicts and redundant processes.