Compare commits
9 Commits
7dcb8af1bb
...
f63172c4cf
| Author | SHA1 | Date | |
|---|---|---|---|
| f63172c4cf | |||
| ff111ef278 | |||
| bf5470ac66 | |||
| c1e3b2881d | |||
| 4024740b82 | |||
| fb4cf1b035 | |||
| d2daf74fca | |||
| e52e818686 | |||
| df6cf94dae |
@@ -28,6 +28,7 @@ Slash commands for quick actions. User-invoked (type `/command` to trigger).
|
|||||||
| `/programmer` | | Code development tasks |
|
| `/programmer` | | Code development tasks |
|
||||||
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
||||||
| `/usage` | `/stats` | View usage statistics |
|
| `/usage` | `/stats` | View usage statistics |
|
||||||
|
| `/external` | `/llm`, `/ext` | Toggle and use external LLM mode |
|
||||||
|
|
||||||
### Kubernetes (`/k8s:*`)
|
### Kubernetes (`/k8s:*`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
---
|
||||||
|
name: external
|
||||||
|
description: Toggle and use external LLM mode (GPT-5.2, Gemini, etc.)
|
||||||
|
aliases: [llm, ext, external-llm]
|
||||||
|
---
|
||||||
|
|
||||||
|
# External LLM Mode
|
||||||
|
|
||||||
|
Route requests to external LLMs via opencode or gemini CLI.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/external # Show current status
|
||||||
|
/external on [reason] # Enable external mode
|
||||||
|
/external off # Disable external mode
|
||||||
|
/external invoke <prompt> # Send prompt to default model
|
||||||
|
/external invoke --model <model> <prompt> # Send to specific model
|
||||||
|
/external invoke --task <task> <prompt> # Route by task type
|
||||||
|
/external models # List available models
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
### Status
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Toggle On/Off
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on --reason "reason"
|
||||||
|
~/.claude/mcp/llm-router/toggle.py off
|
||||||
|
```
|
||||||
|
|
||||||
|
### Invoke
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model MODEL -p "prompt" [--json]
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --task TASK -p "prompt" [--json]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Models by Tier
|
||||||
|
|
||||||
|
### Frontier (strongest)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/gpt-5.2` | opencode | reasoning, fallback |
|
||||||
|
| `github-copilot/gemini-3-pro-preview` | opencode | long context, reasoning |
|
||||||
|
| `gemini/gemini-2.5-pro` | gemini | long context, reasoning |
|
||||||
|
|
||||||
|
### Mid-tier (general purpose)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/claude-sonnet-4.5` | opencode | general, fallback |
|
||||||
|
| `github-copilot/gemini-3-flash-preview` | opencode | fast |
|
||||||
|
| `zai-coding-plan/glm-4.7` | opencode | code generation |
|
||||||
|
| `opencode/big-pickle` | opencode | general |
|
||||||
|
| `gemini/gemini-2.5-flash` | gemini | fast |
|
||||||
|
|
||||||
|
### Lightweight (simple tasks)
|
||||||
|
| Model | Provider | Best For |
|
||||||
|
|-------|----------|----------|
|
||||||
|
| `github-copilot/claude-haiku-4.5` | opencode | simple tasks |
|
||||||
|
|
||||||
|
## Task Routing
|
||||||
|
|
||||||
|
| Task | Routes To | Tier |
|
||||||
|
|------|-----------|------|
|
||||||
|
| `reasoning` | github-copilot/gpt-5.2 | frontier |
|
||||||
|
| `code-generation` | github-copilot/gemini-3-pro-preview | frontier |
|
||||||
|
| `long-context` | gemini/gemini-2.5-pro | frontier |
|
||||||
|
| `fast` | github-copilot/gemini-3-flash-preview | mid-tier |
|
||||||
|
| `general` (default) | github-copilot/claude-sonnet-4.5 | mid-tier |
|
||||||
|
|
||||||
|
## State Files
|
||||||
|
|
||||||
|
- Mode state: `~/.claude/state/external-mode.json`
|
||||||
|
- Model policy: `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
/external on testing # Enable for testing
|
||||||
|
/external invoke "Explain k8s pods" # Use default model (mid-tier)
|
||||||
|
/external invoke --model github-copilot/gpt-5.2 "Complex analysis" # frontier
|
||||||
|
/external invoke --task code-generation "Write a Python function" # routes to frontier
|
||||||
|
/external invoke --task fast "Quick question" # routes to mid-tier
|
||||||
|
/external off # Back to Claude
|
||||||
|
```
|
||||||
@@ -32,12 +32,24 @@ with open('${PA_DIR}/memory/decisions.json') as f:
|
|||||||
" 2>/dev/null || echo "0")
|
" 2>/dev/null || echo "0")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check external mode
|
||||||
|
EXTERNAL_MODE="disabled"
|
||||||
|
if [[ -f "${STATE_DIR}/external-mode.json" ]]; then
|
||||||
|
EXTERNAL_ENABLED=$(jq -r '.enabled // false' "${STATE_DIR}/external-mode.json" 2>/dev/null || echo "false")
|
||||||
|
if [[ "${EXTERNAL_ENABLED}" == "true" ]]; then
|
||||||
|
EXTERNAL_MODE="enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Output context as system reminder format
|
# Output context as system reminder format
|
||||||
echo "SessionStart:Callback hook success: Success"
|
echo "SessionStart:resume hook success: Success"
|
||||||
|
|
||||||
# Add additional context if there's something noteworthy
|
# Add additional context if there's something noteworthy
|
||||||
if [[ "${UNSUMMARIZED}" -gt 0 || "${PENDING_DECISIONS}" -gt 0 ]]; then
|
if [[ "${UNSUMMARIZED}" -gt 0 || "${PENDING_DECISIONS}" -gt 0 || "${EXTERNAL_MODE}" == "enabled" ]]; then
|
||||||
echo "SessionStart hook additional context: "
|
echo "SessionStart hook additional context: "
|
||||||
|
if [[ "${EXTERNAL_MODE}" == "enabled" ]]; then
|
||||||
|
echo "- EXTERNAL MODE ACTIVE: All requests routed to external LLMs"
|
||||||
|
fi
|
||||||
if [[ "${UNSUMMARIZED}" -gt 0 ]]; then
|
if [[ "${UNSUMMARIZED}" -gt 0 ]]; then
|
||||||
echo "- ${UNSUMMARIZED} unsummarized session(s) available for review"
|
echo "- ${UNSUMMARIZED} unsummarized session(s) available for review"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
delegate.py --tier opus -p "complex reasoning task" --json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def is_external_mode() -> bool:
|
||||||
|
"""Check if external-only mode is enabled."""
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_external_model(tier: str) -> str:
|
||||||
|
"""Get the external model equivalent for a Claude tier."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
policy = json.load(f)
|
||||||
|
mapping = policy.get("claude_to_external_map", {})
|
||||||
|
if tier not in mapping:
|
||||||
|
raise ValueError(f"No external mapping for tier: {tier}")
|
||||||
|
return mapping[tier]
|
||||||
|
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str, use_json: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Delegate to appropriate model based on mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tier: Claude tier (opus, sonnet, haiku)
|
||||||
|
prompt: The prompt text
|
||||||
|
use_json: Return JSON output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
"""
|
||||||
|
if is_external_mode():
|
||||||
|
# Use external model
|
||||||
|
model = get_external_model(tier)
|
||||||
|
invoke_script = ROUTER_DIR / "invoke.py"
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(invoke_script), "--model", model, "-p", prompt]
|
||||||
|
if use_json:
|
||||||
|
cmd.append("--json")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"External invoke failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
else:
|
||||||
|
# Use Claude
|
||||||
|
cmd = ["claude", "--print", "--model", tier, prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Claude failed: {result.stderr}")
|
||||||
|
|
||||||
|
response = result.stdout.strip()
|
||||||
|
|
||||||
|
if use_json:
|
||||||
|
return json.dumps({
|
||||||
|
"model": f"claude/{tier}",
|
||||||
|
"response": response,
|
||||||
|
"success": True
|
||||||
|
}, indent=2)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Delegate to Claude or external model based on mode"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tier",
|
||||||
|
required=True,
|
||||||
|
choices=["opus", "sonnet", "haiku"],
|
||||||
|
help="Claude tier (maps to external equivalent when in external mode)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = delegate(args.tier, args.prompt, args.json)
|
||||||
|
print(result)
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"error": str(e), "success": False}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+127
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
invoke.py --task code-generation -p "prompt" --json
|
||||||
|
|
||||||
|
Model selection priority:
|
||||||
|
1. Explicit --model flag
|
||||||
|
2. Task-based routing (--task flag)
|
||||||
|
3. Default from policy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy() -> dict:
|
||||||
|
"""Load model policy from state file."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model(args: argparse.Namespace, policy: dict) -> str:
|
||||||
|
"""Determine which model to use based on args and policy."""
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy.get("task_routing", {}):
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy.get("task_routing", {}).get("default", "copilot/sonnet-4.5")
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict, timeout: int = 600) -> str:
|
||||||
|
"""Invoke the appropriate provider for the given model."""
|
||||||
|
external_models = policy.get("external_models", {})
|
||||||
|
|
||||||
|
if model not in external_models:
|
||||||
|
raise ValueError(f"Unknown model: {model}. Available: {list(external_models.keys())}")
|
||||||
|
|
||||||
|
model_config = external_models[model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config.get("cli_args", [])
|
||||||
|
|
||||||
|
# Import and invoke appropriate provider
|
||||||
|
if cli == "opencode":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt, timeout=timeout)
|
||||||
|
elif cli == "gemini":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt, timeout=timeout)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Invoke external LLM via configured provider"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
help="Explicit model (e.g., copilot/gpt-5.2)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--task",
|
||||||
|
choices=["reasoning", "code-generation", "long-context", "general"],
|
||||||
|
help="Task type for automatic model routing"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON with model info"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=600,
|
||||||
|
help="Timeout in seconds (default: 600)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(model, args.prompt, policy, timeout=args.timeout)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": model,
|
||||||
|
"response": result,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": args.model or "unknown",
|
||||||
|
"error": str(e),
|
||||||
|
"success": False
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper for Google models."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke gemini CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "gemini-3-pro"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If gemini CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"gemini timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "gemini-3-pro"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: gemini.py 'prompt'")
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper for GitHub Copilot, Z.AI, and other providers."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
# OpenCode binary path (linuxbrew installation)
|
||||||
|
OPENCODE_BIN = "/home/linuxbrew/.linuxbrew/bin/opencode"
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke opencode CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "github-copilot/gpt-5.2"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If opencode CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
|
||||||
|
Example invocation:
|
||||||
|
opencode run -m github-copilot/gpt-5.2 "Hello world"
|
||||||
|
"""
|
||||||
|
# Build command: opencode run -m MODEL "prompt"
|
||||||
|
cmd = [OPENCODE_BIN, "run"] + cli_args + [prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"opencode timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "github-copilot/gpt-5.2"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: opencode.py 'prompt'")
|
||||||
Executable
+98
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Toggle external-only mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
toggle.py on [--reason "user requested"]
|
||||||
|
toggle.py off
|
||||||
|
toggle.py status
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
STATE_FILE = Path.home() / ".claude/state/external-mode.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
"""Load current state."""
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
with open(STATE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"enabled": False, "activated_at": None, "reason": None}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(state: dict):
|
||||||
|
"""Save state to file."""
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def enable(reason: Optional[str] = None):
|
||||||
|
"""Enable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": True,
|
||||||
|
"activated_at": datetime.now().isoformat(),
|
||||||
|
"reason": reason or "user-requested"
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode ENABLED")
|
||||||
|
print(f" Activated: {state['activated_at']}")
|
||||||
|
print(f" Reason: {state['reason']}")
|
||||||
|
print("\nAll agent requests will now use external LLMs.")
|
||||||
|
print("Run 'toggle.py off' or '/pa --external off' to disable.")
|
||||||
|
|
||||||
|
|
||||||
|
def disable():
|
||||||
|
"""Disable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": False,
|
||||||
|
"activated_at": None,
|
||||||
|
"reason": None
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode DISABLED")
|
||||||
|
print("\nAll agent requests will now use Claude.")
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
"""Show current mode status."""
|
||||||
|
state = load_state()
|
||||||
|
if state.get("enabled"):
|
||||||
|
print("External-only mode: ENABLED")
|
||||||
|
print(f" Activated: {state.get('activated_at', 'unknown')}")
|
||||||
|
print(f" Reason: {state.get('reason', 'unknown')}")
|
||||||
|
else:
|
||||||
|
print("External-only mode: DISABLED")
|
||||||
|
print(" Using Claude for all requests.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Toggle external-only mode")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# on command
|
||||||
|
on_parser = subparsers.add_parser("on", help="Enable external-only mode")
|
||||||
|
on_parser.add_argument("--reason", help="Reason for enabling")
|
||||||
|
|
||||||
|
# off command
|
||||||
|
subparsers.add_parser("off", help="Disable external-only mode")
|
||||||
|
|
||||||
|
# status command
|
||||||
|
subparsers.add_parser("status", help="Show current mode")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "on":
|
||||||
|
enable(args.reason)
|
||||||
|
elif args.command == "off":
|
||||||
|
disable()
|
||||||
|
elif args.command == "status":
|
||||||
|
status()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,903 @@
|
|||||||
|
# External LLM Integration Implementation Plan
|
||||||
|
|
||||||
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Enable agents to use external LLMs (Copilot, Z.AI, Gemini) via CLI tools with a session toggle.
|
||||||
|
|
||||||
|
**Architecture:** Python router reads model-policy.json, invokes appropriate CLI (opencode/gemini), returns response. State file controls Claude vs external mode. PA exposes toggle commands.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3, subprocess, JSON state files, opencode CLI, gemini CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Create External Mode State File
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/state/external-mode.json`
|
||||||
|
|
||||||
|
**Step 1: Create state file**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > ~/.claude/state/external-mode.json << 'EOF'
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Verify file**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/external-mode.json | jq .`
|
||||||
|
Expected: Valid JSON with `enabled: false`
|
||||||
|
|
||||||
|
**Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/external-mode.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external-mode state file"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Extend Model Policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
**Step 1: Read current file**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/model-policy.json | jq .`
|
||||||
|
|
||||||
|
**Step 2: Add external_models section**
|
||||||
|
|
||||||
|
Add to model-policy.json (after `skill_delegation` section):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"external_models": {
|
||||||
|
"copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "haiku-equivalent"
|
||||||
|
},
|
||||||
|
"zai/glm-4.7": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "zai", "--model", "glm-4.7"],
|
||||||
|
"use_cases": ["code-generation"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"gemini/gemini-3-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-3-pro"],
|
||||||
|
"use_cases": ["long-context"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claude_to_external_map": {
|
||||||
|
"opus": "copilot/gpt-5.2",
|
||||||
|
"sonnet": "copilot/sonnet-4.5",
|
||||||
|
"haiku": "copilot/haiku-4.5"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "copilot/gpt-5.2",
|
||||||
|
"code-generation": "zai/glm-4.7",
|
||||||
|
"long-context": "gemini/gemini-3-pro",
|
||||||
|
"default": "copilot/sonnet-4.5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Validate JSON**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/model-policy.json | jq .`
|
||||||
|
Expected: Valid JSON, no errors
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/model-policy.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external model definitions to policy"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Create Router Directory Structure
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/`
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/`
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/__init__.py`
|
||||||
|
|
||||||
|
**Step 1: Create directories**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.claude/mcp/llm-router/providers
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Create __init__.py**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
touch ~/.claude/mcp/llm-router/providers/__init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify structure**
|
||||||
|
|
||||||
|
Run: `ls -la ~/.claude/mcp/llm-router/`
|
||||||
|
Expected: `providers/` directory exists
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): create llm-router directory structure"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Create OpenCode Provider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
|
||||||
|
**Step 1: Write provider**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper for Copilot, Z.AI, and other providers."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke opencode CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Provider/model args like ["--provider", "copilot", "--model", "gpt-5.2"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If opencode CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["opencode", "--print"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"opencode timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["--provider", "copilot", "--model", "gpt-5.2"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: opencode.py 'prompt'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/providers/opencode.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/providers/opencode.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add opencode provider wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Create Gemini Provider
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
|
||||||
|
**Step 1: Write provider**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper for Google models."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(cli_args: List[str], prompt: str, timeout: int = 300) -> str:
|
||||||
|
"""
|
||||||
|
Invoke gemini CLI with given args and prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli_args: Model args like ["-m", "gemini-3-pro"]
|
||||||
|
prompt: The prompt text
|
||||||
|
timeout: Timeout in seconds (default 5 minutes)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If gemini CLI fails
|
||||||
|
TimeoutError: If request exceeds timeout
|
||||||
|
"""
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
raise TimeoutError(f"gemini timed out after {timeout}s")
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed (exit {result.returncode}): {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Quick test
|
||||||
|
import sys
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
response = invoke(["-m", "gemini-3-pro"], sys.argv[1])
|
||||||
|
print(response)
|
||||||
|
else:
|
||||||
|
print("Usage: gemini.py 'prompt'")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/providers/gemini.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/providers/gemini.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add gemini provider wrapper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Create Main Router (invoke.py)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
|
||||||
|
**Step 1: Write router**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
invoke.py --task code-generation -p "prompt" --json
|
||||||
|
|
||||||
|
Model selection priority:
|
||||||
|
1. Explicit --model flag
|
||||||
|
2. Task-based routing (--task flag)
|
||||||
|
3. Default from policy
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def load_policy() -> dict:
|
||||||
|
"""Load model policy from state file."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model(args: argparse.Namespace, policy: dict) -> str:
|
||||||
|
"""Determine which model to use based on args and policy."""
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy.get("task_routing", {}):
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy.get("task_routing", {}).get("default", "copilot/sonnet-4.5")
|
||||||
|
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict) -> str:
|
||||||
|
"""Invoke the appropriate provider for the given model."""
|
||||||
|
external_models = policy.get("external_models", {})
|
||||||
|
|
||||||
|
if model not in external_models:
|
||||||
|
raise ValueError(f"Unknown model: {model}. Available: {list(external_models.keys())}")
|
||||||
|
|
||||||
|
model_config = external_models[model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config.get("cli_args", [])
|
||||||
|
|
||||||
|
# Import and invoke appropriate provider
|
||||||
|
if cli == "opencode":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt)
|
||||||
|
elif cli == "gemini":
|
||||||
|
sys.path.insert(0, str(ROUTER_DIR))
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Invoke external LLM via configured provider"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model",
|
||||||
|
help="Explicit model (e.g., copilot/gpt-5.2)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--task",
|
||||||
|
choices=["reasoning", "code-generation", "long-context", "general"],
|
||||||
|
help="Task type for automatic model routing"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON with model info"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="Timeout in seconds (default: 300)"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(model, args.prompt, policy)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": model,
|
||||||
|
"response": result,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
output = {
|
||||||
|
"model": args.model or "unknown",
|
||||||
|
"error": str(e),
|
||||||
|
"success": False
|
||||||
|
}
|
||||||
|
print(json.dumps(output, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/invoke.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/invoke.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add main router invoke.py"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 7: Create Delegation Helper
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
|
||||||
|
**Step 1: Write delegation helper**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
delegate.py --tier opus -p "complex reasoning task" --json
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
ROUTER_DIR = Path(__file__).parent
|
||||||
|
|
||||||
|
|
||||||
|
def is_external_mode() -> bool:
|
||||||
|
"""Check if external-only mode is enabled."""
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_external_model(tier: str) -> str:
|
||||||
|
"""Get the external model equivalent for a Claude tier."""
|
||||||
|
policy_file = STATE_DIR / "model-policy.json"
|
||||||
|
with open(policy_file) as f:
|
||||||
|
policy = json.load(f)
|
||||||
|
mapping = policy.get("claude_to_external_map", {})
|
||||||
|
if tier not in mapping:
|
||||||
|
raise ValueError(f"No external mapping for tier: {tier}")
|
||||||
|
return mapping[tier]
|
||||||
|
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str, use_json: bool = False) -> str:
|
||||||
|
"""
|
||||||
|
Delegate to appropriate model based on mode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tier: Claude tier (opus, sonnet, haiku)
|
||||||
|
prompt: The prompt text
|
||||||
|
use_json: Return JSON output
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Model response as string
|
||||||
|
"""
|
||||||
|
if is_external_mode():
|
||||||
|
# Use external model
|
||||||
|
model = get_external_model(tier)
|
||||||
|
invoke_script = ROUTER_DIR / "invoke.py"
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(invoke_script), "--model", model, "-p", prompt]
|
||||||
|
if use_json:
|
||||||
|
cmd.append("--json")
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"External invoke failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
else:
|
||||||
|
# Use Claude
|
||||||
|
cmd = ["claude", "--print", "--model", tier, prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"Claude failed: {result.stderr}")
|
||||||
|
|
||||||
|
response = result.stdout.strip()
|
||||||
|
|
||||||
|
if use_json:
|
||||||
|
return json.dumps({
|
||||||
|
"model": f"claude/{tier}",
|
||||||
|
"response": response,
|
||||||
|
"success": True
|
||||||
|
}, indent=2)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Delegate to Claude or external model based on mode"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--tier",
|
||||||
|
required=True,
|
||||||
|
choices=["opus", "sonnet", "haiku"],
|
||||||
|
help="Claude tier (maps to external equivalent when in external mode)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p", "--prompt",
|
||||||
|
required=True,
|
||||||
|
help="Prompt text"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--json",
|
||||||
|
action="store_true",
|
||||||
|
help="Output as JSON"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = delegate(args.tier, args.prompt, args.json)
|
||||||
|
print(result)
|
||||||
|
except Exception as e:
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"error": str(e), "success": False}, indent=2))
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print(f"Error: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/delegate.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify syntax**
|
||||||
|
|
||||||
|
Run: `python3 -m py_compile ~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/delegate.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add delegation helper"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 8: Create Toggle Script
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `~/.claude/mcp/llm-router/toggle.py`
|
||||||
|
|
||||||
|
**Step 1: Write toggle script**
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Toggle external-only mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
toggle.py on [--reason "user requested"]
|
||||||
|
toggle.py off
|
||||||
|
toggle.py status
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_FILE = Path.home() / ".claude/state/external-mode.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_state() -> dict:
|
||||||
|
"""Load current state."""
|
||||||
|
if STATE_FILE.exists():
|
||||||
|
with open(STATE_FILE) as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {"enabled": False, "activated_at": None, "reason": None}
|
||||||
|
|
||||||
|
|
||||||
|
def save_state(state: dict):
|
||||||
|
"""Save state to file."""
|
||||||
|
with open(STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def enable(reason: str = None):
|
||||||
|
"""Enable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": True,
|
||||||
|
"activated_at": datetime.now().isoformat(),
|
||||||
|
"reason": reason or "user-requested"
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode ENABLED")
|
||||||
|
print(f" Activated: {state['activated_at']}")
|
||||||
|
print(f" Reason: {state['reason']}")
|
||||||
|
print("\nAll agent requests will now use external LLMs.")
|
||||||
|
print("Run 'toggle.py off' or '/pa --external off' to disable.")
|
||||||
|
|
||||||
|
|
||||||
|
def disable():
|
||||||
|
"""Disable external-only mode."""
|
||||||
|
state = {
|
||||||
|
"enabled": False,
|
||||||
|
"activated_at": None,
|
||||||
|
"reason": None
|
||||||
|
}
|
||||||
|
save_state(state)
|
||||||
|
print("External-only mode DISABLED")
|
||||||
|
print("\nAll agent requests will now use Claude.")
|
||||||
|
|
||||||
|
|
||||||
|
def status():
|
||||||
|
"""Show current mode status."""
|
||||||
|
state = load_state()
|
||||||
|
if state.get("enabled"):
|
||||||
|
print("External-only mode: ENABLED")
|
||||||
|
print(f" Activated: {state.get('activated_at', 'unknown')}")
|
||||||
|
print(f" Reason: {state.get('reason', 'unknown')}")
|
||||||
|
else:
|
||||||
|
print("External-only mode: DISABLED")
|
||||||
|
print(" Using Claude for all requests.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Toggle external-only mode")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
# on command
|
||||||
|
on_parser = subparsers.add_parser("on", help="Enable external-only mode")
|
||||||
|
on_parser.add_argument("--reason", help="Reason for enabling")
|
||||||
|
|
||||||
|
# off command
|
||||||
|
subparsers.add_parser("off", help="Disable external-only mode")
|
||||||
|
|
||||||
|
# status command
|
||||||
|
subparsers.add_parser("status", help="Show current mode")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.command == "on":
|
||||||
|
enable(args.reason)
|
||||||
|
elif args.command == "off":
|
||||||
|
disable()
|
||||||
|
elif args.command == "status":
|
||||||
|
status()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Make executable**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ~/.claude/mcp/llm-router/toggle.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Test toggle**
|
||||||
|
|
||||||
|
Run: `~/.claude/mcp/llm-router/toggle.py status`
|
||||||
|
Expected: Shows "External-only mode: DISABLED"
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add mcp/llm-router/toggle.py
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add toggle script"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 9: Update Session Start Hook
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
**Step 1: Read current hook**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
**Step 2: Add external mode check**
|
||||||
|
|
||||||
|
Add before the final output section:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check external mode
|
||||||
|
if [ -f ~/.claude/state/external-mode.json ]; then
|
||||||
|
EXTERNAL_ENABLED=$(jq -r '.enabled // false' ~/.claude/state/external-mode.json)
|
||||||
|
if [ "$EXTERNAL_ENABLED" = "true" ]; then
|
||||||
|
echo "external-mode:enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Verify hook**
|
||||||
|
|
||||||
|
Run: `bash -n ~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
Expected: No output (success)
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add hooks/scripts/session-start.sh
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): announce external mode in session-start"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 10: Add Component Registry Triggers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `~/.claude/state/component-registry.json`
|
||||||
|
|
||||||
|
**Step 1: Read current registry**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/component-registry.json | jq '.skills'`
|
||||||
|
|
||||||
|
**Step 2: Add external-mode skill entry**
|
||||||
|
|
||||||
|
Add to skills array:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "external-mode-toggle",
|
||||||
|
"name": "External Mode Toggle",
|
||||||
|
"description": "Toggle between Claude and external LLMs",
|
||||||
|
"path": "mcp/llm-router/toggle.py",
|
||||||
|
"triggers": [
|
||||||
|
"use external",
|
||||||
|
"switch to external",
|
||||||
|
"external models",
|
||||||
|
"external only",
|
||||||
|
"use copilot",
|
||||||
|
"use opencode",
|
||||||
|
"back to claude",
|
||||||
|
"use claude again",
|
||||||
|
"disable external",
|
||||||
|
"external mode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Validate JSON**
|
||||||
|
|
||||||
|
Run: `cat ~/.claude/state/component-registry.json | jq .`
|
||||||
|
Expected: Valid JSON
|
||||||
|
|
||||||
|
**Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude add state/component-registry.json
|
||||||
|
git -C ~/.claude commit -m "feat(external-llm): add external-mode triggers to registry"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 11: Integration Test
|
||||||
|
|
||||||
|
**Step 1: Test toggle**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on --reason "testing"
|
||||||
|
~/.claude/mcp/llm-router/toggle.py status
|
||||||
|
~/.claude/mcp/llm-router/toggle.py off
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Status changes correctly
|
||||||
|
|
||||||
|
**Step 2: Test router (mock - will fail without actual CLIs)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model copilot/gpt-5.2 -p "Say hello" --json 2>&1 || echo "Expected: CLI not found (normal if opencode not installed)"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Test delegation helper status check**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, '$HOME/.claude/mcp/llm-router')
|
||||||
|
from delegate import is_external_mode
|
||||||
|
print(f'External mode: {is_external_mode()}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: "External mode: False"
|
||||||
|
|
||||||
|
**Step 4: Final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git -C ~/.claude commit --allow-empty -m "feat(external-llm): integration complete - gleaming-routing-mercury"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
After completing all tasks:
|
||||||
|
|
||||||
|
| Component | Status |
|
||||||
|
|-----------|--------|
|
||||||
|
| `state/external-mode.json` | Created |
|
||||||
|
| `state/model-policy.json` | Extended |
|
||||||
|
| `mcp/llm-router/invoke.py` | Created |
|
||||||
|
| `mcp/llm-router/delegate.py` | Created |
|
||||||
|
| `mcp/llm-router/toggle.py` | Created |
|
||||||
|
| `mcp/llm-router/providers/opencode.py` | Created |
|
||||||
|
| `mcp/llm-router/providers/gemini.py` | Created |
|
||||||
|
| `hooks/scripts/session-start.sh` | Updated |
|
||||||
|
| `state/component-registry.json` | Updated |
|
||||||
|
|
||||||
|
**To use:**
|
||||||
|
```bash
|
||||||
|
# Enable external mode
|
||||||
|
~/.claude/mcp/llm-router/toggle.py on
|
||||||
|
|
||||||
|
# Or via PA
|
||||||
|
/pa --external on
|
||||||
|
/pa switch to external models
|
||||||
|
|
||||||
|
# Invoke directly
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --task reasoning -p "Explain quantum computing"
|
||||||
|
|
||||||
|
# Delegate (respects mode)
|
||||||
|
~/.claude/mcp/llm-router/delegate.py --tier sonnet -p "Check disk space"
|
||||||
|
```
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
# Plan: External LLM Integration
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Integrate external LLMs (via subscription-based access) into the agent system for cost optimization, specialized capabilities, and redundancy. Uses `opencode` CLI for Copilot/Z.AI models and `gemini` CLI for Google models.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
- **Cost optimization** — Use cheaper models for simple tasks
|
||||||
|
- **Specialized capabilities** — Access models with unique strengths (GPT-5.2 for reasoning, GLM 4.7 for code)
|
||||||
|
- **Redundancy** — Fallback when Claude is unavailable
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
| Decision | Choice |
|
||||||
|
|----------|--------|
|
||||||
|
| Provider type | Cloud APIs via subscription (not local) |
|
||||||
|
| Providers | GitHub Copilot, Z.AI, Google Gemini |
|
||||||
|
| CLIs | `opencode` (Copilot, Z.AI), `gemini` (Google) |
|
||||||
|
| Integration | Task-specific routing + agent-level assignment |
|
||||||
|
| Toggle | State file (persists across sessions) |
|
||||||
|
| Toggle scope | All agents switch when enabled |
|
||||||
|
|
||||||
|
## Task Routing
|
||||||
|
|
||||||
|
| Task Type | Model |
|
||||||
|
|-----------|-------|
|
||||||
|
| Reasoning chains | copilot/gpt-5.2 |
|
||||||
|
| Code generation | zai/glm-4.7 |
|
||||||
|
| Long context | gemini/gemini-3-pro |
|
||||||
|
| General/fallback | copilot/sonnet-4.5 |
|
||||||
|
|
||||||
|
## Claude-to-External Mapping
|
||||||
|
|
||||||
|
| Claude Tier | External Equivalent |
|
||||||
|
|-------------|---------------------|
|
||||||
|
| opus | copilot/gpt-5.2 |
|
||||||
|
| sonnet | copilot/sonnet-4.5 |
|
||||||
|
| haiku | copilot/haiku-4.5 |
|
||||||
|
|
||||||
|
## Files to Create
|
||||||
|
|
||||||
|
### `~/.claude/state/external-mode.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/invoke.py`
|
||||||
|
|
||||||
|
Main entry point for invoking external LLMs.
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Invoke external LLM via configured provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
invoke.py --model copilot/gpt-5.2 -p "prompt"
|
||||||
|
invoke.py --task reasoning -p "prompt"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
|
||||||
|
def load_policy():
|
||||||
|
with open(STATE_DIR / "model-policy.json") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def resolve_model(args, policy):
|
||||||
|
if args.model:
|
||||||
|
return args.model
|
||||||
|
if args.task and args.task in policy["task_routing"]:
|
||||||
|
return policy["task_routing"][args.task]
|
||||||
|
return policy["task_routing"]["default"]
|
||||||
|
|
||||||
|
def invoke(model: str, prompt: str, policy: dict) -> str:
|
||||||
|
model_config = policy["external_models"][model]
|
||||||
|
cli = model_config["cli"]
|
||||||
|
cli_args = model_config["cli_args"]
|
||||||
|
|
||||||
|
if cli == "opencode":
|
||||||
|
from providers.opencode import invoke as opencode_invoke
|
||||||
|
return opencode_invoke(cli_args, prompt)
|
||||||
|
elif cli == "gemini":
|
||||||
|
from providers.gemini import invoke as gemini_invoke
|
||||||
|
return gemini_invoke(cli_args, prompt)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown CLI: {cli}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("-p", "--prompt", required=True, help="Prompt text")
|
||||||
|
parser.add_argument("--model", help="Explicit model (e.g., copilot/gpt-5.2)")
|
||||||
|
parser.add_argument("--task", help="Task type for routing")
|
||||||
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
policy = load_policy()
|
||||||
|
model = resolve_model(args, policy)
|
||||||
|
result = invoke(args.prompt, model, policy)
|
||||||
|
|
||||||
|
if args.json:
|
||||||
|
print(json.dumps({"model": model, "response": result}))
|
||||||
|
else:
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/providers/opencode.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""OpenCode CLI wrapper."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def invoke(cli_args: list, prompt: str) -> str:
|
||||||
|
cmd = ["opencode", "--print"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"opencode failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/providers/gemini.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Gemini CLI wrapper."""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def invoke(cli_args: list, prompt: str) -> str:
|
||||||
|
cmd = ["gemini"] + cli_args + ["-p", prompt]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=300
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"gemini failed: {result.stderr}")
|
||||||
|
|
||||||
|
return result.stdout.strip()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/mcp/llm-router/delegate.py`
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Agent delegation helper. Routes to external or Claude based on mode.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
delegate.py --tier sonnet -p "prompt"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STATE_DIR = Path.home() / ".claude/state"
|
||||||
|
|
||||||
|
def is_external_mode():
|
||||||
|
mode_file = STATE_DIR / "external-mode.json"
|
||||||
|
if mode_file.exists():
|
||||||
|
with open(mode_file) as f:
|
||||||
|
return json.load(f).get("enabled", False)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delegate(tier: str, prompt: str) -> str:
|
||||||
|
if is_external_mode():
|
||||||
|
policy = json.loads((STATE_DIR / "model-policy.json").read_text())
|
||||||
|
model = policy["claude_to_external_map"][tier]
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(Path.home() / ".claude/mcp/llm-router/invoke.py"),
|
||||||
|
"--model", model, "-p", prompt],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
else:
|
||||||
|
result = subprocess.run(
|
||||||
|
["claude", "--print", "--model", tier, prompt],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--tier", required=True, choices=["opus", "sonnet", "haiku"])
|
||||||
|
parser.add_argument("-p", "--prompt", required=True)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print(delegate(args.tier, args.prompt))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
### `~/.claude/state/model-policy.json`
|
||||||
|
|
||||||
|
Add sections:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"external_models": {
|
||||||
|
"copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"copilot/haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "copilot", "--model", "haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "haiku-equivalent"
|
||||||
|
},
|
||||||
|
"zai/glm-4.7": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["--provider", "zai", "--model", "glm-4.7"],
|
||||||
|
"use_cases": ["code-generation"],
|
||||||
|
"tier": "sonnet-equivalent"
|
||||||
|
},
|
||||||
|
"gemini/gemini-3-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-3-pro"],
|
||||||
|
"use_cases": ["long-context"],
|
||||||
|
"tier": "opus-equivalent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"claude_to_external_map": {
|
||||||
|
"opus": "copilot/gpt-5.2",
|
||||||
|
"sonnet": "copilot/sonnet-4.5",
|
||||||
|
"haiku": "copilot/haiku-4.5"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "copilot/gpt-5.2",
|
||||||
|
"code-generation": "zai/glm-4.7",
|
||||||
|
"long-context": "gemini/gemini-3-pro",
|
||||||
|
"default": "copilot/sonnet-4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/state/component-registry.json`
|
||||||
|
|
||||||
|
Add trigger for external mode:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "external-mode-toggle",
|
||||||
|
"type": "skill",
|
||||||
|
"triggers": [
|
||||||
|
"use external", "switch to external", "external models",
|
||||||
|
"stop using claude", "external only", "use copilot",
|
||||||
|
"back to claude", "use claude again", "disable external"
|
||||||
|
],
|
||||||
|
"action": "toggle-external-mode"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `~/.claude/hooks/scripts/session-start.sh`
|
||||||
|
|
||||||
|
Add external mode announcement:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check external mode
|
||||||
|
if [ -f ~/.claude/state/external-mode.json ]; then
|
||||||
|
if [ "$(jq -r '.enabled' ~/.claude/state/external-mode.json)" = "true" ]; then
|
||||||
|
echo "external-mode:enabled"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Toggle Interface
|
||||||
|
|
||||||
|
### Command-based
|
||||||
|
|
||||||
|
```
|
||||||
|
/pa --external on # Enable external-only mode
|
||||||
|
/pa --external off # Disable, return to Claude
|
||||||
|
/pa --external status # Show current mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Natural language
|
||||||
|
|
||||||
|
| User says | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| "switch to external models" | Enable |
|
||||||
|
| "use copilot for everything" | Enable |
|
||||||
|
| "go back to claude" | Disable |
|
||||||
|
| "are we using external?" | Status |
|
||||||
|
| "use external for this" | One-shot (no persist) |
|
||||||
|
|
||||||
|
### Visual indicator
|
||||||
|
|
||||||
|
When external mode active, PA prefixes responses:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔌 [External: copilot/gpt-5.2]
|
||||||
|
|
||||||
|
<response>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
1. Create `external-mode.json` state file
|
||||||
|
2. Extend `model-policy.json` with external models
|
||||||
|
3. Create `llm-router/` directory and scripts
|
||||||
|
4. Add provider wrappers (opencode, gemini)
|
||||||
|
5. Create delegation helper
|
||||||
|
6. Update PA with toggle commands
|
||||||
|
7. Add component-registry triggers
|
||||||
|
8. Update session-start hook
|
||||||
|
9. Test each provider
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test router directly
|
||||||
|
~/.claude/mcp/llm-router/invoke.py --model copilot/gpt-5.2 -p "Say hello"
|
||||||
|
|
||||||
|
# Test delegation
|
||||||
|
~/.claude/mcp/llm-router/delegate.py --tier sonnet -p "What is 2+2?"
|
||||||
|
|
||||||
|
# Test toggle
|
||||||
|
/pa --external on
|
||||||
|
/pa what time is it?
|
||||||
|
/pa --external off
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
1. Set `external-mode.json` → `enabled: false`
|
||||||
|
2. All operations revert to Claude immediately
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
- Auto-fallback to external when Claude rate-limited
|
||||||
|
- Cost tracking per external model
|
||||||
|
- Response quality comparison metrics
|
||||||
|
- Additional providers (Mistral, local Ollama)
|
||||||
@@ -121,6 +121,14 @@
|
|||||||
"implemented": "2026-01-07",
|
"implemented": "2026-01-07",
|
||||||
"category": "enhancement",
|
"category": "enhancement",
|
||||||
"notes": "This plan - meta!"
|
"notes": "This plan - meta!"
|
||||||
|
},
|
||||||
|
"gleaming-routing-mercury": {
|
||||||
|
"title": "External LLM integration",
|
||||||
|
"status": "implemented",
|
||||||
|
"created": "2026-01-08",
|
||||||
|
"implemented": "2026-01-08",
|
||||||
|
"category": "feature",
|
||||||
|
"notes": "fc-004 - Cloud API integration via opencode/gemini CLIs with session toggle"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,6 +143,23 @@
|
|||||||
"block dangerous",
|
"block dangerous",
|
||||||
"protect"
|
"protect"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"external-mode": {
|
||||||
|
"description": "Toggle between Claude and external LLMs (Copilot, Z.AI, Gemini)",
|
||||||
|
"script": "~/.claude/mcp/llm-router/toggle.py",
|
||||||
|
"triggers": [
|
||||||
|
"external",
|
||||||
|
"use external",
|
||||||
|
"switch to external",
|
||||||
|
"external models",
|
||||||
|
"external only",
|
||||||
|
"use copilot",
|
||||||
|
"use opencode",
|
||||||
|
"back to claude",
|
||||||
|
"use claude again",
|
||||||
|
"disable external",
|
||||||
|
"external mode"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
@@ -182,6 +199,15 @@
|
|||||||
],
|
],
|
||||||
"invokes": "skill:usage"
|
"invokes": "skill:usage"
|
||||||
},
|
},
|
||||||
|
"/external": {
|
||||||
|
"description": "Toggle and use external LLM mode (GPT-5.2, Gemini, etc.)",
|
||||||
|
"aliases": [
|
||||||
|
"/llm",
|
||||||
|
"/ext",
|
||||||
|
"/external-llm"
|
||||||
|
],
|
||||||
|
"invokes": "command:external"
|
||||||
|
},
|
||||||
"/README": {
|
"/README": {
|
||||||
"description": "TODO",
|
"description": "TODO",
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"activated_at": null,
|
||||||
|
"reason": null
|
||||||
|
}
|
||||||
@@ -37,10 +37,12 @@
|
|||||||
"category": "integration",
|
"category": "integration",
|
||||||
"title": "External LLM integration",
|
"title": "External LLM integration",
|
||||||
"description": "Support for non-Claude models in the agent system",
|
"description": "Support for non-Claude models in the agent system",
|
||||||
"priority": "low",
|
"priority": "medium",
|
||||||
"status": "deferred",
|
"status": "resolved",
|
||||||
"created": "2024-12-28",
|
"created": "2024-12-28",
|
||||||
"notes": "For specialized tasks or cost optimization"
|
"resolved": "2026-01-08",
|
||||||
|
"plan": "gleaming-routing-mercury",
|
||||||
|
"notes": "Implemented via opencode CLI. Models: github-copilot/gpt-5.2, zai-coding-plan/glm-4.7, etc. Toggle via /pa --external or natural language."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "fc-005",
|
"id": "fc-005",
|
||||||
|
|||||||
+171
-1
@@ -1 +1,171 @@
|
|||||||
{"infra":{"cluster":"k0s","nodes":3,"arch":"arm64"},"svc":{"gitops":"argocd","mon":"prometheus","alerts":"alertmanager"},"net":{},"hw":{"pi5_8gb":2,"pi3_1gb":1}}
|
{
|
||||||
|
"infra": {
|
||||||
|
"cluster": "k0s",
|
||||||
|
"nodes": 3,
|
||||||
|
"arch": "arm64",
|
||||||
|
"storage": "longhorn",
|
||||||
|
"storage_class": "longhorn",
|
||||||
|
"backup": "longhorn-backup + minio-to-mega"
|
||||||
|
},
|
||||||
|
"hw": {
|
||||||
|
"pi5_8gb": 2,
|
||||||
|
"pi3_1gb": 1,
|
||||||
|
"roles": {
|
||||||
|
"control_plane": "pi5",
|
||||||
|
"workers": ["pi5", "pi3"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"net": {
|
||||||
|
"metallb_pool": "192.168.153.240-192.168.153.254",
|
||||||
|
"ingress_nginx_ip": "192.168.153.240",
|
||||||
|
"ingress_haproxy_ip": "192.168.153.241",
|
||||||
|
"tailnet": "taildb3494.ts.net",
|
||||||
|
"dns_pattern": "<app>.<ns>.<ip>.nip.io"
|
||||||
|
},
|
||||||
|
"svc": {
|
||||||
|
"gitops": "argocd",
|
||||||
|
"monitoring": {
|
||||||
|
"metrics": "kube-prometheus-stack",
|
||||||
|
"logs": "loki-stack",
|
||||||
|
"alerts": "alertmanager",
|
||||||
|
"dashboards": "grafana"
|
||||||
|
},
|
||||||
|
"ingress": ["nginx-ingress-controller", "haproxy-ingress"],
|
||||||
|
"storage": ["longhorn", "local-path-storage", "minio"],
|
||||||
|
"networking": ["metallb", "tailscale-operator"]
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"ai_stack": {
|
||||||
|
"namespace": "ai-stack",
|
||||||
|
"components": ["open-webui", "ollama", "litellm", "searxng", "n8n", "vllm"],
|
||||||
|
"models": ["gpt-oss:120b", "qwen3-coder"],
|
||||||
|
"ollama_host": "100.85.116.57:11434"
|
||||||
|
},
|
||||||
|
"home": ["home-assistant", "pihole", "plex"],
|
||||||
|
"infra": ["gitea", "docker-registry", "kubernetes-dashboard"],
|
||||||
|
"other": ["ghost", "tor-controller", "speedtest-tracker"]
|
||||||
|
},
|
||||||
|
"namespaces": [
|
||||||
|
"ai-stack", "argocd", "monitoring", "loki-system", "longhorn-system",
|
||||||
|
"metallb-system", "minio", "nginx-ingress-controller", "tailscale-operator",
|
||||||
|
"gitea", "home-assistant", "pihole", "pihole2", "plex", "ghost",
|
||||||
|
"kubernetes-dashboard", "docker-registry", "k8s-agent", "tools", "vpa"
|
||||||
|
],
|
||||||
|
"urls": {
|
||||||
|
"grafana": "grafana.monitoring.192.168.153.240.nip.io",
|
||||||
|
"longhorn": "ui.longhorn-system.192.168.153.240.nip.io",
|
||||||
|
"open_webui": "oi.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"searxng": "sx.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"n8n": "n8n.ai-stack.192.168.153.240.nip.io",
|
||||||
|
"minio_console": "console.minio.192.168.153.240.nip.io",
|
||||||
|
"pihole": "pihole.192.168.153.240.nip.io",
|
||||||
|
"k8s_dashboard": "dashboard.kubernetes-dashboards.192.168.153.240.nip.io",
|
||||||
|
"home_assistant": "ha.home-assistant.192.168.153.241.nip.io",
|
||||||
|
"plex": "player.plex.192.168.153.246.nip.io"
|
||||||
|
},
|
||||||
|
"external_llm": {
|
||||||
|
"description": "Route requests to external LLMs via opencode or gemini CLI",
|
||||||
|
"state_file": "~/.claude/state/external-mode.json",
|
||||||
|
"router_dir": "~/.claude/mcp/llm-router/",
|
||||||
|
"commands": {
|
||||||
|
"toggle_on": "~/.claude/mcp/llm-router/toggle.py on --reason 'reason'",
|
||||||
|
"toggle_off": "~/.claude/mcp/llm-router/toggle.py off",
|
||||||
|
"status": "~/.claude/mcp/llm-router/toggle.py status",
|
||||||
|
"invoke": "~/.claude/mcp/llm-router/invoke.py --model MODEL -p 'prompt'"
|
||||||
|
},
|
||||||
|
"providers": ["opencode", "gemini"],
|
||||||
|
"tiers": {
|
||||||
|
"frontier": ["github-copilot/gpt-5.2", "github-copilot/gemini-3-pro-preview", "gemini/gemini-2.5-pro"],
|
||||||
|
"mid-tier": ["github-copilot/gpt-5-mini", "github-copilot/claude-sonnet-4.5", "github-copilot/gemini-3-flash-preview", "opencode/grok-code", "gemini/gemini-2.5-flash"],
|
||||||
|
"lightweight": ["opencode/gpt-5-nano", "zai-coding-plan/glm-4.5-air", "github-copilot/claude-haiku-4.5"]
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "github-copilot/gpt-5.2",
|
||||||
|
"code-generation": "github-copilot/gemini-3-pro-preview",
|
||||||
|
"long-context": "gemini/gemini-2.5-pro",
|
||||||
|
"fast": "github-copilot/gemini-3-flash-preview",
|
||||||
|
"default": "github-copilot/claude-sonnet-4.5"
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"opencode_path": "/home/linuxbrew/.linuxbrew/bin/opencode (NOT /usr/bin/opencode which crashes)",
|
||||||
|
"o3_removed": "github-copilot/o3 not available via GitHub Copilot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workstation": {
|
||||||
|
"hostname": "willlaptop",
|
||||||
|
"ip": "192.168.153.117",
|
||||||
|
"os": "Arch Linux",
|
||||||
|
"desktop": "GNOME",
|
||||||
|
"shell": "fish",
|
||||||
|
"terminal": ["ghostty", "alacritty", "gnome-console"],
|
||||||
|
"network": "systemd-networkd + iwd",
|
||||||
|
"theme": "Dracula",
|
||||||
|
"editors": ["vscode", "zed", "vim"],
|
||||||
|
"browsers": ["firefox", "chromium", "google-chrome", "zen-browser", "epiphany"],
|
||||||
|
"virtualization": ["docker", "podman", "distrobox", "virt-manager", "virtualbox", "gnome-boxes"],
|
||||||
|
"k8s_tools": ["k9s", "k0s-bin", "k0sctl-bin", "argocd", "krew", "kubecolor"],
|
||||||
|
"dev_langs": ["go", "rust", "python", "typescript", "zig", "bun", "node/npm/pnpm"],
|
||||||
|
"ai_local": {
|
||||||
|
"ollama": true,
|
||||||
|
"llama_swap": true,
|
||||||
|
"models": ["Qwen3-4b", "Gemma3-4b"]
|
||||||
|
},
|
||||||
|
"backup": ["restic", "timeshift", "btrbk", "chezmoi"],
|
||||||
|
"dotfiles": "chezmoi"
|
||||||
|
},
|
||||||
|
"repos": {
|
||||||
|
"willlaptop": {
|
||||||
|
"path": "~/Code/active/devops/willlaptop",
|
||||||
|
"remote": "git@gitea-gitea-ssh.taildb3494.ts.net:will/willlaptop.git",
|
||||||
|
"purpose": "Workstation provisioning and config",
|
||||||
|
"structure": {
|
||||||
|
"ansible/": "Machine provisioning playbooks",
|
||||||
|
"ansible/roles/common/": "Hostname, network, users, SSH config",
|
||||||
|
"ansible/roles/packages/": "Package installation (pacman, AUR, flatpak, appimage)",
|
||||||
|
"ansible/roles/packages/files/": "Package lists (pkglist.txt, aur_pkglist.txt, etc)",
|
||||||
|
"docker/": "Local Docker stacks",
|
||||||
|
"scripts/": "Utility scripts (backup, sync, networking)",
|
||||||
|
"MCP/": "MCP server configs",
|
||||||
|
"local_ollama/": "Local Ollama data"
|
||||||
|
},
|
||||||
|
"ansible_tags": ["network", "wifi", "ethernet", "users", "sshd", "pacman", "aur", "flatpak", "appimage"],
|
||||||
|
"docker_stacks": ["file_browser", "minio-longhorn-backup", "rancher-cleanup"],
|
||||||
|
"scripts": ["bridge-up.sh", "chezmoi-sync.sh", "curl-s3.sh", "kvm-bridge-setup.sh",
|
||||||
|
"rclone-sync.sh", "restic-backup.sh", "restic-clean.sh"]
|
||||||
|
},
|
||||||
|
"homelab": {
|
||||||
|
"path": "~/Code/active/devops/homelab/homelab",
|
||||||
|
"remote": "git@github.com:will666/homelab.git",
|
||||||
|
"symlink": "~/.claude/repos/homelab",
|
||||||
|
"structure": {
|
||||||
|
"ansible/": "Ansible playbooks and templates for node provisioning",
|
||||||
|
"argocd/": "ArgoCD Application manifests (one per service)",
|
||||||
|
"charts/": "Helm values and raw manifests per service",
|
||||||
|
"charts/<svc>/values.yaml": "Helm chart values override",
|
||||||
|
"charts/<svc>/manifests/": "Raw K8s manifests (non-Helm resources)",
|
||||||
|
"docker/": "Docker Compose stacks for non-K8s workloads"
|
||||||
|
},
|
||||||
|
"charts": [
|
||||||
|
"ai-stack", "argocd", "argo-workflow", "cdi-operator",
|
||||||
|
"cloudflare-tunnel-ingress-controller", "docker-registry", "ghost",
|
||||||
|
"gitea", "haproxy-ingress", "harbor", "home-assistant", "k0s-backup",
|
||||||
|
"k8s-agent-dashboard", "kube-prometheus-stack", "kubernetes-dashboard",
|
||||||
|
"kubevirt", "local-path-storage", "loki-stack", "longhorn",
|
||||||
|
"longhorn-backup", "metallb", "minio", "minio-to-mega-backup",
|
||||||
|
"nfs-server-longhorn", "nginx-ingress-controller", "pihole", "pihole2",
|
||||||
|
"plex", "speedtest-tracker", "squareffect", "squareserver",
|
||||||
|
"tailscale-operator", "tools", "tor-controller", "traefik-ingress-controller",
|
||||||
|
"willlaptop-backup", "willlaptop-monitoring", "wills-portal"
|
||||||
|
],
|
||||||
|
"docker_stacks": [
|
||||||
|
"protonvpn-proxy", "squareffect", "squareserver", "stable-diffusion-webui"
|
||||||
|
],
|
||||||
|
"conventions": {
|
||||||
|
"argocd_app": "argocd/<name>.yaml points to charts/<name>/",
|
||||||
|
"helm_values": "charts/<name>/values.yaml for Helm overrides",
|
||||||
|
"raw_manifests": "charts/<name>/manifests/ for non-Helm K8s resources",
|
||||||
|
"naming": "ArgoCD app name = namespace name (usually)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -113,5 +113,85 @@
|
|||||||
"default": "haiku",
|
"default": "haiku",
|
||||||
"escalate_on": ["insufficient_context", "reasoning_required", "user_dissatisfied"]
|
"escalate_on": ["insufficient_context", "reasoning_required", "user_dissatisfied"]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"external_models": {
|
||||||
|
"github-copilot/gpt-5.2": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gpt-5.2"],
|
||||||
|
"use_cases": ["reasoning", "fallback"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"github-copilot/claude-sonnet-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/claude-sonnet-4.5"],
|
||||||
|
"use_cases": ["general", "fallback"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"github-copilot/claude-haiku-4.5": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/claude-haiku-4.5"],
|
||||||
|
"use_cases": ["simple"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"github-copilot/gemini-3-pro-preview": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gemini-3-pro-preview"],
|
||||||
|
"use_cases": ["long-context", "reasoning"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"github-copilot/gemini-3-flash-preview": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gemini-3-flash-preview"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"gemini/gemini-2.5-pro": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-2.5-pro"],
|
||||||
|
"use_cases": ["long-context", "reasoning"],
|
||||||
|
"tier": "frontier"
|
||||||
|
},
|
||||||
|
"gemini/gemini-2.5-flash": {
|
||||||
|
"cli": "gemini",
|
||||||
|
"cli_args": ["-m", "gemini-2.5-flash"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"github-copilot/gpt-5-mini": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "github-copilot/gpt-5-mini"],
|
||||||
|
"use_cases": ["fast", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
},
|
||||||
|
"opencode/gpt-5-nano": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "opencode/gpt-5-nano"],
|
||||||
|
"use_cases": ["fast", "simple"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"zai-coding-plan/glm-4.5-air": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "zai-coding-plan/glm-4.5-air"],
|
||||||
|
"use_cases": ["simple", "fast"],
|
||||||
|
"tier": "lightweight"
|
||||||
|
},
|
||||||
|
"opencode/grok-code": {
|
||||||
|
"cli": "opencode",
|
||||||
|
"cli_args": ["-m", "opencode/grok-code"],
|
||||||
|
"use_cases": ["code-generation", "general"],
|
||||||
|
"tier": "mid-tier"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tier_to_external_map": {
|
||||||
|
"frontier": "github-copilot/gpt-5.2",
|
||||||
|
"mid-tier": "github-copilot/gpt-5-mini",
|
||||||
|
"lightweight": "opencode/gpt-5-nano"
|
||||||
|
},
|
||||||
|
"task_routing": {
|
||||||
|
"reasoning": "github-copilot/gpt-5.2",
|
||||||
|
"code-generation": "github-copilot/gemini-3-pro-preview",
|
||||||
|
"long-context": "gemini/gemini-2.5-pro",
|
||||||
|
"fast": "github-copilot/gemini-3-flash-preview",
|
||||||
|
"default": "github-copilot/claude-sonnet-4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,18 @@
|
|||||||
"status": "active",
|
"status": "active",
|
||||||
"added": "2026-01-04"
|
"added": "2026-01-04"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "f6a7b8c9-0123-45fa-1234-666666666666",
|
||||||
|
"instruction": "After reinstalling gmail-mcp package, run ~/.claude/patches/apply-gmail-auth-patch.sh to restore auto re-auth on token expiry.",
|
||||||
|
"status": "active",
|
||||||
|
"added": "2026-01-09"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a7b8c9d0-1234-56ab-2345-777777777777",
|
||||||
|
"instruction": "Homelab repo is at ~/Code/active/devops/homelab/homelab (canonical). ~/.claude/repos/homelab is a symlink to it. Always use the canonical path for new work.",
|
||||||
|
"status": "active",
|
||||||
|
"added": "2026-01-09"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "b2c3d4e5-6789-01bc-def0-222222222222",
|
"id": "b2c3d4e5-6789-01bc-def0-222222222222",
|
||||||
"instruction": "Git workflow: See CLAUDE.md for full process. Use rebase merges, not merge commits.",
|
"instruction": "Git workflow: See CLAUDE.md for full process. Use rebase merges, not merge commits.",
|
||||||
|
|||||||
Reference in New Issue
Block a user