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
15 KiB
| name | description |
|---|---|
| model-routing | Route pipeline roles to AI models via workspace registry and OmniRoute profiles. Understand resolution order, convoy overrides, tier mapping, and OmniRoute integration. Use when configuring models, debugging route failures, or working with model_policy. |
Model Routing
Echodo's model routing system connects pipeline roles (conductor, architect, implementer, reviewer) to AI models via a workspace-scoped registry with layered overrides. The system integrates with OmniRoute for dynamic model selection across 5 profiles (auto, auto/coding, auto/coding-pro, auto/reasoning, auto/reasoning-pro). This skill teaches you how routes resolve, where to configure them, and how the orchestrator dispatches work.
When to Activate
Use this skill when:
- Configuring model providers or role routes in Settings → AI
- Understanding
model_policyin.convoys/*.md - Debugging model resolution failures
- Integrating OmniRoute (cloud or local)
- Logging model metadata in
agent_runsorconvoy_events - Adding new pipeline roles that need model routing
- Testing provider connectivity (
testProvider)
Core Concepts
1. Four-Layer Resolution Order (First Match Wins)
Priority from highest to lowest:
1. Brief override ──→ recommended_model in .convoys/<slug>/brief-<N>-*.md
(Direct per-task model selection)
2. Convoy override ──→ model_policy.roles in .convoys/<slug>.md
(Per-convoy role→alias mapping)
3. Workspace route ──→ role_model_routes row in DB
(Settings → AI → Role Routing table)
4. Pipeline template ──→ default role mapping
(Shipped seed data for new workspaces)
Example: A task has recommended_model: qwen-coder-mlx, its convoy has model_policy.roles: { role-implementer: claude-planning }, and the workspace route maps role-implementer → qwen-coder. The system picks qwen-coder-mlx (brief override wins).
2. Registry Components
| Component | Table | Purpose |
|---|---|---|
| Model Provider | model_providers |
Concrete model config: API endpoint, auth, params |
| Role Route | role_model_routes |
Which alias serves which role |
| Convoy Override | .convoys/*.md frontmatter |
Per-convoy role mapping |
| Brief Override | .convoys/<slug>/brief-*.md frontmatter |
Per-task specific model |
3. OmniRoute Integration
OmniRoute profiles map to Echodo roles:
| OmniRoute Profile | Echodo Role | Use Case |
|---|---|---|
auto/reasoning |
role-conductor, role-architect |
Planning, classification, design |
auto/reasoning-pro |
role-architect (escalation), gate-copilot |
Complex reasoning, escalation path |
auto/coding |
role-implementer, role-doc-writer |
Code/docs implementation |
auto/coding-pro |
role-reviewer, role-implementer (escalation) |
Code review, complex refactoring |
auto |
chat-default, fallback |
General purpose, fastest |
How it works:
- Echodo resolves alias via
resolveModel(workspace, roleId) - Returns
ResolvedModelwith provider type, model ID, base URL, env var name - If provider is
omni_routetype (or base URL points to OmniRoute), OmniRoute resolves the alias to a live endpoint - Worker uses resolved endpoint for inference
4. The ResolvedModel Contract
// Runtime structure (not persisted)
type ResolvedModel = {
alias: string; // "qwen-coder-mlx"
providerType: ProviderType; // "openai_compatible"
modelId: string; // "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit"
baseUrl?: string; // "http://192.168.1.42:8000/v1"
apiKey: string; // From process.env[apiKeyEnvVar]
extraHeaders?: Record<string, string>;
defaultParams?: { temperature?: number; maxTokens?: number };
tier?: "premium" | "fast" | "local" | "auto";
};
Key invariants:
- Never store API keys in DB → only
apiKeyEnvVarnames - Worker must have env var populated at runtime
baseUrlrequired foropenai_compatibleprovider typetieris advisory (cost/performance grouping)
Model Provider Types
Provider Type Matrix
| Type | Client | API Format | Example Alias | Use For |
|---|---|---|---|---|
anthropic |
@ai-sdk/anthropic |
Anthropic Messages API | claude-planning |
Planning, reasoning, complex tasks |
openai |
@ai-sdk/openai |
OpenAI Chat Completion | gpt-4o-mini |
General, chat |
google |
@ai-sdk/google |
Gemini REST API | gemini-flash |
Multimodal, reasoning |
openai_compatible |
@ai-sdk/openai + baseURL |
OpenAI-compatible | qwen-coder-mlx, nim-llama-70b |
Local inference, NIM |
azure_openai |
@ai-sdk/openai + Azure config |
Azure OpenAI | azure-gpt4 |
Enterprise Azure deployments |
omni_route |
Custom client | OmniRoute API | auto/coding |
Dynamic routing across 929 models |
Local Inference Setup Examples
Ollama (Mac):
alias: qwen-coder
provider_type: openai_compatible
model_id: qwen2.5-coder:32b
base_url: http://192.168.1.42:11434/v1
api_key_env_var: OLLAMA_API_KEY # placeholder, any non-empty string
metadata: { tier: local, runtime: ollama }
MLX (Apple Silicon via mlx-openai-server):
alias: qwen-coder-mlx
provider_type: openai_compatible
model_id: mlx-community/Qwen2.5-Coder-32B-Instruct-4bit
base_url: http://192.168.1.42:8000/v1
api_key_env_var: MLX_API_KEY
metadata: { tier: local, runtime: mlx }
Parallel implementers: Use mlx-openai-server (port 8000) over mlx_lm.server (8080) for better batching when orchestrator fans out to multiple implementers.
OmniRoute Cloud:
alias: auto/coding
provider_type: omni_route
model_id: auto/coding
base_url: https://omniroute-cloud.example.com/v1
api_key_env_var: OMNIROUTE_API_KEY
metadata: { tier: auto }
How to Configure
1. Create Provider (Settings → AI → Providers)
// Example: Add local MLX provider
{
alias: "qwen-coder-mlx",
display_name: "Qwen2.5 Coder MLX",
provider_type: "openai_compatible",
model_id: "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit",
base_url: "http://192.168.1.42:8000/v1",
api_key_env_var: "MLX_API_KEY",
metadata: { tier: "local", runtime: "mlx" }
}
Test: Click "Test" button in UI. It sends a small token request to verify connectivity.
2. Map Role to Alias (Settings → AI → Role Routing)
// Example: Route implementers to local MLX
{
role_id: "role-implementer",
provider_alias_id: "qwen-coder-mlx-id"
}
Supported role IDs:
role-conductor,role-architect,role-implementer,role-reviewerrole-ia-architect,role-ux-reviewer,role-design-system-auditor,role-a11y-auditor,role-doc-writergate-copilot,chat-default- Custom roles (string IDs, no migration needed)
3. Convoy Override (.convoys/*.md)
---
name: feature-x
model_policy:
roles:
role-conductor: claude-planning
role-architect: claude-planning
role-implementer: qwen-coder-mlx
role-reviewer: qwen-coder-mlx
escalate_to: claude-planning
---
4. Brief Override (.convoys/<slug>/brief-*.md)
---
brief: implement-feature
recommended_model: qwen-coder-mlx
model_tier: local
---
Resolution API
resolveModel(workspaceId, roleId, context?)
// In packages/shared/src/orchestrator/resolve.ts
function resolveModel(input: {
workspaceId: string;
roleId: string;
briefContext?: { recommended_model?: string };
convoyContext?: { model_policy?: Record<string, string> };
}): ResolvedModel
Implementation steps:
- If
briefContext?.recommended_modelexists, use that alias - Else if
convoyContext?.model_policy?.[roleId]exists, use that alias - Else lookup
role_model_routesrow for(workspaceId, roleId) - Else use default mapping (pipeline template)
- Load provider config for alias from
model_providers - Validate provider is enabled
- Read API key from env var
- Return
ResolvedModel
Orchestrator Usage
// When dispatching a job
const resolved = await resolveModel({
workspaceId: convoy.workspace_id,
roleId: brief.role_id,
briefContext: { recommended_model: brief.recommended_model },
convoyContext: { model_policy: convoy.model_policy }
});
// Job payload includes model spec
await enqueueJob({
roleId: brief.role_id,
briefSlug: brief.slug,
modelSpec: resolved,
// ...
});
Escalation Logic
When model_policy.escalate_to is set or workspace policy defines escalation:
// After N implementer failures (default 2)
if (attempts >= 2 && convoy.model_policy?.escalate_to) {
const escalated = await resolveModel({
workspaceId: convoy.workspace_id,
roleId: roleId,
convoyContext: { model_policy: { [roleId]: convoy.model_policy.escalate_to } }
});
// Re-dispatch with escalated model
}
Logging Model Metadata
agent_runs columns:
| Column | Source | Example |
|---|---|---|
model_alias |
ResolvedModel.alias |
qwen-coder-mlx |
provider_type |
ResolvedModel.providerType |
openai_compatible |
model_id |
ResolvedModel.modelId |
mlx-community/Qwen2.5-Coder-32B-Instruct-4bit |
model_tier |
ResolvedModel.tier |
local |
MCP complete_task fields:
await complete_task({
// ...
modelAlias: "qwen-coder-mlx",
modelTier: "local",
tokensTotal: tokens.input + tokens.output,
});
convoy_events role_complete:
await log_convoy_event({
// ...
metadata: {
model: "qwen-coder-mlx",
tier: "local",
tokens_in: 12345,
tokens_out: 6789,
}
});
Default Mappings (Pipeline Template)
New workspaces get these seed routes:
| Role ID | Default Alias | Tier | Purpose |
|---|---|---|---|
role-conductor |
claude-planning |
premium | Kickoff, classification |
role-architect |
claude-planning |
premium | Design, brief decomposition |
role-ia-architect |
claude-planning |
premium | Information architecture |
role-ux-reviewer |
qwen-coder |
fast | UI/UX review |
role-implementer |
qwen-coder |
fast | Code implementation |
role-reviewer |
qwen-coder |
fast | Code review |
role-design-system-auditor |
qwen-coder |
fast | Design system audit |
role-a11y-auditor |
qwen-coder |
fast | Accessibility audit |
role-doc-writer |
qwen-coder |
fast | Documentation writing |
gate-copilot |
claude-planning |
premium | Gate decision support |
chat-default |
workspace choice | — | Web chat default |
Operators can override these defaults in Settings.
Common Patterns
Setting Up Local MLX + OmniRoute Hybrid
Goal: Use local MLX for coding, OmniRoute for reasoning.
- Create local MLX provider:
qwen-coder-mlx(port 8000) - Create OmniRoute provider:
auto/reasoning(cloud endpoint) - Route roles:
role-implementer→qwen-coder-mlxrole-conductor→auto/reasoningrole-architect→auto/reasoning-pro
- Convoy example:
model_policy: roles: role-conductor: auto/reasoning role-implementer: qwen-coder-mlx
Debugging Resolution Failures
Symptoms: Job fails with "Cannot resolve model for role-implementer"
Debug steps:
- Check brief frontmatter:
recommended_modelpresent? - Check convoy frontmatter:
model_policy.roles[role-implementer]present? - Query DB:
SELECT * FROM role_model_routes WHERE workspace_id = ? AND role_id = 'role-implementer' - Check provider: Is alias enabled in
model_providers? - Test provider: Run
testProviderMCP tool for the alias - Check env var: Is
api_key_env_varpopulated in worker process?
Migration from Cursor Model Slugs
Legacy convoys may have Cursor slugs (claude-4.6-opus-high-thinking). Add legacy alias mapping in workspace metadata:
{
"legacy_model_aliases": {
"claude-4.6-opus-high-thinking": "claude-planning",
"composer-2.5-fast": "qwen-coder"
}
}
During resolution, check legacy_model_aliases[alias] before failing.
OmniRoute as the Sole Provider
If you want all routing via OmniRoute:
- Create OmniRoute providers for all 5 profiles
auto,auto/coding,auto/coding-pro,auto/reasoning,auto/reasoning-pro
- Route roles accordingly
- All inference goes through OmniRoute cloud endpoint
- Advantages: Dynamic model selection, load balancing, fallbacks
- Disadvantages: Network dependency, cost (vs local free)
Examples
Example 1: Workspace with Local + Cloud Mix
Providers:
# Local MLX (free, fast for coding)
- alias: qwen-coder-mlx
provider_type: openai_compatible
base_url: http://mac:8000/v1
tier: local
# Cloud Claude (costly, good for planning)
- alias: claude-planning
provider_type: anthropic
tier: premium
# OmniRoute fallback
- alias: auto/reasoning
provider_type: omni_route
tier: auto
Role routes:
role-conductor→claude-planningrole-implementer→qwen-coder-mlxrole-reviewer→qwen-coder-mlxgate-copilot→auto/reasoning(fallback when Claude unavailable)
Convoy policy:
model_policy:
roles:
role-conductor: claude-planning
role-implementer: qwen-coder-mlx
escalate_to: auto/reasoning
Example 2: Brief Override for Complex Task
Brief: .convoys/optimize-db/brief-5-complex-migration.md
---
brief: complex-db-migration
recommended_model: auto/reasoning-pro
model_tier: premium
---
# Complex schema migration
Needs reasoning about foreign keys and data integrity.
The recommended_model: auto/reasoning-pro overrides the convoy's model_policy.roles[role-implementer].
Example 3: Testing Provider Connectivity
# Via MCP tool
testProvider({ alias: "qwen-coder-mlx" })
# Response
{
"success": true,
"latency_ms": 245,
"model": "mlx-community/Qwen2.5-Coder-32B-Instruct-4bit"
}
# Error case
{
"success": false,
"error": "Connection refused",
"detail": "Check base_url and that mlx-openai-server is running on port 8000"
}
References
- ADR:
docs/adr-model-routing.md(full specification) - Schema:
packages/database/src/schema/model_providers.ts,role_model_routes.ts - Resolver:
packages/shared/src/orchestrator/resolve.ts(or equivalent) - OmniRoute docs:
.convoys/model-routing-registry.mdif exists - Local inference:
docs/adr-model-routing.md#local-mlx-on-apple-silicon-mac - Worker integration:
docs/worker.md#model-resolution
Related Skills
backlog-sync— brief frontmatter withrecommended_modelconvoy-lifecycle— convoy frontmatter withmodel_policyopenhands-worker— usesResolvedModelfor inferenceomniroute-setup— operational guide for OmniRoute integration