**Context:** Echodo automation foundation — orchestrator, worker, and mission-control UI without Cursor as the runtime substrate.
## Decision
Echodo will own a **workspace-scoped model provider registry** and an **extensible role → model routing table**. Every automated pipeline run resolves a concrete provider configuration before inference. Resolution is deterministic, logged on every run, and configurable from Settings without redeploying orchestrator or worker code.
## Problem
The agent-pipeline defines per-role models via:
-`model:` frontmatter in `.cursor/agents/role-*.md` (Cursor-only enforcement)
-`model_policy.roles` in convoy frontmatter (recommendation only)
-`recommended_model` on implementer briefs (optional override)
That works for manual Cursor sessions. It does **not** work when:
- An orchestrator dispatches `role-implementer` unattended
- Planning uses Claude API and implementation uses local Ollama on a Mac
Future: `custom` with a registered adapter — not in v1.
### `role_model_routes` (per workspace)
| Column | Type | Notes |
|--------|------|-------|
| `id` | uuid PK | |
| `workspace_id` | uuid FK | |
| `role_id` | varchar | e.g. `role-implementer`, `gate-copilot` |
| `provider_alias_id` | uuid FK → `model_providers.id` | |
| `enabled` | boolean | |
| `sort_order` | int | UI ordering |
| `created_at` / `updated_at` | timestamptz | |
Unique constraint: `(workspace_id, role_id)`.
`role_id` is a **string**, not a DB enum — new pipeline roles do not require migrations.
### Resolved config (runtime, not persisted)
```typescript
type ResolvedModel = {
alias: string;
providerType: ProviderType;
modelId: string;
baseUrl?: string;
apiKey: string; // read from process.env[apiKeyEnvVar] at call time
extraHeaders?: Record<string,string>;
defaultParams?: { temperature?: number; maxTokens?: number };
tier?: "premium" | "fast" | "local" | "auto";
};
```
Pure function: `resolveModel(input, db)` lives in `packages/shared` (types + zod) with implementation in `packages/ai` or `packages/shared` (no React, no DB imports in shared — DB fetch in tRPC/orchestrator, pass rows into resolver).
## Provider examples
### Local 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 # any non-empty string Ollama accepts
metadata: { tier: local }
```
### Local MLX on Apple Silicon (Mac)
[MLX](https://github.com/ml-explore/mlx) is Apple's on-device ML framework. Echodo does **not** talk to MLX directly — use an **OpenAI-compatible HTTP server** in front of MLX, same `provider_type: openai_compatible` as Ollama.
| Server | Package | Default port | When to use |
|--------|---------|--------------|-------------|
| **mlx_lm.server** | `pip install mlx-lm` | 8080 | Solo dev, single implementer, simplest path |
| **mlx-openai-server** | [cubist38/mlx-openai-server](https://github.com/cubist38/mlx-openai-server) | 8000 | **Parallel implementer fan-out** — continuous batching handles concurrent requests better than the reference server |
| mlx-lm-server (community) | [0xDaizz/mlx-lm-server](https://github.com/0xDaizz/mlx-lm-server) | 8000 | Alternative if mlx-openai-server doesn't fit |
**Parallel implementers note:** When the orchestrator dispatches multiple `role-implementer` jobs at once (disjoint briefs per agent-pipeline `slice_dependencies`), a single local inference process can become the bottleneck. Prefer **mlx-openai-server** (or mlx-lm-server with batching) over the minimal `mlx_lm.server` when running 2+ implementers against the same Mac. Ollama can also serve parallel requests but MLX-native models (`mlx-community/*` on Hugging Face) often perform better on Apple Silicon unified memory. You may run **both** Ollama (`:11434`) and an MLX server (`:8000` / `:8080`) as separate registry aliases and route roles accordingly.
**Networking:** Bind `0.0.0.0` when Echodo (Coolify) or `apps/worker` on another host must reach the Mac over LAN. Restrict firewall to homelab subnet. Do not expose raw local inference to the public internet.
**v1 registry:** No `provider_type: mlx` — HTTP gateway only. Native MLX SDK in `apps/worker` is out of scope.
```yaml
alias: claude-planning
provider_type: anthropic
model_id: claude-sonnet-4-20250514
api_key_env_var: ANTHROPIC_API_KEY
metadata: { tier: premium }
```
### NVIDIA NIM
```yaml
alias: nim-llama-70b
provider_type: openai_compatible
model_id: meta/llama-3.1-70b-instruct
base_url: https://integrate.api.nvidia.com/v1
api_key_env_var: NVIDIA_API_KEY
```
### Gemini
```yaml
alias: gemini-flash
provider_type: google
model_id: gemini-2.0-flash
api_key_env_var: GOOGLE_GENERATIVE_AI_API_KEY
```
### LiteLLM gateway (optional)
```yaml
alias: litellm-gateway
provider_type: openai_compatible
model_id: qwen2.5-coder:32b # or per-role model_id in route table
base_url: http://host:4000/v1
api_key_env_var: LITELLM_MASTER_KEY
```
## Default role routing (seed template)
Shipped when a workspace enables pipeline integration: