463 lines
15 KiB
Markdown
463 lines
15 KiB
Markdown
|
|
---
|
||
|
|
name: model-routing
|
||
|
|
description: 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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):**
|
||
|
|
```yaml
|
||
|
|
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):**
|
||
|
|
```yaml
|
||
|
|
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:**
|
||
|
|
```yaml
|
||
|
|
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)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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`)
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
---
|
||
|
|
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`)
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
---
|
||
|
|
brief: implement-feature
|
||
|
|
recommended_model: qwen-coder-mlx
|
||
|
|
model_tier: local
|
||
|
|
---
|
||
|
|
```
|
||
|
|
|
||
|
|
## Resolution API
|
||
|
|
|
||
|
|
### `resolveModel(workspaceId, roleId, context?)`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
await complete_task({
|
||
|
|
// ...
|
||
|
|
modelAlias: "qwen-coder-mlx",
|
||
|
|
modelTier: "local",
|
||
|
|
tokensTotal: tokens.input + tokens.output,
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
**`convoy_events` role_complete:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
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:**
|
||
|
|
```yaml
|
||
|
|
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:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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:**
|
||
|
|
```yaml
|
||
|
|
# 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:**
|
||
|
|
```yaml
|
||
|
|
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`
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
---
|
||
|
|
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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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`
|
||
|
|
|
||
|
|
## Related Skills
|
||
|
|
|
||
|
|
- `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
|