Phase 1: Add 5 SKILL.md files to .agents/skills/ - backlog-sync: plans/ markdown ↔ DB contract - convoy-lifecycle: status machine, gates, MCP tools, checkpoints - model-routing: role→model via OmniRoute profiles + registry - code-review-two-axis: Standards vs Spec parallel sub-agent review - tdd-red-green: TDD loop with seams Phase 2: OmniRoute operational docs - docs/omniroute-role-mapping.md: profile→role matrix, convoy examples - docs/omniroute-setup.md: step-by-step operator guide Phase 3: Worker OpenHands integration - apps/worker/src/session/load-skills.ts: loads .agents/skills/ - apps/worker/src/session/openhands-session.ts: agent-server REST adapter - apps/worker/src/executor/index.ts: createOpenHandsExecutor() factory - apps/worker/src/env.ts: WORKER_SESSION_TYPE, AGENT_SERVER_URL, SESSION_API_KEY - apps/worker/src/handle-job.ts: routes to OpenHands when configured - apps/worker/src/session/minimal-session.ts: injects skills into system prompt - packages/shared/src/worker/index.ts: re-export WorkerExecutorOptions
384 lines
10 KiB
Markdown
384 lines
10 KiB
Markdown
# OmniRoute Setup Guide
|
|
|
|
**Status:** Draft
|
|
**Date:** 2026-09-07
|
|
**Purpose:** Step-by-step guide for operators to register OmniRoute as a model provider in Echodo, configure role routes, and test that everything works.
|
|
|
|
---
|
|
|
|
## What is OmniRoute?
|
|
|
|
OmniRoute is an OpenAI-compatible model routing gateway. It provides **dynamic model profiles** (`auto`, `auto/coding`, `auto/coding-pro`, `auto/reasoning`, `auto/reasoning-pro`) that automatically resolve to the best available model based on your OmniRoute plan and current load.
|
|
|
|
**Benefits for Echodo:**
|
|
- Single API key, 929+ models available
|
|
- Auto-routing: no need to pick individual models per role
|
|
- Fallback/resilience: if one model is down, OmniRoute switches automatically
|
|
- Works through Echodo's existing `openai_compatible` provider type — zero code changes required
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
1. Echodo is running locally (`pnpm dev`) or deployed (Coolify/Dokploy)
|
|
2. Database is initialized (`pnpm db:push` or `pnpm db:migrate`)
|
|
3. You have an OmniRoute account/API key (or plan to use the local instance)
|
|
|
|
---
|
|
|
|
## Step 1: Get OmniRoute Credentials
|
|
|
|
### Option A: OmniRoute Cloud (recommended)
|
|
|
|
1. Sign up at [omniroute.example.com](https://omniroute.example.com)
|
|
2. Create an API key in Settings → API Keys
|
|
3. Note your:
|
|
- **Base URL:** `https://omniroute.example.com/v1` (check your dashboard)
|
|
- **API Key:** `sk-...` (save securely — not committed to git)
|
|
|
|
### Option B: Self-hosted OmniRoute (local)
|
|
|
|
If running OmniRoute locally (e.g., via Docker):
|
|
|
|
1. Clone/run the OmniRoute server
|
|
2. Note your:
|
|
- **Base URL:** `http://localhost:11434/v1` (or whatever port)
|
|
- **API Key:** `any-non-empty-string` (self-hosted often ignores auth)
|
|
|
|
---
|
|
|
|
## Step 2: Set Environment Variables
|
|
|
|
Add to your `.env` (or Coolify environment):
|
|
|
|
```bash
|
|
# OmniRoute credentials
|
|
OMNIROUTE_BASE_URL=https://omniroute.example.com/v1
|
|
OMNIROUTE_API_KEY=sk-your-key-here
|
|
|
|
# (Already set by default) Database
|
|
DATABASE_URL=postgres://echodo:echodo@localhost:5432/echodo
|
|
|
|
# (Already set by default) Redis (for orchestrator + worker)
|
|
REDIS_URL=redis://localhost:6379
|
|
```
|
|
|
|
**Important:** These env vars must be available to:
|
|
- The **web app** (`apps/web`) — for Settings UI
|
|
- The **worker** (`apps/worker`) — for inference
|
|
- The **orchestrator** (`apps/orchestrator`) — for dispatch
|
|
|
|
---
|
|
|
|
## Step 3: Register OmniRoute Providers in Echodo
|
|
|
|
### Via Settings UI (recommended)
|
|
|
|
1. Open `http://localhost:3001` (web app)
|
|
2. Go to **Settings → AI → Providers**
|
|
3. Click **Add Provider** and create one for each profile:
|
|
|
|
**Provider 1: omni-auto**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Display Name | `OmniRoute Auto` |
|
|
| Alias | `omni-auto` |
|
|
| Provider Type | `openai_compatible` |
|
|
| Model ID | `auto` |
|
|
| Base URL | `${OMNIROUTE_BASE_URL}` (your actual URL) |
|
|
| API Key Env Var | `OMNIROUTE_API_KEY` |
|
|
| Metadata | `{"tier": "auto", "provider": "omniroute"}` |
|
|
| Enabled | ✅ |
|
|
|
|
**Provider 2: omni-coding**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Display Name | `OmniRoute Coding` |
|
|
| Alias | `omni-coding` |
|
|
| Provider Type | `openai_compatible` |
|
|
| Model ID | `auto/coding` |
|
|
| Base URL | (same as above) |
|
|
| API Key Env Var | `OMNIROUTE_API_KEY` |
|
|
| Metadata | `{"tier": "auto", "provider": "omniroute"}` |
|
|
| Enabled | ✅ |
|
|
|
|
**Provider 3: omni-coding-pro**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Display Name | `OmniRoute Coding Pro` |
|
|
| Alias | `omni-coding-pro` |
|
|
| Provider Type | `openai_compatible` |
|
|
| Model ID | `auto/coding-pro` |
|
|
| Base URL | (same) |
|
|
| API Key Env Var | `OMNIROUTE_API_KEY` |
|
|
| Metadata | `{"tier": "auto", "provider": "omniroute"}` |
|
|
| Enabled | ✅ |
|
|
|
|
**Provider 4: omni-reasoning**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Display Name | `OmniRoute Reasoning` |
|
|
| Alias | `omni-reasoning` |
|
|
| Provider Type | `openai_compatible` |
|
|
| Model ID | `auto/reasoning` |
|
|
| Base URL | (same) |
|
|
| API Key Env Var | `OMNIROUTE_API_KEY` |
|
|
| Metadata | `{"tier": "auto", "provider": "omniroute"}` |
|
|
| Enabled | ✅ |
|
|
|
|
**Provider 5: omni-reasoning-pro**
|
|
|
|
| Field | Value |
|
|
|-------|-------|
|
|
| Display Name | `OmniRoute Reasoning Pro` |
|
|
| Alias | `omni-reasoning-pro` |
|
|
| Provider Type | `openai_compatible` |
|
|
| Model ID | `auto/reasoning-pro` |
|
|
| Base URL | (same) |
|
|
| API Key Env Var | `OMNIROUTE_API_KEY` |
|
|
| Metadata | `{"tier": "auto", "provider": "omniroute"}` |
|
|
| Enabled | ✅ |
|
|
|
|
### Via tRPC (for automation)
|
|
|
|
```typescript
|
|
// In a script or test
|
|
await trpc.ai.models.createProvider.mutate({
|
|
alias: "omni-coding",
|
|
displayName: "OmniRoute Coding",
|
|
providerType: "openai_compatible",
|
|
modelId: "auto/coding",
|
|
baseUrl: "https://omniroute.example.com/v1",
|
|
apiKeyEnvVar: "OMNIROUTE_API_KEY",
|
|
metadata: { tier: "auto", provider: "omniroute" }
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Step 4: Map Roles to Profiles
|
|
|
|
### Via Settings UI
|
|
|
|
1. Go to **Settings → AI → Role Routing**
|
|
2. For each role, click **Edit** and select the provider:
|
|
|
|
| Role ID | Select Provider |
|
|
|---------|-----------------|
|
|
| `role-conductor` | `omni-reasoning` |
|
|
| `role-architect` | `omni-reasoning` |
|
|
| `role-ia-architect` | `omni-reasoning` |
|
|
| `role-implementer` | `omni-coding` |
|
|
| `role-reviewer` | `omni-coding-pro` |
|
|
| `role-ux-reviewer` | `omni-coding` |
|
|
| `role-doc-writer` | `omni-coding` |
|
|
| `role-design-system-auditor` | `omni-coding` |
|
|
| `role-a11y-auditor` | `omni-coding` |
|
|
| `gate-copilot` | `omni-reasoning` |
|
|
| `chat-default` | `omni-auto` |
|
|
|
|
### Via tRPC
|
|
|
|
```typescript
|
|
// Example: route implementer to omni-coding
|
|
await trpc.ai.models.upsertRoleRoute.mutate({
|
|
roleId: "role-implementer",
|
|
providerAliasId: "omni-coding-provider-uuid" // get from listProviders
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Step 5: Test Connectivity
|
|
|
|
### Via Settings UI
|
|
|
|
1. Go to **Settings → AI → Providers**
|
|
2. Click the **Test** button next to `omni-coding`
|
|
3. You should see: ✅ `Success — latency: 320ms`
|
|
|
|
### Via MCP (if worker is running)
|
|
|
|
```typescript
|
|
// testProvider
|
|
{
|
|
"tool": "testProvider",
|
|
"arguments": { "alias": "omni-coding" }
|
|
}
|
|
```
|
|
|
|
### Via direct curl
|
|
|
|
```bash
|
|
curl ${OMNIROUTE_BASE_URL}/chat/completions \
|
|
-H "Authorization: Bearer ${OMNIROUTE_API_KEY}" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"model": "auto/coding",
|
|
"messages": [{"role": "user", "content": "Say hello in 3 words"}],
|
|
"max_tokens": 20
|
|
}'
|
|
```
|
|
|
|
Expected: A response like `{"choices": [{"message": {"content": "Hello world!"}}]}`
|
|
|
|
---
|
|
|
|
## Step 6: Test Model Resolution
|
|
|
|
### Verify a specific role resolves correctly
|
|
|
|
Use the debug endpoint (or MCP `resolveForRole`):
|
|
|
|
```typescript
|
|
resolveForRole({ roleId: "role-implementer" })
|
|
// Expected output:
|
|
// {
|
|
// ok: true,
|
|
// model: {
|
|
// alias: "omni-coding",
|
|
// providerType: "openai_compatible",
|
|
// modelId: "auto/coding",
|
|
// baseUrl: "https://omniroute.example.com/v1",
|
|
// apiKey: "<redacted>",
|
|
// tier: "auto"
|
|
// },
|
|
// source: "workspace",
|
|
// resolvedAlias: "omni-coding"
|
|
// }
|
|
```
|
|
|
|
### Verify resolution order works
|
|
|
|
Create a test convoy with a model override:
|
|
|
|
```yaml
|
|
# .convoys/test-resolver.md
|
|
---
|
|
name: test-resolver
|
|
model_policy:
|
|
roles:
|
|
role-implementer: omni-reasoning-pro # overrides workspace route
|
|
---
|
|
```
|
|
|
|
Then `resolveForRole` with `convoyContext`:
|
|
|
|
```typescript
|
|
resolveForRole({
|
|
roleId: "role-implementer",
|
|
convoyContext: { model_policy: { "role-implementer": "omni-reasoning-pro" } }
|
|
})
|
|
// → source: "convoy", alias: "omni-reasoning-pro"
|
|
```
|
|
|
|
---
|
|
|
|
## Step 7: Verify Worker Picks Up the Model
|
|
|
|
1. **Start the worker:**
|
|
```bash
|
|
pnpm --filter @tasks/worker dev
|
|
# or
|
|
WORKER_DRY_RUN=0 ORCHESTRATOR_DRY_RUN=0 REDIS_URL=redis://localhost:6379 pnpm --filter @tasks/worker dev
|
|
```
|
|
|
|
2. **Start the orchestrator:**
|
|
```bash
|
|
pnpm --filter @tasks/orchestrator dev
|
|
```
|
|
|
|
3. **Enqueue a test job** (via MCP `create_convoy` → transition to `ready`)
|
|
|
|
4. **Watch worker logs:**
|
|
- Should see: `Dequeued job for role-implementer, resolved model: omni-coding (auto/coding)`
|
|
- Should see: inference call to OmniRoute endpoint
|
|
|
|
5. **Verify model metadata in `agent_runs`:**
|
|
```sql
|
|
SELECT model_alias, provider_type, model_id, model_tier
|
|
FROM agent_runs
|
|
ORDER BY created_at DESC
|
|
LIMIT 5;
|
|
```
|
|
Expected: `omni-coding | openai_compatible | auto/coding | auto`
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "Provider alias 'omni-coding' is not registered in this workspace"
|
|
|
|
**Cause:** Provider not created yet, or `alias` doesn't match.
|
|
|
|
**Fix:**
|
|
1. Check Settings → AI → Providers for the alias name
|
|
2. Re-run `createProvider` with exact alias
|
|
|
|
### "Environment variable OMNIROUTE_API_KEY is not set"
|
|
|
|
**Cause:** Worker process doesn't have the env var loaded.
|
|
|
|
**Fix:**
|
|
1. Verify `OMNIROUTE_API_KEY` is in `.env` at repo root
|
|
2. Restart the worker process (env vars aren't hot-reloaded)
|
|
3. Check: `echo $OMNIROUTE_API_KEY` in the worker terminal
|
|
|
|
### "Connection refused" or "ECONNREFUSED"
|
|
|
|
**Cause:** OmniRoute server not reachable at `base_url`.
|
|
|
|
**Fix:**
|
|
1. Verify OmniRoute is running: `curl ${OMNIROUTE_BASE_URL}/models`
|
|
2. Check base URL ends with `/v1`
|
|
3. For local OmniRoute: check port (11434 vs 8000 vs other)
|
|
|
|
### "Unsupported provider type"
|
|
|
|
**Cause:** `providerType` value not in `providerTypeSchema`.
|
|
|
|
**Fix:** Must be one of: `anthropic`, `openai`, `google`, `openai_compatible`, `azure_openai`. For OmniRoute, use `openai_compatible`.
|
|
|
|
### Model returns empty or unexpected response
|
|
|
|
**Cause:** OmniRoute profile not available or rate-limited.
|
|
|
|
**Fix:**
|
|
1. Check OmniRoute dashboard for rate limits
|
|
2. Try a different profile (e.g., `auto` instead of `auto/coding`)
|
|
3. Check OmniRoute status page
|
|
|
|
### Resolution returns wrong source
|
|
|
|
**Cause:** Convoy or brief override taking precedence unexpectedly.
|
|
|
|
**Fix:** Remember the priority order:
|
|
1. Brief `recommended_model` (highest)
|
|
2. Convoy `model_policy.roles[roleId]`
|
|
3. Workspace `role_model_routes` row
|
|
4. Pipeline defaults (lowest)
|
|
|
|
If you want workspace route to win, remove `model_policy` from the convoy.
|
|
|
|
---
|
|
|
|
## Security Notes
|
|
|
|
- **API key never in DB:** `api_key_env_var` stores the *name* of the env var, not the value
|
|
- **API key never sent to browser:** Settings UI shows env var name only
|
|
- **Worker reads env var at dispatch time:** Not stored in job payload
|
|
- **Test endpoint runs server-side:** Rate-limited per workspace
|
|
|
|
---
|
|
|
|
## Related Docs
|
|
|
|
- **Role mapping:** `docs/omniroute-role-mapping.md` (profile → role matrix, resolution flow)
|
|
- **ADR:** `docs/adr-model-routing.md` (architecture decision record)
|
|
- **Skill:** `.agents/skills/model-routing/SKILL.md` (developer reference)
|
|
- **Provider schema:** `packages/database/src/schema/model_providers.ts`
|
|
- **Route schema:** `packages/database/src/schema/role_model_routes.ts`
|