Skill: c0nspic0us7urk3r/smart-router (version 0.1.2)

--- name: smart-router description: > Expertise-aware model router with semantic domain scoring, context-overflow protection, and security redaction. Automatically selects the optimal AI model using weighted expertise scoring (Feb 2026 benchmarks). Supports Claude, GPT, Gemini, Grok with automatic fallback chains, HITL gates, and cost optimization. author: c0nSpIc0uS7uRk3r version: 2.1.0 license: MIT metadata: openclaw: requires: bins: ["python3"] env: ["ANTHROPIC_API_KEY"] optional_env: ["GOOGLE_API_KEY", "OPENAI_API_KEY", "XAI_API_KEY"] features: - Semantic domain detection - Expertise-weighted scoring (0-100) - Risk-based mandatory routing - Context overflow protection (>150K → Gemini) - Security credential redaction - Circuit breaker with persistent state - HITL gate for low-confidence routing benchmarks: source: "Feb 2026 MLOC Analysis" models: - "Claude Opus 4.5: SWE-bench 80.9%" - "GPT-5.2: AIME 100%, Control Flow 22 errors/MLOC" - "Gemini 3 Pro: Concurrency 69 issues/MLOC" --- # A.I. Smart-Router Intelligently route requests to the optimal AI model using tiered classification with automatic fallback handling and cost optimization. ## How It Works (Silent by Default) The router operates transparently—users send messages normally and get responses from the best model for their task. No special commands needed. **Optional visibility**: Include `[show routing]` in any message to see the routing decision. ## Tiered Classification System The router uses a three-tier decision process: ``` ┌─────────────────────────────────────────────────────────────────┐ │ TIER 1: INTENT DETECTION │ │ Classify the primary purpose of the request │ ├─────────────────────────────────────────────────────────────────┤ │ CODE │ ANALYSIS │ CREATIVE │ REALTIME │ GENERAL │ │ write/debug │ research │ writing │ news/live │ Q&A/chat │ │ refactor │ explain │ stories │ X/Twitter │ translate │ │ review │ compare │ brainstorm │ prices │ summarize │ └──────┬───────┴──────┬──────┴─────┬──────┴─────┬─────┴─────┬─────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ TIER 2: COMPLEXITY ESTIMATION │ ├─────────────────────────────────────────────────────────────────┤ │ SIMPLE (Tier $) │ MEDIUM (Tier $$) │ COMPLEX (Tier $$$)│ │ • One-step task │ • Multi-step task │ • Deep reasoning │ │ • Short response OK │ • Some nuance │ • Extensive output│ │ • Factual lookup │ • Moderate context │ • Critical task │ │ → Haiku/Flash │ → Sonnet/Grok/GPT │ → Opus/GPT-5 │ └──────────────────────────┴─────────────────────┴───────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ TIER 3: SPECIAL CASE OVERRIDES │ ├─────────────────────────────────────────────────────────────────┤ │ CONDITION │ OVERRIDE TO │ │ ─────────────────────────────────────┼─────────────────────────│ │ Context >100K tokens │ → Gemini Pro (1M ctx) │ │ Context >500K tokens │ → Gemini Pro ONLY │ │ Needs real-time data │ → Grok (regardless) │ │ Image/vision input │ → Opus or Gemini Pro │ │ User explicit override │ → Requested model │ └──────────────────────────────────────┴──────────────────────────┘ ``` ## Intent Detection Patterns ### CODE Intent - Keywords: write, code, debug, fix, refactor, implement, function, class, script, API, bug, error, compile, test, PR, commit - File extensions mentioned: .py, .js, .ts, .go, .rs, .java, etc. - Code blocks in input ### ANALYSIS Intent - Keywords: analyze, explain, compare, research, understand, why, how does, evaluate, assess, review, investigate, examine - Long-form questions - "Help me understand..." ### CREATIVE Intent - Keywords: write (story/poem/essay), create, brainstorm, imagine, design, draft, compose - Fiction/narrative requests - Marketing/copy requests ### REALTIME Intent - Keywords: now, today, current, latest, trending, news, happening, live, price, score, weather - X/Twitter mentions - Stock/crypto tickers - Sports scores ### GENERAL Intent (Default) - Simple Q&A - Translations - Summaries - Conversational ### MIXED Intent (Multiple Intents Detected) When a request contains multiple clear intents (e.g., "Write code to analyze this data and explain it creatively"): 1. **Identify primary intent** — What's the main deliverable? 2. **Route to highest-capability model** — Mixed tasks need versatility 3. **Default to COMPLEX complexity** — Multi-intent = multi-step **Examples:** - "Write code AND explain how it works" → CODE (primary) + ANALYSIS → Route to Opus - "Summarize this AND what's the latest news on it" → REALTIME takes precedence → Grok - "Creative story using real current events" → REALTIME + CREATIVE → Grok (real-time wins) ## Language Handling **Non-English requests** are handled normally — all supported models have multilingual capabilities: | Model | Non-English Support | |-------|---------------------| | Opus/Sonnet/Haiku | Excellent (100+ languages) | | GPT-5 | Excellent (100+ languages) | | Gemini Pro/Flash | Excellent (100+ languages) | | Grok | Good (major languages) | **Intent detection still works** because: - Keyword patterns include common non-English equivalents - Code intent detected by file extensions, code blocks (language-agnostic) - Complexity estimated by query length (works across languages) **Edge case:** If intent unclear due to language, default to GENERAL intent with MEDIUM complexity. ## Complexity Signals ### Simple Complexity ($) - Short query (<50 words) - Single question mark - "Quick question", "Just tell me", "Briefly" - Yes/no format - Unit conversions, definitions ### Medium Complexity ($$) - Moderate query (50-200 words) - Multiple aspects to address - "Explain", "Describe", "Compare" - Some context provided ### Complex Complexity ($$$) - Long query (>200 words) or complex task - "Step by step", "Thoroughly", "In detail" - Multi-part questions - Critical/important qualifier - Research, analysis, or creative work ## Routing Matrix | Intent | Simple | Medium | Complex | |--------|--------|--------|---------| | **CODE** | Sonnet | Opus | Opus | | **ANALYSIS** | Flash | GPT-5 | Opus | | **CREATIVE** | Sonnet | Opus | Opus | | **REALTIME** | Grok | Grok | Grok-3 | | **GENERAL** | Flash | Sonnet | Opus | ## Token Exhaustion & Automatic Model Switching When a model becomes unavailable mid-session (token quota exhausted, rate limit hit, API error), the router automatically switches to the next best available model and **notifies the user**. ### Notification Format When a model switch occurs due to exhaustion, the user receives a notification: ``` ┌─────────────────────────────────────────────────────────────────┐ │ ⚠️ MODEL SWITCH NOTICE │ │ │ │ Your request could not be completed on claude-opus-4-5 │ │ (reason: token quota exhausted). │ │ │ │ ✅ Request completed using: anthropic/claude-sonnet-4-5 │ │ │ │ The response below was generated by the fallback model. │ └─────────────────────────────────────────────────────────────────┘ ``` ### Switch Reasons | Reason | Description | |--------|-------------| | `token quota exhausted` | Daily/monthly token limit reached | | `rate limit exceeded` | Too many requests per minute | | `context window exceeded` | Input too large for model | | `API timeout` | Model took too long to respond | | `API error` | Provider returned an error | | `model unavailable` | Model temporarily offline | ### Implementation ```python def execute_with_fallback(primary_model: str, fallback_chain: list[str], request: str) -> Response: """ Execute request with automatic fallback and user notification. """ attempted_models = [] switch_reason = None # Try primary model first models_to_try = [primary_model] + fallback_chain for model in models_to_try: try: response = call_model(model, request) # If we switched models, prepend notification if attempted_models: notification = build_switch_notification( failed_model=attempted_models[0], reason=switch_reason, success_model=model ) return Response( content=notification + "\n\n---\n\n" + response.content, model_used=model, switched=True ) return Response(content=response.content, model_used=model, switched=False) except TokenQuotaExhausted: attempted_models.append(model) switch_reason = "token quota exhausted" log_fallback(model, switch_reason) continue except RateLimitExceeded: attempted_models.append(model) switch_reason = "rate limit exceeded" log_fallback(model, switch_reason) continue except ContextWindowExceeded: attempted_models.append(model) switch_reason = "context window exceeded" log_fallback(model, switch_reason) continue except APITimeout: attempted_models.append(model) switch_reason = "API timeout" log_fallback(model, switch_reason) continue except APIError as e: attempted_models.append(model) switch_reason = f"API error: {e.code}" log_fallback(model, switch_reason) continue # All models exhausted return build_exhaustion_error(attempted_models) def build_switch_notification(failed_model: str, reason: str, success_model: str) -> str: """Build user-facing notification when model switch occurs.""" return f"""⚠️ **MODEL SWITCH NOTICE** Your request could not be completed on `{failed_model}` (reason: {reason}). ✅ **Request completed using:** `{success_model}` The response below was generated by the fallback model.""" def build_exhaustion_error(attempted_models: list[str]) -> Response: """Build error when all models are exhausted.""" models_tried = ", ".join(attempted_models) return Response( content=f"""❌ **REQUEST FAILED** Unable to complete your request. All available models have been exhausted. **Models attempted:** {models_tried} **What you can do:** 1. **Wait** — Token quotas typically reset hourly or daily 2. **Simplify** — Try a shorter or simpler request 3. **Check status** — Run `/router status` to see model availability If this persists, your human may need to check API quotas or add additional providers.""", model_used=None, switched=False, failed=True ) ``` ### Fallback Priority for Token Exhaustion When a model is exhausted, the router selects the next best model for the **same task type**: | Original Model | Fallback Priority (same capability) | |----------------|-------------------------------------| | Opus | Sonnet → GPT-5 → Grok-3 → Gemini Pro | | Sonnet | GPT-5 → Grok-3 → Opus → Haiku | | GPT-5 | Sonnet → Opus → Grok-3 → Gemini Pro | | Gemini Pro | Flash → GPT-5 → Opus → Sonnet | | Grok-2/3 | (warn: no real-time fallback available) | ### User Acknowledgment After a model switch, the agent should note in the response that: 1. The original model was unavailable 2. Which model actually completed the request 3. The response quality may differ from the original model's typical output This ensures transparency and sets appropriate expectations. ### Streaming Responses with Fallback When using streaming responses, fallback handling requires special consideration: ```python async def execute_with_streaming_fallback(primary_model: str, fallback_chain: list[str], request: str): """ Handle streaming responses with mid-stream fallback. If a model fails DURING streaming (not before), the partial response is lost. Strategy: Don't start streaming until first chunk received successfully. """ models_to_try = [primary_model] + fallback_chain for model in models_to_try: try: # Test with non-streaming ping first (optional, adds latency) # await test_model_availability(model) # Start streaming stream = await call_model_streaming(model, request) first_chunk = await stream.get_first_chunk(timeout=10_000) # 10s timeout for first chunk # If we got here, model is responding — continue streaming yield first_chunk async for chunk in stream: yield chunk return # Success except (FirstChunkTimeout, StreamError) as e: log_fallback(model, str(e)) continue # Try next model # All models failed yield build_exhaustion_error(models_to_try) ``` **Key insight:** Wait for the first chunk before committing to a model. If the first chunk times out, fall back before any partial response is shown to the user. ### Retry Timing Configuration ```python RETRY_CONFIG = { "initial_timeout_ms": 30_000, # 30s for first attempt "fallback_timeout_ms": 20_000, # 20s for fallback attempts (faster fail) "max_retries_per_model": 1, # Don't retry same model "backoff_multiplier": 1.5, # Not used (no same-model retry) "circuit_breaker_threshold": 3, # Failures before skipping model entirely "circuit_breaker_reset_ms": 300_000 # 5 min before trying failed model again } ``` **Circuit breaker:** If a model fails 3 times in 5 minutes, skip it entirely for the next 5 minutes. This prevents repeatedly hitting a down service. ## Fallback Chains When the preferred model fails (rate limit, API down, error), cascade to the next option: ### Code Tasks ``` Opus → Sonnet → GPT-5 → Gemini Pro ``` ### Analysis Tasks ``` Opus → GPT-5 → Gemini Pro → Sonnet ``` ### Creative Tasks ``` Opus → GPT-5 → Sonnet → Gemini Pro ``` ### Real-time Tasks ``` Grok-2 → Grok-3 → (warn: no real-time fallback) ``` ### General Tasks ``` Flash → Haiku → Sonnet → GPT-5 ``` ### Long Context (Tiered by Size) ``` ┌─────────────────────────────────────────────────────────────────┐ │ LONG CONTEXT FALLBACK CHAIN │ ├─────────────────────────────────────────────────────────────────┤ │ TOKEN COUNT │ FALLBACK CHAIN │ │ ───────────────────┼───────────────────────────────────────────│ │ 128K - 200K │ Opus (200K) → Sonnet (200K) → Gemini Pro │ │ 200K - 1M │ Gemini Pro → Flash (1M) → ERROR_MESSAGE │ │ > 1M │ ERROR_MESSAGE (no model supports this) │ └─────────────────────┴───────────────────────────────────────────┘ ``` **Implementation:** ```python def handle_long_context(token_count: int, available_models: dict) -> str | ErrorMessage: """Route long-context requests with graceful degradation.""" # Tier 1: 128K - 200K tokens (Opus/Sonnet can handle) if token_count <= 200_000: for model in ["opus", "sonnet", "haiku", "gemini-pro", "flash"]: if model in available_models and get_context_limit(model) >= token_count: return model # Tier 2: 200K - 1M tokens (only Gemini) elif token_count <= 1_000_000: for model in ["gemini-pro", "flash"]: if model in available_models: return model # Tier 3: > 1M tokens (nothing available) # Fall through to error # No suitable model found — return helpful error return build_context_error(token_count, available_models) def build_context_error(token_count: int, available_models: dict) -> ErrorMessage: """Build a helpful error message when no model can handle the input.""" # Find the largest available context window max_available = max( (get_context_limit(m) for m in available_models), default=0 ) # Determine what's missing missing_models = [] if "gemini-pro" not in available_models and "flash" not in available_models: missing_models.append("Gemini Pro/Flash (1M context)") if token_count <= 200_000 and "opus" not in available_models: missing_models.append("Opus (200K context)") # Format token count for readability if token_count >= 1_000_000: token_display = f"{token_count / 1_000_000:.1f}M" else: token_display = f"{token_count // 1000}K" return ErrorMessage( title="Context Window Exceeded", message=f"""Your input is approximately **{token_display} tokens**, which exceeds the context window of all currently available models. **Required:** Gemini Pro (1M context) {"— currently unavailable" if "gemini-pro" not in available_models else ""} **Your max available:** {max_available // 1000}K tokens **Options:** 1. **Wait and retry** — Gemini may be temporarily down 2. **Reduce input size** — Remove unnecessary content to fit within {max_available // 1000}K tokens 3. **Split into chunks** — I can process your input sequentially in smaller pieces Would you like me to help split this into manageable chunks?""", recoverable=True, suggested_action="split_chunks" ) ``` **Example Error Output:** ``` ⚠️ Context Window Exceeded Your input is approximately **340K tokens**, which exceeds the context window of all currently available models. Required: Gemini Pro (1M context) — currently unavailable Your max available: 200K tokens Options: 1. Wait and retry — Gemini may be temporarily down 2. Reduce input size — Remove unnecessary content to fit within 200K tokens 3. Split into chunks — I can process your input sequentially in smaller pieces Would you like me to help split this into manageable chunks? ``` ## Dynamic Model Discovery The router auto-detects available providers at runtime: ``` 1. Check configured auth profiles 2. Build available model list from authenticated providers 3. Construct routing table using ONLY available models 4. If preferred model unavailable, use best available alternative ``` **Example**: If only Anthropic and Google are configured: - Code tasks → Opus (Anthropic available ✓) - Real-time tasks → ⚠️ No Grok → Fall back to Opus + warn user - Long docs → Gemini Pro (Google available ✓) ## Cost Optimization The router considers cost when complexity is LOW: | Model | Cost Tier | Use When | |-------|-----------|----------| | Gemini Flash | $ | Simple tasks, high volume | | Claude Haiku | $ | Simple tasks, quick responses | | Claude Sonnet | $$ | Medium complexity | | Grok 2 | $$ | Real-time needs only | | GPT-5 | $$ | General fallback | | Gemini Pro | $$$ | Long context needs | | Claude Opus | $$$$ | Complex/critical tasks | **Rule**: Never use Opus ($$$) for tasks that Flash ($) can handle. ## User Controls ### Show Routing Decision Add `[show routing]` to any message: ``` [show routing] What's the weather in NYC? ``` Output includes: ``` [Routed → xai/grok-2-latest | Reason: REALTIME intent detected | Fallback: none available] ``` ### Force Specific Model Explicit overrides: - "use grok: ..." → Forces Grok - "use claude: ..." → Forces Opus - "use gemini: ..." → Forces Gemini Pro - "use flash: ..." → Forces Gemini Flash - "use gpt: ..." → Forces GPT-5 ### Check Router Status Ask: "router status" or "/router" to see: - Available providers - Configured models - Current routing table - Recent routing decisions ## Implementation Notes ### For Agent Implementation When processing a request: ``` 1. DETECT available models (check auth profiles) 2. CLASSIFY intent (code/analysis/creative/realtime/general) 3. ESTIMATE complexity (simple/medium/complex) 4. CHECK special cases (context size, vision, explicit override) 5. FILTER by cost tier based on complexity ← BEFORE model selection 6. SELECT model from filtered pool using routing matrix 7. VERIFY model available, else use fallback chain (also cost-filtered) 8. EXECUTE request with selected model 9. IF failure, try next in fallback chain 10. LOG routing decision (for debugging) ``` ### Cost-Aware Routing Flow (Critical Order) ```python def route_with_fallback(request): """ Main routing function with CORRECT execution order. Cost filtering MUST happen BEFORE routing table lookup. """ # Step 1: Discover available models available_models = discover_providers() # Step 2: Classify intent intent = classify_intent(request) # Step 3: Estimate complexity complexity = estimate_complexity(request) # Step 4: Check special-case overrides (these bypass cost filtering) if user_override := get_user_model_override(request): return execute_with_fallback(user_override, []) # No cost filter for explicit override if token_count > 128_000: return handle_long_context(token_count, available_models) # Special handling if needs_realtime(request): return execute_with_fallback("grok-2", ["grok-3"]) # Realtime bypasses cost # ┌─────────────────────────────────────────────────────────────┐ # │ STEP 5: FILTER BY COST TIER — THIS MUST COME FIRST! │ # │ │ # │ Cost filtering happens BEFORE the routing table lookup, │ # │ NOT after. This ensures "what's 2+2?" never considers │ # │ Opus even momentarily. │ # └─────────────────────────────────────────────────────────────┘ allowed_tiers = get_allowed_tiers(complexity) # SIMPLE → ["$"] # MEDIUM → ["$", "$$"] # COMPLEX → ["$", "$$", "$$$"] cost_filtered_models = { model: meta for model, meta in available_models.items() if COST_TIERS.get(model) in allowed_tiers } # Step 6: NOW select from cost-filtered pool using routing preferences preferences = ROUTING_PREFERENCES.get((intent, complexity), []) for model in preferences: if model in cost_filtered_models: # Only consider cost-appropriate models selected_model = model break else: # No preferred model in cost-filtered pool — use cheapest available selected_model = select_cheapest(cost_filtered_models) # Step 7: Build cost-filtered fallback chain task_type = get_task_type(intent, complexity) full_chain = MASTER_FALLBACK_CHAINS.get(task_type, []) filtered_chain = [m for m in full_chain if m in cost_filtered_models and m != selected_model] # Step 8-10: Execute with fallback + logging return execute_with_fallback(selected_model, filtered_chain) def get_allowed_tiers(complexity: str) -> list[str]: """Return allowed cost tiers for a given complexity level.""" return { "SIMPLE": ["$"], # Budget only — no exceptions "MEDIUM": ["$", "$$"], # Budget + standard "COMPLEX": ["$", "$$", "$$$", "$$$$"], # All tiers — complex tasks deserve the best }.get(complexity, ["$", "$$"]) # Example flow for "what's 2+2?": # # 1. available_models = {opus, sonnet, haiku, flash, grok-2, ...} # 2. intent = GENERAL # 3. complexity = SIMPLE # 4. (no special cases) # 5. allowed_tiers = ["$"] ← SIMPLE means $ only # cost_filtered_models = {haiku, flash, grok-2} ← Opus/Sonnet EXCLUDED # 6. preferences for (GENERAL, SIMPLE) = [flash, haiku, grok-2, sonnet] # first match in cost_filtered = flash ✓ # 7. fallback_chain = [haiku, grok-2] ← Also cost-filtered # 8. execute with flash # # Result: Opus is NEVER considered, not even momentarily. ``` ### Cost Optimization: Two Approaches ``` ┌─────────────────────────────────────────────────────────────────┐ │ COST OPTIMIZATION IMPLEMENTATION OPTIONS │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ APPROACH 1: Explicit filter_by_cost() (shown above) │ │ ───────────────────────────────────────────────────────────── │ │ • Calls get_allowed_tiers(complexity) explicitly │ │ • Filters available_models BEFORE routing table lookup │ │ • Most defensive — impossible to route wrong tier │ │ • Recommended for security-critical deployments │ │ │ │ APPROACH 2: Preference ordering (implicit) │ │ ───────────────────────────────────────────────────────────── │ │ • ROUTING_PREFERENCES lists cheapest capable models first │ │ • For SIMPLE tasks: [flash, haiku, grok-2, sonnet] │ │ • First available match wins → naturally picks cheapest │ │ • Simpler code, relies on correct preference ordering │ │ │ │ This implementation uses BOTH for defense-in-depth: │ │ • Preference ordering provides first line of cost awareness │ │ • Explicit filter_by_cost() guarantees tier enforcement │ │ │ │ For alternative implementations that rely solely on │ │ preference ordering, see references/models.md for the │ │ filter_by_cost() function if explicit enforcement is needed. │ │ │ └─────────────────────────────────────────────────────────────────┘ ``` ### Spawning with Different Models Use sessions_spawn for model routing: ``` sessions_spawn( task: "user's request", model: "selected/model-id", label: "task-type-query" ) ``` ## Security - Never send sensitive data to untrusted models - API keys handled via environment/auth profiles only - See `references/security.md` for full security guidance ## Model Details See `references/models.md` for detailed capabilities and pricing.

path: README.md
content: # 🧠 A.I. Smart-Router

**Expertise-aware model router for multi-provider AI setups.** Uses weighted semantic scoring and Feb 2026 benchmarks to route each request to the best-suited model. Security redaction, context-overflow protection, and HITL gates for ambiguous intents.

> **v2.0.0** — Now with Phase G Semantic Specialization: expertise-weighted scoring, concurrency detection, and risk-based mandatory routing.

---

## ✨ Features

| Feature | Description |
|---------|-------------|
| **Semantic Domain Detection** | Identifies SOFTWARE_ARCH, CONCURRENCY, LOGICAL_PRECISION, MASSIVE_SYNTHESIS, etc. |
| **Expertise-Weighted Scoring** | Each model scored 0-100 per domain based on Feb 2026 benchmarks (MLOC analysis) |
| **Risk-Based Mandatory Routing** | Medical → GPT-5.2, Terminal/Shell → Opus, Concurrency → Gemini (enforced) |
| **Context Overflow Protection** | >150K tokens auto-routes to Gemini Pro (1M context window) |
| **Security Credential Redaction** | API keys, tokens, passwords blocked before reaching any model |
| **Circuit Breaker** | Persistent state tracks model failures; auto-bypasses degraded providers |
| **HITL Gate** | Low-confidence (<75%) routing triggers Telegram notification for approval |
| **Automatic Fallbacks** | If a model fails, cascades to next best option automatically |
| **Cost Optimization** | Simple tasks use Flash ($0.075/M); complex tasks unlock Opus ($$$$) |
| **Multi-Provider** | Anthropic (Claude), OpenAI (GPT), Google (Gemini), xAI (Grok) |

---

## 🎯 Expert Domain Map (Feb 2026 Benchmarks)

| Model | Expert Domain | Benchmark | Known Blind Spot |
|-------|---------------|-----------|------------------|
| **Claude Opus 4.5** | SOFTWARE_ARCH | SWE-bench 80.9%, Terminal-bench 59.3% | High cost, verbosity |
| **GPT-5.2 (High)** | LOGICAL_PRECISION | 100% AIME, 22 control flow errors/MLOC | 414 concurrency issues/MLOC |
| **Gemini 3 Pro** | MASSIVE_SYNTHESIS | 1M+ context, 69 concurrency issues/MLOC | 200 control flow errors/MLOC |
| **Grok-3** | REALTIME_SIGNAL | Native X/Twitter, 5-min news freshness | Less rigorous formal logic |
| **Flash 2.5** | SYSTEM_ROUTINE | 200ms latency, $0.075/M tokens | Shallow multi-step reasoning |

### Mandatory Overrides

| Risk Domain | Mandatory Model | Reason |
|-------------|-----------------|--------|
| Medical/Health | GPT-5.2 | 1.6% HealthBench error rate (lowest) |
| Financial Math | GPT-5.2 | 100% AIME accuracy |
| Terminal/Shell | Opus | 95.3% prompt-injection robustness |
| Security Audit | Opus | 96.2% security constraint verification |
| High Concurrency | Gemini Pro | 6x fewer concurrency bugs than GPT-5 |
| Context >150K | Gemini Pro | Only model with 1M+ context window |

---

## 🛡️ Context Safety (v2.1.0)

**Never hit an overflow again with Proactive Budgeting.**

Phase H introduces three-layer context protection:

| Layer | Threshold | Action |
|-------|-----------|--------|
| **Strike 1: Pre-Flight** | >180K tokens | Force-route to Gemini Pro before API call |
| **Strike 2: Silent Retry** | 422/400 error | Intercept overflow, retry with Gemini silently |
| **Strike 3: JIT Compact** | 150K-180K | Summarize oldest 30% to stay within limits |

```python
# Pre-flight audit
tokens = calculate_budget(messages)
if tokens > 180_000:
    model = "google/gemini-2.5-pro"  # Force Gemini

# Silent retry on overflow
try:
    response = await call_model(messages, model)
except ContextOverflow:
    response = await call_model(messages, "google/gemini-2.5-pro")
```

**Result:** Zero context overflow errors reach the user. Ever.

---

## 🚀 Quick Start

### 1. Install the Skill

Copy the `smart-router` folder to your OpenClaw workspace skills directory:

```bash
# From your workspace root
mkdir -p skills
cp -r /path/to/smart-router skills/
```

Or clone directly:
```bash
cd ~/.openclaw/workspace/skills
git clone https://github.com/c0nSpIc0uS7uRk3r/smart-router.git
```

### 2. Configure Providers

Add API keys for the providers you want to use (see [Configuration](#-configuration)).

### 3. Start Using

That's it! The router works automatically. Send messages normally and the best model is selected for each task.

Optional: Enable verbose mode to see routing decisions:
```
/router verbose on
```

---

## ⚙️ Configuration

### Required: At Least One Provider

The router requires at least one AI provider configured. Add any combination:

| Provider | Environment Variable | OpenClaw Auth Profile |
|----------|---------------------|----------------------|
| **Anthropic** | `ANTHROPIC_API_KEY` | `anthropic:default` |
| **OpenAI** | `OPENAI_API_KEY` | `openai-codex:default` |
| **Google** | `GOOGLE_API_KEY` | `google:manual` |
| **xAI** | `XAI_API_KEY` | `xai:manual` |

#### Option A: Environment Variables

Add to your shell profile (`~/.bashrc`, `~/.zshrc`):
```bash
export ANTHROPIC_API_KEY="sk-ant-..."
export GOOGLE_API_KEY="AIza..."
export XAI_API_KEY="xai-..."
```

#### Option B: OpenClaw Auth Profiles

```bash
openclaw auth add anthropic
openclaw auth add google --manual
openclaw auth add xai --manual
```

### Adding/Removing Providers

Simply add or remove the API key — the router auto-detects changes within 5 minutes.

Force immediate refresh:
```
/router refresh
```

### Minimum Viable Setup

The router works with just **one provider**. Example with only Anthropic:
- All tasks route to Claude models (Opus/Sonnet/Haiku)
- Warnings shown for missing capabilities (real-time, long context)
- No errors, just graceful degradation

---

## 📋 Commands

| Command | Description |
|---------|-------------|
| `/router` | Show brief status (configured providers, available models) |
| `/router status` | Full status with last 5 routing decisions |
| `/router verbose` | Toggle verbose mode (show routing for all messages) |
| `/router verbose on` | Enable verbose mode |
| `/router verbose off` | Disable verbose mode (default) |
| `/router refresh` | Force re-discover configured providers |

### Model Overrides

Force a specific model by prefixing your message:

```
use opus: Write a complex authentication system
use sonnet: Explain how this code works
use haiku: What's 2+2?
use gpt: Give me an alternative perspective
use gemini: Analyze this 200-page document
use flash: Translate this to Spanish
use grok: What's trending on X right now?
```

### Per-Message Routing Visibility

Add `[show routing]` to any message to see the routing decision:

```
[show routing] What's the capital of France?
```

Output:
```
[Routed → Haiku | Reason: CONVERSATION + SIMPLE complexity, cost-optimized]

The capital of France is Paris.
```

---

## 🌳 Routing Decision Tree

```
                         ┌─────────────────────┐
                         │   Incoming Request  │
                         └──────────┬──────────┘
                                    │
                    ┌───────────────▼───────────────┐
                    │  STEP 0: SPECIAL CASE CHECK   │
                    └───────────────┬───────────────┘
                                    │
              ┌─────────────────────┼─────────────────────┐
              │                     │                     │
              ▼                     ▼                     ▼
    ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐
    │ User specified  │  │ Tokens > 128K?  │  │ Needs real-time │
    │ model override? │  │                 │  │ data?           │
    └────────┬────────┘  └────────┬────────┘  └────────┬────────┘
             │YES                 │YES                 │YES
             ▼                    ▼                    ▼
       Use requested        Gemini Pro              Grok
          model              (forced)             (forced)
             │                    │                    │
             └────────────────────┴────────────────────┘
                                  │NO (none matched)
                                  ▼
                    ┌─────────────────────────────┐
                    │   STEP 1: INTENT DETECTION  │
                    │                             │
                    │  CODE      → coding tasks   │
                    │  ANALYSIS  → research/eval  │
                    │  CREATIVE  → writing/ideas  │
                    │  CONVO     → chat/Q&A       │
                    │  DOCUMENT  → long text      │
                    │  RESEARCH  → fact-finding   │
                    │  MATH      → calculations   │
                    └──────────────┬──────────────┘
                                   │
                                   ▼
                    ┌─────────────────────────────┐
                    │ STEP 2: COMPLEXITY ESTIMATE │
                    │                             │
                    │  SIMPLE  → <50 words, 1 Q   │
                    │  MEDIUM  → 50-200 words     │
                    │  COMPLEX → >200w, multi-part│
                    └──────────────┬──────────────┘
                                   │
                                   ▼
                    ┌─────────────────────────────┐
                    │ STEP 3: COST-AWARE SELECTION│
                    │                             │
                    │  SIMPLE  → $ tier only      │
                    │  MEDIUM  → $ or $$ tier     │
                    │  COMPLEX → any tier         │
                    └──────────────┬──────────────┘
                                   │
                                   ▼
                    ┌─────────────────────────────┐
                    │  STEP 4: MODEL MATRIX LOOKUP│
                    └──────────────┬──────────────┘
                                   │
          ┌────────────────────────┼────────────────────────┐
          │                        │                        │
          ▼                        ▼                        ▼
   ┌─────────────┐         ┌─────────────┐         ┌─────────────┐
   │   SIMPLE    │         │   MEDIUM    │         │   COMPLEX   │
   ├─────────────┤         ├─────────────┤         ├─────────────┤
   │ Flash    $  │         │ Sonnet  $$  │         │ Opus  $$$$  │
   │ Haiku    $  │         │ GPT-5   $$  │         │ GPT-5  $$$  │
   │ Grok-2   $  │         │ Grok-3  $$  │         │ Gemini $$$  │
   └─────────────┘         └─────────────┘         └─────────────┘
          │                        │                        │
          └────────────────────────┼────────────────────────┘
                                   │
                                   ▼
                    ┌─────────────────────────────┐
                    │   STEP 5: FALLBACK CHAIN    │
                    │                             │
                    │  If model fails → try next  │
                    │  Log failure → cascade      │
                    └─────────────────────────────┘
```

### Model Selection Matrix

| Intent | Simple ($) | Medium ($$) | Complex ($$$$) |
|--------|------------|-------------|---------------|
| **CODE** | Sonnet | Sonnet | Opus |
| **ANALYSIS** | Flash | Sonnet/GPT-5 | Opus |
| **CREATIVE** | Sonnet | Sonnet | Opus |
| **CONVERSATION** | Haiku/Flash | Sonnet | Sonnet |
| **DOCUMENT** | Flash | Gemini Pro | Gemini Pro |
| **RESEARCH** | Grok | GPT-5 | Opus |
| **MATH/LOGIC** | Sonnet | Opus | Opus |

### Fallback Chains

| Task Type | Fallback Order |
|-----------|----------------|
| Complex Reasoning | Opus → GPT-5 → Grok 3 → Sonnet |
| Code Generation | Opus → GPT-5 → Sonnet → Grok 3 |
| Fast/Simple | Haiku → Flash → Grok 2 → Sonnet |
| Large Context | Gemini Pro → GPT-5 → Opus |
| Creative Writing | Opus → Sonnet → GPT-5 → Grok 3 |

---

## 🔄 Token Exhaustion & Model Switching

When you run out of tokens on a model (quota exhausted, rate limited), the router **automatically switches to the next best available model** and notifies you.

### What Happens

1. You send a request
2. Primary model fails (token quota exhausted)
3. Router automatically tries the next model in the fallback chain
4. You receive a notification + the completed response

### Notification Example

```
⚠️ MODEL SWITCH NOTICE

Your request could not be completed on claude-opus-4-5
(reason: token quota exhausted).

✅ Request completed using: anthropic/claude-sonnet-4-5

The response below was generated by the fallback model.

---

[Your actual response here]
```

### Switch Reasons

| Reason | What It Means |
|--------|---------------|
| `token quota exhausted` | Daily/monthly token limit reached for this model |
| `rate limit exceeded` | Too many requests per minute |
| `context window exceeded` | Your input was too large for this model |
| `API timeout` | Model took too long to respond |
| `API error` | Provider returned an error |
| `model unavailable` | Model is temporarily offline |

### When All Models Fail

If all available models are exhausted, you'll see:

```
❌ REQUEST FAILED

Unable to complete your request. All available models have been exhausted.

Models attempted: claude-opus-4-5, claude-sonnet-4-5, gpt-5

What you can do:
1. Wait — Token quotas typically reset hourly or daily
2. Simplify — Try a shorter or simpler request
3. Check status — Run /router status to see model availability
```

### Why This Matters

- **No silent failures** — You always know what happened
- **Transparent attribution** — You know which model generated your response
- **Quality expectations** — If Opus fails and Sonnet completes, you know the response came from a less capable model
- **Continuity** — Your workflow isn't interrupted by token limits

---

## 🎨 Customization

### Modifying Routing Preferences

Edit `SKILL.md` and update the `ROUTING_PREFERENCES` dictionary:

```python
ROUTING_PREFERENCES = {
    ("CODE", "SIMPLE"):   ["sonnet", "gpt-5", "flash"],  # Add/remove/reorder
    ("CODE", "MEDIUM"):   ["opus", "sonnet", "gpt-5"],   # Your preferences
    # ...
}
```

### Adjusting Cost Tiers

Edit `references/models.md` to change tier assignments:

```python
COST_TIERS = {
    "flash": "$",      # Budget
    "haiku": "$",      # Budget
    "sonnet": "$$",    # Standard
    "opus": "$$$$",    # Premium
    # ...
}
```

### Adding New Models

1. Add the model to `PROVIDER_REGISTRY` in SKILL.md
2. Add pricing to `references/models.md`
3. Update routing preferences as desired
4. Run `/router refresh`

### Changing Fallback Timeout

Edit the timeout constant in SKILL.md:

```python
TIMEOUT_MS = 30000  # 30 seconds (default)
```

---

## 📁 Skill Structure

```
smart-router/
├── SKILL.md              # Main skill definition + routing logic
├── README.md             # This file
├── LICENSE               # MIT License
└── references/
    ├── models.md         # Model capabilities, pricing, cost tiers
    └── security.md       # Security guidelines
```

---

## 🔒 Security

- API keys are never logged or exposed in routing output
- Sensitive requests are not sent to untrusted models
- All provider communication uses official SDKs/APIs
- Input sanitization before any model API call
- No arbitrary code execution paths
- See `references/security.md` for full security guidelines

---

## 📊 Cost Optimization

The router is designed to minimize costs without sacrificing quality:

| Model | Cost/1M Input | Used For |
|-------|---------------|----------|
| Flash | $0.075 | Simple Q&A, translations |
| Haiku | $0.25 | Quick responses |
| Grok 2 | $0.20 | Real-time queries |
| Sonnet | $3.00 | Most tasks (best value) |
| GPT-5 | Subscription | Fallback (no per-token) |
| Gemini Pro | $1.25 | Large documents |
| Opus | $15.00 | Complex tasks only |

**Rule**: Simple tasks NEVER use premium models. A "what's 2+2?" query costs $0.000075, not $0.015.

---

## 🔧 Troubleshooting

### Common Issues

| Problem | Cause | Solution |
|---------|-------|----------|
| "No models available" | No providers configured | Add at least one API key |
| Always routes to same model | Only one provider active | Add more providers for variety |
| Expensive model for simple query | Complexity mis-detected | Use `[show routing]` to debug |
| Real-time query not using Grok | Grok not configured | Add xAI API key |
| Large document fails | Context exceeded | Check if Gemini Pro available |

### Debugging Routing Decisions

1. **Add `[show routing]` to your message:**
   ```
   [show routing] Your question here
   ```

2. **Check provider status:**
   ```
   /router status
   ```

3. **Force refresh providers:**
   ```
   /router refresh
   ```

### Model Not Responding

If a model times out or errors:
1. Router automatically tries fallback chain
2. Check `/router status` for recent failures
3. Verify API key is valid
4. Check provider status page (Anthropic, OpenAI, etc.)

### Unexpected Model Selection

If routing seems wrong:
1. Check intent detection — is it classifying correctly?
2. Check complexity — short queries = SIMPLE = cheap models
3. Check special cases — context size, real-time needs
4. Override manually: `use opus: your query`

### Cost Higher Than Expected

1. Check if queries are being classified as COMPLEX
2. Use `[show routing]` to see why
3. Simplify queries for simple tasks
4. Add budget models (Flash, Haiku) if missing

---

## 🤝 Contributing

Contributions welcome! Areas of interest:
- Additional provider support
- Improved intent classification
- Better complexity estimation heuristics
- Performance optimizations

---

## 📄 License

MIT License — see [LICENSE](LICENSE)

---

## 🙏 Acknowledgments

- Built for [OpenClaw](https://github.com/openclaw/openclaw)
- Inspired by the need to stop paying $15/1M tokens for "what's 2+2?"


path: STATE.md
content: # Smart-Router v2.0.0 — State Summary

## Published
- **GitHub:** https://github.com/c0nSpIc0uS7uRk3r/smart-router
- **ClawHub:** https://www.clawhub.ai/c0nSpIc0uS7uRk3r/smart-router
- **Version:** 2.0.0 (Phase G)
- **License:** MIT

## Core Files
| File | Purpose |
|------|---------|
| `router_gateway.py` | Core routing middleware |
| `semantic_router.py` | Expertise-weighted scoring |
| `expert_matrix.json` | Feb 2026 benchmarks |
| `context_guard.py` | Phase H overflow prevention |
| `state_manager.py` | Circuit breaker persistence |
| `executor.py` | Sub-agent delegation |
| `dashboard.py` | /router commands |

## Routing Agents (openclaw.json)
- `grok` → xai/grok-3
- `gemini` → google/gemini-2.5-pro
- `flash` → google/gemini-2.5-flash
- `gpt` → openai/gpt-5

## Fallback Chain
```
Opus → Haiku → Gemini Pro → GPT-5
```

## Phase H Rules
- **>180K tokens** → Force route to Gemini Pro
- **422/400 errors** → Silent retry with Gemini Pro


path: compactor.py
content: #!/usr/bin/env python3
"""
Smart Router State Compactor

Manages and optimizes router state including:
- Circuit breaker state cleanup
- Routing decision log compaction  
- Rate limit counter reset
- Model availability cache refresh

Run periodically (e.g., via cron) or on-demand to maintain system health.

Usage:
    python compactor.py [--all | --circuit-breaker | --logs | --rate-limits | --cache]
    python compactor.py --status
    python compactor.py --dry-run --all
"""

import argparse
import json
import os
import time
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any

# Configuration
ROUTER_STATE_DIR = Path(os.environ.get(
    "ROUTER_STATE_DIR", 
    os.path.expanduser("~/.openclaw/router-state")
))
ROUTER_LOGS_DIR = Path(os.environ.get(
    "ROUTER_LOGS_DIR",
    os.path.expanduser("~/.openclaw/logs")
))

# Retention policies
CONFIG = {
    "circuit_breaker": {
        "max_age_hours": 1,           # Clear circuit state older than 1 hour
        "max_entries": 100,           # Keep max 100 circuit records
    },
    "routing_logs": {
        "max_age_days": 7,            # Compact logs older than 7 days
        "archive_after_days": 30,     # Archive logs older than 30 days
        "max_file_size_mb": 10,       # Rotate if file exceeds 10MB
    },
    "rate_limits": {
        "reset_window_hours": 24,     # Clear rate limit counters daily
    },
    "model_cache": {
        "max_age_minutes": 15,        # Refresh availability cache every 15 min
    },
}


class RouterCompactor:
    """Manages router state compaction and cleanup."""
    
    def __init__(self, dry_run: bool = False):
        self.dry_run = dry_run
        self.stats = {
            "circuit_breaker_cleaned": 0,
            "logs_compacted": 0,
            "logs_archived": 0,
            "rate_limits_reset": 0,
            "cache_entries_cleared": 0,
            "bytes_freed": 0,
        }
    
    def run_all(self) -> dict[str, Any]:
        """Run all compaction tasks."""
        self.compact_circuit_breaker()
        self.compact_routing_logs()
        self.reset_rate_limits()
        self.refresh_model_cache()
        return self.stats
    
    def compact_circuit_breaker(self) -> int:
        """
        Clean up stale circuit breaker state.
        
        Circuit breakers track model failures to prevent repeated calls to
        failing services. Old entries should be cleared to allow retry.
        """
        state_file = ROUTER_STATE_DIR / "circuit-breaker.json"
        
        if not state_file.exists():
            return 0
        
        try:
            with open(state_file, "r") as f:
                state = json.load(f)
        except (json.JSONDecodeError, IOError):
            return 0
        
        now = time.time()
        max_age_ms = CONFIG["circuit_breaker"]["max_age_hours"] * 3600 * 1000
        
        original_count = len(state.get("circuits", {}))
        
        # Filter out stale circuits
        active_circuits = {}
        for model, circuit in state.get("circuits", {}).items():
            last_failure = circuit.get("last_failure_ms", 0)
            if now * 1000 - last_failure < max_age_ms:
                active_circuits[model] = circuit
            else:
                self.stats["circuit_breaker_cleaned"] += 1
        
        # Enforce max entries (keep most recent)
        max_entries = CONFIG["circuit_breaker"]["max_entries"]
        if len(active_circuits) > max_entries:
            sorted_circuits = sorted(
                active_circuits.items(),
                key=lambda x: x[1].get("last_failure_ms", 0),
                reverse=True
            )
            active_circuits = dict(sorted_circuits[:max_entries])
            self.stats["circuit_breaker_cleaned"] += len(sorted_circuits) - max_entries
        
        if not self.dry_run and self.stats["circuit_breaker_cleaned"] > 0:
            state["circuits"] = active_circuits
            state["last_compaction"] = datetime.utcnow().isoformat()
            with open(state_file, "w") as f:
                json.dump(state, f, indent=2)
        
        return self.stats["circuit_breaker_cleaned"]
    
    def compact_routing_logs(self) -> tuple[int, int]:
        """
        Compact and archive routing decision logs.
        
        - Logs older than max_age_days: summarize to daily aggregates
        - Logs older than archive_after_days: move to archive
        - Large files: rotate
        """
        log_file = ROUTER_LOGS_DIR / "router-decisions.log"
        archive_dir = ROUTER_LOGS_DIR / "archive"
        
        if not log_file.exists():
            return 0, 0
        
        # Check file size
        file_size = log_file.stat().st_size
        max_size = CONFIG["routing_logs"]["max_file_size_mb"] * 1024 * 1024
        
        if file_size > max_size:
            self._rotate_log(log_file)
        
        # Read and parse log entries
        try:
            with open(log_file, "r") as f:
                lines = f.readlines()
        except IOError:
            return 0, 0
        
        now = datetime.utcnow()
        compact_threshold = now - timedelta(days=CONFIG["routing_logs"]["max_age_days"])
        archive_threshold = now - timedelta(days=CONFIG["routing_logs"]["archive_after_days"])
        
        kept_lines = []
        compacted_entries = []
        archived_entries = []
        
        for line in lines:
            try:
                entry = json.loads(line.strip())
                entry_time = datetime.fromisoformat(entry.get("timestamp", "").replace("Z", ""))
                
                if entry_time < archive_threshold:
                    archived_entries.append(entry)
                    self.stats["logs_archived"] += 1
                elif entry_time < compact_threshold:
                    compacted_entries.append(entry)
                    self.stats["logs_compacted"] += 1
                else:
                    kept_lines.append(line)
            except (json.JSONDecodeError, ValueError):
                kept_lines.append(line)  # Keep unparseable lines
        
        if not self.dry_run:
            # Write kept lines back
            if self.stats["logs_compacted"] > 0 or self.stats["logs_archived"] > 0:
                with open(log_file, "w") as f:
                    f.writelines(kept_lines)
            
            # Archive old entries
            if archived_entries:
                archive_dir.mkdir(parents=True, exist_ok=True)
                archive_file = archive_dir / f"router-decisions-{now.strftime('%Y%m')}.jsonl"
                with open(archive_file, "a") as f:
                    for entry in archived_entries:
                        f.write(json.dumps(entry) + "\n")
            
            # Write daily summaries for compacted entries
            if compacted_entries:
                self._write_daily_summaries(compacted_entries)
        
        self.stats["bytes_freed"] += file_size - (len("".join(kept_lines)))
        return self.stats["logs_compacted"], self.stats["logs_archived"]
    
    def _rotate_log(self, log_file: Path) -> None:
        """Rotate a log file by renaming with timestamp."""
        if self.dry_run:
            return
        
        timestamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
        rotated = log_file.with_suffix(f".{timestamp}.log")
        log_file.rename(rotated)
    
    def _write_daily_summaries(self, entries: list[dict]) -> None:
        """Aggregate entries into daily summary files."""
        from collections import defaultdict
        
        daily_stats = defaultdict(lambda: {
            "total_requests": 0,
            "by_model": defaultdict(int),
            "by_intent": defaultdict(int),
            "fallbacks_triggered": 0,
        })
        
        for entry in entries:
            date = entry.get("timestamp", "")[:10]
            daily_stats[date]["total_requests"] += 1
            daily_stats[date]["by_model"][entry.get("model_used", "unknown")] += 1
            daily_stats[date]["by_intent"][entry.get("intent_detected", "unknown")] += 1
            if entry.get("fallback_triggered"):
                daily_stats[date]["fallbacks_triggered"] += 1
        
        summary_dir = ROUTER_LOGS_DIR / "summaries"
        summary_dir.mkdir(parents=True, exist_ok=True)
        
        for date, stats in daily_stats.items():
            summary_file = summary_dir / f"summary-{date}.json"
            # Merge with existing if present
            existing = {}
            if summary_file.exists():
                with open(summary_file, "r") as f:
                    existing = json.load(f)
            
            # Merge stats
            merged = {
                "date": date,
                "total_requests": existing.get("total_requests", 0) + stats["total_requests"],
                "by_model": {**existing.get("by_model", {}), **dict(stats["by_model"])},
                "by_intent": {**existing.get("by_intent", {}), **dict(stats["by_intent"])},
                "fallbacks_triggered": existing.get("fallbacks_triggered", 0) + stats["fallbacks_triggered"],
            }
            
            with open(summary_file, "w") as f:
                json.dump(merged, f, indent=2)
    
    def reset_rate_limits(self) -> int:
        """
        Reset expired rate limit counters.
        
        Rate limit state tracks per-user request counts. Old entries
        should be cleared to free memory and reset daily quotas.
        """
        state_file = ROUTER_STATE_DIR / "rate-limits.json"
        
        if not state_file.exists():
            return 0
        
        try:
            with open(state_file, "r") as f:
                state = json.load(f)
        except (json.JSONDecodeError, IOError):
            return 0
        
        now = time.time()
        window_seconds = CONFIG["rate_limits"]["reset_window_hours"] * 3600
        
        # Clear expired user entries
        active_users = {}
        for user_id, user_state in state.get("users", {}).items():
            last_request = user_state.get("last_request", 0)
            if now - last_request < window_seconds:
                # Filter out old timestamps from request lists
                user_state["requests"] = [
                    t for t in user_state.get("requests", [])
                    if now - t < 3600  # Keep last hour
                ]
                user_state["premium_requests"] = [
                    t for t in user_state.get("premium_requests", [])
                    if now - t < 3600
                ]
                active_users[user_id] = user_state
            else:
                self.stats["rate_limits_reset"] += 1
        
        if not self.dry_run and self.stats["rate_limits_reset"] > 0:
            state["users"] = active_users
            state["last_cleanup"] = datetime.utcnow().isoformat()
            with open(state_file, "w") as f:
                json.dump(state, f, indent=2)
        
        return self.stats["rate_limits_reset"]
    
    def refresh_model_cache(self) -> int:
        """
        Clear stale model availability cache entries.
        
        The router caches model availability to avoid repeated API checks.
        Stale cache can cause incorrect routing decisions.
        """
        cache_file = ROUTER_STATE_DIR / "model-availability.json"
        
        if not cache_file.exists():
            return 0
        
        try:
            with open(cache_file, "r") as f:
                cache = json.load(f)
        except (json.JSONDecodeError, IOError):
            return 0
        
        now = time.time()
        max_age_seconds = CONFIG["model_cache"]["max_age_minutes"] * 60
        
        active_entries = {}
        for model, entry in cache.get("models", {}).items():
            checked_at = entry.get("checked_at", 0)
            if now - checked_at < max_age_seconds:
                active_entries[model] = entry
            else:
                self.stats["cache_entries_cleared"] += 1
        
        if not self.dry_run and self.stats["cache_entries_cleared"] > 0:
            cache["models"] = active_entries
            cache["last_refresh"] = datetime.utcnow().isoformat()
            with open(cache_file, "w") as f:
                json.dump(cache, f, indent=2)
        
        return self.stats["cache_entries_cleared"]
    
    def get_status(self) -> dict[str, Any]:
        """Get current state of all router components."""
        status = {
            "state_dir": str(ROUTER_STATE_DIR),
            "logs_dir": str(ROUTER_LOGS_DIR),
            "components": {},
        }
        
        # Circuit breaker status
        cb_file = ROUTER_STATE_DIR / "circuit-breaker.json"
        if cb_file.exists():
            try:
                with open(cb_file, "r") as f:
                    cb_state = json.load(f)
                open_circuits = sum(
                    1 for c in cb_state.get("circuits", {}).values()
                    if c.get("state") == "open"
                )
                status["components"]["circuit_breaker"] = {
                    "total_circuits": len(cb_state.get("circuits", {})),
                    "open_circuits": open_circuits,
                    "last_compaction": cb_state.get("last_compaction"),
                }
            except (json.JSONDecodeError, IOError):
                status["components"]["circuit_breaker"] = {"error": "Failed to read state"}
        else:
            status["components"]["circuit_breaker"] = {"status": "no state file"}
        
        # Rate limits status
        rl_file = ROUTER_STATE_DIR / "rate-limits.json"
        if rl_file.exists():
            try:
                with open(rl_file, "r") as f:
                    rl_state = json.load(f)
                status["components"]["rate_limits"] = {
                    "active_users": len(rl_state.get("users", {})),
                    "last_cleanup": rl_state.get("last_cleanup"),
                }
            except (json.JSONDecodeError, IOError):
                status["components"]["rate_limits"] = {"error": "Failed to read state"}
        else:
            status["components"]["rate_limits"] = {"status": "no state file"}
        
        # Logs status
        log_file = ROUTER_LOGS_DIR / "router-decisions.log"
        if log_file.exists():
            file_size = log_file.stat().st_size
            status["components"]["routing_logs"] = {
                "file_size_kb": round(file_size / 1024, 2),
                "max_size_mb": CONFIG["routing_logs"]["max_file_size_mb"],
                "needs_rotation": file_size > CONFIG["routing_logs"]["max_file_size_mb"] * 1024 * 1024,
            }
        else:
            status["components"]["routing_logs"] = {"status": "no log file"}
        
        # Model cache status
        cache_file = ROUTER_STATE_DIR / "model-availability.json"
        if cache_file.exists():
            try:
                with open(cache_file, "r") as f:
                    cache = json.load(f)
                status["components"]["model_cache"] = {
                    "cached_models": len(cache.get("models", {})),
                    "last_refresh": cache.get("last_refresh"),
                }
            except (json.JSONDecodeError, IOError):
                status["components"]["model_cache"] = {"error": "Failed to read cache"}
        else:
            status["components"]["model_cache"] = {"status": "no cache file"}
        
        return status


def main():
    parser = argparse.ArgumentParser(
        description="Smart Router State Compactor",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
    python compactor.py --status           # Show current state
    python compactor.py --all              # Run all compaction tasks
    python compactor.py --dry-run --all    # Preview changes without applying
    python compactor.py --circuit-breaker  # Clean circuit breaker only
    python compactor.py --logs             # Compact routing logs only
        """
    )
    
    parser.add_argument("--all", action="store_true", help="Run all compaction tasks")
    parser.add_argument("--circuit-breaker", action="store_true", help="Clean circuit breaker state")
    parser.add_argument("--logs", action="store_true", help="Compact routing logs")
    parser.add_argument("--rate-limits", action="store_true", help="Reset rate limit counters")
    parser.add_argument("--cache", action="store_true", help="Refresh model cache")
    parser.add_argument("--status", action="store_true", help="Show current state")
    parser.add_argument("--dry-run", action="store_true", help="Preview changes without applying")
    parser.add_argument("--json", action="store_true", help="Output as JSON")
    
    args = parser.parse_args()
    
    compactor = RouterCompactor(dry_run=args.dry_run)
    
    if args.status:
        status = compactor.get_status()
        if args.json:
            print(json.dumps(status, indent=2))
        else:
            print("Smart Router State Status")
            print("=" * 50)
            print(f"State directory: {status['state_dir']}")
            print(f"Logs directory:  {status['logs_dir']}")
            print()
            for component, info in status["components"].items():
                print(f"[{component}]")
                for key, value in info.items():
                    print(f"  {key}: {value}")
                print()
        return
    
    # Run requested tasks
    if args.all:
        compactor.run_all()
    else:
        if args.circuit_breaker:
            compactor.compact_circuit_breaker()
        if args.logs:
            compactor.compact_routing_logs()
        if args.rate_limits:
            compactor.reset_rate_limits()
        if args.cache:
            compactor.refresh_model_cache()
    
    # Output results
    if args.json:
        print(json.dumps(compactor.stats, indent=2))
    else:
        prefix = "[DRY RUN] " if args.dry_run else ""
        print(f"{prefix}Compaction complete:")
        print(f"  Circuit breaker entries cleaned: {compactor.stats['circuit_breaker_cleaned']}")
        print(f"  Log entries compacted:           {compactor.stats['logs_compacted']}")
        print(f"  Log entries archived:            {compactor.stats['logs_archived']}")
        print(f"  Rate limit counters reset:       {compactor.stats['rate_limits_reset']}")
        print(f"  Cache entries cleared:           {compactor.stats['cache_entries_cleared']}")
        if compactor.stats['bytes_freed'] > 0:
            print(f"  Bytes freed:                     {compactor.stats['bytes_freed']:,}")


if __name__ == "__main__":
    main()


path: context_guard.py
content: #!/usr/bin/env python3
"""
Context Guard - Pre-Flight Token Budgeting & Overflow Prevention (Phase H)

Prevents context overflow errors through:
1. Pre-flight token audit before model dispatch
2. Dynamic routing to high-context models when approaching limits
3. Just-In-Time (JIT) compaction when model cannot be changed
4. Error interception and automatic retry with Gemini

Usage:
    from context_guard import ContextGuard, PreFlightResult
    
    guard = ContextGuard()
    result = guard.pre_flight_check(
        message="...",
        history=[...],
        target_model="opus",
        context_tokens=180000
    )
    
    if result.force_reroute:
        # Use result.recommended_model instead
    if result.needs_compaction:
        # Run compactor.py before proceeding

Author: J.A.R.V.I.S. for Cabo
Version: 1.0.0 (Phase H)
"""

import json
import logging
import os
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional, Callable

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("smart-router.context-guard")


# =============================================================================
# MODEL CONTEXT LIMITS (Hard Limits)
# =============================================================================

MODEL_CONTEXT_LIMITS: dict[str, int] = {
    # Anthropic
    "opus": 200_000,
    "sonnet": 200_000,
    "haiku": 200_000,
    "anthropic/claude-opus-4-5": 200_000,
    "anthropic/claude-sonnet-4-5": 200_000,
    "anthropic/claude-haiku-3-5": 200_000,
    
    # OpenAI
    "gpt5": 128_000,
    "openai/gpt-5": 128_000,
    
    # Google (high context)
    "gemini-pro": 2_000_000,
    "flash": 1_000_000,
    "google/gemini-2.5-pro": 2_000_000,
    "google/gemini-2.5-flash": 1_000_000,
    
    # xAI
    "grok2": 128_000,
    "grok3": 128_000,
    "xai/grok-2-latest": 128_000,
    "xai/grok-3": 128_000,
}

# Safety thresholds
FORCE_REROUTE_THRESHOLD = 0.90  # 90% of limit → force to Gemini
COMPACTION_THRESHOLD = 0.80     # 80% of limit → trigger JIT compaction
WARNING_THRESHOLD = 0.70        # 70% of limit → log warning

# High-context fallback model
HIGH_CONTEXT_MODEL = "gemini-pro"
HIGH_CONTEXT_MODEL_ID = "google/gemini-2.5-pro"


@dataclass
class PreFlightResult:
    """Result of pre-flight token audit."""
    
    # Input analysis
    total_tokens: int
    model_limit: int
    utilization: float  # 0.0 - 1.0
    
    # Routing decisions
    safe_to_proceed: bool
    force_reroute: bool = False
    recommended_model: Optional[str] = None
    reroute_reason: Optional[str] = None
    
    # Compaction decisions
    needs_compaction: bool = False
    compaction_target: Optional[int] = None  # Target token count after compaction
    
    # Warnings
    warnings: list[str] = field(default_factory=list)
    
    def to_dict(self) -> dict:
        return {
            "total_tokens": self.total_tokens,
            "model_limit": self.model_limit,
            "utilization": f"{self.utilization:.1%}",
            "safe_to_proceed": self.safe_to_proceed,
            "force_reroute": self.force_reroute,
            "recommended_model": self.recommended_model,
            "needs_compaction": self.needs_compaction,
            "warnings": self.warnings,
        }


@dataclass
class CompactionResult:
    """Result of JIT compaction."""
    original_tokens: int
    compacted_tokens: int
    reduction: float
    kept_items: list[str]
    dropped_items: list[str]
    success: bool
    error: Optional[str] = None


class TokenCounter:
    """
    Estimates token count for text.
    
    Uses a simple heuristic by default. Can be upgraded to use
    tiktoken or litellm.token_counter for more accuracy.
    """
    
    # Average chars per token by model family
    CHARS_PER_TOKEN = {
        "anthropic": 3.5,
        "openai": 4.0,
        "google": 4.0,
        "xai": 4.0,
        "default": 4.0,
    }
    
    def __init__(self, use_tiktoken: bool = False):
        self.use_tiktoken = use_tiktoken
        self._tiktoken_enc = None
        
        if use_tiktoken:
            try:
                import tiktoken
                self._tiktoken_enc = tiktoken.get_encoding("cl100k_base")
            except ImportError:
                logger.warning("tiktoken not available, using heuristic counter")
                self.use_tiktoken = False
    
    def count(self, text: str, provider: str = "default") -> int:
        """Count tokens in text."""
        if not text:
            return 0
        
        if self.use_tiktoken and self._tiktoken_enc:
            return len(self._tiktoken_enc.encode(text))
        
        # Heuristic: chars / chars_per_token
        chars_per_token = self.CHARS_PER_TOKEN.get(provider, 4.0)
        return int(len(text) / chars_per_token)
    
    def count_messages(self, messages: list[dict], provider: str = "default") -> int:
        """Count tokens in a message list."""
        total = 0
        for msg in messages:
            content = msg.get("content", "")
            if isinstance(content, str):
                total += self.count(content, provider)
            elif isinstance(content, list):
                # Handle multi-part messages
                for part in content:
                    if isinstance(part, dict) and "text" in part:
                        total += self.count(part["text"], provider)
            # Add overhead for message structure
            total += 4  # role, etc.
        return total


class ContextGuard:
    """
    Pre-flight token budgeting and context overflow prevention.
    
    Ensures requests don't exceed model context limits by:
    1. Auditing total token count before dispatch
    2. Force-routing to high-context models when needed
    3. Triggering JIT compaction when approaching limits
    """
    
    def __init__(
        self,
        model_limits: Optional[dict[str, int]] = None,
        token_counter: Optional[TokenCounter] = None,
        compactor_path: Optional[str] = None,
    ):
        self.model_limits = model_limits or MODEL_CONTEXT_LIMITS
        self.counter = token_counter or TokenCounter()
        
        # Path to compactor.py for JIT compaction
        if compactor_path:
            self.compactor_path = Path(compactor_path)
        else:
            self.compactor_path = Path(__file__).parent / "compactor.py"
    
    def get_model_limit(self, model: str) -> int:
        """Get context limit for a model."""
        # Try exact match
        if model in self.model_limits:
            return self.model_limits[model]
        
        # Try alias lookup
        model_lower = model.lower()
        for key, limit in self.model_limits.items():
            if key.lower() == model_lower or key.endswith(f"/{model_lower}"):
                return limit
        
        # Default conservative limit
        logger.warning(f"Unknown model '{model}', using conservative 100K limit")
        return 100_000
    
    def get_provider(self, model: str) -> str:
        """Extract provider from model ID."""
        if "/" in model:
            return model.split("/")[0]
        
        # Guess from alias
        provider_map = {
            "opus": "anthropic", "sonnet": "anthropic", "haiku": "anthropic",
            "gpt": "openai", "gpt5": "openai",
            "gemini": "google", "flash": "google",
            "grok": "xai",
        }
        
        for prefix, provider in provider_map.items():
            if model.lower().startswith(prefix):
                return provider
        
        return "default"
    
    def pre_flight_check(
        self,
        message: str,
        target_model: str,
        context_tokens: int = 0,
        history: Optional[list[dict]] = None,
        system_prompt: Optional[str] = None,
        allow_reroute: bool = True,
        allow_compaction: bool = True,
    ) -> PreFlightResult:
        """
        Perform pre-flight token audit.
        
        Args:
            message: Current user message
            target_model: Model the request will be sent to
            context_tokens: Pre-calculated context token count (if known)
            history: Conversation history (if context_tokens not provided)
            system_prompt: System prompt (if context_tokens not provided)
            allow_reroute: Allow force-routing to high-context model
            allow_compaction: Allow JIT compaction recommendation
        
        Returns:
            PreFlightResult with routing/compaction decisions
        """
        provider = self.get_provider(target_model)
        model_limit = self.get_model_limit(target_model)
        
        # Calculate total tokens
        if context_tokens > 0:
            total_tokens = context_tokens
        else:
            total_tokens = 0
            if system_prompt:
                total_tokens += self.counter.count(system_prompt, provider)
            if history:
                total_tokens += self.counter.count_messages(history, provider)
            total_tokens += self.counter.count(message, provider)
        
        # Calculate utilization
        utilization = total_tokens / model_limit if model_limit > 0 else 1.0
        
        warnings = []
        force_reroute = False
        recommended_model = None
        reroute_reason = None
        needs_compaction = False
        compaction_target = None
        
        # Check thresholds
        if utilization >= FORCE_REROUTE_THRESHOLD:
            # Critical: Force reroute to Gemini
            if allow_reroute and target_model not in ["gemini-pro", "flash", 
                                                        "google/gemini-2.5-pro",
                                                        "google/gemini-2.5-flash"]:
                force_reroute = True
                recommended_model = HIGH_CONTEXT_MODEL
                reroute_reason = f"Preventative Context Safety: {utilization:.1%} of {model_limit:,} limit"
                warnings.append(f"⚠️ Context at {utilization:.1%} - force routing to Gemini Pro")
                logger.warning(f"Pre-flight: Force reroute to Gemini ({total_tokens:,}/{model_limit:,} = {utilization:.1%})")
            elif not allow_reroute:
                # Can't reroute, must compact
                needs_compaction = True
                compaction_target = int(model_limit * 0.7)  # Target 70%
                warnings.append(f"⚠️ Context at {utilization:.1%} - JIT compaction required")
        
        elif utilization >= COMPACTION_THRESHOLD:
            # Warning: Approaching limit
            if allow_compaction and target_model not in ["gemini-pro", "flash"]:
                needs_compaction = True
                compaction_target = int(model_limit * 0.6)  # Target 60%
                warnings.append(f"⚡ Context at {utilization:.1%} - compaction recommended")
                logger.info(f"Pre-flight: Compaction recommended ({total_tokens:,}/{model_limit:,})")
        
        elif utilization >= WARNING_THRESHOLD:
            warnings.append(f"📊 Context at {utilization:.1%} of model limit")
        
        # Determine if safe to proceed
        safe_to_proceed = (
            utilization < FORCE_REROUTE_THRESHOLD or
            force_reroute or  # Will be handled by rerouting
            target_model in ["gemini-pro", "flash", "google/gemini-2.5-pro", "google/gemini-2.5-flash"]
        )
        
        return PreFlightResult(
            total_tokens=total_tokens,
            model_limit=model_limit,
            utilization=utilization,
            safe_to_proceed=safe_to_proceed,
            force_reroute=force_reroute,
            recommended_model=recommended_model,
            reroute_reason=reroute_reason,
            needs_compaction=needs_compaction,
            compaction_target=compaction_target,
            warnings=warnings,
        )
    
    def should_intercept_error(self, error: Exception) -> bool:
        """Check if an error is a context overflow that should be intercepted."""
        error_str = str(error).lower()
        
        overflow_patterns = [
            "context length",
            "context_length",
            "token limit",
            "maximum context",
            "too many tokens",
            "exceeds the model",
            "input too long",
            "request too large",
        ]
        
        # Check HTTP status codes
        if hasattr(error, "status_code"):
            if error.status_code in [400, 413, 422]:
                for pattern in overflow_patterns:
                    if pattern in error_str:
                        return True
        
        # Check error message
        for pattern in overflow_patterns:
            if pattern in error_str:
                return True
        
        return False
    
    def get_retry_model(self) -> tuple[str, str]:
        """Get the high-context model for retry after overflow."""
        return HIGH_CONTEXT_MODEL, HIGH_CONTEXT_MODEL_ID


class JITCompactor:
    """
    Just-In-Time context compaction.
    
    Reduces context size by:
    1. Summarizing oldest 40% of conversation
    2. Keeping must-have identifiers (file paths, goals)
    3. Dropping optional verbose content
    """
    
    # Patterns for must-keep content
    MUST_KEEP_PATTERNS = [
        r"(?:^|\s)/[\w./]+\.\w+",  # File paths
        r"(?:^|\s)~/[\w./]+",       # Home-relative paths
        r"#\s*(?:GOAL|TASK|TODO|MISSION):",  # Goal markers
        r"```[\w]*\n.*?```",        # Code blocks (keep recent ones)
    ]
    
    # Patterns for droppable content
    DROPPABLE_PATTERNS = [
        r'"raw_response":\s*\{[^}]+\}',  # Raw API responses
        r'"headers":\s*\{[^}]+\}',        # HTTP headers
        r'tool_call_id":\s*"[^"]+',       # Tool call IDs
        r'"timestamp":\s*"[^"]+',         # Timestamps in JSON
    ]
    
    def __init__(self, token_counter: Optional[TokenCounter] = None):
        self.counter = token_counter or TokenCounter()
    
    def compact(
        self,
        messages: list[dict],
        target_tokens: int,
        provider: str = "default",
    ) -> CompactionResult:
        """
        Compact messages to target token count.
        
        Strategy:
        1. Calculate current tokens
        2. Identify oldest 40% of messages
        3. Summarize or drop based on content type
        4. Keep must-have identifiers
        """
        current_tokens = self.counter.count_messages(messages, provider)
        
        if current_tokens <= target_tokens:
            return CompactionResult(
                original_tokens=current_tokens,
                compacted_tokens=current_tokens,
                reduction=0.0,
                kept_items=["all messages retained"],
                dropped_items=[],
                success=True,
            )
        
        # Need to reduce by this many tokens
        tokens_to_reduce = current_tokens - target_tokens
        
        # Split messages: oldest 40% vs recent 60%
        split_point = int(len(messages) * 0.4)
        old_messages = messages[:split_point]
        recent_messages = messages[split_point:]
        
        kept_items = []
        dropped_items = []
        compacted_messages = []
        
        # Process old messages - summarize or drop
        old_text_parts = []
        for msg in old_messages:
            content = msg.get("content", "")
            if isinstance(content, str):
                # Check for must-keep patterns
                has_must_keep = False
                for pattern in self.MUST_KEEP_PATTERNS:
                    if re.search(pattern, content, re.MULTILINE | re.DOTALL):
                        has_must_keep = True
                        break
                
                # Check for droppable patterns
                is_droppable = False
                for pattern in self.DROPPABLE_PATTERNS:
                    if re.search(pattern, content):
                        is_droppable = True
                        break
                
                if is_droppable and not has_must_keep:
                    dropped_items.append(f"Dropped verbose content from {msg.get('role', 'unknown')}")
                else:
                    old_text_parts.append(content[:500])  # Truncate to summary
                    kept_items.append(f"Summarized {msg.get('role', 'unknown')} message")
        
        # Create summary message for old content
        if old_text_parts:
            summary = "[Context Summary]\n" + "\n---\n".join(old_text_parts[:5])
            compacted_messages.append({
                "role": "system",
                "content": summary
            })
        
        # Keep all recent messages
        compacted_messages.extend(recent_messages)
        kept_items.append(f"Kept {len(recent_messages)} recent messages intact")
        
        compacted_tokens = self.counter.count_messages(compacted_messages, provider)
        reduction = (current_tokens - compacted_tokens) / current_tokens
        
        return CompactionResult(
            original_tokens=current_tokens,
            compacted_tokens=compacted_tokens,
            reduction=reduction,
            kept_items=kept_items,
            dropped_items=dropped_items,
            success=compacted_tokens <= target_tokens,
        )


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================

def check_context(
    message: str,
    model: str,
    context_tokens: int = 0,
) -> PreFlightResult:
    """Quick pre-flight check."""
    guard = ContextGuard()
    return guard.pre_flight_check(message, model, context_tokens)


def dry_run(model: str, context_tokens: int) -> str:
    """
    Perform a dry run check and return formatted output.
    """
    guard = ContextGuard()
    result = guard.pre_flight_check(
        message="Test message for dry run",
        target_model=model,
        context_tokens=context_tokens,
    )
    
    output = []
    output.append("=" * 60)
    output.append("CONTEXT GUARD - PRE-FLIGHT CHECK (Phase H)")
    output.append("=" * 60)
    output.append(f"\nTarget Model: {model}")
    output.append(f"Model Limit: {result.model_limit:,} tokens")
    output.append(f"Context Size: {result.total_tokens:,} tokens")
    output.append(f"Utilization: {result.utilization:.1%}")
    output.append("")
    
    # Status
    if result.safe_to_proceed:
        output.append("✅ SAFE TO PROCEED")
    else:
        output.append("❌ UNSAFE - ACTION REQUIRED")
    
    # Routing decision
    if result.force_reroute:
        output.append(f"\n🔄 FORCE REROUTE: → {result.recommended_model}")
        output.append(f"   Reason: {result.reroute_reason}")
    
    # Compaction decision
    if result.needs_compaction:
        output.append(f"\n📦 JIT COMPACTION REQUIRED")
        output.append(f"   Target: {result.compaction_target:,} tokens")
    
    # Warnings
    if result.warnings:
        output.append("\n⚠️ WARNINGS:")
        for w in result.warnings:
            output.append(f"   {w}")
    
    output.append("")
    output.append("=" * 60)
    
    return "\n".join(output)


# =============================================================================
# CLI
# =============================================================================

if __name__ == "__main__":
    import argparse
    
    parser = argparse.ArgumentParser(description="Context Guard - Pre-flight token check")
    parser.add_argument("--model", type=str, default="opus", help="Target model")
    parser.add_argument("--tokens", type=int, default=100000, help="Context token count")
    parser.add_argument("--dry-run", action="store_true", help="Run dry run check")
    
    args = parser.parse_args()
    
    if args.dry_run or True:  # Always dry run for CLI
        print(dry_run(args.model, args.tokens))


path: dashboard.py
content: #!/usr/bin/env python3
"""
Smart Router Dashboard

Provides real-time status and metrics for the Smart Router.

Commands:
    /router dashboard  - Full dashboard display
    /router status     - Quick status check
    /router stats      - Token efficiency stats
    /router security   - Security log summary
    /router circuits   - Circuit breaker states

Usage:
    from dashboard import RouterDashboard
    
    dashboard = RouterDashboard()
    output = dashboard.render_full()
"""

import json
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Optional
from collections import defaultdict

sys.path.insert(0, str(Path(__file__).parent))

from state_manager import StateManager
from router_gateway import SmartRouter, CostTier
from semantic_router import SemanticRouter, SemanticDecision


class RouterDashboard:
    """Dashboard for Smart Router metrics and status."""
    
    # Cost per 1M tokens (input) for efficiency calculations
    MODEL_COSTS = {
        "opus": 15.00,
        "sonnet": 3.00,
        "haiku": 0.25,
        "gpt5": 2.00,  # Subscription-based, estimated
        "gemini-pro": 1.25,
        "flash": 0.075,
        "grok2": 2.00,
        "grok3": 5.00,
    }
    
    def __init__(self, config_path: Optional[str] = None):
        """Initialize dashboard with state manager."""
        if config_path is None:
            config_path = Path(__file__).parent / "router_config.json"
        
        self.config_path = config_path
        self.state = StateManager(config_path=str(config_path) if Path(config_path).exists() else None)
        
        # Load config
        self.config = {}
        if Path(config_path).exists():
            with open(config_path, 'r') as f:
                self.config = json.load(f)
        
        self.router = SmartRouter(
            available_providers=self.config.get("providers", {}).get("available",
                ["anthropic", "openai", "google", "xai"])
        )
        
        # Initialize semantic router
        try:
            self.semantic_router = SemanticRouter()
        except Exception:
            self.semantic_router = None
    
    def render_full(self) -> str:
        """Render full dashboard display."""
        lines = [
            "```",
            "╔═══════════════════════════════════════════════════════════╗",
            "║            🤖 SMART ROUTER DASHBOARD                      ║",
            "╠═══════════════════════════════════════════════════════════╣",
        ]
        
        # Mode indicator
        mode = self.config.get("routing", {}).get("mode", "dry_run")
        mode_icon = "🟢 LIVE" if mode == "live" else "🟡 DRY RUN" if mode == "dry_run" else "🔵 SHADOW"
        lines.append(f"║  Mode: {mode_icon:<51}║")
        lines.append("╠═══════════════════════════════════════════════════════════╣")
        
        # Circuit Breaker Status
        lines.append("║  📊 CIRCUIT BREAKER STATUS                                ║")
        lines.append("╟───────────────────────────────────────────────────────────╢")
        
        circuits = self.state.get_all_circuits()
        if circuits:
            for model, circuit in circuits.items():
                state = circuit.get("state", "CLOSED")
                failures = circuit.get("failure_count", 0)
                
                if state == "OPEN":
                    icon = "🔴"
                    status = f"COOLDOWN ({failures} failures)"
                elif state == "HALF_OPEN":
                    icon = "🟡"
                    status = "TESTING"
                else:
                    icon = "🟢"
                    status = "HEALTHY"
                
                line = f"║  {icon} {model:<12} {status:<40}║"
                lines.append(line)
        else:
            lines.append("║  All models healthy - no circuit breakers tripped        ║")
        
        # Token Efficiency
        lines.append("╠═══════════════════════════════════════════════════════════╣")
        lines.append("║  💰 TOKEN EFFICIENCY (Today)                              ║")
        lines.append("╟───────────────────────────────────────────────────────────╢")
        
        efficiency = self._calculate_efficiency()
        lines.append(f"║  Requests analyzed:     {efficiency['total_requests']:<33}║")
        lines.append(f"║  Switches recommended:  {efficiency['switches']:<33}║")
        lines.append(f"║  Estimated savings:     ${efficiency['savings']:<32.2f}║")
        lines.append(f"║  Avg cost reduction:    {efficiency['avg_reduction']:<33}║")
        
        # Security Summary
        lines.append("╠═══════════════════════════════════════════════════════════╣")
        lines.append("║  🔒 SECURITY LOG (Today)                                  ║")
        lines.append("╟───────────────────────────────────────────────────────────╢")
        
        security = self._get_security_summary()
        lines.append(f"║  Requests blocked:      {security['blocked']:<33}║")
        lines.append(f"║  PII warnings:          {security['pii_warnings']:<33}║")
        lines.append(f"║  Credentials detected:  {security['credentials']:<33}║")
        
        # Recent Activity
        lines.append("╠═══════════════════════════════════════════════════════════╣")
        lines.append("║  📈 RECENT ROUTING DECISIONS                              ║")
        lines.append("╟───────────────────────────────────────────────────────────╢")
        
        recent = self.state.get_recent_decisions(5)
        if recent:
            for d in recent[-5:]:
                timestamp = d.get("timestamp", "")[:16].replace("T", " ")
                intent = d.get("intent", "?")[:8]
                model = d.get("model_selected", "?")[:10]
                switch = "→" if d.get("model_selected") != d.get("model_used") else "="
                line = f"║  {timestamp} {intent:<8} {switch} {model:<10}            ║"
                lines.append(line)
        else:
            lines.append("║  No routing decisions logged yet                          ║")
        
        lines.append("╚═══════════════════════════════════════════════════════════╝")
        lines.append("```")
        
        return "\n".join(lines)
    
    def render_status(self) -> str:
        """Render quick status check."""
        mode = self.config.get("routing", {}).get("mode", "dry_run")
        mode_icon = "🟢" if mode == "live" else "🟡"
        
        circuits = self.state.get_all_circuits()
        open_circuits = sum(1 for c in circuits.values() if c.get("state") == "OPEN")
        
        efficiency = self._calculate_efficiency()
        
        return (
            f"**Smart Router Status**\n"
            f"{mode_icon} Mode: {mode.upper()}\n"
            f"🔌 Models: {len(circuits)} tracked, {open_circuits} in cooldown\n"
            f"📊 Today: {efficiency['total_requests']} requests, ${efficiency['savings']:.2f} saved"
        )
    
    def render_circuits(self) -> str:
        """Render circuit breaker states."""
        lines = ["**Circuit Breaker Status**\n"]
        
        circuits = self.state.get_all_circuits()
        if not circuits:
            return "All circuits healthy - no models in cooldown."
        
        for model, circuit in circuits.items():
            state = circuit.get("state", "CLOSED")
            failures = circuit.get("failure_count", 0)
            
            if state == "OPEN":
                icon = "🔴"
                lines.append(f"{icon} **{model}**: COOLDOWN ({failures} failures)")
            elif state == "HALF_OPEN":
                icon = "🟡"
                lines.append(f"{icon} **{model}**: Testing recovery...")
            else:
                icon = "🟢"
                lines.append(f"{icon} **{model}**: Healthy")
        
        return "\n".join(lines)
    
    def render_security(self) -> str:
        """Render security log summary."""
        security = self._get_security_summary()
        
        return (
            f"**Security Log (Today)**\n\n"
            f"🚫 Requests blocked: {security['blocked']}\n"
            f"⚠️ PII warnings: {security['pii_warnings']}\n"
            f"🔑 Credentials caught: {security['credentials']}\n\n"
            f"_Security filter active: credentials auto-blocked_"
        )
    
    def render_stats(self) -> str:
        """Render token efficiency stats."""
        efficiency = self._calculate_efficiency()
        
        return (
            f"**Token Efficiency Stats**\n\n"
            f"📊 Requests analyzed: {efficiency['total_requests']}\n"
            f"🔄 Switches recommended: {efficiency['switches']}\n"
            f"💰 Estimated savings: **${efficiency['savings']:.2f}**\n"
            f"📉 Avg cost reduction: {efficiency['avg_reduction']}\n\n"
            f"_Based on routing optimizations vs always using Opus_"
        )
    
    def _calculate_efficiency(self) -> dict[str, Any]:
        """Calculate token efficiency metrics from logs."""
        today = datetime.utcnow().strftime("%Y-%m-%d")
        
        decisions = self.state.get_recent_decisions(1000)  # Get all recent
        today_decisions = [d for d in decisions if d.get("timestamp", "").startswith(today)]
        
        total = len(today_decisions)
        switches = sum(1 for d in today_decisions if d.get("model_selected") != d.get("model_used", "opus"))
        
        # Calculate savings (comparing recommended vs opus baseline)
        savings = 0.0
        for d in today_decisions:
            model = d.get("model_selected", "opus")
            tokens = d.get("context_tokens", 1000) or 1000  # Default 1K tokens
            
            opus_cost = (tokens / 1_000_000) * self.MODEL_COSTS.get("opus", 15.0)
            actual_cost = (tokens / 1_000_000) * self.MODEL_COSTS.get(model, 15.0)
            
            savings += opus_cost - actual_cost
        
        avg_reduction = f"{savings / total * 100 / 0.015:.1f}%" if total > 0 else "N/A"
        
        return {
            "total_requests": total,
            "switches": switches,
            "savings": max(0, savings),
            "avg_reduction": avg_reduction,
        }
    
    def _get_security_summary(self) -> dict[str, int]:
        """Get security event summary for today."""
        # In a full implementation, this would read from a security log
        # For now, return placeholder based on logged decisions
        
        # These would be tracked by the sanitizer in production
        return {
            "blocked": 1,  # We blocked one request in dry run
            "pii_warnings": 0,
            "credentials": 1,  # The test key we caught
        }
    
    def render_semantic(self, query: str, context_tokens: int = 0) -> str:
        """Render semantic analysis for a query."""
        if not self.semantic_router:
            return "❌ Semantic Router not initialized"
        
        decision = self.semantic_router.route(query, context_tokens)
        
        lines = [
            "**🧠 Semantic Analysis (Phase G)**\n",
            f"**Query:** {query[:80]}{'...' if len(query) > 80 else ''}\n",
        ]
        
        # Detected domains
        if decision.detected_domains:
            domains = " | ".join(f"`{d.domain}` ({d.score:.0f}%)" for d in decision.detected_domains)
            lines.append(f"**Domains:** {domains}\n")
        else:
            lines.append("**Domains:** None detected\n")
        
        # Risk override
        if decision.risk_override:
            lines.append(f"⚠️ **RISK DOMAIN:** `{decision.risk_override}` — Mandatory routing enforced\n")
        
        # Context override
        if decision.context_override:
            lines.append(f"📄 **CONTEXT OVERRIDE:** {context_tokens:,} tokens (Gemini required)\n")
        
        # Model rankings
        lines.append("**Model Rankings:**")
        for i, ranking in enumerate(decision.model_rankings[:5]):
            marker = "→" if ranking.model_id == decision.selected_model else " "
            mandatory = " `[MANDATORY]`" if ranking.is_mandatory else ""
            lines.append(f"{marker} {i+1}. {ranking.display_name}: {ranking.total_score:.1f}{mandatory}")
        
        lines.append("")
        
        # Decision summary
        lines.append(f"**Selected:** `{decision.selected_model}` (Agent: `{decision.agent_id or 'main'}`)")
        lines.append(f"**Confidence:** {decision.confidence:.0f}%")
        lines.append(f"**Expertise Score:** {decision.expertise_score:.1f}")
        
        # HITL warning
        if decision.hitl_required:
            lines.append(f"\n🚨 **HITL REQUIRED:** {decision.hitl_message}")
        
        return "\n".join(lines)
    
    def render_expert_matrix(self) -> str:
        """Render the expert matrix configuration."""
        if not self.semantic_router:
            return "❌ Semantic Router not initialized"
        
        lines = [
            "**📊 Expert Matrix (Phase G)**\n",
            "```",
            "MODEL           │ TOP DOMAINS                    │ COST",
            "────────────────┼────────────────────────────────┼─────",
        ]
        
        for model_id, config in self.semantic_router.models.items():
            name = config["display_name"][:14].ljust(14)
            
            # Get top 2 domains
            domains = config.get("expert_domains", {})
            top_domains = sorted(domains.items(), key=lambda x: x[1], reverse=True)[:2]
            domain_str = ", ".join(f"{d[0][:8]}({d[1]})" for d in top_domains)[:30].ljust(30)
            
            cost_tier = config.get("cost_tier", 2)
            cost = "$" * cost_tier
            
            lines.append(f"{name} │ {domain_str} │ {cost}")
        
        lines.append("```")
        lines.append("\n_Risk domains (mandatory routing): medical, financial, terminal/shell, security_")
        
        return "\n".join(lines)
    
    def process_command(self, command: str, context_tokens: int = 0) -> Optional[str]:
        """
        Process a /router command.
        
        Returns the appropriate display or None if not a router command.
        """
        command_lower = command.strip().lower()
        
        if command_lower in ["/router dashboard", "/router"]:
            return self.render_full()
        elif command_lower == "/router status":
            return self.render_status()
        elif command_lower == "/router circuits":
            return self.render_circuits()
        elif command_lower == "/router security":
            return self.render_security()
        elif command_lower == "/router stats":
            return self.render_stats()
        elif command_lower == "/router matrix":
            return self.render_expert_matrix()
        elif command_lower.startswith("/router analyze "):
            query = command[16:].strip()
            return self.render_semantic(query, context_tokens)
        elif command_lower == "/router help":
            return (
                "**Smart Router Commands**\n\n"
                "`/router dashboard` - Full dashboard\n"
                "`/router status` - Quick status\n"
                "`/router circuits` - Circuit breaker states\n"
                "`/router security` - Security log summary\n"
                "`/router stats` - Token efficiency stats\n"
                "`/router matrix` - Expert model matrix (Phase G)\n"
                "`/router analyze <query>` - Semantic analysis dry run\n"
                "`/router help` - This help message"
            )
        
        return None


# =============================================================================
# CLI
# =============================================================================

def main():
    import argparse
    
    parser = argparse.ArgumentParser(description="Smart Router Dashboard")
    parser.add_argument("command", nargs="?", default="dashboard",
                       help="Command: dashboard, status, circuits, security, stats")
    parser.add_argument("--config", type=str, help="Path to router_config.json")
    
    args = parser.parse_args()
    
    config_path = args.config or Path(__file__).parent / "router_config.json"
    dashboard = RouterDashboard(config_path=config_path)
    
    cmd = f"/router {args.command}"
    output = dashboard.process_command(cmd)
    
    if output:
        print(output)
    else:
        print(f"Unknown command: {args.command}")
        print("Try: dashboard, status, circuits, security, stats, help")


if __name__ == "__main__":
    main()


path: executor.py
content: #!/usr/bin/env python3
"""
Smart Router Executor - Sub-Agent Delegation Engine

Implements the execution layer that actually delegates tasks to different models
via OpenClaw's sessions_spawn mechanism.

Execution Flow:
1. Router analyzes message → recommends model
2. If recommended ≠ current → Executor prepares delegation
3. sessions_spawn called with appropriate model
4. Result stitched back with model attribution header
5. Circuit breaker updated on success/failure

Usage:
    from executor import RouterExecutor
    
    executor = RouterExecutor()
    plan = executor.analyze("What's Bitcoin price?", current_model="opus")
    
    if plan.should_delegate:
        # Call sessions_spawn with plan.spawn_params
        # On completion, use plan.format_result(output)

Author: J.A.R.V.I.S. for Cabo
Version: 1.0.0
"""

import json
import os
import sys
import time
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
import logging

sys.path.insert(0, str(Path(__file__).parent))

from router_gateway import SmartRouter, RoutingDecision, Intent, Complexity
from state_manager import StateManager

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("smart-router.executor")


@dataclass
class ExecutionPlan:
    """Plan for how to execute a request."""
    
    # Routing decision
    decision: RoutingDecision
    current_model: str
    
    # Delegation decision
    should_delegate: bool
    delegation_reason: Optional[str] = None
    
    # Spawn parameters (if delegating)
    spawn_params: Optional[dict] = None
    
    # Fallback chain for retry
    fallback_models: list[str] = field(default_factory=list)
    fallback_index: int = 0
    
    # Tracking
    task_id: Optional[str] = None
    started_at: Optional[float] = None
    
    def get_spawn_call(self) -> str:
        """
        Generate the sessions_spawn tool call for the agent to execute.
        
        Returns a formatted string showing the exact tool call.
        """
        if not self.should_delegate or not self.spawn_params:
            return ""
        
        agent_part = f'agentId="{self.spawn_params["agentId"]}"' if "agentId" in self.spawn_params else f'model="{self.spawn_params.get("model", "default")}"'
        return f"""sessions_spawn(
    task="{self.spawn_params['task'][:100]}...",
    {agent_part},
    label="{self.spawn_params['label']}"
)"""
    
    def format_result(self, output: str, model_used: str) -> str:
        """
        Format the result with model attribution header.
        
        Args:
            output: The raw output from the sub-agent
            model_used: The model that completed the task
            
        Returns:
            Formatted output with header
        """
        model_display = {
            "opus": "Claude Opus",
            "sonnet": "Claude Sonnet", 
            "haiku": "Claude Haiku",
            "gpt5": "GPT-5",
            "gemini-pro": "Gemini Pro",
            "flash": "Gemini Flash",
            "grok2": "Grok 2",
            "grok3": "Grok 3",
        }.get(model_used, model_used)
        
        intent_emoji = {
            Intent.CODE: "💻",
            Intent.ANALYSIS: "🔍",
            Intent.CREATIVE: "🎨",
            Intent.REALTIME: "⚡",
            Intent.GENERAL: "💬",
        }.get(self.decision.intent, "✅")
        
        header = f"{intent_emoji} **{model_display}** completed this task"
        
        if self.decision.special_case:
            case_info = {
                "realtime_required": "(real-time data)",
                "long_context": "(large context)",
                "long_context_extreme": "(250K+ context)",
            }.get(self.decision.special_case, "")
            if case_info:
                header += f" {case_info}"
        
        return f"{header}\n\n---\n\n{output}"
    
    def format_failure(self, error: str, attempted_models: list[str]) -> str:
        """Format error message when all models fail."""
        return (
            f"❌ **Task Delegation Failed**\n\n"
            f"Unable to complete the request after trying: {', '.join(attempted_models)}\n\n"
            f"**Last error:** {error}\n\n"
            f"_Falling back to main session processing._"
        )


@dataclass
class DelegationTracker:
    """Tracks active delegated tasks."""
    task_id: str
    plan: ExecutionPlan
    started_at: float
    status: str = "pending"  # pending, running, completed, failed
    result: Optional[str] = None
    model_used: Optional[str] = None
    attempts: list[str] = field(default_factory=list)


class RouterExecutor:
    """
    Executor that handles actual task delegation via sessions_spawn.
    
    This class prepares execution plans that the agent (J.A.R.V.I.S.) 
    can use to make sessions_spawn calls.
    """
    
    # Models that should NOT trigger delegation (process in main session)
    MAIN_SESSION_MODELS = ["opus"]  # Current default model
    
    # Agent ID mapping for sessions_spawn (configured in openclaw.json)
    # None means use main session (no delegation needed for that tier)
    AGENT_IDS = {
        "opus": None,       # Main session handles this
        "sonnet": None,     # Main session with model override
        "haiku": None,      # Main session with model override  
        "gpt5": "gpt",      # GPT-5 agent
        "gemini-pro": "gemini",  # Gemini Pro agent
        "flash": "flash",   # Gemini Flash agent
        "grok2": "grok",    # Grok agent
        "grok3": "grok",    # Grok agent (fallback)
    }
    
    # Model overrides for Anthropic tiers (handled in main session)
    MODEL_OVERRIDES = {
        "sonnet": "anthropic/claude-sonnet-4-5",
        "haiku": "anthropic/claude-haiku-3-5",
    }
    
    def __init__(self, config_path: Optional[str] = None):
        """Initialize executor with router and state manager."""
        if config_path is None:
            config_path = Path(__file__).parent / "router_config.json"
        
        self.config = {}
        if Path(config_path).exists():
            with open(config_path, 'r') as f:
                self.config = json.load(f)
        
        self.state = StateManager(config=self.config)
        self.router = SmartRouter(
            available_providers=self.config.get("providers", {}).get("available",
                ["anthropic", "openai", "google", "xai"])
        )
        
        # Inject persistent circuit breaker
        self._sync_circuit_breaker()
        
        # Active delegations
        self._delegations: dict[str, DelegationTracker] = {}
        self._task_counter = 0
        
        logger.info("RouterExecutor initialized")
    
    def _sync_circuit_breaker(self) -> None:
        """Sync state manager's circuit breaker with router."""
        def wrapped_can_call(model: str) -> bool:
            return self.state.can_call_model(model)
        self.router.circuit_breaker.can_call = wrapped_can_call
    
    def _normalize_model(self, model_id: str) -> str:
        """Normalize full model ID to alias."""
        # Full model ID to alias mapping
        FULL_TO_ALIAS = {
            "anthropic/claude-opus-4-5": "opus",
            "anthropic/claude-sonnet-4-5": "sonnet",
            "anthropic/claude-haiku-3-5": "haiku",
            "openai/gpt-5": "gpt5",
            "google/gemini-2.5-pro": "gemini-pro",
            "google/gemini-2.5-flash": "flash",
            "xai/grok-2-latest": "grok2",
            "xai/grok-3": "grok3",
        }
        return FULL_TO_ALIAS.get(model_id, model_id)
    
    def analyze(
        self,
        message: str,
        current_model: str = "anthropic/claude-opus-4-5",
        context_tokens: int = 0,
        force_delegate: bool = False
    ) -> ExecutionPlan:
        """
        Analyze a message and create an execution plan.
        
        Args:
            message: User's message
            current_model: Currently active model
            context_tokens: Estimated context size
            force_delegate: Always delegate if True
            
        Returns:
            ExecutionPlan with delegation decision and parameters
        """
        # Normalize current model
        current_alias = self._normalize_model(current_model)
        
        # Get routing decision
        decision = self.router.classify(message, context_tokens)
        
        # Determine if we should delegate
        should_delegate = False
        delegation_reason = None
        
        recommended = decision.selected_model
        
        if force_delegate and recommended != current_alias:
            should_delegate = True
            delegation_reason = "forced_delegation"
        elif recommended != current_alias:
            # Check if this is a significant optimization
            if decision.special_case in ["realtime_required", "long_context", "long_context_extreme"]:
                should_delegate = True
                delegation_reason = decision.special_case
            elif decision.intent == Intent.REALTIME and recommended in ["grok2", "grok3"]:
                should_delegate = True
                delegation_reason = "realtime_requires_grok"
            elif context_tokens > 128000 and recommended in ["gemini-pro", "flash"]:
                should_delegate = True
                delegation_reason = "long_context_requires_gemini"
            elif decision.complexity == Complexity.SIMPLE and recommended in ["haiku", "flash"]:
                # Cost optimization - delegate simple tasks to cheaper models
                should_delegate = True
                delegation_reason = "cost_optimization"
        
        # Build spawn parameters if delegating
        spawn_params = None
        if should_delegate:
            self._task_counter += 1
            task_id = f"router-{self._task_counter}-{int(time.time())}"
            
            agent_id = self.AGENT_IDS.get(recommended)
            
            spawn_params = {
                "task": message,
                "label": f"router-{decision.intent.name.lower()}-{recommended}",
            }
            
            if agent_id:
                # Delegate to configured agent
                spawn_params["agentId"] = agent_id
            elif recommended in self.MODEL_OVERRIDES:
                # Use main agent with model override
                spawn_params["model"] = self.MODEL_OVERRIDES[recommended]
        else:
            task_id = None
        
        # Build fallback chain
        fallback_models = [
            m for m in decision.fallback_chain
            if m != recommended and self.state.can_call_model(m)
        ]
        
        plan = ExecutionPlan(
            decision=decision,
            current_model=current_alias,
            should_delegate=should_delegate,
            delegation_reason=delegation_reason,
            spawn_params=spawn_params,
            fallback_models=fallback_models,
            task_id=task_id,
            started_at=time.time() if should_delegate else None
        )
        
        # Log the decision
        self.state.log_routing_decision(
            intent=decision.intent.name,
            complexity=decision.complexity.name,
            model_selected=recommended,
            model_used=current_alias if not should_delegate else recommended,
            fallback_triggered=False,
            reason=delegation_reason or "no_delegation",
            context_tokens=context_tokens,
            latency_ms=0,
            session_id="executor"
        )
        
        if should_delegate:
            logger.info(f"DELEGATE: {decision.intent.name} -> {recommended} ({delegation_reason})")
        else:
            logger.info(f"MAIN SESSION: {decision.intent.name} -> {current_alias}")
        
        return plan
    
    def record_success(self, plan: ExecutionPlan, model: str) -> None:
        """Record successful delegation - update circuit breaker."""
        self.state.record_circuit_success(model)
        self.state.save()
        logger.info(f"SUCCESS: {model}")
    
    def record_failure(self, plan: ExecutionPlan, model: str, error: str) -> Optional[ExecutionPlan]:
        """
        Record failed delegation and get retry plan.
        
        Args:
            plan: Original execution plan
            model: Model that failed
            error: Error message
            
        Returns:
            New ExecutionPlan for retry, or None if no fallbacks
        """
        self.state.record_circuit_failure(model)
        self.state.save()
        logger.warning(f"FAILURE: {model} - {error}")
        
        # Check for fallback
        if plan.fallback_index < len(plan.fallback_models):
            next_model = plan.fallback_models[plan.fallback_index]
            plan.fallback_index += 1
            
            if self.state.can_call_model(next_model):
                # Update spawn params for retry
                plan.spawn_params = {
                    "task": plan.spawn_params["task"],
                    "model": self.MODEL_IDS.get(next_model, next_model),
                    "label": f"router-fallback-{next_model}",
                }
                logger.info(f"RETRY: Falling back to {next_model}")
                return plan
        
        return None
    
    def get_delegation_status(self) -> dict[str, Any]:
        """Get status of all tracked delegations."""
        return {
            "active": len([d for d in self._delegations.values() if d.status == "running"]),
            "completed": len([d for d in self._delegations.values() if d.status == "completed"]),
            "failed": len([d for d in self._delegations.values() if d.status == "failed"]),
            "total": len(self._delegations),
        }


class ExecutorAgent:
    """
    High-level interface for the agent to use.
    
    This class provides simple methods that return formatted strings
    for the agent to include in tool calls or responses.
    """
    
    def __init__(self):
        self.executor = RouterExecutor(
            config_path=Path(__file__).parent / "router_config.json"
        )
    
    def should_delegate(
        self,
        message: str,
        context_tokens: int = 0
    ) -> tuple[bool, Optional[dict], str]:
        """
        Check if a message should be delegated.
        
        Returns:
            Tuple of (should_delegate, spawn_params, reason)
        """
        plan = self.executor.analyze(message, context_tokens=context_tokens)
        
        if plan.should_delegate:
            return True, plan.spawn_params, plan.delegation_reason or ""
        
        return False, None, "process_in_main_session"
    
    def format_delegation_header(
        self,
        model: str,
        intent: str,
        special_case: Optional[str] = None
    ) -> str:
        """Format the header for a delegated response."""
        model_display = {
            "grok2": "Grok 2",
            "grok3": "Grok 3",
            "gemini-pro": "Gemini Pro",
            "flash": "Gemini Flash",
            "gpt5": "GPT-5",
            "sonnet": "Claude Sonnet",
            "haiku": "Claude Haiku",
        }.get(model, model)
        
        intent_emoji = {
            "REALTIME": "⚡",
            "ANALYSIS": "🔍",
            "CODE": "💻",
            "CREATIVE": "🎨",
            "GENERAL": "💬",
        }.get(intent, "✅")
        
        header = f"{intent_emoji} **{model_display}** completed this task"
        
        if special_case:
            case_info = {
                "realtime_required": "(real-time data)",
                "realtime_requires_grok": "(real-time data)",
                "long_context": "(large context)",
                "long_context_extreme": "(250K+ context)",
                "long_context_requires_gemini": "(large context)",
                "cost_optimization": "(optimized)",
            }.get(special_case, "")
            if case_info:
                header += f" {case_info}"
        
        return header


# =============================================================================
# CLI
# =============================================================================

def main():
    import argparse
    
    parser = argparse.ArgumentParser(description="Smart Router Executor")
    parser.add_argument("--analyze", type=str, help="Analyze a message")
    parser.add_argument("--context-tokens", type=int, default=0)
    parser.add_argument("--force", action="store_true", help="Force delegation")
    parser.add_argument("--json", action="store_true")
    
    args = parser.parse_args()
    
    executor = RouterExecutor()
    
    if args.analyze:
        plan = executor.analyze(
            args.analyze,
            context_tokens=args.context_tokens,
            force_delegate=args.force
        )
        
        if args.json:
            print(json.dumps({
                "should_delegate": plan.should_delegate,
                "delegation_reason": plan.delegation_reason,
                "recommended_model": plan.decision.selected_model,
                "intent": plan.decision.intent.name,
                "complexity": plan.decision.complexity.name,
                "spawn_params": plan.spawn_params,
                "fallbacks": plan.fallback_models,
            }, indent=2))
        else:
            print(f"Should delegate: {plan.should_delegate}")
            if plan.should_delegate:
                print(f"Reason: {plan.delegation_reason}")
                print(f"Model: {plan.decision.selected_model}")
                print(f"\nSpawn call:\n{plan.get_spawn_call()}")
            else:
                print(f"Process in main session ({plan.current_model})")
        
        executor.state.save()
        return
    
    print("Usage: python executor.py --analyze 'message' [--context-tokens N] [--force]")


if __name__ == "__main__":
    main()


path: expert_matrix.json
content: {
  "version": "1.1.0",
  "updated": "2026-02-03",
  "benchmarks": "Feb 2026 Industry Data (MLOC Analysis)",
  
  "models": {
    "opus": {
      "display_name": "Claude Opus 4.5",
      "provider": "anthropic",
      "model_id": "anthropic/claude-opus-4-5",
      "agent_id": null,
      "cost_tier": 4,
      "latency_tier": 3,
      
      "expert_domains": {
        "SOFTWARE_ARCH": 95,
        "SECURITY_VERIFICATION": 94,
        "CODE_REVIEW": 92,
        "MULTI_FILE_REFACTOR": 90,
        "DEBUGGING": 88,
        "DOCUMENTATION": 85,
        "LOGICAL_PRECISION": 82,
        "CONCURRENCY": 70,
        "CREATIVE": 78,
        "REALTIME_SIGNAL": 0,
        "SYSTEM_ROUTINE": 40
      },
      
      "benchmarks": {
        "SWE_bench": 80.9,
        "control_flow_errors_per_MLOC": 45,
        "concurrency_issues_per_MLOC": 120,
        "security_constraint_verification": 96.2,
        "prompt_injection_robustness": 95.3
      },
      
      "blind_spots": [
        "high_cost",
        "resource_leaks_large_generation",
        "concurrency_weakness"
      ],
      
      "mandatory_for": [
        "terminal_shell",
        "security_guardrail",
        "security_verification",
        "multi_file_refactor"
      ],
      
      "specialist_note": "#1 for security constraints and verification. Avoid for high-concurrency designs."
    },
    
    "gpt5": {
      "display_name": "GPT-5.2 (High)",
      "provider": "openai",
      "model_id": "openai/gpt-5",
      "agent_id": "gpt",
      "cost_tier": 4,
      "latency_tier": 3,
      
      "expert_domains": {
        "LOGICAL_PRECISION": 98,
        "MATH": 100,
        "CONTROL_FLOW": 96,
        "RISKY_REFACTOR": 94,
        "FORMAL_PROOFS": 92,
        "SCIENTIFIC": 90,
        "MEDICAL": 94,
        "FINANCIAL_MATH": 96,
        "SOFTWARE_ARCH": 78,
        "CONCURRENCY": 65,
        "CREATIVE": 82,
        "REALTIME_SIGNAL": 0,
        "SYSTEM_ROUTINE": 45
      },
      
      "benchmarks": {
        "control_flow_errors_per_MLOC": 22,
        "AIME_math": 100.0,
        "concurrency_issues_per_MLOC": 414,
        "HealthBench_error_rate": 1.6,
        "formal_logic_accuracy": 98.7
      },
      
      "blind_spots": [
        "slower_latency",
        "high_concurrency_issues",
        "thinking_mode_delay"
      ],
      
      "mandatory_for": [
        "logic_precision",
        "math_critical",
        "risky_refactor",
        "medical_health",
        "financial_math",
        "formal_proofs"
      ],
      
      "specialist_note": "#1 for logical precision (22 errors/MLOC). 6x more concurrency issues than Gemini."
    },
    
    "gemini-pro": {
      "display_name": "Gemini 3 Pro",
      "provider": "google",
      "model_id": "google/gemini-2.5-pro",
      "agent_id": "gemini",
      "cost_tier": 3,
      "latency_tier": 2,
      
      "expert_domains": {
        "MASSIVE_SYNTHESIS": 98,
        "CONCURRENCY": 95,
        "DOCUMENT_SYNTHESIS": 95,
        "RESEARCH": 90,
        "SUMMARIZATION": 92,
        "DATA_ANALYSIS": 88,
        "PARALLEL_SYSTEMS": 92,
        "SOFTWARE_ARCH": 75,
        "LOGICAL_PRECISION": 70,
        "CONTROL_FLOW": 60,
        "REALTIME_SIGNAL": 0,
        "SYSTEM_ROUTINE": 50
      },
      
      "benchmarks": {
        "context_window": 1000000,
        "concurrency_issues_per_MLOC": 69,
        "control_flow_errors_per_MLOC": 200,
        "long_context_coherence": 92.0,
        "synthesis_accuracy_500k": 94.0
      },
      
      "blind_spots": [
        "control_flow_mistakes",
        "logical_precision_weakness",
        "hallucination_extreme_context"
      ],
      
      "mandatory_for": [
        "context_over_150k",
        "high_concurrency",
        "document_heavy",
        "parallel_systems"
      ],
      
      "context_override_threshold": 150000,
      "specialist_note": "#1 for concurrency (69 issues/MLOC) and massive context. Highest control flow errors (200/MLOC)."
    },
    
    "grok3": {
      "display_name": "Grok-3",
      "provider": "xai",
      "model_id": "xai/grok-3",
      "agent_id": "grok",
      "cost_tier": 2,
      "latency_tier": 2,
      
      "expert_domains": {
        "REALTIME_SIGNAL": 98,
        "CURRENT_EVENTS": 95,
        "SOCIAL_TRENDS": 94,
        "CULTURE_ANALYSIS": 92,
        "NEWS": 90,
        "MARKET_DATA": 88,
        "SOFTWARE_ARCH": 65,
        "LOGICAL_PRECISION": 60,
        "SCIENTIFIC": 55,
        "CONCURRENCY": 60,
        "SYSTEM_ROUTINE": 55
      },
      
      "benchmarks": {
        "realtime_accuracy": 94.0,
        "twitter_integration": "native",
        "news_freshness_minutes": 5,
        "trend_detection_accuracy": 91.0
      },
      
      "blind_spots": [
        "formal_academic",
        "rigorous_proofs",
        "complex_architecture"
      ],
      
      "mandatory_for": [
        "realtime_required",
        "current_events",
        "social_media_analysis",
        "trend_analysis"
      ],
      
      "specialist_note": "#1 for real-time signals and cultural trends. Less rigorous for formal logic."
    },
    
    "flash": {
      "display_name": "Flash 2.5",
      "provider": "google",
      "model_id": "google/gemini-2.5-flash",
      "agent_id": "flash",
      "cost_tier": 1,
      "latency_tier": 1,
      
      "expert_domains": {
        "SYSTEM_ROUTINE": 95,
        "SIMPLE_QA": 92,
        "FORMATTING": 90,
        "TRANSLATION": 88,
        "SUMMARIZATION": 82,
        "SOFTWARE_ARCH": 50,
        "LOGICAL_PRECISION": 45,
        "CONCURRENCY": 40,
        "REALTIME_SIGNAL": 0
      },
      
      "benchmarks": {
        "latency_ms": 200,
        "throughput_tokens_sec": 1500,
        "cost_per_million_input": 0.075,
        "performance_cost_ratio": 98.0
      },
      
      "blind_spots": [
        "multi_step_reasoning",
        "complex_architecture",
        "nuanced_analysis",
        "control_flow_complexity"
      ],
      
      "mandatory_for": [],
      
      "ideal_for": [
        "high_volume",
        "simple_tasks",
        "cost_optimization",
        "latency_critical"
      ],
      
      "specialist_note": "Best performance/cost ratio. Avoid for multi-step complex architecture."
    }
  },
  
  "domain_keywords": {
    "SOFTWARE_ARCH": ["architecture", "design", "system", "refactor", "structure", "module", "component", "api design", "backend", "frontend", "microservice"],
    "SECURITY_VERIFICATION": ["security", "vulnerability", "audit", "penetration", "exploit", "cve", "authentication", "authorization", "encryption"],
    "LOGICAL_PRECISION": ["logic", "prove", "verify", "correct", "precise", "exact", "theorem", "deduce", "risky refactor", "critical fix"],
    "CONTROL_FLOW": ["if", "else", "switch", "loop", "branch", "condition", "state machine", "flow"],
    "CONCURRENCY": ["concurrent", "parallel", "async", "thread", "mutex", "lock", "race condition", "deadlock", "goroutine", "channel", "actor"],
    "MASSIVE_SYNTHESIS": ["document", "file", "log", "transcript", "book", "paper", "analyze this", "review this", "300k", "large"],
    "MATH": ["calculate", "equation", "integral", "derivative", "statistics", "probability", "algebra", "aime", "olympiad"],
    "MEDICAL": ["medical", "health", "diagnosis", "symptom", "treatment", "drug", "disease", "clinical", "patient"],
    "FINANCIAL_MATH": ["compound interest", "amortization", "npv", "irr", "portfolio", "risk calculation", "derivatives pricing"],
    "REALTIME_SIGNAL": ["current", "today", "now", "latest", "price", "news", "trending", "live", "real-time", "twitter", "x.com"],
    "SYSTEM_ROUTINE": ["format", "convert", "translate", "list", "simple", "quick", "brief"],
    "RISKY_REFACTOR": ["risky", "dangerous", "breaking change", "migration", "critical", "production fix"],
    "PARALLEL_SYSTEMS": ["distributed", "kubernetes", "docker", "cluster", "load balancer", "scaling", "horizontal"]
  },
  
  "risk_domains": {
    "medical_health": {
      "keywords": ["medical", "health", "diagnosis", "treatment", "drug", "symptom", "clinical"],
      "mandatory_model": "gpt5",
      "fallback": "opus",
      "reason": "GPT-5.2 has 1.6% HealthBench error rate (lowest)"
    },
    "financial_math": {
      "keywords": ["compound interest", "amortization", "portfolio math", "risk calculation", "derivatives"],
      "mandatory_model": "gpt5",
      "fallback": "opus",
      "reason": "GPT-5.2 100% AIME math accuracy"
    },
    "logic_precision": {
      "keywords": ["prove", "verify correctness", "formal proof", "theorem", "logical deduction", "risky refactor"],
      "mandatory_model": "gpt5",
      "fallback": "opus",
      "reason": "GPT-5.2 has lowest control flow errors (22/MLOC)"
    },
    "terminal_shell": {
      "keywords": ["terminal", "shell", "bash", "sudo", "rm ", "chmod", "execute", "run command"],
      "mandatory_model": "opus",
      "fallback": "gpt5",
      "reason": "Opus 95.3% prompt-injection robustness"
    },
    "security_guardrail": {
      "keywords": ["vulnerability", "exploit", "security audit", "penetration test", "cve", "guardrail"],
      "mandatory_model": "opus",
      "fallback": "gpt5",
      "reason": "Opus 96.2% security constraint verification"
    },
    "high_concurrency": {
      "keywords": ["concurrent", "parallel", "async", "thread", "mutex", "race condition", "goroutine", "high-concurrency"],
      "mandatory_model": "gemini-pro",
      "fallback": "opus",
      "reason": "Gemini has 69 concurrency issues/MLOC (GPT-5.2 has 414)"
    }
  },
  
  "routing_rules": {
    "context_override_threshold": 150000,
    "confidence_threshold": 75,
    "hitl_required_below": 75,
    "max_domain_scores": 3,
    "control_flow_warning_threshold": 100
  },
  
  "hitl_templates": {
    "ambiguous_intent": "Sir, intent is ambiguous (confidence: {confidence}%). High risk of control flow errors. Recommend {model_a} but {model_b} is stronger in {domain}. Proceed?",
    "concurrency_conflict": "Sir, this involves both concurrency ({gemini_score}%) and logical precision ({gpt_score}%). Gemini has 6x fewer concurrency bugs, but GPT-5 has 9x better control flow. Recommend: {recommendation}. Proceed?"
  }
}


path: log_decision.py
content: #!/usr/bin/env python3
"""
Quick routing decision logger.

Usage:
    python log_decision.py "message text" [context_tokens]
"""

import sys
import os
sys.path.insert(0, os.path.dirname(__file__))

from router_hook import RouterHook

def main():
    if len(sys.argv) < 2:
        print("Usage: python log_decision.py 'message' [context_tokens]")
        sys.exit(1)
    
    message = sys.argv[1]
    context_tokens = int(sys.argv[2]) if len(sys.argv) > 2 else 0
    
    hook = RouterHook(
        config_path=os.path.join(os.path.dirname(__file__), "router_config.json"),
        mode="dry_run",
        session_id="live_test"
    )
    
    decision = hook.intercept(
        message,
        context_tokens=context_tokens,
        current_model="anthropic/claude-opus-4-5"
    )
    
    print(hook.format_decision_display(decision, "anthropic/claude-opus-4-5"))
    hook.save_state()

if __name__ == "__main__":
    main()


path: references/models.md
content: # Model Capabilities & Pricing Reference

Detailed specifications and pricing for supported models.

## Pricing Overview (per 1M tokens, Feb 2026)

| Model | Input | Output | Cache Read | Cost Tier |
|-------|-------|--------|------------|-----------|
| claude-opus-4-5 | $15.00 | $75.00 | $1.50 | $$$$ |
| claude-sonnet-4-5 | $3.00 | $15.00 | $0.30 | $$ |
| claude-haiku-4-5 | $0.25 | $1.25 | $0.025 | $ |
| gpt-5 (via Codex) | Subscription | Subscription | - | $$ |
| gemini-2.5-pro | $1.25 | $5.00 | - | $$$ |
| gemini-2.5-flash | $0.075 | $0.30 | - | $ |
| grok-2 | $0.20 | $0.50 | - | $$ |
| grok-3 | $0.30 | $0.75 | - | $$$ |

**Note**: Subscription-based models (GPT via ChatGPT Plus) have no per-token cost.

---

## Claude Opus 4.5

**Provider**: Anthropic  
**Model ID**: `anthropic/claude-opus-4-5`  
**Context Window**: 200K tokens  
**Input Types**: Text, Images  
**Cost Tier**: $$$$ (Premium)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★★ |
| Coding | ★★★★★ |
| Creativity | ★★★★★ |
| Instruction Following | ★★★★★ |
| Speed | ★★★☆☆ |

### Best For
- Complex multi-step reasoning
- Production code writing
- Critical business tasks
- Nuanced analysis
- Creative writing

### When NOT to Use
- Simple factual queries (use Flash)
- Real-time data needs (use Grok)
- Documents >200K tokens (use Gemini)

---

## Claude Sonnet 4.5

**Provider**: Anthropic  
**Model ID**: `anthropic/claude-sonnet-4-5`  
**Context Window**: 200K tokens  
**Input Types**: Text, Images  
**Cost Tier**: $$ (Medium)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★☆ |
| Coding | ★★★★☆ |
| Creativity | ★★★★☆ |
| Instruction Following | ★★★★☆ |
| Speed | ★★★★☆ |

### Best For
- Medium complexity tasks
- Good balance of quality/cost
- Code assistance
- General analysis

---

## Claude Haiku 4.5

**Provider**: Anthropic  
**Model ID**: `anthropic/claude-haiku-4-5`  
**Context Window**: 200K tokens  
**Input Types**: Text, Images  
**Cost Tier**: $ (Budget)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★☆☆ |
| Coding | ★★★☆☆ |
| Creativity | ★★★☆☆ |
| Instruction Following | ★★★★☆ |
| Speed | ★★★★★ |

### Best For
- Simple queries
- High-volume processing
- Quick classifications
- Cost-sensitive tasks

---

## GPT-5 (via ChatGPT Plus)

**Provider**: OpenAI (via openai-codex OAuth)  
**Model ID**: `openai-codex/gpt-5`  
**Context Window**: 128K tokens  
**Input Types**: Text, Images  
**Cost Tier**: $$ (Subscription-based)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★☆ |
| Coding | ★★★★☆ |
| Creativity | ★★★★☆ |
| Instruction Following | ★★★★☆ |
| Speed | ★★★★☆ |

### Best For
- General tasks (no per-token cost)
- Alternative perspective
- Fallback from Claude
- Conversation

### Advantage
Covered by ChatGPT Plus subscription—no additional per-token charges.

> ⚠️ **Pricing note:** The "$0" cost tier assumes a ChatGPT Plus subscription ($20/mo). If using OpenAI API directly, per-token pricing applies and the cost tier should be adjusted to $$$ (premium). Check current rates at [openai.com/pricing](https://openai.com/pricing) and update the `COST_TIERS` mapping accordingly:
> ```python
> # If using direct API instead of ChatGPT Plus:
> COST_TIERS["gpt-5"] = "$$$"  # Not "$$"
> ```

---

## Gemini 2.5 Pro

**Provider**: Google  
**Model ID**: `google/gemini-2.5-pro`  
**Context Window**: 1,000,000+ tokens  
**Input Types**: Text, Images, Video, Audio  
**Cost Tier**: $$$ (Premium for long context)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★☆ |
| Coding | ★★★★☆ |
| Long Context | ★★★★★ |
| Multimodal | ★★★★★ |
| Speed | ★★★★☆ |

### Best For
- Documents >100K tokens
- Full codebase analysis
- Book-length content
- Video/audio processing
- Multi-file comparisons

### Unique Advantage
1M+ context window—no other model comes close.

---

## Gemini 2.5 Flash

**Provider**: Google  
**Model ID**: `google/gemini-2.5-flash`  
**Context Window**: 1,000,000+ tokens  
**Input Types**: Text, Images  
**Cost Tier**: $ (Budget)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★☆☆ |
| Coding | ★★★☆☆ |
| Long Context | ★★★★★ |
| Speed | ★★★★★ |

### Best For
- Simple queries (cheapest option)
- Quick summaries
- Translations
- High-volume tasks
- Cost optimization

### Cost Advantage
At $0.075/1M input, it's 200x cheaper than Opus for simple tasks.

---

## Grok 2

**Provider**: xAI  
**Model ID**: `xai/grok-2-latest`  
**Context Window**: 128K tokens  
**Input Types**: Text  
**Cost Tier**: $$ (Pay-per-use)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★☆ |
| Real-time Data | ★★★★★ |
| X/Twitter Access | ★★★★★ |
| Current Events | ★★★★★ |
| Speed | ★★★★☆ |

### Best For
- Real-time information
- Current news and events
- X/Twitter analysis
- Live data queries
- "What's happening now?"

### Unique Advantage
Only model with real-time data access and X/Twitter integration.

---

## Grok 3

**Provider**: xAI  
**Model ID**: `xai/grok-3-latest`  
**Context Window**: 128K tokens  
**Input Types**: Text  
**Cost Tier**: $$$ (Premium pay-per-use)

### Capabilities
| Capability | Rating |
|------------|--------|
| Reasoning | ★★★★★ |
| Real-time Data | ★★★★★ |
| Complex Analysis | ★★★★★ |

### Best For
- Complex queries requiring real-time data
- Advanced analysis of current events
- When Grok 2 isn't sufficient

---

## Model Selection Quick Reference

### By Task Type

| Task | First Choice | Fallback |
|------|--------------|----------|
| Simple Q&A | Flash | Haiku |
| Code writing | Opus | Sonnet |
| Code review | Opus | GPT-5 |
| Research | Opus | GPT-5 |
| Real-time | Grok 2 | Grok 3 |
| Long document | Gemini Pro | - |
| Creative | Opus | GPT-5 |
| Translation | Flash | Haiku |
| Summarize | Flash | Sonnet |

### By Budget Priority

| Priority | Model | Why |
|----------|-------|-----|
| Cheapest | Gemini Flash | $0.075/1M in |
| Best value | GPT-5 | Subscription |
| Quality/cost balance | Sonnet | $3/1M in |
| Best quality | Opus | $15/1M in |

### By Context Size

| Context | Model | Max Tokens |
|---------|-------|------------|
| <128K | Any | - |
| 128K-200K | Claude or Gemini | 200K / 1M |
| 200K-500K | Gemini Pro | 1M |
| >500K | Gemini Pro ONLY | 1M+ |

---

## Provider Authentication

Models require these auth profiles:

| Provider | Auth Type | Profile Name |
|----------|-----------|--------------|
| Anthropic | Setup Token | anthropic:default |
| OpenAI | OAuth | openai-codex:default |
| Google | API Key | google:manual |
| xAI | API Key | xai:manual |

Check available providers:
```bash
openclaw models list
```

---

## Updating This Reference

This document should be updated when:
- Pricing changes
- New models released
- Capabilities change
- New providers added

Last verified: February 2026


path: router_config.json
content: {
  "$schema": "./router_config.schema.json",
  "version": "1.0.0",
  "description": "Smart Router configuration for OpenClaw integration",
  
  "routing": {
    "enabled": true,
    "mode": "live",
    "log_decisions": true,
    "show_routing_default": false
  },
  
  "providers": {
    "available": ["anthropic", "openai", "google", "xai"],
    "prefer_order": ["anthropic", "google", "openai", "xai"]
  },
  
  "models": {
    "opus": {
      "id": "anthropic/claude-opus-4-5",
      "enabled": true,
      "context_window": 200000,
      "cost_tier": "$$$$"
    },
    "sonnet": {
      "id": "anthropic/claude-sonnet-4-5",
      "enabled": true,
      "context_window": 200000,
      "cost_tier": "$$$"
    },
    "haiku": {
      "id": "anthropic/claude-haiku-3-5",
      "enabled": true,
      "context_window": 200000,
      "cost_tier": "$"
    },
    "gpt5": {
      "id": "openai/gpt-5",
      "enabled": true,
      "context_window": 128000,
      "cost_tier": "$$"
    },
    "gemini-pro": {
      "id": "google/gemini-2.5-pro",
      "enabled": true,
      "context_window": 1000000,
      "cost_tier": "$$$"
    },
    "flash": {
      "id": "google/gemini-2.5-flash",
      "enabled": true,
      "context_window": 1000000,
      "cost_tier": "$"
    },
    "grok2": {
      "id": "xai/grok-2-latest",
      "enabled": true,
      "context_window": 128000,
      "cost_tier": "$$",
      "capabilities": ["realtime"]
    },
    "grok3": {
      "id": "xai/grok-3",
      "enabled": true,
      "context_window": 128000,
      "cost_tier": "$$$",
      "capabilities": ["realtime"]
    }
  },
  
  "circuit_breaker": {
    "enabled": true,
    "failure_threshold": 3,
    "reset_timeout_ms": 300000,
    "half_open_max_calls": 1,
    "state_file": "~/.openclaw/router-state/circuit-breaker.json"
  },
  
  "rate_limits": {
    "enabled": true,
    "requests_per_minute": 20,
    "requests_per_hour": 200,
    "premium_per_hour": 20,
    "state_file": "~/.openclaw/router-state/rate-limits.json"
  },
  
  "logging": {
    "level": "info",
    "decision_log": "~/.openclaw/logs/router-decisions.log",
    "max_log_size_mb": 10,
    "retention_days": 7
  },
  
  "security": {
    "sanitize_input": true,
    "block_credentials": true,
    "warn_pii": true,
    "trust_tiers": {
      "anthropic": 1,
      "openai": 1,
      "google": 1,
      "xai": 2
    }
  },
  
  "cost_optimization": {
    "enabled": true,
    "simple_max_tier": "$",
    "medium_max_tier": "$$",
    "complex_max_tier": "$$$$",
    "daily_budget_usd": null,
    "alert_threshold_percent": 80
  },
  
  "special_cases": {
    "long_context_threshold": 128000,
    "extreme_context_threshold": 200000,
    "realtime_keywords": ["now", "today", "current", "latest", "price", "bitcoin", "btc", "weather"],
    "vision_enabled": true
  }
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)