clawdie-ai/doc/MULTI-PROVIDER-ARCHITECTURE.md
Mevy Assistant c633fdcc49 Remove legacy agent IDs + tighten task API
- Canonicalize controlplane agent IDs/roles to: sysadmin, db-admin, git-admin (drop *_agent variants).

- Add DB migration to rewrite existing *_agent rows and references to canonical IDs.

- Tighten POST /api/controlplane/tasks contract: require assigned_to (remove agent_id alias).

- Update tests and docs to match canonical IDs.

---

Build: pass (just typecheck)

Tests: pass — 1536 passed (92 files) (just test)
2026-04-19 06:54:28 +00:00

13 KiB

Multi-Provider Architecture

Date: 07.apr.2026 Status: Design — not yet implemented. src/provider-router.ts does not exist. PI_TUI_REASONING_PROVIDER config var not yet added. Use as a blueprint when implementing. Current default: zai/glm-5-turbo (configured via PI_TUI_PROVIDER and PI_TUI_MODEL in .env). Runtime changes via just pi-config.


Goal

Clawdie should accept credentials from any LLM provider at install time, and route requests to the best provider for each task at runtime.

This is not a handoff document. It is a living design reference.


Supported Providers

Provider Auth Var Cost Strength Use Case
Z.ai (GLM) ZAI_API_KEY Low Good Default routine tasks
Anthropic ANTHROPIC_API_KEY High Excellent Complex reasoning, orchestrator agent
Claude OAuth CLAUDE_CODE_OAUTH_TOKEN High Excellent Claude Code integration
OpenAI OPENAI_API_KEY High Excellent GPT models, Codex
OpenRouter OPENROUTER_API_KEY Medium Varies Multi-model access
Ollama Local (no key) Zero Limited Offline, routine skill tasks
llama.cpp Local (no key) Zero Limited Offline, routine skill tasks
Future (Mistral, etc.) <PROVIDER>_API_KEY TBD TBD Extensible

Architecture

Two Layers

┌─────────────────────────────────────────────────┐
│ Install Time (firstboot wizard)                 │
│                                                 │
│  "Which providers do you have keys for?"        │
│  ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐                │
│  │Zai│ │Ant│ │GPT│ │Oll│ │...│ ← multi-select  │
│  └───┘ └───┘ └───┘ └───┘ └───┘                │
│                                                 │
│  "Preferred for routine tasks?"  → [dropdown]   │
│  "Preferred for complex reasoning?" → [dropdown]│
│                                                 │
│  Writes to: /root/.clawdie/.env                 │
└─────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────┐
│ Runtime (Clawdie service)                       │
│                                                 │
│  Provider Registry (config.ts)                  │
│    ↓ reads all available keys from .env         │
│                                                 │
│  Provider Router (NEW)                          │
│    ↓ maps task profile → provider               │
│                                                 │
│  Agent Spawner (controlplane-runner.ts)         │
│    ↓ injects PI_TUI_PROVIDER per task           │
└─────────────────────────────────────────────────┘

Install Time

Shell Wizard (firstboot.sh)

After timezone and SSH key, add provider selection:

# ── LLM Provider Configuration ──────────────────────────────────────

_dialog --msgbox \
  "LLM Provider Setup\n\n" \
  "Clawdie supports multiple AI providers.\n" \
  "You can configure one or more now, or skip and edit .env later." 12 70

# Collect optional API keys
ZAI_API_KEY=$(_dialog --passwordbox \
  "Z.ai API Key (optional).\n\n" \
  "Default provider. Low cost, good quality.\n" \
  "Leave blank to skip." 12 70 "${ZAI_API_KEY:-}")

ANTHROPIC_API_KEY=$(_dialog --passwordbox \
  "Anthropic API Key (optional).\n\n" \
  "Claude models. High quality reasoning.\n" \
  "Leave blank to skip." 12 70 "${ANTHROPIC_API_KEY:-}")

CLAUDE_CODE_OAUTH_TOKEN=$(_dialog --passwordbox \
  "Claude Code OAuth Token (optional).\n\n" \
  "Run 'claude setup-token' elsewhere, paste here.\n" \
  "Leave blank to skip." 12 70 "")

OPENAI_API_KEY=$(_dialog --passwordbox \
  "OpenAI API Key (optional).\n\n" \
  "GPT and Codex models.\n" \
  "Leave blank to skip." 12 70 "${OPENAI_API_KEY:-}")

OPENROUTER_API_KEY=$(_dialog --passwordbox \
  "OpenRouter API Key (optional).\n\n" \
  "Access to multiple model providers.\n" \
  "Leave blank to skip." 12 70 "${OPENROUTER_API_KEY:-}")

# Detect local LLM
if pkg info ollama >/dev/null 2>&1; then
  FEATURE_OLLAMA="YES"
fi
if pkg info llama-cpp >/dev/null 2>&1; then
  FEATURE_LLAMA_CPP="YES"
fi

# Preferred provider selection
PROVIDER_COUNT=0
[ -n "$ZAI_API_KEY" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
[ -n "$ANTHROPIC_API_KEY" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
[ -n "$OPENAI_API_KEY" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
[ -n "$OPENROUTER_API_KEY" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
[ "$FEATURE_OLLAMA" = "YES" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))
[ "$FEATURE_LLAMA_CPP" = "YES" ] && PROVIDER_COUNT=$((PROVIDER_COUNT + 1))

if [ "$PROVIDER_COUNT" -ge 2 ]; then
  DEFAULT_PROVIDER=$(_dialog --menu \
    "Default provider for routine tasks:" 15 60 5 \
    "zai"        "Z.ai (GLM) — low cost" \
    "ollama"     "Ollama — local, free" \
    "llama.cpp"  "llama.cpp — local, free" \
    "openai"     "OpenAI — GPT models" \
    "anthropic"  "Anthropic — Claude" \
    "openrouter" "OpenRouter — multi-model" 2>&1)

  REASONING_PROVIDER=$(_dialog --menu \
    "Provider for complex reasoning:" 15 60 5 \
    "anthropic"  "Anthropic — Claude" \
    "openai"     "OpenAI — GPT/Codex" \
    "zai"        "Z.ai (GLM)" \
    "openrouter" "OpenRouter — multi-model" 2>&1)
else
  DEFAULT_PROVIDER="${PI_TUI_PROVIDER:-zai}"
  REASONING_PROVIDER="${PI_TUI_PROVIDER:-zai}"
fi

GUI Wizard (QML)

Add a new ProviderPage.qml (between UserPage and SummaryPage):

  • Checkboxes for each provider (multi-select)
  • Password fields appear dynamically for checked providers
  • Dropdowns for default + reasoning provider (if 2+ checked)
  • Auto-detect Ollama/llama.cpp presence

Config Output

All provider keys + preferences written to .env:

# Provider keys
ZAI_API_KEY="..."
ANTHROPIC_API_KEY="..."
CLAUDE_CODE_OAUTH_TOKEN="..."
OPENAI_API_KEY="..."
OPENROUTER_API_KEY="..."

# Provider preferences
PI_TUI_PROVIDER="zai"                    # default routine
PI_TUI_REASONING_PROVIDER="anthropic"    # complex reasoning

# Local LLM
FEATURE_OLLAMA="YES"
FEATURE_LLAMA_CPP="NO"

Runtime

Provider Registry (config.ts)

Already partially exists. Extend to expose all available providers:

interface ProviderInfo {
  name: string;
  provider: string;
  available: boolean;
  keySet: boolean;
  cost: 'zero' | 'low' | 'medium' | 'high';
  strength: 'limited' | 'good' | 'excellent';
}

export const AVAILABLE_PROVIDERS: ProviderInfo[] = [
  {
    name: 'Z.ai (GLM)',
    provider: 'zai',
    available: !!ZAI_API_KEY,
    keySet: !!ZAI_API_KEY,
    cost: 'low',
    strength: 'good',
  },
  {
    name: 'Anthropic',
    provider: 'anthropic',
    available: !!ANTHROPIC_API_KEY,
    keySet: !!ANTHROPIC_API_KEY,
    cost: 'high',
    strength: 'excellent',
  },
  {
    name: 'OpenAI',
    provider: 'openai',
    available: !!OPENAI_API_KEY,
    keySet: !!OPENAI_API_KEY,
    cost: 'high',
    strength: 'excellent',
  },
  {
    name: 'OpenRouter',
    provider: 'openrouter',
    available: !!OPENROUTER_API_KEY,
    keySet: !!OPENROUTER_API_KEY,
    cost: 'medium',
    strength: 'good',
  },
  {
    name: 'Ollama',
    provider: 'ollama',
    available: FEATURE_OLLAMA,
    keySet: true,
    cost: 'zero',
    strength: 'limited',
  },
  {
    name: 'llama.cpp',
    provider: 'llama.cpp',
    available: FEATURE_LLAMA_CPP,
    keySet: true,
    cost: 'zero',
    strength: 'limited',
  },
];

Provider Router (NEW: src/provider-router.ts)

Maps task context to the best provider:

interface TaskProfile {
  agentRole: string; // orchestrator, sysadmin, db-admin, git-admin
  skillMatched: boolean; // skill catalog matched exactly
  complexity: 'routine' | 'moderate' | 'complex';
  requiresReasoning: boolean;
}

interface ProviderChoice {
  provider: string;
  model?: string;
  reason: string;
}

function routeProvider(
  profile: TaskProfile,
  config: { defaultProvider: string; reasoningProvider: string },
): ProviderChoice {
  // 1. Skill-matched routine task → default provider (or local)
  if (profile.skillMatched && profile.complexity === 'routine') {
    return {
      provider: config.defaultProvider,
      reason: 'skill-matched routine task',
    };
  }

  // 2. Complex reasoning → reasoning provider
  if (profile.requiresReasoning || profile.agentRole === 'orchestrator') {
    return {
      provider: config.reasoningProvider,
      reason: 'complex reasoning required',
    };
  }

  // 3. Default
  return {
    provider: config.defaultProvider,
    reason: 'default provider',
  };
}

Integration with Control Plane

controlplane-runner.ts already builds a pi spawn command. The provider router injects the provider per-task:

// In controlplane-heartbeat.ts, before spawning:
const providerChoice = routeProvider(
  { agentRole: agent.role, skillMatched: match.confidence !== 'none', ... },
  { defaultProvider: PI_TUI_PROVIDER, reasoningProvider: PI_TUI_REASONING_PROVIDER },
);

const runConfig = buildControlplaneRunCommand({
  ...existingOpts,
  provider: providerChoice.provider,  // NEW field
});

// In controlplane-runner.ts, buildControlplaneRunCommand sets:
// env.PI_TUI_PROVIDER = opts.provider

Routing Rules (Default)

Scenario Route To Rationale
Skill-matched (exact) PI_TUI_PROVIDER (default) Deterministic, no reasoning needed
Skill-matched (pattern) PI_TUI_PROVIDER (default) Likely routine
No skill match, non-orchestrator PI_TUI_PROVIDER (default) Conservative
Orchestrator agent, any task PI_TUI_REASONING_PROVIDER Orchestrator needs best reasoning
Any agent, task marked "critical" PI_TUI_REASONING_PROVIDER Safety net
Budget exhausted on cloud provider ollama or llama.cpp Fallback to local
No cloud keys configured ollama or llama.cpp Only option

Fallback Chain

When primary provider fails (rate limit, outage, key invalid):

Primary → Secondary → Tertiary → Local LLM → Error

Example with full config:

anthropic → openai → zai → ollama → error

Example with minimal config:

zai → ollama → error

The fallback chain is auto-generated from available providers, sorted by strength (excellent > good > limited).


Adding a New Provider

  1. Add key variable to readEnvFile() in config.ts
  2. Add ProviderInfo entry to AVAILABLE_PROVIDERS
  3. Add pi profile support (if pi supports it)
  4. Add to firstboot wizard provider list
  5. Add to GUI provider checkboxes

No router changes needed — it works off the provider registry.


Open Questions

  1. Should PI_TUI_REASONING_PROVIDER be a new config var, or should we use a PROVIDER_ROUTING JSON config for more complex rules?

    Recommendation: Start with two vars (PI_TUI_PROVIDER + PI_TUI_REASONING_PROVIDER). Add JSON routing config only when someone needs per-agent or per-skill routing.

  2. Should the provider router be a separate module or embedded in controlplane-runner?

    Recommendation: Separate src/provider-router.ts. Keeps runner focused on spawn command construction. Router is testable independently.

  3. Should the GUI provider page be a separate QML page or merged into UserPage?

    Recommendation: Separate ProviderPage.qml. Provider config is conceptually different from user identity. Keeps UserPage simple.

  4. How to handle providers that pi doesn't support yet?

    Recommendation: Provider router checks pi capability before routing. If pi doesn't support the provider, skip it in the fallback chain and log a warning.