Submatter/.agents/skills/model-routing/SKILL.md
Randall Stillwell 3fc7d01585 feat(worker): OpenHands session bootstrap + 5 Agent Skills
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
2026-09-07 15:10:02 -05:00

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_policy in .convoys/*.md
  • Debugging model resolution failures
  • Integrating OmniRoute (cloud or local)
  • Logging model metadata in agent_runs or convoy_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:

  1. Echodo resolves alias via resolveModel(workspace, roleId)
  2. Returns ResolvedModel with provider type, model ID, base URL, env var name
  3. If provider is omni_route type (or base URL points to OmniRoute), OmniRoute resolves the alias to a live endpoint
  4. 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 apiKeyEnvVar names
  • Worker must have env var populated at runtime
  • baseUrl required for openai_compatible provider type
  • tier is 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-reviewer
  • role-ia-architect, role-ux-reviewer, role-design-system-auditor, role-a11y-auditor, role-doc-writer
  • gate-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:

  1. If briefContext?.recommended_model exists, use that alias
  2. Else if convoyContext?.model_policy?.[roleId] exists, use that alias
  3. Else lookup role_model_routes row for (workspaceId, roleId)
  4. Else use default mapping (pipeline template)
  5. Load provider config for alias from model_providers
  6. Validate provider is enabled
  7. Read API key from env var
  8. 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.

  1. Create local MLX provider: qwen-coder-mlx (port 8000)
  2. Create OmniRoute provider: auto/reasoning (cloud endpoint)
  3. Route roles:
    • role-implementer → qwen-coder-mlx
    • role-conductor → auto/reasoning
    • role-architect → auto/reasoning-pro
  4. 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:

  1. Check brief frontmatter: recommended_model present?
  2. Check convoy frontmatter: model_policy.roles[role-implementer] present?
  3. Query DB: SELECT * FROM role_model_routes WHERE workspace_id = ? AND role_id = 'role-implementer'
  4. Check provider: Is alias enabled in model_providers?
  5. Test provider: Run testProvider MCP tool for the alias
  6. Check env var: Is api_key_env_var populated 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:

  1. Create OmniRoute providers for all 5 profiles
    • auto, auto/coding, auto/coding-pro, auto/reasoning, auto/reasoning-pro
  2. Route roles accordingly
  3. All inference goes through OmniRoute cloud endpoint
  4. Advantages: Dynamic model selection, load balancing, fallbacks
  5. 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-planning
  • role-implementer → qwen-coder-mlx
  • role-reviewer → qwen-coder-mlx
  • gate-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.md if exists
  • Local inference: docs/adr-model-routing.md#local-mlx-on-apple-silicon-mac
  • Worker integration: docs/worker.md#model-resolution
  • backlog-sync — brief frontmatter with recommended_model
  • convoy-lifecycle — convoy frontmatter with model_policy
  • openhands-worker — uses ResolvedModel for inference
  • omniroute-setup — operational guide for OmniRoute integration