feat(v0.5.0): weekly update 2026-03-16 — 48 new skills (total 387)

This commit is contained in:
MyClaw AI
2026-03-23 02:03:45 +00:00
parent 43d9a8c788
commit 708f572924
496 changed files with 83579 additions and 10 deletions
@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "ai-model-router-v2",
"installedVersion": "1.1.0",
"installedAt": 1773626688277
}
+119
View File
@@ -0,0 +1,119 @@
---
name: ai-model-router
description: Intelligent AI model router that automatically switches between two configured models (local for simple tasks, cloud for complex ones). Detects local models (Ollama, LM Studio) automatically, routes based on task complexity and privacy. Use when users ask to "switch model", "use local/cloud model", or mention API keys/passwords (triggers privacy mode). Trigger on: sensitive data detection (forces local), complex tasks like "design architecture" (uses cloud), or configuration requests.
version: 1.0.0
---
# AI Model Router
Compact, intelligent model routing that just works.
## Quick Start
```bash
# Install
npx clawhub@latest install ai-model-router
# First run - auto-detects your models
python3 skill/core/router.py "What is Python?"
# List available models
python3 skill/core/router.py --list
```
## How It Works
```
Your Request → Analyze → Select Model
Simple? → Primary (fast/cheap)
Complex? → Secondary (capable)
Private? → Primary (forced)
```
## Scoring (from model-router-premium)
| Pattern | Points |
|---------|--------|
| Microservices, architecture | +10 |
| Design, implement, optimize | +5 |
| Explain, analyze, compare | +3 |
| **Syntax, example, "what is"** | **-3** |
**Threshold: 5** (simple vs complex)
## Features
| Feature | Status |
|---------|--------|
| Auto-detect local models | ✓ (Ollama, LM Studio) |
| Cloud model registry | ✓ (7 built-in) |
| Privacy detection | ✓ (API keys, passwords) |
| Context tracking | ✓ (conversations) |
| JSON config | ✓ (optional) |
| CLI interface | ✓ |
| **Core code size** | **~200 lines** |
## CLI
```bash
# Route a task
python3 skill/core/router.py "Design a system"
python3 skill/core/router.py "What is a for loop?"
# Options
--json # JSON output
--force primary # Force primary model
--list # List all models
--status # Show status
```
## Python API
```python
from skill.core.router import RouterCore
router = RouterCore()
result = router.route("Design microservices")
print(result.model_name) # "Claude Opus 4"
print(result.reason) # "complex_task(score=15)"
print(result.confidence) # 0.75
```
## Configuration (Optional)
Create `~/.model-router/models.json`:
```json
{
"primary_model": {"id": "ollama:llama3:8b"},
"secondary_model": {"id": "anthropic:claude-opus-4"},
"models": [...]
}
```
**Without config**: Auto-detects local + uses cloud registry.
## Privacy Protection
Automatically forces primary (local) when sensitive data detected:
- API keys (`sk-...`, `api_key`)
- Passwords (`password`, `passwd`)
- Tokens (`bearer`, `secret`)
- Emails, SSN, credit cards
## Files
- `core/router.py` - Core routing engine (~200 lines)
- `modules/detector.py` - Auto-detection (optional)
- `modules/context.py` - Context tracking (optional)
## Inspired By
- **model-router-premium**: Simple scoring logic, cost-aware routing
- **Model Router v1**: Full feature set, documentation
This version combines:
- The **simplicity** of model-router-premium (~200 lines)
- The **features** of ai-model-router (privacy, auto-detect, context)
+6
View File
@@ -0,0 +1,6 @@
{
"ownerId": "kn7bhjxh3e3b8tfpz5gnaghv3s83179v",
"slug": "ai-model-router-v2",
"version": "1.1.0",
"publishedAt": 1773626201372
}
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Quick install script
echo "🚀 AI Model Router v2.0"
echo ""
echo "Installing..."
echo ""
echo "Quick test:"
echo " python3 skill/core/router.py 'What is a for loop?'"
echo ""
echo "List models:"
echo " python3 skill/core/router.py --list"
+12
View File
@@ -0,0 +1,12 @@
{
"name": "ai-model-router",
"version": "1.1.0",
"description": "Compact intelligent AI model router with auto-detection, privacy protection, and context tracking. Simple core (~200 lines) + optional modules.",
"keywords": ["ai", "router", "local", "cloud", "privacy", "context"],
"author": "yuldrone",
"license": "MIT",
"clawhub": {
"slug": "ai-model-router",
"category": "optimization"
}
}
+119
View File
@@ -0,0 +1,119 @@
---
name: ai-model-router
description: Intelligent AI model router that automatically switches between two configured models (local for simple tasks, cloud for complex ones). Detects local models (Ollama, LM Studio) automatically, routes based on task complexity and privacy. Use when users ask to "switch model", "use local/cloud model", or mention API keys/passwords (triggers privacy mode). Trigger on: sensitive data detection (forces local), complex tasks like "design architecture" (uses cloud), or configuration requests.
version: 1.0.0
---
# AI Model Router
Compact, intelligent model routing that just works.
## Quick Start
```bash
# Install
npx clawhub@latest install ai-model-router
# First run - auto-detects your models
python3 skill/core/router.py "What is Python?"
# List available models
python3 skill/core/router.py --list
```
## How It Works
```
Your Request → Analyze → Select Model
Simple? → Primary (fast/cheap)
Complex? → Secondary (capable)
Private? → Primary (forced)
```
## Scoring (from model-router-premium)
| Pattern | Points |
|---------|--------|
| Microservices, architecture | +10 |
| Design, implement, optimize | +5 |
| Explain, analyze, compare | +3 |
| **Syntax, example, "what is"** | **-3** |
**Threshold: 5** (simple vs complex)
## Features
| Feature | Status |
|---------|--------|
| Auto-detect local models | ✓ (Ollama, LM Studio) |
| Cloud model registry | ✓ (7 built-in) |
| Privacy detection | ✓ (API keys, passwords) |
| Context tracking | ✓ (conversations) |
| JSON config | ✓ (optional) |
| CLI interface | ✓ |
| **Core code size** | **~200 lines** |
## CLI
```bash
# Route a task
python3 skill/core/router.py "Design a system"
python3 skill/core/router.py "What is a for loop?"
# Options
--json # JSON output
--force primary # Force primary model
--list # List all models
--status # Show status
```
## Python API
```python
from skill.core.router import RouterCore
router = RouterCore()
result = router.route("Design microservices")
print(result.model_name) # "Claude Opus 4"
print(result.reason) # "complex_task(score=15)"
print(result.confidence) # 0.75
```
## Configuration (Optional)
Create `~/.model-router/models.json`:
```json
{
"primary_model": {"id": "ollama:llama3:8b"},
"secondary_model": {"id": "anthropic:claude-opus-4"},
"models": [...]
}
```
**Without config**: Auto-detects local + uses cloud registry.
## Privacy Protection
Automatically forces primary (local) when sensitive data detected:
- API keys (`sk-...`, `api_key`)
- Passwords (`password`, `passwd`)
- Tokens (`bearer`, `secret`)
- Emails, SSN, credit cards
## Files
- `core/router.py` - Core routing engine (~200 lines)
- `modules/detector.py` - Auto-detection (optional)
- `modules/context.py` - Context tracking (optional)
## Inspired By
- **model-router-premium**: Simple scoring logic, cost-aware routing
- **Model Router v1**: Full feature set, documentation
This version combines:
- The **simplicity** of model-router-premium (~200 lines)
- The **features** of ai-model-router (privacy, auto-detect, context)
@@ -0,0 +1,426 @@
#!/usr/bin/env python3
"""
Model Router Core - Compact routing engine
Minimal, readable, extensible.
Design principles from model-router-premium:
- Keep decision logic small and deterministic
- Default to cheapest/fastest for simple tasks
- Escalate to stronger models when complex
New features retained:
- Privacy detection
- Local model auto-detection
- Context tracking (optional)
"""
import json
import os
import re
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Optional, Literal, Callable
@dataclass
class Model:
"""Model definition"""
id: str
name: str
provider: str
type: str # "local" or "cloud"
cost_score: float = 1.0 # lower = cheaper
power_score: float = 50.0 # higher = more capable
capabilities: list = None
requires_api_key: bool = False
api_key_env: str = None
def __post_init__(self):
if self.capabilities is None:
self.capabilities = ["chat", "general"]
@dataclass
class RouteResult:
"""Routing decision result"""
model_id: str
model_name: str
model_type: str
reason: str
confidence: float
complexity_score: int = 0
privacy_detected: list = None
context_id: str = None
is_switch: bool = False
previous_model: str = None
def __post_init__(self):
if self.privacy_detected is None:
self.privacy_detected = []
class RouterCore:
"""
Core routing engine - minimal and readable.
Inspired by model-router-premium:
- Simple scoring logic
- Cost-aware routing
- Capability matching
Enhanced with:
- Privacy detection
- Local model detection
"""
# Privacy patterns (sensitive data)
PRIVACY_PATTERNS = [
r"sk-[a-zA-Z0-9_-]{15,}", # Stripe, API keys
r"api[_-]?key\s*[:=]\s*['\"]?[a-zA-Z0-9_-]{10,}",
r"password\s*[:=]\s*['\"]?.{6,}",
r"secret\s*[:=]\s*['\"]?.{10,}",
r"bearer\s+[a-zA-Z0-9_-]{20,}",
r"\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[A-Z|a-z]{2,}\b", # email
]
# Complexity patterns (from model-router-premium + additions)
COMPLEX_PATTERNS = {
# Very high complexity (+10)
"microservices": 10, "architecture": 10, "scalable": 10,
"multi-step": 10, "comprehensive": 10, "end-to-end": 10,
# High complexity (+5)
"design": 5, "implement": 5, "optimize": 5,
"explain": 3, "analyze": 3, "compare": 3,
# Simple (-3)
"syntax": -3, "example": -3, "what is": -3,
}
def __init__(self, config_path: Optional[str] = None):
self.config_path = config_path or Path.home() / ".model-router" / "models.json"
self.config_dir = Path(self.config_path).parent
self.config_dir.mkdir(parents=True, exist_ok=True)
self.models = []
self.primary_id = None
self.secondary_id = None
# Optional modules (loaded on demand)
self._context = None
self._detector = None
self._load_config()
@property
def context(self):
"""Lazy load context module"""
if self._context is None:
try:
from modules.context import ContextManager
self._context = ContextManager(self.config_dir)
except ImportError:
self._context = False
return self._context
@property
def detector(self):
"""Lazy load detector module"""
if self._detector is None:
from modules.detector import ModelDetector
self._detector = ModelDetector()
return self._detector
def _load_config(self):
"""Load model configuration"""
if os.path.exists(self.config_path):
with open(self.config_path) as f:
config = json.load(f)
self.primary_id = config.get("primary_model", {}).get("id")
self.secondary_id = config.get("secondary_model", {}).get("id")
self.models = [self._dict_to_model(m) for m in config.get("models", [])]
else:
# Auto-detect local models if no config
self._auto_detect()
def _auto_detect(self):
"""Auto-detect available models"""
try:
from modules.detector import ModelDetector
detector = ModelDetector()
local_models = detector.detect_local()
cloud_models = detector.get_cloud_registry()
self.models = local_models + cloud_models
# Set defaults
if local_models:
self.primary_id = local_models[0].id
if cloud_models:
self.secondary_id = cloud_models[0].id
except ImportError:
# Fallback models (always available)
self.models = [
Model("ollama:llama3:8b", "Llama 3 8B", "Ollama", "local", 0, 35),
Model("anthropic:claude-haiku-4", "Claude Haiku 4", "Anthropic", "cloud", 3, 60, requires_api_key=True, api_key_env="ANTHROPIC_API_KEY"),
]
self.primary_id = self.models[0].id
self.secondary_id = self.models[1].id
# Ensure we always have primary and secondary set
if not self.primary_id and self.models:
self.primary_id = self.models[0].id
if not self.secondary_id and len(self.models) > 1:
self.secondary_id = self.models[1].id
elif not self.secondary_id and self.models:
self.secondary_id = self.models[0].id
def _dict_to_model(self, d: dict) -> Model:
"""Convert dict to Model"""
return Model(
id=d.get("id", ""),
name=d.get("name", ""),
provider=d.get("provider", ""),
type=d.get("type", "cloud"),
cost_score=d.get("cost_score", 1.0),
power_score=d.get("power_score", 50.0),
capabilities=d.get("capabilities", []),
requires_api_key=d.get("requires_api_key", False),
api_key_env=d.get("api_key_env"),
)
def get_model(self, model_id: str) -> Optional[Model]:
"""Get model by ID"""
for m in self.models:
if m.id == model_id:
return m
return None
def check_privacy(self, text: str) -> tuple[bool, list]:
"""Check for sensitive data"""
detected = []
for pattern in self.PRIVACY_PATTERNS:
if re.search(pattern, text, re.IGNORECASE):
detected.append(pattern)
return len(detected) > 0, detected
def score_complexity(self, text: str) -> int:
"""
Score task complexity.
Returns 0-50+ (higher = more complex)
Based on model-router-premium heuristics:
- Length: longer = more complex
- Keywords: complex words add points
"""
text_lower = text.lower()
score = 0
# Length scoring (from model-router-premium)
length = len(text)
if length > 200:
score += 3
elif length > 80:
score += 2
elif length > 40:
score += 1
# Keyword scoring (simplified)
for keyword, points in self.COMPLEX_PATTERNS.items():
if keyword in text_lower:
score += points
return max(0, score)
def route(
self,
task: str,
force: Optional[Literal["primary", "secondary"]] = None,
conversation_id: Optional[str] = None,
enable_context: bool = True
) -> RouteResult:
"""
Route a task to the appropriate model.
Args:
task: The user's task/request
force: Force primary or secondary model
conversation_id: Continue existing conversation
enable_context: Use context tracking
Returns:
RouteResult with selected model and reasoning
"""
# Privacy check - always routes to primary (usually local)
has_privacy, privacy_detected = self.check_privacy(task)
if has_privacy:
primary = self.get_model(self.primary_id)
if primary:
return RouteResult(
model_id=primary.id,
model_name=primary.name,
model_type="primary",
reason=f"privacy_detected:{len(privacy_detected)}_patterns",
confidence=1.0,
privacy_detected=privacy_detected,
)
# Forced model
if force == "primary":
m = self.get_model(self.primary_id)
return RouteResult(m.id, m.name, "primary", "forced", 1.0)
if force == "secondary":
m = self.get_model(self.secondary_id)
return RouteResult(m.id, m.name, "secondary", "forced", 1.0)
# Score complexity
complexity = self.score_complexity(task)
# Get context if available
context_data = None
previous_model = None
is_switch = False
if enable_context and self.context and conversation_id:
context_data = self.context.get_context(conversation_id)
if context_data:
previous_model = context_data.get("last_model")
# Decision: primary vs secondary
# Threshold at 5 (simpler than model-router-premium's 3)
threshold = 5
primary = self.get_model(self.primary_id)
secondary = self.get_model(self.secondary_id)
if not primary or not secondary:
# Fallback - use any available model
if self.models:
m = self.models[0]
return RouteResult(
m.id, m.name, m.type, "no_config", 0.5,
complexity_score=complexity
)
# Last resort - create default model
return RouteResult(
model_id="ollama:llama3:8b",
model_name="Llama 3 8B",
model_type="primary",
reason="auto_detected_fallback",
confidence=0.5,
complexity_score=complexity
)
if complexity < threshold:
selected = primary
selected_type = "primary"
reason = f"simple_task(score={complexity})"
else:
selected = secondary
selected_type = "secondary"
reason = f"complex_task(score={complexity})"
# Check switch
if previous_model and previous_model != selected.id:
is_switch = True
reason += f",switch_from:{previous_model}"
# Calculate confidence
if selected_type == "primary":
confidence = max(0.5, (threshold - complexity) / threshold)
else:
confidence = min(1.0, (complexity - threshold) / 20 + 0.5)
return RouteResult(
model_id=selected.id,
model_name=selected.name,
model_type=selected_type,
reason=reason,
confidence=confidence,
complexity_score=complexity,
context_id=conversation_id,
is_switch=is_switch,
previous_model=previous_model,
)
def record_message(
self,
conversation_id: str,
role: str,
content: str,
model_id: str,
model_type: str
):
"""Record a message in context (if enabled)"""
if self.context:
self.context.add_message(
conv_id=conversation_id,
role=role,
content=content,
model_used=model_id,
model_type=model_type,
)
def get_conversation(self, conversation_id: str):
"""Get conversation context"""
if self.context:
return self.context.get_context(conversation_id)
return None
def list_models(self) -> list:
"""List all available models"""
return [asdict(m) for m in self.models]
def get_status(self) -> dict:
"""Get router status"""
return {
"primary_id": self.primary_id,
"secondary_id": self.secondary_id,
"model_count": len(self.models),
"context_enabled": bool(self.context),
"config_path": str(self.config_path),
}
def main():
"""Simple CLI"""
import argparse
p = argparse.ArgumentParser(description="Model Router - Compact & Fast")
p.add_argument("task", nargs="*", help="Task to route")
p.add_argument("--json", action="store_true", help="JSON output")
p.add_argument("--force", choices=["primary", "secondary"])
p.add_argument("--list", action="store_true", help="List models")
p.add_argument("--status", action="store_true", help="Show status")
args = p.parse_args()
router = RouterCore()
if args.list:
for m in router.models:
print(f"{m.id:30} {m.name:20} [{m.type}]")
return
if args.status:
status = router.get_status()
for k, v in status.items():
print(f"{k}: {v}")
return
if not args.task:
p.print_help()
return
task = " ".join(args.task)
result = router.route(task, force=args.force)
if args.json:
print(json.dumps(asdict(result), indent=2))
else:
symbol = "1️⃣" if result.model_type == "primary" else "2️⃣"
print(f"{symbol} {result.model_name}")
print(f" Reason: {result.reason}")
print(f" Confidence: {result.confidence:.0%}")
if __name__ == "__main__":
main()
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
"""
Context Manager - Track conversations across model switches
Optional module - only imported if enable_context=True
Keeps core router small.
"""
import json
import hashlib
from datetime import datetime
from pathlib import Path
from typing import Optional, Dict, Any
class ContextManager:
"""Lightweight conversation context tracking"""
def __init__(self, config_dir: Path):
self.config_dir = config_dir
self.contexts_file = config_dir / "contexts.json"
self.contexts: Dict[str, Dict] = {}
self._load()
def _load(self):
"""Load contexts from disk"""
if self.contexts_file.exists():
try:
with open(self.contexts_file) as f:
self.contexts = json.load(f)
except Exception:
self.contexts = {}
def _save(self):
"""Save contexts to disk"""
with open(self.contexts_file, "w") as f:
json.dump(self.contexts, f, indent=2)
def get_or_create(self, message: str, conv_id: Optional[str] = None) -> str:
"""Get existing or create new conversation"""
if conv_id and conv_id in self.contexts:
return conv_id
# Create new ID
content = f"{message}_{datetime.now().isoformat()}"
new_id = hashlib.sha256(content.encode()).hexdigest()[:12]
self.contexts[new_id] = {
"id": new_id,
"started_at": datetime.now().isoformat(),
"messages": [],
"transitions": 0,
"last_model": "",
}
self._save()
return new_id
def add_message(self, conv_id: str, role: str, content: str,
model_used: str, model_type: str):
"""Add a message to conversation"""
if conv_id not in self.contexts:
return
ctx = self.contexts[conv_id]
# Track transitions
if ctx["last_model"] and ctx["last_model"] != model_used:
ctx["transitions"] += 1
ctx["last_model"] = model_used
ctx["messages"].append({
"role": role,
"content": content[:200], # Truncate for storage
"model": model_used,
"type": model_type,
"time": datetime.now().isoformat(),
})
self._save()
def get_context(self, conv_id: str) -> Optional[Dict]:
"""Get conversation context"""
if conv_id not in self.contexts:
return None
ctx = self.contexts[conv_id]
return {
"conversation_id": conv_id,
"started_at": ctx["started_at"],
"message_count": len(ctx["messages"]),
"transitions": ctx["transitions"],
"last_model": ctx["last_model"],
"messages": ctx["messages"][-10:], # Last 10 messages
}
def list_all(self) -> list:
"""List all conversations"""
return [
{
"id": cid,
"messages": len(ctx["messages"]),
"transitions": ctx["transitions"],
}
for cid, ctx in self.contexts.items()
]
@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""
Model Detector - Safe module for detecting local AI models
Security:
- No subprocess execution (removed)
- No HTTP requests (removed)
- Read-only operations only
"""
import json
import os
from dataclasses import dataclass
from typing import List
@dataclass
class ModelInfo:
"""Model information - read-only"""
id: str
name: str
provider: str
type: str
cost_score: float = 0
power_score: float = 50
capabilities: List[str] = None
def __post_init__(self):
if self.capabilities is None:
self.capabilities = ["chat"]
class ModelDetector:
"""
Detect available AI models safely.
Only reads from:
- Ollama config files (read-only)
- Environment variables (read-only)
"""
def detect_local(self) -> List[ModelInfo]:
"""Detect local models from Ollama config"""
models = []
# Check Ollama models.json (read-only, safe)
ollama_models = self._read_ollama_models()
models.extend(ollama_models)
return models
def _read_ollama_models(self) -> List[ModelInfo]:
"""
Read Ollama models from config file.
Safe: read-only file operation.
"""
models = []
config_paths = [
os.path.expanduser("~/.ollama/models.json"),
"/usr/share/ollama/models.json",
]
for config_path in config_paths:
if os.path.exists(config_path):
try:
with open(config_path, "r") as f:
data = json.load(f)
for model_name in data.keys():
# Estimate power score from name
power = 30
name_lower = model_name.lower()
if "70b" in name_lower:
power = 80
elif "34b" in name_lower or "33b" in name_lower:
power = 70
elif "14b" in name_lower or "13b" in name_lower:
power = 50
elif "8b" in name_lower or "7b" in name_lower:
power = 35
elif "3b" in name_lower or "2b" in name_lower:
power = 20
models.append(ModelInfo(
id=f"ollama:{model_name}",
name=model_name,
provider="Ollama",
type="local",
cost_score=0,
power_score=power,
))
break # Use first valid config
except Exception:
pass
return models
def get_cloud_registry(self) -> List[ModelInfo]:
"""Return built-in cloud model registry (no external calls)"""
return [
ModelInfo("anthropic:claude-haiku-4", "Claude Haiku 4", "Anthropic", "cloud", 3, 60),
ModelInfo("anthropic:claude-sonnet-4", "Claude Sonnet 4", "Anthropic", "cloud", 5, 80),
ModelInfo("anthropic:claude-opus-4", "Claude Opus 4", "Anthropic", "cloud", 8, 95),
ModelInfo("openai:gpt-4o-mini", "GPT-4o Mini", "OpenAI", "cloud", 1, 50),
ModelInfo("openai:gpt-4o", "GPT-4o", "OpenAI", "cloud", 5, 85),
]
def detect_all(self) -> List[ModelInfo]:
"""Detect all available models"""
return self.detect_local() + self.get_cloud_registry()