feat(openhands): add @omniroute/openhands-plugin — config generator + skill

New package that wires OpenHands agent-server to OmniRoute:
- env.ts: generates OpenHands .env (LLM_MODEL, LLM_BASE_URL, LLM_API_KEY,
  OH_PERSISTENCE_DIR, PERMITTED_CORS_ORIGINS)
- docker.ts: Docker Compose + docker run generators with the field-proven
  fixes baked in (privileged sandbox, host.docker.internal:host-gateway,
  persistence volume, CORS)
- model-map.ts: OpenHands model names → OmniRoute model/combo IDs
- cli.ts: omniroute-openhands <env|compose|docker-run|models>
- tests: 8 cases (env, model map, compose, docker run) — all passing
- skills/omni-openhands/SKILL.md + README catalog entry
This commit is contained in:
Egor
2026-07-31 09:40:53 +03:00
parent 2baf2f4820
commit 0975bc8ca9
15 changed files with 2811 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
node_modules
dist
*.log
.DS_Store
.env

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 OmniRoute contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,118 @@
# @omniroute/openhands-plugin
OpenHands integration for the **OmniRoute AI Gateway**. Generates the OpenHands
environment and Docker config that wires an OpenHands agent-server to a running
OmniRoute instance — with the integration gotchas already handled.
## Why
Running OpenHands against OmniRoute directly hits several wall:
1. **Model name mismatch** — OpenHands sends `model: "deepseek-chat"`, OmniRoute
uses provider-prefixed IDs (`ds/deepseek-v4-flash`) or combos.
2. **Python 3.13 sandbox**`socket.socketpair()` fails under Docker's default
seccomp profile; the agent-server needs `privileged: true`.
3. **Host reachability** — the sandbox can't resolve `localhost` to the OmniRoute
host; needs `host.docker.internal:host-gateway`.
4. **Lost state** — conversations die with the container unless
`OH_PERSISTENCE_DIR` is a host volume.
5. **CORS** — the dashboard origin can't reach agent-server unless
`PERMITTED_CORS_ORIGINS` allows it.
This plugin encodes all of that into one command.
## Install
```bash
npm install -g @omniroute/openhands-plugin
# or: npx @omniroute/openhands-plugin ...
```
## Quick start
Generate the OpenHands `.env`:
```bash
omniroute-openhands env \
--api-key sk-... \
--model deepseek-chat \
--url http://192.168.3.106:20128
```
Generate a `docker-compose.yml` service:
```bash
omniroute-openhands compose \
--api-key sk-... \
--model glm-5.2 \
--persistence-dir /Users/me/.openhands-state \
--cors-origins http://100.73.44.17:3000
```
Or a plain `docker run`:
```bash
omniroute-openhands docker-run \
--api-key sk-... \
--model vivanta-core \
--persistence-dir /Users/me/.openhands-state
```
## Commands
| Command | Description |
|---------|-------------|
| `env` | Print OpenHands `.env` contents |
| `compose` | Print a Docker Compose service block |
| `docker-run` | Print a full `docker run` command |
| `models` | Print the default OpenHands → OmniRoute model map |
### Common options
| Flag | Description | Default |
|------|-------------|---------|
| `--api-key` | OmniRoute API key (`sk-...`) | — |
| `--model` | OpenHands model name or OmniRoute combo | — |
| `--url` | OmniRoute base URL | `http://localhost:20128` |
| `--persistence-dir` | Host dir for conversation state | `.openhands-state` |
| `--cors-origins` | Comma-separated allowed origins | `localhost:3000,3001` |
| `--sandbox-image` | OpenHands sandbox base image | — |
## Model mapping
OpenHands-friendly names are mapped to OmniRoute IDs/combo names:
| OpenHands sends | OmniRoute resolves to |
|-----------------|----------------------|
| `deepseek-chat` | `ds/deepseek-v4-flash` |
| `deepseek-reasoner` | `ds/deepseek-v4-pro` |
| `glm-5.2` | `nvidia/z-ai/glm-5.2` |
| `gpt-4o` | `openai/gpt-4o` |
| `claude-sonnet-4.5` | `anthropic/claude-sonnet-4.5` |
| ... | ... |
Or just pass an OmniRoute combo name (e.g. `--model vivanta-core`) — the Model
Alias Resolver and combo router accept it directly.
## Library usage
```ts
import {
buildOpenHandsEnv,
serializeOpenHandsEnv,
buildOpenHandsCompose,
resolveOpenHandsModel,
} from "@omniroute/openhands-plugin";
const env = buildOpenHandsEnv({
apiKey: "sk-...",
model: resolveOpenHandsModel("deepseek-chat"),
omnirouteUrl: "http://localhost:20128",
persistenceDir: "/Users/me/.openhands-state",
});
console.log(serializeOpenHandsEnv(env));
```
## License
MIT — same as OmniRoute.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,79 @@
{
"name": "@omniroute/openhands-plugin",
"version": "0.1.0",
"description": "OpenHands integration for the OmniRoute AI Gateway. Generates OpenHands env + Docker Compose config (model mapping, sandbox, CORS, persistence) so OpenHands agents talk to OmniRoute out of the box.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"omniroute-openhands": "./dist/cli.js"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./env": {
"types": "./dist/env.d.ts",
"import": "./dist/env.js"
},
"./docker": {
"types": "./dist/docker.d.ts",
"import": "./dist/docker.js"
},
"./model-map": {
"types": "./dist/model-map.d.ts",
"import": "./dist/model-map.js"
}
},
"files": [
"dist",
"README.md",
"LICENSE"
],
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/env.test.ts tests/model-map.test.ts tests/docker.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
"omniroute",
"openhands",
"open-hands",
"openhands-plugin",
"openai-compatible",
"docker",
"agent"
],
"author": "OmniRoute contributors",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/diegosouzapw/OmniRoute.git",
"directory": "@omniroute/openhands-plugin"
},
"bugs": {
"url": "https://github.com/diegosouzapw/OmniRoute/issues"
},
"homepage": "https://github.com/diegosouzapw/OmniRoute/tree/main/%40omniroute/openhands-plugin#readme",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"@omniroute/open-sse": "*"
},
"peerDependenciesMeta": {
"@omniroute/open-sse": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"@types/node": "^22.19.19",
"tsup": "^8.5.1",
"tsx": "^4.22.3"
}
}

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env node
/**
* @omniroute/openhands-plugin CLI — generate OpenHands .env / Docker config
* for a running OmniRoute instance.
*
* Usage:
* omniroute-openhands env --api-key sk-... --model deepseek-chat [--url http://localhost:20128]
* omniroute-openhands compose --api-key sk-... --model deepseek-chat [--persistence-dir /path]
* omniroute-openhands docker-run --api-key sk-... --model deepseek-chat
* omniroute-openhands models (print the default model map)
*/
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
import { DEFAULT_OPENHANDS_MODEL_MAP } from "./model-map.ts";
function parseArgs(argv: string[]): Record<string, string> {
const out: Record<string, string> = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
const next = argv[i + 1];
if (next !== undefined && !next.startsWith("--")) {
out[key] = next;
i++;
} else {
out[key] = "true";
}
}
return out;
}
function requireArgs(args: Record<string, string>, names: string[]): void {
for (const name of names) {
if (!args[name]) {
console.error(`Missing required --${name}`);
process.exit(2);
}
}
}
const [cmd, ...rest] = process.argv.slice(2);
const args = parseArgs(rest);
switch (cmd) {
case "env": {
requireArgs(args, ["api-key", "model"]);
const env = buildOpenHandsEnv({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"],
corsOrigins: args["cors-origins"]?.split(","),
});
process.stdout.write(serializeOpenHandsEnv(env));
break;
}
case "compose": {
requireArgs(args, ["api-key", "model"]);
process.stdout.write(
buildOpenHandsCompose({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
corsOrigins: args["cors-origins"]?.split(","),
sandboxBaseImage: args["sandbox-image"],
})
);
break;
}
case "docker-run": {
requireArgs(args, ["api-key", "model"]);
process.stdout.write(
buildOpenHandsDockerRun({
apiKey: args["api-key"],
model: args.model,
omnirouteUrl: args.url,
persistenceDir: args["persistence-dir"] ?? ".openhands-state",
corsOrigins: args["cors-origins"]?.split(","),
sandboxBaseImage: args["sandbox-image"],
})
);
break;
}
case "models": {
for (const [name, target] of Object.entries(DEFAULT_OPENHANDS_MODEL_MAP)) {
process.stdout.write(`${name}\t->\t${target}\n`);
}
break;
}
default:
console.error(
"Usage: omniroute-openhands <env|compose|docker-run|models> [options]\n" +
"Options:\n" +
" --api-key <sk-...> OmniRoute API key (required for env/compose/docker-run)\n" +
" --model <name> OpenHands model name or OmniRoute combo\n" +
" --url <base> OmniRoute URL (default http://localhost:20128)\n" +
" --persistence-dir <path> Host dir for conversation state\n" +
" --cors-origins <a,b,...> Allowed CORS origins\n" +
" --sandbox-image <image> OpenHands sandbox base image"
);
process.exit(1);
}

View File

@@ -0,0 +1,92 @@
/**
* OpenHands agent-server Docker Compose generator for OmniRoute.
*
* Bakes in the integration fixes that were needed to run OpenHands against
* OmniRoute reliably:
* - `privileged: true` — Python 3.13 socket.socketpair() needs it under
* Docker's default seccomp profile
* - `extra_hosts` — host.docker.internal → host-gateway so the
* sandbox can reach OmniRoute on the host
* - host volume for OH_PERSISTENCE_DIR so conversations survive `docker rm`
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to hit agent-server
*/
export interface OpenHandsDockerOptions {
/** Agent-server image (default: the official OpenHands runtime image). */
image?: string;
/** Container name (default: openhands-agent). */
containerName?: string;
/** Model name to pass via LLM_MODEL. */
model: string;
/** OmniRoute API key. */
apiKey: string;
/** OmniRoute base URL reachable from the sandbox (default http://localhost:20128). */
omnirouteUrl?: string;
/** Host directory for OH_PERSISTENCE_DIR (must match env.ts persistenceDir). */
persistenceDir: string;
/** CORS origins to permit. */
corsOrigins?: string[];
/** Sandbox base image (defaults to OpenHands default). */
sandboxBaseImage?: string;
/** Set true to use host networking instead of extra_hosts. */
hostNetwork?: boolean;
}
export function buildOpenHandsCompose(opts: OpenHandsDockerOptions): string {
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
const containerName = opts.containerName ?? "openhands-agent";
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const lines: string[] = [];
lines.push(`services:`);
lines.push(` openhands:`);
lines.push(` image: ${image}`);
lines.push(` container_name: ${containerName}`);
lines.push(` privileged: true`);
lines.push(` environment:`);
lines.push(` LLM_MODEL: "${opts.model}"`);
lines.push(` LLM_BASE_URL: "${omnirouteHost}/v1"`);
lines.push(` LLM_API_KEY: "${opts.apiKey}"`);
lines.push(` OH_PERSISTENCE_DIR: "/opt/.openhands-state"`);
lines.push(` PERMITTED_CORS_ORIGINS: "${cors.join(",")}"`);
if (opts.sandboxBaseImage) {
lines.push(` SANDBOX_BASE_IMAGE: "${opts.sandboxBaseImage}"`);
}
lines.push(` volumes:`);
lines.push(` - ${opts.persistenceDir}:/opt/.openhands-state`);
lines.push(` extra_hosts:`);
lines.push(` - "host.docker.internal:host-gateway"`);
return lines.join("\n") + "\n";
}
/**
* docker run equivalent of {@link buildOpenHandsCompose} — returns the full
* `docker run` command line.
*/
export function buildOpenHandsDockerRun(opts: OpenHandsDockerOptions): string {
const image = opts.image ?? "docker.all-hands.dev/all-hands-ai/openhands:latest";
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const parts = [
"docker run",
"--privileged",
"--add-host host.docker.internal:host-gateway",
`-e LLM_MODEL="${opts.model}"`,
`-e LLM_BASE_URL="${omnirouteHost}/v1"`,
`-e LLM_API_KEY="${opts.apiKey}"`,
`-e OH_PERSISTENCE_DIR=/opt/.openhands-state`,
`-e PERMITTED_CORS_ORIGINS="${cors.join(",")}"`,
`-v "${opts.persistenceDir}:/opt/.openhands-state"`,
image,
];
return parts.join(" ") + "\n";
}

View File

@@ -0,0 +1,63 @@
/**
* OpenHands `.env` generator for the OmniRoute AI Gateway.
*
* Produces the OpenHands environment that points an OpenHands agent-server at
* a running OmniRoute instance and fixes the integration gotchas found in the
* field:
* - LLM_MODEL — OpenHands-friendly model name → OmniRoute model/combo
* - LLM_BASE_URL — OmniRoute OpenAI-compatible endpoint
* - LLM_API_KEY — OmniRoute key (sk-...)
* - OH_PERSISTENCE_DIR — host-mounted SQLite/conversation persistence
* - PERMITTED_CORS_ORIGINS — allow the dashboard origin to reach agent-server
*/
export interface OpenHandsEnvOptions {
/** OmniRoute base URL as seen from the agent-server (default localhost:20128). */
omnirouteUrl?: string;
/** OmniRoute API key (sk-...). */
apiKey: string;
/** OpenHands model name (e.g. "deepseek-chat") or OmniRoute combo/model. */
model: string;
/** Host directory for OH_PERSISTENCE_DIR (default: current dir + .openhands-state). */
persistenceDir?: string;
/** CORS origins that must reach the agent-server (default dashboard origin + localhost). */
corsOrigins?: string[];
/** Optional OpenHands sandbox base image. */
sandboxBaseImage?: string;
}
export function buildOpenHandsEnv(opts: OpenHandsEnvOptions): Record<string, string> {
const omnirouteHost = (opts.omnirouteUrl ?? "http://localhost:20128").replace(/\/+$/, "");
const persistence = opts.persistenceDir ?? `${process.cwd()}/.openhands-state`;
const cors =
opts.corsOrigins && opts.corsOrigins.length > 0
? opts.corsOrigins
: ["http://localhost:3000", "http://localhost:3001"];
const env: Record<string, string> = {
LLM_MODEL: opts.model,
LLM_BASE_URL: `${omnirouteHost}/v1`,
LLM_API_KEY: opts.apiKey,
OH_PERSISTENCE_DIR: persistence,
PERMITTED_CORS_ORIGINS: cors.join(","),
};
if (opts.sandboxBaseImage) {
env.SANDBOX_BASE_IMAGE = opts.sandboxBaseImage;
}
return env;
}
/**
* Serialize the env record to `.env` file content (KEY=VALUE lines).
* Values are not quoted unless they contain whitespace or `#`.
*/
export function serializeOpenHandsEnv(env: Record<string, string>): string {
const lines: string[] = [];
for (const [key, value] of Object.entries(env)) {
const needsQuotes = /[\s#]/.test(value);
lines.push(needsQuotes ? `${key}="${value}"` : `${key}=${value}`);
}
return lines.join("\n") + "\n";
}

View File

@@ -0,0 +1,18 @@
/**
* @omniroute/openhands-plugin — OpenHands integration for the OmniRoute AI Gateway.
*
* Generates the OpenHands environment and Docker Compose / docker run config
* that wires an OpenHands agent-server to a running OmniRoute instance:
* model mapping, sandbox privileges, host-gateway networking, persistent
* conversation state and CORS.
*/
export { buildOpenHandsEnv, serializeOpenHandsEnv } from "./env.ts";
export type { OpenHandsEnvOptions } from "./env.ts";
export { buildOpenHandsCompose, buildOpenHandsDockerRun } from "./docker.ts";
export type { OpenHandsDockerOptions } from "./docker.ts";
export {
DEFAULT_OPENHANDS_MODEL_MAP,
resolveOpenHandsModel,
buildOpenHandsModel,
} from "./model-map.ts";
export type { OpenHandsModelMap } from "./model-map.ts";

View File

@@ -0,0 +1,64 @@
/**
* OpenHands → OmniRoute model mapping.
*
* OpenHands sends `model: "<LLM_MODEL>"` and expects the OpenAI-compatible
* endpoint to accept that exact string. OmniRoute uses provider-prefixed
* model IDs (`ds/deepseek-v4-flash`) and combo names. This module maps
* common OpenHands-friendly names to the OmniRoute model/combo they should
* resolve to, and back-fills the `LLM_MODEL` value for OpenHands.
*/
export interface OpenHandsModelMap {
/** OpenHands-friendly model name (e.g. "deepseek-chat") */
[openHandsName: string]: string;
}
/**
* Default mapping for the model names OpenHands and the broader ecosystem
* commonly send. Values are OmniRoute model IDs or combo names. Extend or
* override via {@link resolveOpenHandsModel}.
*/
export const DEFAULT_OPENHANDS_MODEL_MAP: OpenHandsModelMap = Object.freeze({
// DeepSeek
"deepseek-chat": "ds/deepseek-v4-flash",
"deepseek-reasoner": "ds/deepseek-v4-pro",
// Claude / Anthropic
"claude-sonnet-4.5": "anthropic/claude-sonnet-4.5",
"claude-opus-4.1": "anthropic/claude-opus-4.1",
"claude-haiku-4.5": "anthropic/claude-haiku-4.5",
// GPT / OpenAI
"gpt-4o": "openai/gpt-4o",
"gpt-4o-mini": "openai/gpt-4o-mini",
"gpt-5": "openai/gpt-5",
// Gemini
"gemini-2.5-flash": "gemini/gemini-2.5-flash",
"gemini-2.5-pro": "gemini/gemini-2.5-pro",
// GLM / Z.AI (NVIDIA NIM free endpoint)
"glm-5.2": "nvidia/z-ai/glm-5.2",
});
/**
* Resolve the OmniRoute model ID for an OpenHands-friendly model name.
* Returns the input unchanged when no mapping exists (OmniRoute will try to
* resolve it as a literal model/combo).
*/
export function resolveOpenHandsModel(
openHandsModel: string,
map: OpenHandsModelMap = DEFAULT_OPENHANDS_MODEL_MAP
): string {
if (!openHandsModel) return openHandsModel;
const mapped = map[openHandsModel];
return mapped ?? openHandsModel;
}
/**
* Build the `LLM_MODEL` value for OpenHands from an OmniRoute model ID/combo.
*
* OpenHands only surfaces the literal `LLM_MODEL` string in its UI, so for
* OmniRoute combos (e.g. "vivanta-core") that's already the right value.
* For provider-prefixed IDs, we return them as-is — the OmniRoute Model
* Alias Resolver accepts both the raw ID and aliases on the `/v1` endpoint.
*/
export function buildOpenHandsModel(omnirouteModelOrCombo: string): string {
return omnirouteModelOrCombo;
}

View File

@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import { buildOpenHandsEnv, serializeOpenHandsEnv } from "../src/env.ts";
import { resolveOpenHandsModel, buildOpenHandsModel } from "../src/model-map.ts";
import { buildOpenHandsCompose, buildOpenHandsDockerRun } from "../src/docker.ts";
test("buildOpenHandsEnv produces LLM vars pointing at OmniRoute", () => {
const env = buildOpenHandsEnv({
apiKey: "sk-test-123",
model: "deepseek-chat",
omnirouteUrl: "http://192.168.3.106:20128",
persistenceDir: "/opt/state",
corsOrigins: ["http://100.73.44.17:3000"],
});
assert.equal(env.LLM_MODEL, "deepseek-chat");
assert.equal(env.LLM_BASE_URL, "http://192.168.3.106:20128/v1");
assert.equal(env.LLM_API_KEY, "sk-test-123");
assert.equal(env.OH_PERSISTENCE_DIR, "/opt/state");
assert.equal(env.PERMITTED_CORS_ORIGINS, "http://100.73.44.17:3000");
});
test("serializeOpenHandsEnv quotes values with whitespace/#", () => {
const out = serializeOpenHandsEnv({ LLM_MODEL: "deepseek-chat", LLM_BASE_URL: "http://localhost:20128/v1" });
const lines = out.trim().split("\n");
assert.ok(lines.some((l) => l.startsWith("LLM_MODEL=deepseek-chat")));
assert.ok(lines.some((l) => l.startsWith("LLM_BASE_URL=http://localhost:20128/v1")));
});
test("resolveOpenHandsModel maps known names to OmniRoute IDs", () => {
assert.equal(resolveOpenHandsModel("deepseek-chat"), "ds/deepseek-v4-flash");
assert.equal(resolveOpenHandsModel("glm-5.2"), "nvidia/z-ai/glm-5.2");
assert.equal(resolveOpenHandsModel("gpt-4o"), "openai/gpt-4o");
});
test("resolveOpenHandsModel passes unknown names through unchanged", () => {
assert.equal(resolveOpenHandsModel("vivanta-core"), "vivanta-core");
assert.equal(resolveOpenHandsModel(""), "");
});
test("resolveOpenHandsModel accepts custom map overrides", () => {
const custom = { "my-alias": "nvidia/z-ai/glm-5.2" };
assert.equal(resolveOpenHandsModel("my-alias", custom), "nvidia/z-ai/glm-5.2");
assert.equal(resolveOpenHandsModel("deepseek-chat", custom), "deepseek-chat");
});
test("buildOpenHandsModel passes combo names through", () => {
assert.equal(buildOpenHandsModel("vivanta-core"), "vivanta-core");
assert.equal(buildOpenHandsModel("ds/deepseek-v4-flash"), "ds/deepseek-v4-flash");
});
test("buildOpenHandsCompose includes privileged, extra_hosts, volume, CORS", () => {
const compose = buildOpenHandsCompose({
apiKey: "sk-x",
model: "deepseek-chat",
persistenceDir: "/Users/me/.openhands-state",
corsOrigins: ["http://localhost:3000"],
});
assert.ok(compose.includes("privileged: true"), "privileged present");
assert.ok(compose.includes("host.docker.internal:host-gateway"), "host-gateway present");
assert.ok(compose.includes("/Users/me/.openhands-state"), "persistence volume present");
assert.ok(compose.includes("LLM_BASE_URL: \"http://localhost:20128/v1\""), "base url present");
assert.ok(compose.includes("PERMITTED_CORS_ORIGINS: \"http://localhost:3000\""), "cors present");
});
test("buildOpenHandsDockerRun produces a runnable docker command", () => {
const run = buildOpenHandsDockerRun({
apiKey: "sk-x",
model: "glm-5.2",
persistenceDir: "/opt/state",
});
assert.ok(run.startsWith("docker run"));
assert.ok(run.includes("--privileged"));
assert.ok(run.includes("--add-host host.docker.internal:host-gateway"));
assert.ok(run.includes("LLM_MODEL=\"glm-5.2\""));
assert.ok(run.includes("LLM_BASE_URL=\"http://localhost:20128/v1\""));
});

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2022"],
"types": ["node"],
"ignoreDeprecations": "6.0",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true,
"declaration": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"noUncheckedIndexedAccess": false,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules", "tests"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts", "src/cli.ts"],
format: ["esm"],
dts: true,
clean: true,
sourcemap: false,
splitting: false,
treeshake: false,
target: "node18",
outDir: "dist",
minify: false,
cjsInterop: false,
});

View File

@@ -45,6 +45,7 @@ Each manifest URL follows the pattern:
| `omni-db-backups` | Database & Backups | Trigger system backups, restore from backup files, and manage the SQLite database lifecycle. Supports export, import, and incremental snapshot strategies. |
| `omni-webhooks` | Webhooks | Register, list, test, and remove webhook endpoints. Configure event subscriptions (request.completed, provider.error, budget.exceeded, etc.) and manage delivery retries. |
| `omni-mcp` | MCP Server | Connect to the OmniRoute MCP server (37 tools, 3 transports: SSE/stdio/HTTP). Covers routing, cache, compression, memory, skills, providers, and audit tools across 16 permission scopes. |
| `omni-openhands` | OpenHands Integration | Wire OpenHands agent-server to OmniRoute. Generate env/Docker config: model mapping, sandbox privileges, host-gateway, persistence, CORS. |
| `omni-agents-a2a` | Agents & A2A Protocol | Interact with OmniRoute via JSON-RPC 2.0 agent-to-agent protocol. 6 built-in A2A skills: smart-routing, quota-management, provider-discovery, cost-analysis, health-report, list-capabilities. |
| `omni-version-manager` | Version Manager | Install, start, stop, restart, and update embedded services (9Router, CLIProxyAPI). Monitor service status, retrieve logs, and configure auto-start. |
| `omni-inference` | Inference (OpenAI-compatible) | The core OpenAI-compatible inference endpoints: chat completions, embeddings, images, audio (TTS/STT), moderations, rerank, and the Responses API. |

View File

@@ -0,0 +1,100 @@
---
name: omni-openhands
description: "Integrate OpenHands agent-server with OmniRoute. Generate OpenHands env/Docker config (LLM_MODEL, LLM_BASE_URL, LLM_API_KEY, sandbox privileges, host-gateway, persistence, CORS)."
---
# OmniRoute — OpenHands Integration
Wire an OpenHands agent-server to a running OmniRoute instance.
## Overview
OpenHands is a Docker/Python agent runtime. Pointing it at OmniRoute requires
model-name mapping, sandbox privileges (Python 3.13 socketpair), host-gateway
networking, persistent state and CORS. The `@omniroute/openhands-plugin` package
generates all of it.
## Install
```bash
npm install -g @omniroute/openhands-plugin
# or: npx @omniroute/openhands-plugin ...
```
## Generate OpenHands `.env`
```bash
omniroute-openhands env \
--api-key sk-... \
--model deepseek-chat \
--url http://192.168.3.106:20128 \
--persistence-dir /Users/me/.openhands-state \
--cors-origins http://100.73.44.17:3000
```
Output:
```
LLM_MODEL=deepseek-chat
LLM_BASE_URL=http://192.168.3.106:20128/v1
LLM_API_KEY=sk-...
OH_PERSISTENCE_DIR=/Users/me/.openhands-state
PERMITTED_CORS_ORIGINS=http://100.73.44.17:3000
```
## Generate Docker Compose
```bash
omniroute-openhands compose \
--api-key sk-... \
--model glm-5.2 \
--persistence-dir /Users/me/.openhands-state
```
Produces a service with:
- `privileged: true` — required for Python 3.13 `socket.socketpair()` under
Docker seccomp
- `extra_hosts: host.docker.internal:host-gateway` — sandbox reaches OmniRoute
on the host
- host volume for `OH_PERSISTENCE_DIR` — conversations survive `docker rm`
- `PERMITTED_CORS_ORIGINS` — dashboard origin can hit agent-server
## Model mapping
OpenHands sends model names OmniRoute doesn't recognize. Map them:
| OpenHands | OmniRoute |
|-----------|-----------|
| `deepseek-chat` | `ds/deepseek-v4-flash` |
| `deepseek-reasoner` | `ds/deepseek-v4-pro` |
| `glm-5.2` | `nvidia/z-ai/glm-5.2` |
| `gpt-4o` | `openai/gpt-4o` |
| `claude-sonnet-4.5` | `anthropic/claude-sonnet-4.5` |
Or pass an OmniRoute combo name directly (`--model vivanta-core`) — OmniRoute's
Model Alias Resolver and combo router accept it.
List the full map:
```bash
omniroute-openhands models
```
## CLI reference
| Command | Description |
|---------|-------------|
| `env` | Print OpenHands `.env` |
| `compose` | Print Docker Compose service block |
| `docker-run` | Print a `docker run` command |
| `models` | Print the default model map |
| Flag | Description | Default |
|------|-------------|---------|
| `--api-key` | OmniRoute API key | — |
| `--model` | OpenHands model or OmniRoute combo | — |
| `--url` | OmniRoute base URL | `http://localhost:20128` |
| `--persistence-dir` | Host state dir | `.openhands-state` |
| `--cors-origins` | Allowed origins | `localhost:3000,3001` |
| `--sandbox-image` | Sandbox base image | — |