How to Install and Configure 9Router and CLIProxyAPI as Local Providers in OmniRoute
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 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 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. Because 9Router is distributed as a pure-Node package via npm, the installer creates a private 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 — in the version_manager table defined in 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:
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. 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.
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 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 and returns once the binary is ready:
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, 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 databaseDATA_DIR— the OmniRoute data directoryDISABLE_MITM=trueDISABLE_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:
curl -X POST http://localhost:20128/api/services/9router/start \
-H "Content-Type: application/json" \
-d '{"port":4000}'
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:
curl -X POST http://localhost:20128/api/services/9router/stop
curl -X POST http://localhost:20128/api/services/cliproxy/stop
Check the current runtime status with a GET request:
curl http://localhost:20128/api/services/9router/status
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:
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, 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
ServiceSupervisorinsrc/lib/services/ServiceSupervisor.ts. - Installation is service-specific:
src/lib/services/installers/ninerouter.tsrunsnpm install, whilesrc/lib/services/installers/cliproxy.tsdownloads and symlinks a compiled binary viasrc/lib/versionManager/binaryManager.ts. - Configuration is stored per-service under
$DATA_DIR/services/<service>/, using environment variables for 9Router and aconfig.yamlfile for CLIProxyAPI. - Lifecycle control — install, start, stop, status, and logs — is exposed through loopback-only REST endpoints under
/api/services/*, protected byisLocalOnlyPath()insrc/server/authz/routeGuard.ts. - Runtime monitoring includes health probes against
/health, ring-buffer log capture, and error sanitization viasanitizeErrorMessage().
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. For 9Router, this includes the path to 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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →