Initial commit
This commit is contained in:
247
agents/meta.config.router/README.md
Normal file
247
agents/meta.config.router/README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Agent: meta.config.router
|
||||
|
||||
## Purpose
|
||||
|
||||
Configure Claude Code Router for Betty to support multi-model LLM routing across environments. This agent creates or previews a `config.json` file at `~/.claude-code-router/config.json` with model providers, routing profiles, and audit metadata.
|
||||
|
||||
## Version
|
||||
|
||||
0.1.0
|
||||
|
||||
## Status
|
||||
|
||||
active
|
||||
|
||||
## Reasoning Mode
|
||||
|
||||
oneshot
|
||||
|
||||
## Capabilities
|
||||
|
||||
- Generate multi-model LLM router configurations
|
||||
- Validate router configuration inputs for correctness
|
||||
- Apply configurations to filesystem with audit trails
|
||||
- Support multiple output modes (preview, file, both)
|
||||
- Work across local, cloud, and CI environments
|
||||
- Ensure deterministic and portable configurations
|
||||
|
||||
## Skills Available
|
||||
|
||||
- `config.validate.router` - Validates router configuration inputs
|
||||
- `config.generate.router` - Generates router configuration JSON
|
||||
- `audit.log` - Records audit events for configuration changes
|
||||
|
||||
## Inputs
|
||||
|
||||
### llm_backends (required)
|
||||
- **Type**: List of objects
|
||||
- **Description**: Backend provider configurations
|
||||
- **Schema**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "string (e.g., openrouter, ollama, claude)",
|
||||
"api_base_url": "string (API endpoint URL)",
|
||||
"api_key": "string (optional for local providers)",
|
||||
"models": ["string (model identifiers)"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### routing_rules (required)
|
||||
- **Type**: Dictionary
|
||||
- **Description**: Mapping of Claude routing contexts to provider/model pairs
|
||||
- **Contexts**: default, think, background, longContext
|
||||
- **Schema**:
|
||||
```json
|
||||
{
|
||||
"default": { "provider": "string", "model": "string" },
|
||||
"think": { "provider": "string", "model": "string" },
|
||||
"background": { "provider": "string", "model": "string" },
|
||||
"longContext": { "provider": "string", "model": "string" }
|
||||
}
|
||||
```
|
||||
|
||||
### output_mode (optional)
|
||||
- **Type**: enum
|
||||
- **Values**: "preview" | "file" | "both"
|
||||
- **Default**: "preview"
|
||||
- **Description**: Output mode for configuration
|
||||
|
||||
### apply_config (optional)
|
||||
- **Type**: boolean
|
||||
- **Default**: false
|
||||
- **Description**: Write config to disk if true
|
||||
|
||||
### metadata (optional)
|
||||
- **Type**: object
|
||||
- **Description**: Optional audit metadata (initiator, environment, etc.)
|
||||
|
||||
## Outputs
|
||||
|
||||
### routing_config
|
||||
- **Type**: object
|
||||
- **Description**: Rendered router config as JSON
|
||||
|
||||
### write_status
|
||||
- **Type**: string
|
||||
- **Values**: "success" | "skipped" | "error"
|
||||
- **Description**: Status of file write operation
|
||||
|
||||
### audit_id
|
||||
- **Type**: string
|
||||
- **Description**: Unique trace ID for configuration event
|
||||
|
||||
## Behavior
|
||||
|
||||
1. Validates inputs via `config.validate.router`
|
||||
2. Constructs valid router config using `config.generate.router`
|
||||
3. If `apply_config=true` and `output_mode≠preview`, writes config to: `~/.claude-code-router/config.json`
|
||||
4. Outputs JSON config regardless of write action
|
||||
5. Logs audit record via `audit.log` with:
|
||||
- timestamp
|
||||
- initiator
|
||||
- hash of input
|
||||
- environment fingerprint
|
||||
|
||||
## Usage Example
|
||||
|
||||
```bash
|
||||
# Preview configuration (no file write)
|
||||
/meta/config.router --routing_config_path=router-config.yaml
|
||||
|
||||
# Apply configuration to disk
|
||||
/meta/config.router --routing_config_path=router-config.yaml --apply_config=true
|
||||
|
||||
# Both preview and write
|
||||
/meta/config.router --routing_config_path=router-config.yaml --apply_config=true --output_mode=both
|
||||
```
|
||||
|
||||
## Example Input (YAML)
|
||||
|
||||
```yaml
|
||||
llm_backends:
|
||||
- name: openrouter
|
||||
api_base_url: https://openrouter.ai/api/v1
|
||||
api_key: ${OPENROUTER_API_KEY}
|
||||
models:
|
||||
- anthropic/claude-3.5-sonnet
|
||||
- openai/gpt-4
|
||||
|
||||
- name: ollama
|
||||
api_base_url: http://localhost:11434/v1
|
||||
models:
|
||||
- llama3.1:70b
|
||||
- codellama:34b
|
||||
|
||||
routing_rules:
|
||||
default:
|
||||
provider: openrouter
|
||||
model: anthropic/claude-3.5-sonnet
|
||||
|
||||
think:
|
||||
provider: openrouter
|
||||
model: anthropic/claude-3.5-sonnet
|
||||
|
||||
background:
|
||||
provider: ollama
|
||||
model: llama3.1:70b
|
||||
|
||||
longContext:
|
||||
provider: openrouter
|
||||
model: anthropic/claude-3.5-sonnet
|
||||
|
||||
metadata:
|
||||
initiator: user@example.com
|
||||
environment: production
|
||||
purpose: Multi-model routing for development
|
||||
```
|
||||
|
||||
## Example Output
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"generated_at": "2025-11-01T12:34:56Z",
|
||||
"backends": [
|
||||
{
|
||||
"name": "openrouter",
|
||||
"api_base_url": "https://openrouter.ai/api/v1",
|
||||
"api_key": "${OPENROUTER_API_KEY}",
|
||||
"models": [
|
||||
"anthropic/claude-3.5-sonnet",
|
||||
"openai/gpt-4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ollama",
|
||||
"api_base_url": "http://localhost:11434/v1",
|
||||
"models": [
|
||||
"llama3.1:70b",
|
||||
"codellama:34b"
|
||||
]
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"default": {
|
||||
"provider": "openrouter",
|
||||
"model": "anthropic/claude-3.5-sonnet"
|
||||
},
|
||||
"think": {
|
||||
"provider": "openrouter",
|
||||
"model": "anthropic/claude-3.5-sonnet"
|
||||
},
|
||||
"background": {
|
||||
"provider": "ollama",
|
||||
"model": "llama3.1:70b"
|
||||
},
|
||||
"longContext": {
|
||||
"provider": "openrouter",
|
||||
"model": "anthropic/claude-3.5-sonnet"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"generated_by": "meta.config.router",
|
||||
"schema_version": "1.0.0",
|
||||
"initiator": "user@example.com",
|
||||
"environment": "production",
|
||||
"purpose": "Multi-model routing for development"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
- `filesystem:read` - Read router config input files
|
||||
- `filesystem:write` - Write config to ~/.claude-code-router/config.json
|
||||
|
||||
## Artifacts
|
||||
|
||||
### Consumes
|
||||
- `router-config-input` - User-provided router configuration inputs
|
||||
|
||||
### Produces
|
||||
- `llm-router-config` - Complete Claude Code Router configuration file
|
||||
- `audit-log-entry` - Audit trail entry for configuration events
|
||||
|
||||
## Tags
|
||||
|
||||
llm, router, configuration, meta, infra, openrouter, claude, ollama, multi-model
|
||||
|
||||
## Environments
|
||||
|
||||
- local
|
||||
- cloud
|
||||
- ci
|
||||
|
||||
## Requires Human Approval
|
||||
|
||||
false
|
||||
|
||||
## Notes
|
||||
|
||||
- The config is deterministic and portable across environments
|
||||
- API keys can use environment variable substitution (e.g., ${OPENROUTER_API_KEY})
|
||||
- Local providers (localhost/127.0.0.1) don't require API keys
|
||||
- All configuration changes are audited for traceability
|
||||
- The agent supports preview mode to verify configuration before applying
|
||||
92
agents/meta.config.router/agent.yaml
Normal file
92
agents/meta.config.router/agent.yaml
Normal file
@@ -0,0 +1,92 @@
|
||||
name: meta.config.router
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Configure Claude Code Router for Betty to support multi-model LLM routing across environments.
|
||||
This agent creates or previews a config.json file at ~/.claude-code-router/config.json with
|
||||
model providers, routing profiles, and audit metadata. Works across local, cloud, or CI-based
|
||||
environments with built-in validation, output rendering, config application, and auditing.
|
||||
status: active
|
||||
reasoning_mode: oneshot
|
||||
|
||||
capabilities:
|
||||
- Generate multi-model LLM router configurations
|
||||
- Validate router configuration inputs for correctness
|
||||
- Apply configurations to filesystem with audit trails
|
||||
- Support multiple output modes (preview, file, both)
|
||||
- Work across local, cloud, and CI environments
|
||||
- Ensure deterministic and portable configurations
|
||||
|
||||
skills_available:
|
||||
- config.validate.router
|
||||
- config.generate.router
|
||||
- audit.log
|
||||
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
|
||||
artifact_metadata:
|
||||
consumes:
|
||||
- type: router-config-input
|
||||
description: User-provided router configuration inputs (backends, routing rules, metadata)
|
||||
file_pattern: "*-router-input.{json,yaml}"
|
||||
content_type: application/json
|
||||
required: true
|
||||
|
||||
produces:
|
||||
- type: llm-router-config
|
||||
description: Complete Claude Code Router configuration file
|
||||
file_pattern: "config.json"
|
||||
content_type: application/json
|
||||
schema: schemas/router-config.json
|
||||
|
||||
- type: audit-log-entry
|
||||
description: Audit trail entry for configuration events
|
||||
file_pattern: "audit_log.json"
|
||||
content_type: application/json
|
||||
|
||||
system_prompt: |
|
||||
You are the meta.config.router agent for the Betty Framework.
|
||||
|
||||
Your responsibilities:
|
||||
1. Validate router configuration inputs using config.validate.router
|
||||
2. Generate valid router config JSON using config.generate.router
|
||||
3. Write config to ~/.claude-code-router/config.json when apply_config=true
|
||||
4. Provide preview, file write, or both modes based on output_mode
|
||||
5. Log audit records with timestamp, initiator, and environment fingerprint
|
||||
|
||||
Inputs you expect:
|
||||
- llm_backends: List of provider configs (name, api_base_url, api_key, models)
|
||||
- routing_rules: Mapping of routing contexts (default, think, background, longContext)
|
||||
- output_mode: "preview" | "file" | "both" (default: preview)
|
||||
- apply_config: boolean (write to disk if true)
|
||||
- metadata: Optional audit metadata (initiator, environment, etc.)
|
||||
|
||||
Outputs you generate:
|
||||
- routing_config: Complete router configuration JSON
|
||||
- write_status: "success" | "skipped" | "error"
|
||||
- audit_id: Unique trace ID for the configuration event
|
||||
|
||||
Workflow:
|
||||
1. Call config.validate.router with llm_backends and routing_rules
|
||||
2. If validation fails, return errors and exit
|
||||
3. Call config.generate.router to create the config JSON
|
||||
4. If apply_config=true and output_mode≠preview, write to ~/.claude-code-router/config.json
|
||||
5. Call audit.log to record the configuration event
|
||||
6. Return config, write status, and audit ID
|
||||
|
||||
Environment awareness:
|
||||
- Detect local vs cloud vs CI environment
|
||||
- Adjust file paths accordingly
|
||||
- Include environment fingerprint in audit metadata
|
||||
|
||||
tags:
|
||||
- llm
|
||||
- router
|
||||
- configuration
|
||||
- meta
|
||||
- infra
|
||||
- openrouter
|
||||
- claude
|
||||
- ollama
|
||||
- multi-model
|
||||
323
agents/meta.config.router/meta_config_router.py
Executable file
323
agents/meta.config.router/meta_config_router.py
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent: meta.config.router
|
||||
Configure Claude Code Router for multi-model LLM support
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
import subprocess
|
||||
import yaml
|
||||
|
||||
|
||||
class MetaConfigRouter:
|
||||
"""Configure Claude Code Router for Betty Framework"""
|
||||
|
||||
def __init__(self):
|
||||
self.betty_root = Path(__file__).parent.parent.parent
|
||||
self.skills_root = self.betty_root / "skills"
|
||||
self.audit_log_path = self.betty_root / "registry" / "audit_log.json"
|
||||
|
||||
def run(
|
||||
self,
|
||||
routing_config_path: str,
|
||||
apply_config: bool = False,
|
||||
output_mode: str = "preview"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Main execution method
|
||||
|
||||
Args:
|
||||
routing_config_path: Path to router config input file (YAML or JSON)
|
||||
apply_config: Whether to write config to disk
|
||||
output_mode: "preview" | "file" | "both"
|
||||
|
||||
Returns:
|
||||
Result with routing_config, write_status, and audit_id
|
||||
"""
|
||||
print(f"🔧 meta.config.router v0.1.0")
|
||||
print(f"📋 Config input: {routing_config_path}")
|
||||
print(f"📝 Output mode: {output_mode}")
|
||||
print(f"💾 Apply config: {apply_config}")
|
||||
print()
|
||||
|
||||
# Load input config
|
||||
config_input = self._load_config_input(routing_config_path)
|
||||
|
||||
# Extract inputs
|
||||
llm_backends = config_input.get("llm_backends", [])
|
||||
routing_rules = config_input.get("routing_rules", {})
|
||||
config_options = config_input.get("config_options", {})
|
||||
metadata = config_input.get("metadata", {}) # For audit logging only
|
||||
|
||||
# Step 1: Validate inputs
|
||||
print("🔍 Validating router configuration...")
|
||||
validation_result = self._validate_config(llm_backends, routing_rules)
|
||||
|
||||
if not validation_result["valid"]:
|
||||
print("❌ Validation failed:")
|
||||
for error in validation_result["errors"]:
|
||||
print(f" - {error}")
|
||||
return {
|
||||
"success": False,
|
||||
"errors": validation_result["errors"],
|
||||
"warnings": validation_result["warnings"]
|
||||
}
|
||||
|
||||
if validation_result["warnings"]:
|
||||
print("⚠️ Warnings:")
|
||||
for warning in validation_result["warnings"]:
|
||||
print(f" - {warning}")
|
||||
|
||||
print("✅ Validation passed")
|
||||
print()
|
||||
|
||||
# Step 2: Generate router config
|
||||
print("🏗️ Generating router configuration...")
|
||||
router_config = self._generate_config(
|
||||
llm_backends,
|
||||
routing_rules,
|
||||
config_options
|
||||
)
|
||||
print("✅ Configuration generated")
|
||||
print()
|
||||
|
||||
# Step 3: Write config if requested
|
||||
write_status = "skipped"
|
||||
config_path = None
|
||||
|
||||
if apply_config and output_mode != "preview":
|
||||
print("💾 Writing configuration to disk...")
|
||||
config_path, write_status = self._write_config(router_config)
|
||||
|
||||
if write_status == "success":
|
||||
print(f"✅ Configuration written to: {config_path}")
|
||||
else:
|
||||
print(f"❌ Failed to write configuration")
|
||||
print()
|
||||
|
||||
# Step 4: Log audit record
|
||||
print("📝 Logging audit record...")
|
||||
audit_id = self._log_audit(
|
||||
config_input=config_input,
|
||||
write_status=write_status,
|
||||
metadata=metadata
|
||||
)
|
||||
print(f"✅ Audit ID: {audit_id}")
|
||||
print()
|
||||
|
||||
# Step 5: Output results
|
||||
result = {
|
||||
"success": True,
|
||||
"routing_config": router_config,
|
||||
"write_status": write_status,
|
||||
"audit_id": audit_id
|
||||
}
|
||||
|
||||
if config_path:
|
||||
result["config_path"] = str(config_path)
|
||||
|
||||
# Display preview if requested
|
||||
if output_mode in ["preview", "both"]:
|
||||
print("📄 Router Configuration Preview:")
|
||||
print("─" * 80)
|
||||
print(json.dumps(router_config, indent=2))
|
||||
print("─" * 80)
|
||||
print()
|
||||
|
||||
return result
|
||||
|
||||
def _load_config_input(self, config_path: str) -> Dict[str, Any]:
|
||||
"""Load router config input from YAML or JSON file"""
|
||||
path = Path(config_path)
|
||||
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Config file not found: {config_path}")
|
||||
|
||||
with open(path, 'r') as f:
|
||||
if path.suffix in ['.yaml', '.yml']:
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
return json.load(f)
|
||||
|
||||
def _validate_config(
|
||||
self,
|
||||
llm_backends: List[Dict[str, Any]],
|
||||
routing_rules: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Validate router configuration using config.validate.router skill"""
|
||||
validator_script = self.skills_root / "config.validate.router" / "validate_router.py"
|
||||
|
||||
config_json = json.dumps({
|
||||
"llm_backends": llm_backends,
|
||||
"routing_rules": routing_rules
|
||||
})
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(validator_script), config_json],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
return {
|
||||
"valid": False,
|
||||
"errors": [f"Validation error: {e}"],
|
||||
"warnings": []
|
||||
}
|
||||
|
||||
def _generate_config(
|
||||
self,
|
||||
llm_backends: List[Dict[str, Any]],
|
||||
routing_rules: Dict[str, Any],
|
||||
config_options: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate router configuration using config.generate.router skill"""
|
||||
generator_script = self.skills_root / "config.generate.router" / "generate_router.py"
|
||||
|
||||
input_json = json.dumps({
|
||||
"llm_backends": llm_backends,
|
||||
"routing_rules": routing_rules,
|
||||
"config_options": config_options
|
||||
})
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(generator_script), input_json],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
return json.loads(result.stdout)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Config generation failed: {e}")
|
||||
|
||||
def _write_config(self, router_config: Dict[str, Any]) -> tuple[Path, str]:
|
||||
"""Write router config to ~/.claude-code-router/config.json"""
|
||||
try:
|
||||
config_dir = Path.home() / ".claude-code-router"
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
config_path = config_dir / "config.json"
|
||||
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(router_config, f, indent=2)
|
||||
|
||||
return config_path, "success"
|
||||
except Exception as e:
|
||||
print(f"Error writing config: {e}")
|
||||
return None, "error"
|
||||
|
||||
def _log_audit(
|
||||
self,
|
||||
config_input: Dict[str, Any],
|
||||
write_status: str,
|
||||
metadata: Dict[str, Any]
|
||||
) -> str:
|
||||
"""Log audit record for configuration event"""
|
||||
audit_id = str(uuid.uuid4())
|
||||
|
||||
# Calculate hash of input
|
||||
input_hash = hashlib.sha256(
|
||||
json.dumps(config_input, sort_keys=True).encode()
|
||||
).hexdigest()[:16]
|
||||
|
||||
audit_entry = {
|
||||
"audit_id": audit_id,
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
"agent": "meta.config.router",
|
||||
"version": "0.1.0",
|
||||
"action": "router_config_generated",
|
||||
"write_status": write_status,
|
||||
"input_hash": input_hash,
|
||||
"environment": self._detect_environment(),
|
||||
"initiator": metadata.get("initiator", "unknown"),
|
||||
"metadata": metadata
|
||||
}
|
||||
|
||||
# Append to audit log
|
||||
try:
|
||||
if self.audit_log_path.exists():
|
||||
with open(self.audit_log_path, 'r') as f:
|
||||
audit_log = json.load(f)
|
||||
else:
|
||||
audit_log = []
|
||||
|
||||
audit_log.append(audit_entry)
|
||||
|
||||
with open(self.audit_log_path, 'w') as f:
|
||||
json.dump(audit_log, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to write audit log: {e}")
|
||||
|
||||
return audit_id
|
||||
|
||||
def _detect_environment(self) -> str:
|
||||
"""Detect execution environment (local, cloud, ci)"""
|
||||
if os.getenv("CI"):
|
||||
return "ci"
|
||||
elif os.getenv("CLOUD_ENV"):
|
||||
return "cloud"
|
||||
else:
|
||||
return "local"
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entrypoint"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: meta_config_router.py <routing_config_path> [--apply_config] [--output_mode=<mode>]")
|
||||
print()
|
||||
print("Arguments:")
|
||||
print(" routing_config_path Path to router config input file (YAML or JSON)")
|
||||
print(" --apply_config Write config to ~/.claude-code-router/config.json")
|
||||
print(" --output_mode=MODE Output mode: preview, file, or both (default: preview)")
|
||||
sys.exit(1)
|
||||
|
||||
# Parse arguments
|
||||
routing_config_path = sys.argv[1]
|
||||
apply_config = "--apply_config" in sys.argv or "--apply-config" in sys.argv
|
||||
output_mode = "preview"
|
||||
|
||||
for arg in sys.argv[2:]:
|
||||
if arg.startswith("--output_mode=") or arg.startswith("--output-mode="):
|
||||
output_mode = arg.split("=")[1]
|
||||
|
||||
# Run agent
|
||||
agent = MetaConfigRouter()
|
||||
try:
|
||||
result = agent.run(
|
||||
routing_config_path=routing_config_path,
|
||||
apply_config=apply_config,
|
||||
output_mode=output_mode
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
print("✅ meta.config.router completed successfully")
|
||||
print(f"📋 Audit ID: {result['audit_id']}")
|
||||
print(f"💾 Write status: {result['write_status']}")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("❌ meta.config.router failed")
|
||||
for error in result.get("errors", []):
|
||||
print(f" - {error}")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user