diff --git a/.env.example b/.env.example index 9378e66..27bc16d 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,33 @@ # Shannon Environment Configuration # Copy this file to .env and fill in your credentials -# Anthropic API Key (required - choose one) +# Recommended output token configuration for larger tool outputs +CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 + +# ============================================================================= +# OPTION 1: Direct Anthropic (default, no router) +# ============================================================================= ANTHROPIC_API_KEY=your-api-key-here # OR use OAuth token instead # CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here + +# ============================================================================= +# OPTION 2: Router Mode (use alternative providers) +# ============================================================================= +# Enable router mode by running: ./shannon start ... ROUTER=true +# Then configure ONE of the providers below: + +# --- OpenAI --- +# OPENAI_API_KEY=sk-your-openai-key +# ROUTER_DEFAULT=openai,gpt-4o + +# --- Google Gemini --- +GEMINI_API_KEY=your-gemini-key +ROUTER_DEFAULT=gemini,gemini-2.5-pro + +# ============================================================================= +# Available Models +# ============================================================================= +# OpenAI: gpt-4o, gpt-4o-mini +# Gemini: gemini-2.5-pro, gemini-2.5-flash diff --git a/CLAUDE.md b/CLAUDE.md index 04bce8c..2d49db7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,7 @@ CONFIG= YAML configuration file for authentication and testing pa OUTPUT= Custom output directory for session folder (default: ./audit-logs/) PIPELINE_TESTING=true Use minimal prompts and fast retry intervals (10s instead of 5min) REBUILD=true Force Docker rebuild with --no-cache (use when code changes aren't picked up) +ROUTER=true Route requests through claude-code-router for multi-model support (see limitations below) ``` ### Generate TOTP for Authentication @@ -284,6 +285,16 @@ Missing tools can be skipped using `PIPELINE_TESTING=true` mode during developme - `subfinder` - Subdomain discovery - `whatweb` - Web technology detection +### Router Mode Limitations +When using `ROUTER=true` to route requests through claude-code-router (e.g., to use OpenAI models): + +**Cost tracking shows $0.00**: The Claude Agent SDK expects `total_cost_usd` in the result message, which is Anthropic-specific. OpenAI's API returns token counts in `usage` but not a cost field, and the router doesn't translate this. This is a known limitation of the router, not a Shannon bug. + +**Workarounds:** +- Accept $0 costs when using router mode (recommended for dev/testing) +- Use Anthropic directly for production runs where cost tracking matters +- Use external tools like `ccusage` for post-hoc token analysis + ### Diagnostic & Utility Scripts ```bash # View Temporal workflow history diff --git a/configs/router-config.json b/configs/router-config.json new file mode 100644 index 0000000..8d043e4 --- /dev/null +++ b/configs/router-config.json @@ -0,0 +1,31 @@ +{ + "HOST": "0.0.0.0", + "APIKEY": "shannon-router-key", + "LOG": true, + "LOG_LEVEL": "info", + "NON_INTERACTIVE_MODE": true, + "API_TIMEOUT_MS": 600000, + "Providers": [ + { + "name": "openai", + "api_base_url": "https://api.openai.com/v1/chat/completions", + "api_key": "$OPENAI_API_KEY", + "models": ["gpt-4o", "gpt-4o-mini"], + "transformer": { + "use": [["maxtoken", { "max_tokens": 16384 }]] + } + }, + { + "name": "gemini", + "api_base_url": "https://generativelanguage.googleapis.com/v1beta/models/", + "api_key": "$GEMINI_API_KEY", + "models": ["gemini-2.5-pro", "gemini-2.5-flash"], + "transformer": { + "use": ["gemini"] + } + } + ], + "Router": { + "default": "$ROUTER_DEFAULT" + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 211e2bf..afb7d3d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -20,6 +20,8 @@ services: environment: - TEMPORAL_ADDRESS=temporal:7233 - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-} # Optional: route through claude-code-router + - ANTHROPIC_AUTH_TOKEN=${ANTHROPIC_AUTH_TOKEN:-} # Auth token for router - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-} - CLAUDE_CODE_MAX_OUTPUT_TOKENS=${CLAUDE_CODE_MAX_OUTPUT_TOKENS:-64000} depends_on: @@ -36,5 +38,33 @@ services: security_opt: - seccomp:unconfined + # Optional: claude-code-router for multi-model support + # Start with: ROUTER=true ./shannon start ... + router: + image: node:20-slim + profiles: ["router"] # Only starts when explicitly requested + command: > + sh -c "apt-get update && apt-get install -y gettext-base && + npm install -g @musistudio/claude-code-router && + mkdir -p /root/.claude-code-router && + envsubst < /config/router-config.json > /root/.claude-code-router/config.json && + ccr start" + ports: + - "3456:3456" + volumes: + - ./configs/router-config.json:/config/router-config.json:ro + environment: + - HOST=0.0.0.0 + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} + - OPENAI_API_KEY=${OPENAI_API_KEY:-} + - GEMINI_API_KEY=${GEMINI_API_KEY:-} + - ROUTER_DEFAULT=${ROUTER_DEFAULT:-openai,gpt-4o} + healthcheck: + test: ["CMD", "node", "-e", "require('http').get('http://localhost:3456/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + volumes: temporal-data: diff --git a/shannon b/shannon index 61f8a4e..c979d76 100755 --- a/shannon +++ b/shannon @@ -35,6 +35,7 @@ Options for 'start': CONFIG= Configuration file (YAML) OUTPUT= Output directory for reports (default: ./audit-logs/) PIPELINE_TESTING=true Use minimal prompts for fast testing + ROUTER=true Route requests through claude-code-router (multi-model support) Options for 'stop': CLEAN=true Remove all data including volumes @@ -63,6 +64,7 @@ parse_args() { CLEAN=*) CLEAN="${arg#CLEAN=}" ;; PIPELINE_TESTING=*) PIPELINE_TESTING="${arg#PIPELINE_TESTING=}" ;; REBUILD=*) REBUILD="${arg#REBUILD=}" ;; + ROUTER=*) ROUTER="${arg#ROUTER=}" ;; esac done } @@ -121,10 +123,16 @@ cmd_start() { exit 1 fi - # Check for API key + # Check for API key (router mode can use OPENAI_API_KEY or GEMINI_API_KEY instead) if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then - echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" - exit 1 + if [ "$ROUTER" = "true" ] && { [ -n "$OPENAI_API_KEY" ] || [ -n "$GEMINI_API_KEY" ]; }; then + # Router mode with alternative provider - set a placeholder for SDK init + export ANTHROPIC_API_KEY="router-mode" + else + echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" + echo " (or use ROUTER=true with OPENAI_API_KEY or GEMINI_API_KEY)" + exit 1 + fi fi # Determine container path for REPO @@ -149,6 +157,33 @@ cmd_start() { export OUTPUT_DIR="$OUTPUT" fi + # Handle ROUTER flag - start claude-code-router for multi-model support + if [ "$ROUTER" = "true" ]; then + # Check if router is already running + if docker compose -f "$COMPOSE_FILE" --profile router ps router 2>/dev/null | grep -q "running"; then + echo "Router already running, skipping startup..." + else + echo "Starting claude-code-router..." + + # Check for OpenAI API key + if [ -z "$OPENAI_API_KEY" ] && [ -z "$GEMINI_API_KEY" ]; then + echo "WARNING: Neither OPENAI_API_KEY nor GEMINI_API_KEY set. Router may not work." + fi + + # Start router with profile + docker compose -f "$COMPOSE_FILE" --profile router up -d router + + # Give router a few seconds to start (health check disabled for now - TODO: debug later) + echo "Waiting for router to start..." + sleep 5 + fi + + # Set ANTHROPIC_BASE_URL to route through router + export ANTHROPIC_BASE_URL="http://router:3456" + # Set auth token to match router's APIKEY + export ANTHROPIC_AUTH_TOKEN="shannon-router-key" + fi + # Ensure containers are running (starts them if needed) ensure_containers @@ -226,9 +261,9 @@ cmd_stop() { parse_args "$@" if [ "$CLEAN" = "true" ]; then - docker compose -f "$COMPOSE_FILE" down -v + docker compose -f "$COMPOSE_FILE" --profile router down -v else - docker compose -f "$COMPOSE_FILE" down + docker compose -f "$COMPOSE_FILE" --profile router down fi }