Skill: austindixson/friday-router (version 1.6.2)

--- name: friday-router displayName: IntentRouter description: Your AI's Smart Traffic Director—precisely matching OpenClaw tasks to the perfect LLM. Intelligent orchestration with OpenRouter. Security-focused: no gateway auth exposure. version: 1.7.0 --- # IntentRouter **Your AI's Smart Traffic Director: Precisely Matching Your OpenClaw Tasks to the Perfect LLM.** **v1.7.0 — Security-focused release.** COMPLEX tier, absolute paths. Tested and working with OpenClaw TUI delegation. **Removed gateway auth secret exposure and gateway management for improved security.** IntentRouter analyzes your tasks and directs them to the best LLM—MiniMax 2.5 for code, Kimi k2.5 for creative, Grok Fast for research. Eliminate guesswork; route with purpose. **Security improvements in v1.7.0:** Removed gateway auth token/password from router output. Gateway management removed - use [gateway-guard](https://clawhub.ai/skills/gateway-guard) skill separately. FACEPALM integration removed - use [FACEPALM](https://github.com/RuneweaverStudios/FACEPALM) skill separately. **Requirements:** **OpenRouter** — All model IDs use the `openrouter/...` prefix. Configure OpenClaw with an OpenRouter API key so one auth profile covers every tier. **Config access:** This skill reads ONLY its own `config.json` file (located in the skill directory) for model configuration and routing rules. It does NOT read `openclaw.json`, does NOT access gateway tokens/passwords, and does NOT perform any gateway management operations. Router output contains only: `task`, `model`, `sessionTarget`, and optional `label` — no secrets or credentials. **Default / orchestrator model:** Gemini 2.5 Flash (`openrouter/google/gemini-2.5-flash`) — fast, cheap, reliable at tool-calling. The router delegates tasks to tier-specific sub-agents (Kimi for creative, MiniMax 2.5 for code, etc.). ## Orchestrator flow (mandatory for task requests) When you are the **main agent** (Gemini 2.5 Flash) and the user gives you a **task** (code, research, write, create, design, poem, story, fix, build, etc.), you must **delegate** it. You do **not** perform the task yourself; a sub-agent does. **Steps (all three required):** 1. **Run IntentRouter** to get spawn params: ```bash python3 workspace/skills/friday-router/scripts/router.py spawn --json "<user message>" ``` Example output: `{"task":"write a poem about Mai","model":"openrouter/moonshotai/kimi-k2.5","sessionTarget":"isolated"}` 2. **Immediately call `sessions_spawn`** with the router's output. Use the **exact `model`** from the JSON. Example: ``` sessions_spawn(task="write a poem about Mai", model="openrouter/moonshotai/kimi-k2.5", sessionTarget="isolated") ``` Do **not** change the `model` value. If the router said `openrouter/moonshotai/kimi-k2.5`, pass exactly that. 3. **Forward the sub-agent's reply** to the user. That reply IS the task output. Say "Using: Kimi k2.5" (the model that actually ran). Never say "Using: Claude Sonnet 4" or any model that didn't run. **Output hygiene:** If the sub-agent result contains internal text ("A subagent task … completed", "Findings:", "Stats:", "sessionKey", "Summarize this naturally"), strip that block and show only the final user-facing content to the user. **If `sessions_spawn` returns an error** (e.g. `device_token_mismatch`): tell the user delegation failed and suggest checking gateway status or using the `gateway-guard` skill. Do **not** do the task yourself. **Hard-stop rule:** If `sessions_spawn` fails or is skipped, return only the delegation error and next-step fix. Do not write the requested output directly. **No-classify execution rule:** For real user tasks, do not execute via `classify`. `classify` is diagnostics only. Execution must use `spawn --json` -> `sessions_spawn`. **Label gate:** Only print `Using: <model>` after successful spawn. If no successful spawn, do not print a `Using:` label. **Output hygiene:** Never return internal orchestration metadata to the user (no session keys/IDs, transcript paths, runtime/token stats, or internal "summarize this" instructions). Forward only clean user-facing content. **Exception:** Meta-questions ("what model are you?", "how does routing work?") you answer yourself. **Security note:** This skill does NOT expose gateway auth secrets (tokens/passwords) in its output. Gateway management functionality has been removed. Use the separate `gateway-guard` skill if gateway auth management is needed. ## Model Selection (Austin's Prefs) | Use Case | Primary (OpenRouter) | Fallback | |----------|---------------------|----------| | **Default / orchestrator** | Gemini 2.5 Flash | — | | **Fast/cheap** | Gemini 2.5 Flash | Gemini 1.5 Flash, Haiku | | **Reasoning** | GLM-5 | Minimax 2.5 | | **Creative/Frontend** | Kimi k2.5 | — | | **Research** | Grok Fast | — | | **Code/Engineering** | MiniMax 2.5 | Qwen2.5-Coder | | **Quality/Complex** | GLM 4.7 Flash | GLM 4.7, Sonnet 4, GPT-4o | | **Vision/Images** | GPT-4o | — | All model IDs use `openrouter/` prefix (e.g. `openrouter/moonshotai/kimi-k2.5`). ## Usage ### CLI ```bash python scripts/router.py default # Show default model python scripts/router.py classify "fix lint errors" # Classify → tier + model python scripts/router.py spawn --json "write a poem" # JSON for sessions_spawn (no gateway secrets) python scripts/router.py models # List all models ``` **Note:** Gateway auth management is not included. Use `gateway-guard` skill separately if needed. ### sessions_spawn examples **Creative task (poem):** ``` router output: {"task":"write a poem","model":"openrouter/moonshotai/kimi-k2.5","sessionTarget":"isolated"} → sessions_spawn(task="write a poem", model="openrouter/moonshotai/kimi-k2.5", sessionTarget="isolated") ``` **Code task (bug fix):** ``` router output: {"task":"fix the login bug","model":"openrouter/minimax/minimax-m2.5","sessionTarget":"isolated"} → sessions_spawn(task="fix the login bug", model="openrouter/minimax/minimax-m2.5", sessionTarget="isolated") ``` **Research task:** ``` router output: {"task":"research best LLMs","model":"openrouter/x-ai/grok-4.1-fast","sessionTarget":"isolated"} → sessions_spawn(task="research best LLMs", model="openrouter/x-ai/grok-4.1-fast", sessionTarget="isolated") ``` ## Tier Detection - **FAST**: check, get, list, show, status, monitor, fetch, simple - **REASONING**: prove, logic, analyze, derive, math, step by step - **CREATIVE**: creative, write, story, design, UI, UX, frontend, website (website/frontend/landing projects → Kimi k2.5 only; do not use CODE tier) - **RESEARCH**: research, find, search, lookup, web, information - **CODE**: code, function, debug, fix, implement, refactor, test, React, JWT (code/API only; not website builds) - **QUALITY**: complex, architecture, design, system, comprehensive - **VISION**: image, picture, photo, screenshot, visual ## What Changed from Original | Bug | Fix | |-----|-----| | Simple indicators inverted (high match = complex) | Now correctly: high simple keyword match = FAST tier | | Agentic tasks not bumping tier | Multi-step tasks now properly bump to CODE tier | | Vision tasks misclassified | Vision keywords now take priority over other classifications | | Code keywords not detected | Added React, JWT, API, and other common code terms | | Confidence always low | Now varies appropriately based on keyword match strength |

path: _meta.json
content: {
  "schema": "openclaw.skill.v1",
  "name": "friday-router",
  "version": "1.7.0",
  "displayName": "IntentRouter",
  "slogan": "Your AI's Smart Traffic Director: Precisely Matching Your OpenClaw Tasks to the Perfect LLM.",
  "description": "Optimize your OpenClaw agents' performance with IntentRouter, the intelligent LLM orchestration skill. This powerful tool precisely analyzes your tasks and directs them to the best LLM for the job—whether it's MiniMax 2.5 for code, Kimi k2.5 for creative prose, or Grok Fast for deep dives into web research. IntentRouter eliminates guesswork, ensuring your agents achieve faster, more accurate, and more cost-effective results by aligning every task with the right AI expert. Upgrade your OpenClaw ecosystem into a high-efficiency powerhouse, allowing IntentRouter to manage the intelligent dispatch of your models. Stop wasting resources and start routing with purpose!",
  "author": "Austin",
  "tags": ["router", "model-selection", "openrouter", "cost-optimization"],
  "platform": "openclaw",
  "requires": {
    "config": {
      "paths": ["config.json"],
      "description": "Reads only its own config.json file (in skill directory) for model configuration and routing rules. Does NOT read openclaw.json or access gateway secrets."
    },
    "env": {
      "optional": ["OPENCLAW_HOME"],
      "description": "OPENCLAW_HOME is optional and not used by this skill. Router only reads its own config.json."
    }
  },
  "security": {
    "noGatewaySecrets": true,
    "noProcessManagement": true,
    "noConfigFileAccess": "Only reads its own config.json, does not access openclaw.json or gateway credentials"
  }
}


path: config.json
content: {
  "name": "OpenRouterRouter | Codename: Centipede",
  "description": "Your AI's Smart Traffic Director: Precisely Matching Your OpenClaw Tasks to the Perfect LLM. Intelligent LLM orchestration—MiniMax 2.5 for code, Kimi k2.5 for creative, Grok Fast for research. Route with purpose.",
  "version": "1.5.0",
  "author": "Austin",
  "default_model": "openrouter/google/gemini-2.5-flash",
  "models": [
    {
      "id": "openrouter/google/gemini-2.5-flash-lite",
      "alias": "Gemini-Flash",
      "tier": "FAST",
      "provider": "openrouter",
      "input_cost_per_m": 0.075,
      "output_cost_per_m": 0.30,
      "context_window": 1000000,
      "capabilities": ["text", "code", "vision"],
      "use_for": ["fast responses", "monitoring", "status checks", "summaries"]
    },
    {
      "id": "openrouter/google/gemini-2.5-flash",
      "alias": "Gemini-2.5-Flash",
      "tier": "FAST",
      "provider": "openrouter",
      "input_cost_per_m": 0.30,
      "output_cost_per_m": 2.50,
      "context_window": 1048576,
      "capabilities": ["text", "code", "vision"],
      "use_for": ["simple tasks", "fast responses", "monitoring"]
    },
    {
      "id": "openrouter/anthropic/claude-3-haiku",
      "alias": "Haiku",
      "tier": "FAST",
      "provider": "openrouter",
      "input_cost_per_m": 0.25,
      "output_cost_per_m": 1.25,
      "context_window": 200000,
      "capabilities": ["text", "code"],
      "use_for": ["fast responses", "fallback for flash"]
    },
    {
      "id": "openrouter/z-ai/glm-5",
      "alias": "GLM-5",
      "tier": "REASONING",
      "provider": "openrouter",
      "input_cost_per_m": 0.10,
      "output_cost_per_m": 0.10,
      "context_window": 128000,
      "capabilities": ["text", "reasoning", "chain-of-thought"],
      "use_for": ["reasoning", "math", "logic", "analysis"]
    },
    {
      "id": "openrouter/minimax/minimax-m2.5",
      "alias": "Minimax-2.5",
      "tier": "CODE",
      "provider": "openrouter",
      "input_cost_per_m": 0.10,
      "output_cost_per_m": 0.10,
      "context_window": 128000,
      "capabilities": ["text", "reasoning", "code"],
      "use_for": ["code generation", "debugging", "refactoring", "reasoning fallback"]
    },
    {
      "id": "openrouter/moonshotai/kimi-k2.5",
      "alias": "Kimi-k2.5",
      "tier": "CREATIVE",
      "provider": "openrouter",
      "input_cost_per_m": 0.20,
      "output_cost_per_m": 0.20,
      "context_window": 128000,
      "capabilities": ["text", "creative", "writing"],
      "use_for": ["creative writing", "frontend design", "UI/UX", "stories"]
    },
    {
      "id": "openrouter/x-ai/grok-4.1-fast",
      "alias": "Grok-Fast",
      "tier": "RESEARCH",
      "provider": "openrouter",
      "input_cost_per_m": 0.10,
      "output_cost_per_m": 0.10,
      "context_window": 131072,
      "capabilities": ["text", "research", "web search"],
      "use_for": ["research", "web lookups", "fact-finding"]
    },
    {
      "id": "openrouter/qwen/qwen3-coder-flash",
      "alias": "Qwen-Coder",
      "tier": "CODE",
      "provider": "openrouter",
      "input_cost_per_m": 0.10,
      "output_cost_per_m": 0.10,
      "context_window": 32768,
      "capabilities": ["text", "code"],
      "use_for": ["code fallback"]
    },
    {
      "id": "openrouter/z-ai/glm-4.7-flash",
      "alias": "GLM-4.7-Flash",
      "tier": "QUALITY",
      "provider": "openrouter",
      "input_cost_per_m": 0.06,
      "output_cost_per_m": 0.40,
      "context_window": 128000,
      "capabilities": ["text", "code", "reasoning"],
      "use_for": ["default capable model", "orchestrator", "complex code", "architecture"]
    },
    {
      "id": "openrouter/z-ai/glm-4.7",
      "alias": "GLM-4.7",
      "tier": "QUALITY",
      "provider": "openrouter",
      "input_cost_per_m": 0.40,
      "output_cost_per_m": 1.50,
      "context_window": 202752,
      "capabilities": ["text", "code", "reasoning"],
      "use_for": ["complex code", "architecture", "fallback"]
    },
    {
      "id": "openrouter/anthropic/claude-sonnet-4",
      "alias": "Claude-Sonnet-4",
      "tier": "QUALITY",
      "provider": "openrouter",
      "input_cost_per_m": 3.00,
      "output_cost_per_m": 15.00,
      "context_window": 200000,
      "capabilities": ["text", "code", "reasoning", "vision"],
      "use_for": ["complex code", "architecture", "fallback"]
    },
    {
      "id": "openrouter/anthropic/claude-3.5-sonnet",
      "alias": "Sonnet",
      "tier": "QUALITY",
      "provider": "openrouter",
      "input_cost_per_m": 3.00,
      "output_cost_per_m": 15.00,
      "context_window": 200000,
      "capabilities": ["text", "code", "reasoning", "vision"],
      "use_for": ["complex code", "architecture", "high-quality output"]
    },
    {
      "id": "openrouter/openai/gpt-4o",
      "alias": "GPT-4o",
      "tier": "VISION",
      "provider": "openrouter",
      "input_cost_per_m": 2.50,
      "output_cost_per_m": 10.00,
      "context_window": 128000,
      "capabilities": ["text", "code", "vision"],
      "use_for": ["image analysis", "vision tasks"]
    }
  ],
  "routing_rules": {
    "FAST": {
      "primary": "openrouter/google/gemini-2.5-flash",
      "fallback": ["openrouter/google/gemini-2.5-flash-lite", "openrouter/anthropic/claude-3-haiku"],
      "keywords": ["check", "get", "list", "show", "status", "monitor", "fetch", "simple", "quick"]
    },
    "REASONING": {
      "primary": "openrouter/z-ai/glm-5",
      "fallback": ["openrouter/minimax/minimax-m2.5"],
      "keywords": ["reason", "prove", "logic", "analyze", "derive", "math", "think", "step by step"]
    },
    "CREATIVE": {
      "primary": "openrouter/moonshotai/kimi-k2.5",
      "fallback": [],
      "keywords": ["creative", "write", "story", "design", "UI", "UX", "frontend", "website", "style", "artistic"]
    },
    "RESEARCH": {
      "primary": "openrouter/x-ai/grok-4.1-fast",
      "fallback": [],
      "keywords": ["research", "find", "search", "lookup", "web", "information", "fact", "review"]
    },
    "CODE": {
      "primary": "openrouter/minimax/minimax-m2.5",
      "fallback": ["openrouter/qwen/qwen3-coder-flash"],
      "keywords": ["code", "function", "class", "debug", "fix", "implement", "refactor", "lint", "test"]
    },
    "QUALITY": {
      "primary": "openrouter/z-ai/glm-4.7-flash",
      "fallback": ["openrouter/z-ai/glm-4.7", "openrouter/anthropic/claude-sonnet-4", "openrouter/anthropic/claude-3.5-sonnet", "openrouter/openai/gpt-4o"],
      "keywords": ["complex", "architecture", "design", "system", "comprehensive", "thorough"]
    },
    "COMPLEX": {
      "primary": "openrouter/z-ai/glm-4.7-flash",
      "fallback": ["openrouter/z-ai/glm-4.7", "openrouter/anthropic/claude-sonnet-4", "openrouter/anthropic/claude-3.5-sonnet", "openrouter/openai/gpt-4o"],
      "keywords": ["complex", "architecture", "design", "system"]
    },
    "VISION": {
      "primary": "openrouter/openai/gpt-4o",
      "fallback": [],
      "keywords": ["image", "picture", "photo", "screenshot", "visual", "analyze image"]
    }
  }
}


path: REVIEW-name-conformity.md
content: # User-facing name conformity review

**Standard:** **OpenRouterRouter | Codename: Centipede** (and in prose: "OpenRouterRouter (codename: Centipede)").

**Reviewed:** Critical stable repo content (v1.5.0).

---

## Conformant

| Location | User-facing text |
|----------|------------------|
| **README.md** | Title: `# OpenRouterRouter \| Codename: Centipede`; intro "OpenRouterRouter (codename: Centipede)"; step "Run OpenRouterRouter". |
| **SKILL.md** | `displayName: OpenRouterRouter \| Codename: Centipede`; same title and intro. |
| **_meta.json** | `displayName`: "OpenRouterRouter \| Codename: Centipede"; description uses "OpenRouterRouter (codename: Centipede)" and "OpenRouterRouter" throughout. |
| **config.json** | `name`: "OpenRouterRouter \| Codename: Centipede". |
| **scripts/router.py** | Module docstring and CLI banner: "OpenRouterRouter \| Codename: Centipede"; version print "OpenRouterRouter \| Codename: Centipede v1.5.0". |
| **scripts/gateway_watchdog.py** | Docstring: "OpenRouterRouter (codename: Centipede) / friday-router skill"; uses gateway-guard skill for guard script. |

---

## Intentional (not user-facing)

- **Package/skill name:** `friday-router` in `_meta.json` `name`, SKILL frontmatter `name`, and `clawhub install friday-router` — kept for backward compatibility; not a user-facing product name.
- **Paths:** `workspace/skills/friday-router/` in SKILL.md — install path, not product name.
- **Code:** Class name `FridayRouter` in README "In-code usage" and in `router.py` — internal API; no change required for user-facing conformity.

---

## Fix applied during review

- **scripts/router.py** — Module docstring said "Version 1.4.0"; updated to "Version 1.5.0" to match repo version.

---

## Summary

User-facing surfaces consistently use **OpenRouterRouter | Codename: Centipede** (or "OpenRouterRouter (codename: Centipede)" in prose). No leftover "IntentRouter" or "Friday Router" in user-facing copy. One version string in the router docstring was corrected to 1.5.0.


path: README.md
content: # IntentRouter

**Your AI's Smart Traffic Director: Precisely Matching Your OpenClaw Tasks to the Perfect LLM.**

**v1.7.0 — This version is tested and working.** COMPLEX tier, absolute paths for TUI delegation. **Security-focused release:** Removed gateway auth secret exposure and gateway management functionality for improved security rating.

IntentRouter is the intelligent LLM orchestration skill for OpenClaw. It precisely analyzes your tasks and directs them to the best LLM for the job—MiniMax 2.5 for code, Kimi k2.5 for creative prose, Grok Fast for web research. Route with purpose; stop wasting resources.

**Security improvements in v1.7.0:** Removed gateway auth token/password exposure from router output. Gateway management functionality has been removed - use the separate [gateway-guard](https://clawhub.ai/skills/gateway-guard) skill if gateway auth management is needed. FACEPALM troubleshooting integration has been removed - use the separate [FACEPALM](https://github.com/RuneweaverStudios/FACEPALM) skill if troubleshooting is needed.

## Requirements

- **OpenRouter** — All model delegation uses OpenRouter (`openrouter/...` prefix). Configure OpenClaw with an OpenRouter API key so one auth profile covers every model.

## Default behavior

**Session default / orchestrator:** Gemini 2.5 Flash (`openrouter/google/gemini-2.5-flash`) — fast, cheap, reliable at tool-calling.

The router delegates tasks to tier-specific sub-agents (Kimi for creative, MiniMax 2.5 for code, etc.) via `sessions_spawn`. Simple tasks (check, status, list) down-route to Gemini 2.5 Flash.

---

## Orchestrator flow (task delegation)

The **main agent (Gemini 2.5 Flash)** does not do user tasks itself. For every user **task** (code, research, write, build, etc.):

1. Run IntentRouter: `python scripts/router.py spawn --json "<user message>"` and parse the JSON.
2. Call **sessions_spawn** with the `task` and `model` from the router output (use the exact `model` value).
3. Forward the sub-agent's result to the user.

**Example:**
```
router: {"task":"write a poem","model":"openrouter/moonshotai/kimi-k2.5","sessionTarget":"isolated"}
→ sessions_spawn(task="write a poem", model="openrouter/moonshotai/kimi-k2.5", sessionTarget="isolated")
→ Forward Kimi k2.5's poem to user. Say "Using: Kimi k2.5".
```

**Exception:** Meta-questions ("what model are you?") you answer yourself.

---

## Quick start

```bash
npm install -g clawhub
clawhub install friday-router

python scripts/router.py default
python scripts/router.py classify "your task description"
```

---

## Features

- **Orchestrator** — Gemini 2.5 Flash delegates to tier-specific sub-agents via `sessions_spawn`
- Fixed scoring bugs from original intelligent-router
- 7 tiers: FAST, REASONING, CREATIVE, RESEARCH, CODE, QUALITY, VISION
- All models via OpenRouter (single API key)
- Config-driven: `config.json` for models and routing rules
- **Security-focused** — No gateway auth secret exposure, no process management

---

## Models

| Tier | Model | Cost/M (in/out) |
|------|-------|-----------------|
| **Default / orchestrator** | Gemini 2.5 Flash | $0.30 / $2.50 |
| FAST | Gemini 2.5 Flash | $0.30 / $2.50 |
| REASONING | GLM-5 | $0.10 / $0.10 |
| CREATIVE | Kimi k2.5 | $0.20 / $0.20 |
| RESEARCH | Grok Fast | $0.10 / $0.10 |
| CODE | MiniMax 2.5 | $0.10 / $0.10 |
| QUALITY | GLM 4.7 Flash | $0.06 / $0.40 |
| VISION | GPT-4o | $2.50 / $10.00 |

**Fallbacks:** FAST → Gemini 1.5 Flash, Haiku; QUALITY → GLM 4.7, Sonnet 4, GPT-4o; CODE → Qwen Coder; REASONING → Minimax 2.5.

---

## CLI usage

```bash
python scripts/router.py default                          # Show default model
python scripts/router.py classify "fix lint errors"        # Classify → tier + model
python scripts/router.py score "build a React auth system" # Detailed scoring
python scripts/router.py cost "design a landing page"      # Cost estimate
python scripts/router.py spawn "research best LLMs"        # Spawn params (human)
python scripts/router.py spawn --json "research best LLMs" # JSON for sessions_spawn (no gateway secrets)
python scripts/router.py models                            # List all models
```

**Note:** Gateway auth management is not included in this skill. Use the separate `gateway-guard` skill if you need gateway auth checking or management.

---

## In-code usage

```python
from scripts.router import FridayRouter

router = FridayRouter()

default = router.get_default_model()
tier = router.classify_task("check server status")        # → "FAST"
result = router.recommend_model("build auth system")       # → {tier, model, fallback, reasoning}
spawn = router.spawn_agent("fix this bug", label="bugfix") # → {params: {task, model, sessionTarget}}
cost = router.estimate_cost("design landing page")         # → {tier, model, cost, currency}
```

---

## Tier detection

| Tier | Example keywords |
|------|------------------|
| **FAST** | check, get, list, show, status, monitor, fetch, simple |
| **REASONING** | prove, logic, analyze, derive, math, step by step |
| **CREATIVE** | creative, write, story, design, UI, UX, frontend, website (website projects → Kimi k2.5 only) |
| **RESEARCH** | research, find, search, lookup, web, information |
| **CODE** | code, function, debug, fix, implement, refactor, test, React, JWT (not website builds) |
| **QUALITY** | complex, architecture, design, system, comprehensive |
| **VISION** | image, picture, photo, screenshot, visual |

- **Vision** has priority: if vision keywords are present, task is VISION regardless of other keywords.
- **Agentic** tasks (multi-step) are bumped to at least CODE.

---

---

## Changelog

### v1.7.0 (Security-focused release)

**Removed functionality (for improved security rating):**
- **Gateway guard integration** — Removed `gateway_watchdog.py` and gateway auth management functionality. Use the separate [gateway-guard](https://clawhub.ai/skills/gateway-guard) skill for gateway auth checking and management.
- **Gateway auth secret exposure** — Removed `get_openclaw_gateway_config()` function that exposed gateway tokens/passwords in router output. Router spawn output no longer includes `gatewayToken`, `gatewayPassword`, `gatewayAuthMode`, or `gatewayPort`.
- **FACEPALM troubleshooting integration** — Removed troubleshooting loop detection and automatic FACEPALM invocation. Use the separate [FACEPALM](https://github.com/RuneweaverStudios/FACEPALM) skill if intelligent troubleshooting is needed.

**Security improvements:**
- Router now only handles model routing with no credential exposure
- No process management capabilities (no gateway restart/kill operations)
- Clean separation of concerns: routing vs. gateway management vs. troubleshooting

**Migration:**
- If you need gateway auth management: `clawhub install gateway-guard`
- If you need troubleshooting: Install [FACEPALM](https://github.com/RuneweaverStudios/FACEPALM) separately

---

## Configuration

- **`config.json`** — Model list and `routing_rules` per tier; `default_model` (e.g. `openrouter/google/gemini-2.5-flash`) for session default and orchestrator.
- Router loads `config.json` from the parent of `scripts/` (skill root).

---

## Related skills

- **[gateway-guard](https://clawhub.ai/skills/gateway-guard)** — Gateway auth management (use separately if needed)
- **[FACEPALM](https://github.com/RuneweaverStudios/FACEPALM)** — Intelligent troubleshooting (use separately if needed)
- **[what-just-happened](https://clawhub.ai/skills/what-just-happened)** — Summarizes gateway restarts

---

## License / author

Austin. Part of the OpenClaw skills ecosystem.


path: scripts/router.py
content: #!/usr/bin/env python3
"""
OpenRouterRouter | Codename: Centipede (friday-router skill)
Version 1.7.0

Fixed bugs from original intelligent-router:
- Simple indicators now properly invert (high match = SIMPLE, not complex)
- Agentic tasks properly bump to at least MEDIUM tier
- Code keywords actually detected
- Confidence scores vary appropriately

Features:
- Austin's preferred models (Flash, Haiku, GLM-5, Kimi, Grok, etc.)
- Keyword-based routing for obvious matches
- Weighted scoring for nuanced classification
- OpenClaw integration for spawning sub-agents
"""

import json
import math
import os
import re
import sys
from pathlib import Path

# OpenClaw imports (if available)
try:
    import openclaw
    HAS_OPENCLAW = True
except ImportError:
    HAS_OPENCLAW = False


# Removed get_openclaw_gateway_config() - gateway auth secrets should not be exposed in router output
# Use gateway-guard skill separately if gateway auth management is needed


class FridayRouter:
    """Austin's intelligent model router with fixed scoring."""
    
    # Simple indicators that suggest SIMPLE/Fast tasks (NOT inverted anymore)
    SIMPLE_KEYWORDS = [
        'check', 'get', 'fetch', 'list', 'show', 'display', 'status',
        'what is', 'how much', 'tell me', 'find', 'search', 'summarize',
        'monitor', 'watch', 'read', 'look', 'simple', 'quick', 'fast'
    ]
    
    # Complex indicators that suggest QUALITY/Code tasks
    COMPLEX_KEYWORDS = [
        'build', 'create', 'implement', 'architect', 'design', 'system',
        'comprehensive', 'thorough', 'complex', 'multi', 'full-stack',
        'authentication', 'authorization', 'database', 'api', 'service'
    ]
    
    # Code-related keywords
    CODE_KEYWORDS = [
        'code', 'function', 'class', 'method', 'debug', 'fix', 'bug',
        'refactor', 'lint', 'test', 'unit', 'integration', 'component',
        'module', 'package', 'library', 'framework', 'import', 'export',
        'react', 'vue', 'angular', 'node', 'python', 'javascript',
        'typescript', 'rust', 'go', 'java', 'api', 'endpoint'
    ]
    
    # Reasoning keywords
    REASONING_KEYWORDS = [
        'prove', 'theorem', 'proof', 'derive', 'logic', 'reason',
        'analyze', 'reasoning', 'step by step', 'why', 'how does',
        'explain', 'mathematical', 'induction', 'deduction'
    ]
    
    # Creative keywords
    CREATIVE_KEYWORDS = [
        'creative', 'write', 'story', 'poem', 'article', 'blog',
        'design', 'UI', 'UX', 'frontend', 'website', 'landing',
        'copy', 'narrative', 'brainstorm', 'idea', 'concept'
    ]
    
    # Research keywords
    RESEARCH_KEYWORDS = [
        'research', 'find', 'search', 'lookup', 'web', 'information',
        'fact', 'review', 'compare', 'vs', 'versus', 'difference',
        'summary of', 'what are', 'best', 'top', 'alternatives'
    ]
    
    # Agentic/action keywords (multi-step tasks)
    AGENTIC_KEYWORDS = [
        'run', 'test', 'fix', 'deploy', 'edit', 'build', 'create',
        'implement', 'execute', 'refactor', 'migrate', 'integrate',
        'setup', 'configure', 'install', 'compile', 'debug'
    ]
    
    def __init__(self, config_path=None):
        """Initialize router with config file."""
        if config_path is None:
            # Default to config.json in parent directory of script
            script_dir = Path(__file__).parent
            config_path = script_dir.parent / 'config.json'
        
        self.config_path = Path(config_path)
        self.config = self._load_config()
        
        # Removed troubleshooting loop detection and FACEPALM integration
        # Use FACEPALM skill separately if troubleshooting is needed
    
    def _load_config(self):
        """Load and parse configuration file."""
        if not self.config_path.exists():
            raise FileNotFoundError(f"Configuration file not found: {self.config_path}")
        
        with open(self.config_path, 'r') as f:
            return json.load(f)
    
    def _keyword_match(self, text, keywords):
        """Count keyword matches (case-insensitive)."""
        text_lower = text.lower()
        return sum(1 for kw in keywords if kw.lower() in text_lower)
    
    def classify_task(self, task_description, return_details=False):
        """
        Classify a task into a tier using keyword matching + scoring.
        
        Returns: FAST, REASONING, CREATIVE, RESEARCH, CODE, QUALITY, or VISION
        """
        text = task_description.lower()
        
        # First, check for exact keyword tier matches (highest priority)
        tier_scores = {}
        
        # Count matches for each tier
        tier_scores['FAST'] = self._keyword_match(task_description, self.SIMPLE_KEYWORDS)
        tier_scores['REASONING'] = self._keyword_match(task_description, self.REASONING_KEYWORDS)
        tier_scores['CREATIVE'] = self._keyword_match(task_description, self.CREATIVE_KEYWORDS)
        tier_scores['RESEARCH'] = self._keyword_match(task_description, self.RESEARCH_KEYWORDS)
        tier_scores['CODE'] = self._keyword_match(task_description, self.CODE_KEYWORDS)
        tier_scores['COMPLEX'] = self._keyword_match(task_description, self.COMPLEX_KEYWORDS)
        
        # Check for vision keywords (highest priority - if image/picture/photo/screenshot present, force VISION)
        vision_keywords = ['image', 'picture', 'photo', 'screenshot', 'visual', 'see', 'describe what']
        vision_matches = self._keyword_match(task_description, vision_keywords)
        tier_scores['VISION'] = vision_matches
        
        # If vision keywords present, this IS a vision task - override other classifications
        if vision_matches > 0:
            return {
                'tier': 'VISION',
                'confidence': min(vision_matches / 3.0, 1.0),
                'tier_scores': {'VISION': vision_matches},
                'is_agentic': False
            }
        
        # Agentic task detection - if multi-step, bump to at least CODE
        agentic_count = self._keyword_match(task_description, self.AGENTIC_KEYWORDS)
        multi_step_patterns = [
            r'\bfirst\b.*\bthen\b', r'\bstep\s+\d+', r'\d+\.\s+\w+',
            r'\bnext\b', r'\bafter\b', r'\bfinally\b', r',\s*then\b'
        ]
        is_multi_step = any(re.search(p, text) for p in multi_step_patterns)
        
        if agentic_count >= 2 or is_multi_step:
            # Multi-step task - ensure at least CODE tier
            tier_scores['CODE'] += 2
            if tier_scores['FAST'] > 0:
                tier_scores['FAST'] = 0  # Override FAST if agentic
        
        # Find best matching tier
        if max(tier_scores.values()) == 0:
            # No keywords matched - default to FAST
            best_tier = 'FAST'
        else:
            best_tier = max(tier_scores, key=tier_scores.get)
        
        # Website/frontend projects → CREATIVE (Kimi k2.5), never CODE
        website_project_keywords = [
            'website', 'web site', 'landing page', 'landing', 'frontend',
            'community site', 'online community', 'build a site', 'new site'
        ]
        if self._keyword_match(task_description, website_project_keywords) > 0:
            best_tier = 'CREATIVE'
        
        # Map COMPLEX to CODE for our tier system
        if best_tier == 'COMPLEX':
            # If also has code keywords, use CODE tier
            if tier_scores['CODE'] > 0:
                best_tier = 'CODE'
            else:
                best_tier = 'QUALITY'
        
        # Special handling: if both COMPLEX and FAST matched, prefer CODE/QUALITY
        if tier_scores['COMPLEX'] > 0 and tier_scores['FAST'] > 0:
            if tier_scores['COMPLEX'] >= tier_scores['FAST']:
                best_tier = 'QUALITY' if tier_scores['COMPLEX'] >= 2 else 'CODE'
        
        # Calculate confidence based on match strength
        max_score = max(tier_scores.values())
        confidence = min(max_score / 5.0, 1.0)  # Cap at 1.0, normalize around 5 matches = 100%
        
        result = {
            'tier': best_tier,
            'confidence': round(confidence, 3),
            'tier_scores': {k: v for k, v in tier_scores.items() if v > 0},
            'is_agentic': agentic_count >= 2 or is_multi_step
        }
        
        if not return_details:
            return result['tier']
        
        return result
    
    def get_default_model(self):
        """Return the default model (Gemini 2.5 Flash). Used for session default and orchestrator."""
        default_id = self.config.get('default_model')
        if not default_id:
            # Fallback: QUALITY tier primary
            tier_rules = self.config.get('routing_rules', {}).get('QUALITY', {})
            default_id = tier_rules.get('primary')
        for m in self.config.get('models', []):
            if m['id'] == default_id:
                return m
        return None
    
    def recommend_model(self, task_description):
        """Classify task and recommend the best model."""
        classification = self.classify_task(task_description, return_details=True)
        tier = classification['tier']
        
        # Get routing rules for this tier
        routing_rules = self.config.get('routing_rules', {})
        tier_rules = routing_rules.get(tier, {})
        
        # Get primary model
        primary_id = tier_rules.get('primary')
        
        # Find model in config
        model = None
        for m in self.config.get('models', []):
            if m['id'] == primary_id:
                model = m
                break
        
        # Fallback
        fallback = None
        fallback_ids = tier_rules.get('fallback', [])
        if fallback_ids:
            for fb_id in fallback_ids:
                for m in self.config.get('models', []):
                    if m['id'] == fb_id:
                        fallback = m
                        break
        
        return {
            'tier': tier,
            'model': model,
            'fallback': fallback,
            'classification': classification,
            'reasoning': self._explain(tier, classification)
        }
    
    def _explain(self, tier, classification):
        """Provide reasoning for tier selection."""
        explanations = {
            'FAST': 'Simple, quick task - monitoring, checks, summaries',
            'REASONING': 'Logical analysis, math, step-by-step reasoning',
            'CREATIVE': 'Creative writing, design, frontend work',
            'RESEARCH': 'Information lookup, web search, fact-finding',
            'CODE': 'Code generation, debugging, implementation',
            'COMPLEX': 'Code generation, debugging, implementation',  # Maps to CODE
            'QUALITY': 'Complex, comprehensive, architectural work',
            'VISION': 'Image analysis, visual understanding'
        }
        
        explanation = explanations.get(tier, 'General task')
        
        if classification.get('is_agentic'):
            explanation += ' [Multi-step agentic task detected]'
        
        return explanation
    
    def estimate_cost(self, task_description):
        """Estimate cost for a task."""
        result = self.recommend_model(task_description)
        model = result['model']
        
        if not model:
            return {'error': 'No model found for tier'}
        
        # Rough token estimates
        token_estimate = {
            'FAST': {'in': 500, 'out': 200},
            'REASONING': {'in': 2000, 'out': 1500},
            'CREATIVE': {'in': 1500, 'out': 2000},
            'RESEARCH': {'in': 1000, 'out': 500},
            'CODE': {'in': 2000, 'out': 1500},
            'QUALITY': {'in': 5000, 'out': 3000},
            'VISION': {'in': 500, 'out': 500}
        }
        
        tokens = token_estimate.get(result['tier'], {'in': 500, 'out': 200})
        
        input_cost = (tokens['in'] / 1_000_000) * model['input_cost_per_m']
        output_cost = (tokens['out'] / 1_000_000) * model['output_cost_per_m']
        
        return {
            'tier': result['tier'],
            'model': model['alias'],
            'cost': round(input_cost + output_cost, 4),
            'currency': 'USD'
        }
    
    def spawn_agent(self, task, session_target='isolated', label=None):
        """Spawn an OpenClaw sub-agent with the appropriate model."""
        recommendation = self.recommend_model(task)
        model = recommendation['model']
        
        if not model:
            raise ValueError(f"No model found for tier: {recommendation['tier']}")
        
        # Build the spawn params (no gateway auth secrets)
        params = {
            'task': task,
            'model': model['id'],
            'sessionTarget': session_target
        }
        
        if label:
            params['label'] = label
        
        return {
            'params': params,
            'recommendation': recommendation
        }


def main():
    """CLI entry point."""
    if len(sys.argv) < 2:
        print("OpenRouterRouter | Codename: Centipede v1.7.0")
        print("\nUsage:")
        print("  router.py default                Show session default model (capable by default)")
        print("  router.py classify <task>       Classify task and recommend model")
        print("  router.py score <task>          Show detailed scoring")
        print("  router.py cost <task>           Estimate cost")
        print("  router.py models               List all models")
        print("  router.py spawn [--json] <task>  Show spawn params for OpenClaw (--json for machine-readable)")
        sys.exit(1)
    
    command = sys.argv[1]
    router = FridayRouter()
    
    # Parse --json for spawn command
    output_json = False
    if command == 'spawn' and len(sys.argv) > 2 and sys.argv[2] == '--json':
        output_json = True
        sys.argv.pop(2)  # remove --json so task = ' '.join(sys.argv[2:])
    
    if command == 'default':
        m = router.get_default_model()
        if not m:
            print("❌ No default model configured (missing default_model or QUALITY primary in config)")
            sys.exit(1)
        print("🎯 Session default model (capable by default):\n")
        print(f"   {m['alias']} ({m['id']})")
        print(f"   Cost: ${m['input_cost_per_m']}/${m['output_cost_per_m']} per M")
        print(f"   Use for: {', '.join(m.get('use_for', []))}")
        print("\n   Simple tasks down-route to FAST tier (e.g. Gemini 2.5 Flash).")
    
    elif command == 'classify':
        task = ' '.join(sys.argv[2:])
        result = router.recommend_model(task)
        
        print(f"📋 Task: {task}")
        print(f"\n🎯 Classification: {result['tier']}")
        print(f"   Confidence: {result['classification']['confidence']:.1%}")
        print(f"   Reasoning: {result['reasoning']}")
        
        if result['model']:
            m = result['model']
            print(f"\n🤖 Recommended Model:")
            print(f"   {m['alias']} ({m['id']})")
            print(f"   Cost: ${m['input_cost_per_m']}/${m['output_cost_per_m']} per M")
            print(f"   Use for: {', '.join(m.get('use_for', []))}")
        
        if result['fallback']:
            fb = result['fallback']
            print(f"\n🔄 Fallback: {fb['alias']} ({fb['id']})")
    
    elif command == 'score':
        task = ' '.join(sys.argv[2:])
        result = router.classify_task(task, return_details=True)
        
        print(f"📋 Task: {task}")
        print(f"\n🎯 Tier: {result['tier']}")
        print(f"   Confidence: {result['confidence']:.1%}")
        print(f"   Agentic: {'Yes' if result['is_agentic'] else 'No'}")
        
        print(f"\n📊 Tier Scores:")
        for tier, score in sorted(result['tier_scores'].items(), key=lambda x: x[1], reverse=True):
            bar = '█' * score
            print(f"   {tier:10} {bar} ({score})")
    
    elif command == 'cost':
        task = ' '.join(sys.argv[2:])
        result = router.estimate_cost(task)
        
        if 'error' in result:
            print(f"❌ Error: {result['error']}")
        else:
            print(f"📋 Task: {task}")
            print(f"\n💰 Cost Estimate:")
            print(f"   Tier: {result['tier']}")
            print(f"   Model: {result['model']}")
            print(f"   Est. Cost: ${result['cost']} {result['currency']}")
    
    elif command == 'models':
        print("📦 Configured Models:\n")
        for model in router.config.get('models', []):
            print(f"  {model['alias']:20} [{model['tier']:8}] {model['id']}")
            print(f"                         ${model['input_cost_per_m']}/${model['output_cost_per_m']}/M")
    
    elif command == 'spawn':
        task = ' '.join(sys.argv[2:])
        if not task:
            print("❌ Error: spawn requires a task string", file=sys.stderr)
            sys.exit(1)
        result = router.spawn_agent(task)
        
        if output_json:
            # Machine-readable: single JSON object for sessions_spawn
            # Note: Gateway auth secrets are NOT included - use gateway-guard skill separately if needed
            out = {k: v for k, v in result['params'].items()}
            print(json.dumps(out))
        else:
            print(f"📋 Task: {task}")
            print(f"\n🚀 OpenClaw Spawn Params:")
            print(f"   model: {result['params']['model']}")
            print(f"   sessionTarget: {result['params']['sessionTarget']}")
            print(f"\n📦 Full recommendation:")
            print(f"   Tier: {result['recommendation']['tier']}")
            print(f"   Model: {result['recommendation']['model']['alias']}")
    
    else:
        print(f"Unknown command: {command}")
        sys.exit(1)


if __name__ == '__main__':
    main()


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