# OmniRoute Role Mapping **Status:** Draft **Date:** 2026-09-07 **Purpose:** Map Echodo pipeline roles to OmniRoute profiles so the orchestrator/worker can route each role to the right model automatically. --- ## Overview OmniRoute is an OpenAI-compatible gateway that fronts **929+ models** with a small set of **dynamic profiles**. Instead of configuring individual model IDs per role, we configure **profiles** that OmniRoute resolves to a live model endpoint: | Profile | Purpose | Typical Resolves To | |---------|---------|---------------------| | `auto` | Fastest general-purpose | Small/fast model | | `auto/coding` | Code-specialized | Coding-tuned model | | `auto/coding-pro` | Complex code tasks, reviews | High-quality coding model | | `auto/reasoning` | Planning, reasoning, architecture | Reasoning-tuned model | | `auto/reasoning-pro` | Complex reasoning, escalation | Top reasoning model | OmniRoute is integrated into Echodo as an **`openai_compatible` provider** — the same way Ollama and MLX are, but pointing at the OmniRoute base URL and using profile names as `modelId`. --- ## Profile → Role Matrix ### Recommended Defaults | Role ID | OmniRoute Profile | Why | |---------|-------------------|-----| | `role-conductor` | `auto/reasoning` | Convoy kickoff: classify, skip flags, success metric | | `role-architect` | `auto/reasoning` | Design, brief decomposition, architecture | | `role-architect` (escalation) | `auto/reasoning-pro` | Complex/hard design problems | | `role-ia-architect` | `auto/reasoning` | Information architecture planning | | `role-implementer` | `auto/coding` | Default code implementation | | `role-implementer` (escalation) | `auto/coding-pro` | After 2 failed attempts | | `role-reviewer` | `auto/coding-pro` | Two-axis code review needs quality | | `role-ux-reviewer` | `auto/coding` | UI review | | `role-doc-writer` | `auto/coding` | Docs are simpler than code | | `role-design-system-auditor` | `auto/coding` | Design system review | | `role-a11y-auditor` | `auto/coding` | Accessibility review | | `gate-copilot` | `auto/reasoning` | Summarize for human gate | | `chat-default` | `auto` | Web chat default | ### OmniRoute-Only Workspace (Simple Setup) If a workspace does **not** use local models (no Ollama/MLX), just create **5 OmniRoute providers** (one per profile) and route every role to the relevant profile: ```yaml # Workspace model providers (via Settings → AI → Providers) - alias: omni-auto provider_type: openai_compatible model_id: auto base_url: ${OMNIROUTE_BASE_URL} # e.g. https://omniroute.example.com/v1 api_key_env_var: OMNIROUTE_API_KEY metadata: { tier: auto, provider: omniroute } - alias: omni-coding provider_type: openai_compatible model_id: auto/coding base_url: ${OMNIROUTE_BASE_URL} api_key_env_var: OMNIROUTE_API_KEY metadata: { tier: auto, provider: omniroute } - alias: omni-coding-pro provider_type: openai_compatible model_id: auto/coding-pro base_url: ${OMNIROUTE_BASE_URL} api_key_env_var: OMNIROUTE_API_KEY metadata: { tier: auto, provider: omniroute } - alias: omni-reasoning provider_type: openai_compatible model_id: auto/reasoning base_url: ${OMNIROUTE_BASE_URL} api_key_env_var: OMNIROUTE_API_KEY metadata: { tier: auto, provider: omniroute } - alias: omni-reasoning-pro provider_type: openai_compatible model_id: auto/reasoning-pro base_url: ${OMNIROUTE_BASE_URL} api_key_env_var: OMNIROUTE_API_KEY metadata: { tier: auto, provider: omniroute } ``` Then map roles: | Role ID | Alias | |---------|-------| | `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` | ### Hybrid Setup (Local + OmniRoute) For a homelab that wants free local inference AND cloud fallback: | Role ID | Default | Escalation | |---------|---------|------------| | `role-conductor` | `omni-reasoning` (cloud) | — | | `role-architect` | `omni-reasoning-pro` (cloud) | — | | `role-implementer` | `qwen-coder-mlx` (local) | `omni-coding-pro` (cloud) | | `role-reviewer` | `qwen-coder-mlx` (local) | `omni-coding-pro` (cloud) | | `role-ux-reviewer` | `qwen-coder-mlx` (local) | `omni-coding` | | `role-doc-writer` | `qwen-coder-mlx` (local) | `omni-coding` | | `gate-copilot` | `omni-reasoning` (cloud) | — | | `chat-default` | `omni-auto` (cloud) | — | Hardcoded in convoy `model_policy`: ```yaml model_policy: roles: role-conductor: omni-reasoning role-architect: omni-reasoning-pro role-implementer: qwen-coder-mlx role-reviewer: omni-coding-pro role-doc-writer: qwen-coder-mlx escalate_to: omni-coding-pro ``` --- ## How Resolution Works Echodo's `resolveModel(workspaceId, roleId, ctx?)` (pure function in `packages/shared/src/model-routing/resolve-model.ts`) applies this order: ``` 1. Brief override → recommended_model in .convoys//brief-*.md 2. Convoy override → model_policy.roles[roleId] in .convoys/.md 3. Workspace route → role_model_routes row in DB 4. Pipeline template → PIPELINE_DEFAULT_ROLE_ROUTES (shipped defaults) ``` The resolved `ResolvedModel` object is passed to the worker. The worker creates a Vercel AI SDK `LanguageModel` via `createLanguageModelFromResolved` (`packages/ai/src/providers/resolve-client.ts`). **Because OmniRoute is `openai_compatible`:** it flows through the existing `case "openai_compatible"` branch, so **no code change is required** in `resolve-client.ts` — it uses `createOpenAI({ apiKey, baseURL })` with `modelId` = profile name. --- ## Resolved Model Example For `role-implementer` → `omni-coding`: ```typescript { alias: "omni-coding", providerType: "openai_compatible", modelId: "auto/coding", baseUrl: "https://omniroute.example.com/v1", apiKey: "sk-...", // read from OMNIROUTE_API_KEY at runtime tier: "auto" } ``` The worker's `createLanguageModelFromResolved` builds: ```typescript const openai = createOpenAI({ apiKey: "sk-...", baseURL: "https://omniroute.example.com/v1", }); const model = openai("auto/coding"); ``` OmniRoute then routes `auto/coding` to an appropriate live model per its own logic. --- ## Convoy Frontmatter ### OmniRoute-Only Convoy ```yaml --- name: feature-auth classification: server-only success_metric: POST /auth/register works and returns 201 skip: - ia - ux - design - a11y - visual - smoke status: draft created: 2026-09-07 model_policy: roles: role-conductor: omni-reasoning role-architect: omni-reasoning role-implementer: omni-coding role-reviewer: omni-coding-pro escalate_to: omni-coding-pro --- ``` ### Hybrid Convoy ```yaml --- name: feature-local-first model_policy: roles: role-conductor: omni-reasoning role-architect: omni-reasoning role-implementer: qwen-coder-mlx role-reviewer: qwen-coder-mlx escalate_to: omni-coding-pro --- ``` ### Brief Override ```yaml --- brief: complex-db-migration recommended_model: omni-reasoning-pro # overrides convoy + workspace model_tier: auto --- ``` --- ## Environment Variables | Variable | Where | Purpose | |----------|-------|---------| | `OMNIROUTE_BASE_URL` | Worker + web process env | OmniRoute endpoint (must end in `/v1`) | | `OMNIROUTE_API_KEY` | Worker + web process env | Autogenerated by OpenHands/OmniRoute | These names are **referenced** in `model_providers.apiKeyEnvVar` — the actual values live in deploy env (Coolify/Dokploy/Mac), never in the DB or committed files. --- ## Testing the Mapping ### Via tRPC ```typescript // Debug endpoint: resolve a role to a model resolveForRole({ roleId: "role-implementer" }) // → { ok: true, model: { alias: "omni-coding", modelId: "auto/coding", ... } } ``` ### Via MCP testProvider ```typescript testProvider({ alias: "omni-coding" }) // → { success: true, latency_ms: 320, model: "auto/coding" } ``` ### Via direct OpenAI-compatible call ```bash curl ${OMNIROUTE_BASE_URL}/models \ -H "Authorization: Bearer ${OMNIROUTE_API_KEY}" # → List of available models (929+ for OmniRoute) ``` --- ## Default Pipeline Template After OmniRoute Update `PIPELINE_DEFAULT_ROLE_ROUTES` in `packages/shared/src/model-routing/constants.ts` if OmniRoute is the default for new workspaces: ```typescript export const PIPELINE_DEFAULT_ROLE_ROUTES: Readonly> = { "role-conductor": "omni-reasoning", "role-architect": "omni-reasoning", "role-ia-architect": "omni-reasoning", "role-ux-reviewer": "omni-coding", "role-implementer": "omni-coding", "role-reviewer": "omni-coding-pro", "role-design-system-auditor": "omni-coding", "role-a11y-auditor": "omni-coding", "role-doc-writer": "omni-coding", "gate-copilot": "omni-reasoning", "chat-default": "omni-auto", }; ``` *(Note: changing the template only affects new workspaces — existing workspace routes in the DB take precedence.)* --- ## Open Questions 1. **Should the template switch to OmniRoute aliases by default, or stay Claude/Qwen and let operators pick OmniRoute?** Recommend: keep Claude/Qwen seed, document OmniRoute as an option until it's proven. 2. **Does OmniRoute support streaming + function calling?** Needed for the worker tool loop. Verify via a live test before routing implementer roles to OmniRoute. 3. **Rate limits / cost:** OmniRoute `auto/*` profiles rotate models — need token/cost tracking in `agent_runs` to monitor spend. --- ## References - **Resolver:** `packages/shared/src/model-routing/resolve-model.ts` - **Schemas:** `packages/shared/src/model-routing/schemas.ts` - **Constants:** `packages/shared/src/model-routing/constants.ts` - **Client builder:** `packages/ai/src/providers/resolve-client.ts` - **Provider schema:** `packages/database/src/schema/model_providers.ts` - **Route schema:** `packages/database/src/schema/role_model_routes.ts` - **ADR:** `docs/adr-model-routing.md` - **Skill:** `.agents/skills/model-routing/SKILL.md`