Initial commit
This commit is contained in:
96
skills/config.validate.router/skill.yaml
Normal file
96
skills/config.validate.router/skill.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
name: config.validate.router
|
||||
version: 0.1.0
|
||||
description: Validates Claude Code Router configuration inputs for correctness, completeness, and schema compliance
|
||||
status: active
|
||||
|
||||
inputs:
|
||||
- name: llm_backends
|
||||
type: array
|
||||
required: true
|
||||
description: List of backend provider configurations
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Provider name (e.g., openrouter, ollama, claude)
|
||||
api_base_url:
|
||||
type: string
|
||||
description: Base URL for the provider API
|
||||
api_key:
|
||||
type: string
|
||||
description: API key (optional for local providers)
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of model identifiers
|
||||
required:
|
||||
- name
|
||||
- api_base_url
|
||||
- models
|
||||
|
||||
- name: routing_rules
|
||||
type: object
|
||||
required: true
|
||||
description: Dictionary mapping Claude routing contexts to provider/model pairs
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
default:
|
||||
type: object
|
||||
think:
|
||||
type: object
|
||||
background:
|
||||
type: object
|
||||
longContext:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
|
||||
outputs:
|
||||
- name: validation_result
|
||||
type: object
|
||||
description: Validation result with status and errors
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
valid:
|
||||
type: boolean
|
||||
errors:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
warnings:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
|
||||
artifact_metadata:
|
||||
consumes:
|
||||
- type: router-config-input
|
||||
description: Raw router configuration input before validation
|
||||
content_type: application/json
|
||||
schema: schemas/router-config-input.json
|
||||
|
||||
produces:
|
||||
- type: validation-report
|
||||
description: Validation report with errors and warnings
|
||||
file_pattern: "*-validation-report.json"
|
||||
content_type: application/json
|
||||
schema: schemas/validation-report.json
|
||||
|
||||
entrypoints:
|
||||
- command: /skill/config/validate/router
|
||||
handler: validate_router.py
|
||||
runtime: python
|
||||
|
||||
permissions:
|
||||
- filesystem:read
|
||||
|
||||
tags:
|
||||
- validation
|
||||
- config
|
||||
- router
|
||||
- llm
|
||||
195
skills/config.validate.router/validate_router.py
Executable file
195
skills/config.validate.router/validate_router.py
Executable file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Skill: config.validate.router
|
||||
Validates Claude Code Router configuration inputs
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
||||
class RouterConfigValidator:
|
||||
"""Validates router configuration for Claude Code Router"""
|
||||
|
||||
# Claude Code Router supports these routing contexts
|
||||
VALID_ROUTING_CONTEXTS = {"default", "think", "background", "longContext", "webSearch", "image"}
|
||||
REQUIRED_PROVIDER_FIELDS = {"name", "api_base_url", "models"}
|
||||
REQUIRED_ROUTING_FIELDS = {"provider", "model"}
|
||||
|
||||
def __init__(self):
|
||||
self.errors: List[str] = []
|
||||
self.warnings: List[str] = []
|
||||
|
||||
def validate(self, llm_backends: List[Dict[str, Any]], routing_rules: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate router configuration
|
||||
|
||||
Args:
|
||||
llm_backends: List of backend provider configs
|
||||
routing_rules: Dictionary of routing context mappings
|
||||
|
||||
Returns:
|
||||
Validation result with status, errors, and warnings
|
||||
"""
|
||||
self.errors = []
|
||||
self.warnings = []
|
||||
|
||||
# Validate backends
|
||||
self._validate_backends(llm_backends)
|
||||
|
||||
# Validate routing rules
|
||||
self._validate_routing_rules(routing_rules, llm_backends)
|
||||
|
||||
return {
|
||||
"valid": len(self.errors) == 0,
|
||||
"errors": self.errors,
|
||||
"warnings": self.warnings
|
||||
}
|
||||
|
||||
def _validate_backends(self, backends: List[Dict[str, Any]]) -> None:
|
||||
"""Validate backend provider configurations"""
|
||||
if not backends:
|
||||
self.errors.append("llm_backends cannot be empty")
|
||||
return
|
||||
|
||||
seen_names = set()
|
||||
for idx, backend in enumerate(backends):
|
||||
# Check required fields
|
||||
missing = self.REQUIRED_PROVIDER_FIELDS - set(backend.keys())
|
||||
if missing:
|
||||
self.errors.append(f"Backend {idx}: missing required fields {missing}")
|
||||
|
||||
# Check name uniqueness
|
||||
name = backend.get("name")
|
||||
if name:
|
||||
if name in seen_names:
|
||||
self.errors.append(f"Duplicate backend name: {name}")
|
||||
seen_names.add(name)
|
||||
|
||||
# Validate models list
|
||||
models = backend.get("models", [])
|
||||
if not isinstance(models, list):
|
||||
self.errors.append(f"Backend {name or idx}: 'models' must be a list")
|
||||
elif not models:
|
||||
self.errors.append(f"Backend {name or idx}: 'models' cannot be empty")
|
||||
|
||||
# Validate API base URL format
|
||||
api_base_url = backend.get("api_base_url", "")
|
||||
if api_base_url and not (api_base_url.startswith("http://") or
|
||||
api_base_url.startswith("https://")):
|
||||
self.warnings.append(
|
||||
f"Backend {name or idx}: api_base_url should start with http:// or https://"
|
||||
)
|
||||
|
||||
# Check for API key in local providers
|
||||
if "localhost" in api_base_url or "127.0.0.1" in api_base_url:
|
||||
if backend.get("api_key"):
|
||||
self.warnings.append(
|
||||
f"Backend {name or idx}: Local provider has api_key (may be unnecessary)"
|
||||
)
|
||||
elif not backend.get("api_key"):
|
||||
self.warnings.append(
|
||||
f"Backend {name or idx}: Remote provider missing api_key"
|
||||
)
|
||||
|
||||
def _validate_routing_rules(
|
||||
self,
|
||||
routing_rules: Dict[str, Any],
|
||||
backends: List[Dict[str, Any]]
|
||||
) -> None:
|
||||
"""Validate routing rule mappings"""
|
||||
if not routing_rules:
|
||||
self.errors.append("routing_rules cannot be empty")
|
||||
return
|
||||
|
||||
# Build provider-model map
|
||||
provider_models = {}
|
||||
for backend in backends:
|
||||
name = backend.get("name")
|
||||
models = backend.get("models", [])
|
||||
if name:
|
||||
provider_models[name] = set(models)
|
||||
|
||||
# Validate each routing context
|
||||
for context, rule in routing_rules.items():
|
||||
# Warn about unknown contexts
|
||||
if context not in self.VALID_ROUTING_CONTEXTS:
|
||||
self.warnings.append(f"Unknown routing context: {context}")
|
||||
|
||||
# Check required fields
|
||||
if not isinstance(rule, dict):
|
||||
self.errors.append(f"Routing rule '{context}' must be an object")
|
||||
continue
|
||||
|
||||
missing = self.REQUIRED_ROUTING_FIELDS - set(rule.keys())
|
||||
if missing:
|
||||
self.errors.append(
|
||||
f"Routing rule '{context}': missing required fields {missing}"
|
||||
)
|
||||
continue
|
||||
|
||||
provider = rule.get("provider")
|
||||
model = rule.get("model")
|
||||
|
||||
# Validate provider exists
|
||||
if provider not in provider_models:
|
||||
self.errors.append(
|
||||
f"Routing rule '{context}': unknown provider '{provider}'"
|
||||
)
|
||||
continue
|
||||
|
||||
# Validate model exists for provider
|
||||
if model not in provider_models[provider]:
|
||||
self.errors.append(
|
||||
f"Routing rule '{context}': model '{model}' not available "
|
||||
f"in provider '{provider}'"
|
||||
)
|
||||
|
||||
# Check for missing essential contexts
|
||||
essential = {"default"}
|
||||
missing_essential = essential - set(routing_rules.keys())
|
||||
if missing_essential:
|
||||
self.errors.append(f"Missing essential routing contexts: {missing_essential}")
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI entrypoint"""
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({
|
||||
"valid": False,
|
||||
"errors": ["Usage: validate_router.py <config_json>"],
|
||||
"warnings": []
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
config = json.loads(sys.argv[1])
|
||||
|
||||
validator = RouterConfigValidator()
|
||||
result = validator.validate(
|
||||
llm_backends=config.get("llm_backends", []),
|
||||
routing_rules=config.get("routing_rules", {})
|
||||
)
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(0 if result["valid"] else 1)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
print(json.dumps({
|
||||
"valid": False,
|
||||
"errors": [f"Invalid JSON: {e}"],
|
||||
"warnings": []
|
||||
}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({
|
||||
"valid": False,
|
||||
"errors": [f"Validation error: {e}"],
|
||||
"warnings": []
|
||||
}))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user