Discount Claude Code – Script
Save the script as a .sh file and you should be able to install it without needing to go through the guide
#!/usr/bin/env bash
#
# install-claude-code-openrouter.sh
#
# Interactive installer for an always-on Claude Code environment that routes
# entirely through OpenRouter via a LiteLLM gateway and the Claude Code Router
# (CCR). Sets up three cost-tiered models with a failover mesh and manual
# /model switching. No local model, no Azure, no classifier.
#
# Run as your UNPRIVILEGED service user (the one that owns rootless Docker),
# in a real SSH login session (never via su). It will use sudo only for the
# system-package and Docker-engine phases, prompting as needed.
#
# Safe to re-run: every file it writes is backed up first, and every step
# that changes the system asks for confirmation.
#
set -uo pipefail
# ---------------------------------------------------------------------------
# Pretty output helpers
# ---------------------------------------------------------------------------
if [ -t 1 ]; then
BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GRN=$'\033[32m'
YLW=$'\033[33m'; BLU=$'\033[34m'; RST=$'\033[0m'
else
BOLD=""; DIM=""; RED=""; GRN=""; YLW=""; BLU=""; RST=""
fi
say() { printf "%s\n" "$*"; }
info() { printf "%s[*]%s %s\n" "$BLU" "$RST" "$*"; }
ok() { printf "%s[ok]%s %s\n" "$GRN" "$RST" "$*"; }
warn() { printf "%s[!]%s %s\n" "$YLW" "$RST" "$*"; }
err() { printf "%s[x]%s %s\n" "$RED" "$RST" "$*" >&2; }
hdr() { printf "\n%s===== %s =====%s\n" "$BOLD" "$*" "$RST"; }
die() { err "$*"; exit 1; }
# Ask a yes/no question, default No unless DEFY given.
confirm() {
local prompt="$1" default="${2:-N}" reply
local hint="[y/N]"; [ "$default" = "Y" ] && hint="[Y/n]"
while true; do
printf "%s %s " "$prompt" "$hint"
read -r reply || reply=""
reply="${reply:-$default}"
case "$reply" in
[Yy]*) return 0 ;;
[Nn]*) return 1 ;;
*) echo "Please answer y or n." ;;
esac
done
}
# Prompt for a non-empty value, showing an example. Echoes result on stdout.
ask() {
# ask "Label" "example" [allow_empty]
local label="$1" example="$2" allow_empty="${3:-no}" val=""
while true; do
printf "%s\n" "$label" >&2
[ -n "$example" ] && printf " %sexample: %s%s\n" "$DIM" "$example" "$RST" >&2
printf "> " >&2
read -r val || val=""
if [ -z "$val" ] && [ "$allow_empty" != "yes" ]; then
warn "Value cannot be empty."
continue
fi
break
done
printf "%s" "$val"
}
# Prompt for a secret (no echo). Echoes result on stdout.
ask_secret() {
local label="$1" example="$2" val=""
while true; do
printf "%s\n" "$label" >&2
[ -n "$example" ] && printf " %sexample: %s%s\n" "$DIM" "$example" "$RST" >&2
printf "> " >&2
read -rs val || val=""
printf "\n" >&2
if [ -z "$val" ]; then warn "Value cannot be empty."; continue; fi
break
done
printf "%s" "$val"
}
backup_file() {
local f="$1"
if [ -e "$f" ]; then
local b="${f}.bak.$(date +%Y%m%d-%H%M%S)"
cp -a "$f" "$b" && info "Backed up $f -> $b"
fi
}
need_cmd() { command -v "$1" >/dev/null 2>&1; }
# ---------------------------------------------------------------------------
# Pre-flight
# ---------------------------------------------------------------------------
hdr "Claude Code + OpenRouter installer"
say "This sets up Claude Code to run through OpenRouter models via a LiteLLM"
say "gateway and the Claude Code Router, with three switchable cost tiers and"
say "automatic failover. It will ask you a few questions, then build everything."
say ""
say "${DIM}You can stop any time with Ctrl-C. Files are backed up before changes.${RST}"
if [ "$(id -u)" = "0" ]; then
die "Do not run this as root. Run as your unprivileged service user (e.g. the 'claude' user) over SSH."
fi
SERVICE_USER="$(id -un)"
HOME_DIR="$HOME"
info "Running as service user: ${BOLD}$SERVICE_USER${RST} (home: $HOME_DIR)"
if ! confirm "Is this the correct unprivileged service user to install under?" "Y"; then
die "Re-run as the correct user."
fi
# sudo availability (needed for system phases)
HAVE_SUDO=no
if need_cmd sudo && sudo -v 2>/dev/null; then HAVE_SUDO=yes; fi
# ---------------------------------------------------------------------------
# Gather all inputs up front
# ---------------------------------------------------------------------------
hdr "Step 1 of 7: Collect your settings"
say "You will choose THREE OpenRouter models as cost tiers (cheapest to priciest)."
say "For each, you need its OpenRouter slug (the provider/model identifier from"
say "the model's OpenRouter page) and a short friendly name you'll use to switch"
say "to it. Names must be lowercase letters, digits, and hyphens only."
say ""
validate_name() {
# lowercase letters, digits, hyphens; not empty; not starting/ending with hyphen
[[ "$1" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]
}
ask_tier() {
# ask_tier "Tier label" "name-example" "slug-example"
# sets globals: __NAME __SLUG
local tier_label="$1" name_ex="$2" slug_ex="$3" nm sl
while true; do
nm="$(ask "${tier_label} -- friendly name:" "$name_ex")"
if validate_name "$nm"; then break; fi
warn "Name must be lowercase letters/digits/hyphens, e.g. $name_ex"
done
sl="$(ask "${tier_label} -- OpenRouter slug:" "$slug_ex")"
__NAME="$nm"; __SLUG="$sl"
}
ask_tier "Tier 1 (CHEAPEST / budget)" "deepseek-flash" "deepseek/deepseek-v4-flash"
BUDGET_NAME="$__NAME"; BUDGET_SLUG="$__SLUG"
ask_tier "Tier 2 (MID)" "mimo" "xiaomi/mimo-v2.5-pro"
MID_NAME="$__NAME"; MID_SLUG="$__SLUG"
ask_tier "Tier 3 (TOP / best quality)" "qwen-max" "qwen/qwen3.7-max"
TOP_NAME="$__NAME"; TOP_SLUG="$__SLUG"
# uniqueness check on names
if [ "$BUDGET_NAME" = "$MID_NAME" ] || [ "$BUDGET_NAME" = "$TOP_NAME" ] || [ "$MID_NAME" = "$TOP_NAME" ]; then
die "The three tier names must be different from each other."
fi
say ""
say "Which tier should be the DEFAULT (used unless you switch mid-session)?"
say " 1) $BUDGET_NAME (cheapest -- start cheap, escalate by hand)"
say " 2) $MID_NAME"
say " 3) $TOP_NAME ${GRN}(recommended)${RST} best quality, still far cheaper than Anthropic"
say "${DIM}The recommended choice is the top tier: everyday sessions get the best"
say "quality automatically, and you drop to a cheaper tier by hand to economize.${RST}"
DEFAULT_NAME=""
while [ -z "$DEFAULT_NAME" ]; do
printf "> choose 1, 2, or 3 [default: 3]: "
read -r choice || choice=""
choice="${choice:-3}"
case "$choice" in
1) DEFAULT_NAME="$BUDGET_NAME" ;;
2) DEFAULT_NAME="$MID_NAME" ;;
3) DEFAULT_NAME="$TOP_NAME" ;;
*) warn "Enter 1, 2, or 3." ;;
esac
done
ok "Default tier: $DEFAULT_NAME"
say ""
say "Reasoning on the budget tier: the cheapest model can run at maximum"
say "reasoning effort for better answers, at the cost of speed. The other two"
say "tiers run without forced reasoning."
BUDGET_REASONING=no
if confirm "Enable MAX reasoning on the budget tier ($BUDGET_NAME)?" "Y"; then
BUDGET_REASONING=yes
fi
say ""
OPENROUTER_KEY="$(ask_secret "Your OpenRouter API key (input hidden):" "sk-or-v1-...")"
# Secrets: reuse existing master/router keys if a secrets file already has them
SECRETS="$HOME_DIR/.config/claude-secrets.env"
EXISTING_MASTER=""; EXISTING_CCR=""
if [ -f "$SECRETS" ]; then
EXISTING_MASTER="$(grep -E '^LITELLM_MASTER_KEY=' "$SECRETS" | head -1 | cut -d= -f2- || true)"
EXISTING_CCR="$(grep -E '^CCR_APIKEY=' "$SECRETS" | head -1 | cut -d= -f2- || true)"
fi
gen_key() {
if need_cmd openssl; then openssl rand -hex 32
else head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n'; fi
}
if [ -n "$EXISTING_MASTER" ]; then
LITELLM_MASTER_KEY="$EXISTING_MASTER"; info "Reusing existing LITELLM_MASTER_KEY."
else
LITELLM_MASTER_KEY="sk-local-$(gen_key)"; info "Generated a new LITELLM_MASTER_KEY."
fi
if [ -n "$EXISTING_CCR" ]; then
CCR_APIKEY="$EXISTING_CCR"; info "Reusing existing CCR_APIKEY."
else
CCR_APIKEY="$(gen_key)"; info "Generated a new CCR_APIKEY."
fi
# ---------------------------------------------------------------------------
# Summary + go/no-go
# ---------------------------------------------------------------------------
hdr "Review"
cat <<SUMMARY
Service user: $SERVICE_USER
Home: $HOME_DIR
Budget tier: $BUDGET_NAME -> openai/$BUDGET_SLUG (max reasoning: $BUDGET_REASONING)
Mid tier: $MID_NAME -> openai/$MID_SLUG
Top tier: $TOP_NAME -> openai/$TOP_SLUG
Default tier: $DEFAULT_NAME
Secrets file: $SECRETS
Gateway config: $HOME_DIR/litellm/config.yaml
Router config: $HOME_DIR/.claude-code-router/config.json
Router service: ccr.service (systemd --user)
OpenRouter key: ${OPENROUTER_KEY:0:10}... (hidden)
SUMMARY
say ""
if ! confirm "Proceed with installation using these settings?" "Y"; then
die "Aborted by user. Nothing was changed."
fi
# ---------------------------------------------------------------------------
# Step 2: system packages + toolchain (optional / sudo)
# ---------------------------------------------------------------------------
hdr "Step 2 of 7: System packages and toolchain"
if [ "$HAVE_SUDO" = "yes" ]; then
if confirm "Install/refresh base packages (curl, git, build tools, etc.) via apt?" "Y"; then
sudo apt update && sudo apt -y install \
ca-certificates curl wget gnupg git build-essential ripgrep jq htop tmux \
python3 python3-venv pipx || warn "apt step had issues; continuing."
ok "Base packages done."
else
info "Skipping base packages."
fi
else
warn "sudo not available; skipping base-package install. Ensure curl, git, python3 are present."
fi
# mise + node + python
if ! need_cmd mise && [ ! -x "$HOME_DIR/.local/bin/mise" ]; then
if confirm "Install 'mise' version manager (for Node/Python)?" "Y"; then
curl -fsSL https://mise.run | sh || die "mise install failed."
if ! grep -q 'mise activate bash' "$HOME_DIR/.bashrc" 2>/dev/null; then
echo 'eval "$(~/.local/bin/mise activate bash)"' >> "$HOME_DIR/.bashrc"
fi
fi
fi
# activate mise for this script
if [ -x "$HOME_DIR/.local/bin/mise" ]; then
eval "$("$HOME_DIR/.local/bin/mise" activate bash)" 2>/dev/null || true
"$HOME_DIR/.local/bin/mise" use -g node@22 python@3.12 >/dev/null 2>&1 || warn "mise use node/python had issues."
fi
# Resolve node
NODE_BIN=""
if need_cmd node; then NODE_BIN="$(readlink -f "$(command -v node)")"; fi
if [ -z "$NODE_BIN" ]; then
die "Node not found after toolchain step. Install Node 22 (e.g. via mise) and re-run."
fi
ok "Node: $NODE_BIN ($("$NODE_BIN" --version 2>/dev/null))"
# Claude Code
if ! need_cmd claude && [ ! -x "$HOME_DIR/.local/bin/claude" ]; then
if confirm "Install Claude Code now?" "Y"; then
curl -fsSL https://claude.ai/install.sh | bash || die "Claude Code install failed."
fi
fi
export PATH="$HOME_DIR/.local/bin:$PATH"
if need_cmd claude; then ok "Claude Code: $(claude --version 2>/dev/null || echo present)"; else
warn "Claude Code not detected on PATH; you can install it later, but launchers need it."
fi
# ---------------------------------------------------------------------------
# Step 3: Docker (rootless) check
# ---------------------------------------------------------------------------
hdr "Step 3 of 7: Docker (rootless) for the gateway"
if ! need_cmd docker; then
warn "Docker is not installed."
if [ "$HAVE_SUDO" = "yes" ] && confirm "Install Docker Engine and set up rootless mode now?" "Y"; then
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin uidmap dbus-user-session slirp4netns fuse-overlayfs || die "Docker install failed."
sudo systemctl disable --now docker.service docker.socket 2>/dev/null || true
dockerd-rootless-setuptool.sh install || die "Rootless setup failed (are you in a real SSH login, not su?)."
systemctl --user enable --now docker || die "Could not start rootless docker."
grep -q 'DOCKER_HOST=unix' "$HOME_DIR/.bashrc" 2>/dev/null || \
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> "$HOME_DIR/.bashrc"
export DOCKER_HOST="unix:///run/user/$(id -u)/docker.sock"
else
die "Docker is required for the gateway. Install it (rootless) and re-run."
fi
fi
# make sure we talk to the rootless socket
export DOCKER_HOST="${DOCKER_HOST:-unix:///run/user/$(id -u)/docker.sock}"
if ! docker info >/dev/null 2>&1; then
warn "Cannot talk to Docker. If you just installed it, you may need to start the user service:"
say " systemctl --user enable --now docker"
die "Docker not reachable. Fix the above and re-run."
fi
ok "Docker reachable."
# ---------------------------------------------------------------------------
# Step 4: secrets file
# ---------------------------------------------------------------------------
hdr "Step 4 of 7: Secrets file"
mkdir -p "$HOME_DIR/.config"
backup_file "$SECRETS"
umask 077
cat > "$SECRETS" <<EOF
# Managed by install-claude-code-openrouter.sh
LITELLM_MASTER_KEY=$LITELLM_MASTER_KEY
CCR_APIKEY=$CCR_APIKEY
OPENROUTER_API_KEY=$OPENROUTER_KEY
EOF
chmod 600 "$SECRETS"
ok "Wrote $SECRETS (chmod 600)."
# load into this script's environment
set -a; . "$SECRETS"; set +a
# ---------------------------------------------------------------------------
# Step 5: LiteLLM gateway config + container
# ---------------------------------------------------------------------------
hdr "Step 5 of 7: LiteLLM gateway"
mkdir -p "$HOME_DIR/litellm"
GW="$HOME_DIR/litellm/config.yaml"
backup_file "$GW"
# Build the budget-tier reasoning block conditionally
if [ "$BUDGET_REASONING" = "yes" ]; then
BUDGET_REASON_LINE=" reasoning_effort: xhigh"
BUDGET_DROP_LINE=' additional_drop_params: ["thinking", "enable_thinking"]'
else
BUDGET_REASON_LINE=""
BUDGET_DROP_LINE=' additional_drop_params: ["reasoning", "thinking", "enable_thinking"]'
fi
{
echo "model_list:"
echo " # ---- OpenRouter pay-per-use tiers (cheapest to most expensive) ----"
echo ""
echo " # Tier 1 (cheapest${BUDGET_REASONING:+, max reasoning})"
echo " - model_name: $BUDGET_NAME"
echo " litellm_params:"
echo " model: openai/$BUDGET_SLUG"
echo " api_base: https://openrouter.ai/api/v1"
echo " api_key: os.environ/OPENROUTER_API_KEY"
[ -n "$BUDGET_REASON_LINE" ] && echo "$BUDGET_REASON_LINE"
echo " drop_params: true"
echo "$BUDGET_DROP_LINE"
echo ""
echo " # Tier 2 (mid)"
echo " - model_name: $MID_NAME"
echo " litellm_params:"
echo " model: openai/$MID_SLUG"
echo " api_base: https://openrouter.ai/api/v1"
echo " api_key: os.environ/OPENROUTER_API_KEY"
echo " drop_params: true"
echo ' additional_drop_params: ["reasoning", "thinking", "enable_thinking"]'
echo ""
echo " # Tier 3 (top)"
echo " - model_name: $TOP_NAME"
echo " litellm_params:"
echo " model: openai/$TOP_SLUG"
echo " api_base: https://openrouter.ai/api/v1"
echo " api_key: os.environ/OPENROUTER_API_KEY"
echo " drop_params: true"
echo ' additional_drop_params: ["reasoning", "thinking", "enable_thinking"]'
echo ""
echo "litellm_settings:"
echo " drop_params: true"
echo " modify_params: true"
echo " num_retries: 2"
echo " request_timeout: 300"
echo " fallbacks: [{\"$BUDGET_NAME\": [\"$MID_NAME\", \"$TOP_NAME\"]}, {\"$MID_NAME\": [\"$BUDGET_NAME\", \"$TOP_NAME\"]}, {\"$TOP_NAME\": [\"$MID_NAME\", \"$BUDGET_NAME\"]}]"
} > "$GW"
# validate YAML
if python3 -c "import yaml,sys; yaml.safe_load(open(sys.argv[1])); print('ok')" "$GW" >/dev/null 2>&1; then
ok "Gateway config written and valid: $GW"
else
die "Generated gateway config is not valid YAML. Inspect $GW"
fi
# (Re)create the container
if docker ps -a --format '{{.Names}}' | grep -qx litellm; then
warn "An existing 'litellm' container was found."
if confirm "Remove and recreate it with the new config/keys? (required to pick up key changes)" "Y"; then
docker rm -f litellm >/dev/null 2>&1 || true
else
info "Leaving existing container; restarting it to reload mounted config."
docker restart litellm >/dev/null 2>&1 || true
fi
fi
if ! docker ps -a --format '{{.Names}}' | grep -qx litellm; then
info "Starting LiteLLM container..."
docker run -d --name litellm --restart unless-stopped \
-p 127.0.0.1:4000:4000 \
-v "$HOME_DIR/litellm/config.yaml:/app/config.yaml" \
-e LITELLM_MASTER_KEY="$LITELLM_MASTER_KEY" \
-e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
docker.litellm.ai/berriai/litellm:main-stable \
--config /app/config.yaml --port 4000 >/dev/null || die "Failed to start LiteLLM container."
fi
info "Waiting for gateway to come up..."
GW_OK=no
for _ in $(seq 1 20); do
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4000/v1/models -H "Authorization: Bearer $LITELLM_MASTER_KEY" || true)"
if [ "$code" = "200" ]; then GW_OK=yes; break; fi
sleep 2
done
if [ "$GW_OK" = "yes" ]; then ok "Gateway is up (HTTP 200)."; else
warn "Gateway did not return 200 yet. Recent logs:"
docker logs --tail 20 litellm 2>&1 | sed 's/^/ /'
die "Gateway not healthy. Fix the above and re-run (your inputs are saved in the config)."
fi
# verify each model actually answers
hdr "Verifying each tier responds through OpenRouter"
ALL_OK=yes
for M in "$BUDGET_NAME" "$MID_NAME" "$TOP_NAME"; do
resp="$(curl -s http://127.0.0.1:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"reply with one word: ok\"}],\"max_tokens\":2000}")"
verdict="$(printf '%s' "$resp" | python3 -c "import sys,json
try:
d=json.load(sys.stdin)
if 'choices' in d:
c=(d['choices'][0]['message'].get('content') or '').strip()
print('OK' if c else 'EMPTY')
else:
print('ERR:'+str(d.get('error',d))[:90])
except Exception as e:
print('PARSE-ERR')" 2>/dev/null)"
if [ "$verdict" = "OK" ]; then ok "$M responded."
else err "$M did not respond cleanly ($verdict)"; ALL_OK=no; fi
done
if [ "$ALL_OK" != "yes" ]; then
warn "At least one tier failed. Common causes: wrong slug, or OpenRouter key/credit."
if ! confirm "Continue anyway and finish router setup?" "N"; then
die "Stopping so you can fix the model(s). Re-run after correcting the slug or key."
fi
fi
# ---------------------------------------------------------------------------
# Step 6: CCR install + config + service
# ---------------------------------------------------------------------------
hdr "Step 6 of 7: Claude Code Router"
if ! need_cmd ccr; then
info "Installing Claude Code Router (npm global)..."
npm install -g @musistudio/claude-code-router >/dev/null 2>&1 || die "CCR install failed."
fi
CCR_CLI="$(readlink -f "$(command -v ccr)" 2>/dev/null || true)"
[ -n "$CCR_CLI" ] || die "Could not locate the ccr CLI after install."
ok "CCR CLI: $CCR_CLI"
mkdir -p "$HOME_DIR/.claude-code-router"
RC="$HOME_DIR/.claude-code-router/config.json"
backup_file "$RC"
cat > "$RC" <<EOF
{
"LOG": true,
"HOST": "127.0.0.1",
"PORT": 3456,
"APIKEY": "\$CCR_APIKEY",
"Providers": [
{
"name": "cloud",
"api_base_url": "http://127.0.0.1:4000/v1/chat/completions",
"api_key": "\$LITELLM_MASTER_KEY",
"models": ["$BUDGET_NAME", "$MID_NAME", "$TOP_NAME"]
}
],
"Router": {
"default": "cloud,$DEFAULT_NAME",
"background": "cloud,$DEFAULT_NAME",
"think": "cloud,$DEFAULT_NAME",
"longContext": "cloud,$DEFAULT_NAME",
"longContextThreshold": 200000,
"fallback": "cloud,$DEFAULT_NAME"
}
}
EOF
if python3 -c "import json,sys; json.load(open(sys.argv[1])); print('ok')" "$RC" >/dev/null 2>&1; then
ok "Router config written and valid: $RC"
else
die "Generated router config is not valid JSON. Inspect $RC"
fi
# systemd user service
mkdir -p "$HOME_DIR/.config/systemd/user"
UNIT="$HOME_DIR/.config/systemd/user/ccr.service"
backup_file "$UNIT"
NODE_DIR="$(dirname "$NODE_BIN")"
cat > "$UNIT" <<EOF
[Unit]
Description=Claude Code Router
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=$SECRETS
Environment=PATH=$NODE_DIR:/usr/local/bin:/usr/bin:/bin
Environment=HOME=$HOME_DIR
ExecStartPre=/bin/rm -f $HOME_DIR/.claude-code-router/.claude-code-router.pid
ExecStart=$NODE_BIN $CCR_CLI start
Restart=on-failure
RestartSec=5
StandardOutput=append:$HOME_DIR/.claude-code-router/ccr-service.log
StandardError=append:$HOME_DIR/.claude-code-router/ccr-service.log
[Install]
WantedBy=default.target
EOF
ok "Wrote service unit: $UNIT"
# stop any hand-started instance, then enable+start
ccr stop >/dev/null 2>&1 || true
pkill -f 'claude-code-router' >/dev/null 2>&1 || true
rm -f "$HOME_DIR/.claude-code-router/.claude-code-router.pid"
sleep 1
systemctl --user daemon-reload
systemctl --user enable --now ccr.service >/dev/null 2>&1 || true
sleep 3
CCR_OK=no
for _ in $(seq 1 10); do
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3456/ -H "Authorization: Bearer $CCR_APIKEY" || true)"
if [ "$code" = "200" ]; then CCR_OK=yes; break; fi
sleep 1
done
if [ "$CCR_OK" = "yes" ]; then ok "Router is up (HTTP 200)."; else
warn "Router did not return 200. Recent service log:"
tail -n 20 "$HOME_DIR/.claude-code-router/ccr-service.log" 2>/dev/null | sed 's/^/ /'
warn "You can investigate with: systemctl --user status ccr.service"
fi
# ---------------------------------------------------------------------------
# Step 7: launchers + settings
# ---------------------------------------------------------------------------
hdr "Step 7 of 7: Launchers"
mkdir -p "$HOME_DIR/.claude-ccr"
if [ ! -f "$HOME_DIR/.claude-ccr/settings.json" ]; then
echo '{"theme":"dark"}' > "$HOME_DIR/.claude-ccr/settings.json"
fi
YOLO=no
if confirm "Also add a no-prompts 'yolo' launcher (skips permission prompts)?" "Y"; then
YOLO=yes
# suppress the one-time dangerous-mode warning
python3 - "$HOME_DIR/.claude-ccr/settings.json" <<'PYEOF'
import json,sys,os
p=sys.argv[1]
d=json.load(open(p)) if os.path.exists(p) else {}
d["skipDangerousModePermissionPrompt"]=True
json.dump(d,open(p,"w"),indent=2)
PYEOF
fi
BRC="$HOME_DIR/.bashrc"
backup_file "$BRC"
# Remove any prior copies we installed before (idempotent)
if grep -q '# >>> claude-ccr launchers >>>' "$BRC" 2>/dev/null; then
sed -i '/# >>> claude-ccr launchers >>>/,/# <<< claude-ccr launchers <<</d' "$BRC"
info "Removed previous launcher block from .bashrc"
fi
{
echo ""
echo "# >>> claude-ccr launchers >>>"
echo "claude-ccr() {"
echo " set -a; source \"$SECRETS\"; set +a"
echo " CLAUDE_CONFIG_DIR=$HOME_DIR/.claude-ccr \\"
echo " ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \\"
echo " ANTHROPIC_AUTH_TOKEN=\"\$CCR_APIKEY\" \\"
echo " CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 \\"
echo " claude \"\$@\""
echo "}"
if [ "$YOLO" = "yes" ]; then
echo "claude-ccr-yolo() {"
echo " set -a; source \"$SECRETS\"; set +a"
echo " CLAUDE_CONFIG_DIR=$HOME_DIR/.claude-ccr \\"
echo " ANTHROPIC_BASE_URL=http://127.0.0.1:3456 \\"
echo " ANTHROPIC_AUTH_TOKEN=\"\$CCR_APIKEY\" \\"
echo " CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 \\"
echo " claude --dangerously-skip-permissions \"\$@\""
echo "}"
fi
echo "# <<< claude-ccr launchers <<<"
} >> "$BRC"
ok "Added launcher function(s) to $BRC"
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
hdr "Installation complete"
cat <<DONE
${GRN}Everything is set up.${RST}
Default model: $DEFAULT_NAME
Switch tiers in-session with the model command (argument form):
/model cloud,$BUDGET_NAME
/model cloud,$MID_NAME
/model cloud,$TOP_NAME
(The built-in /model picker only lists Anthropic models and cannot show
yours -- always use the argument form above.)
To start using it, open a NEW terminal (so the launchers load), then run:
${BOLD}claude-ccr${RST}${YOLO:+ or ${BOLD}claude-ccr-yolo${RST}}
Or load the launchers into THIS shell right now:
source "$BRC"
Useful checks:
Gateway models: curl -s http://127.0.0.1:4000/v1/models -H "Authorization: Bearer \$LITELLM_MASTER_KEY"
Router health: curl -s http://127.0.0.1:3456/ -H "Authorization: Bearer \$CCR_APIKEY" -o /dev/null -w '%{http_code}\n'
Gateway logs: docker logs -f litellm
Router status: systemctl --user status ccr.service
Notes:
* Changing a MODEL/failover setting: edit ~/litellm/config.yaml, then
docker restart litellm
* Changing a KEY: you must RECREATE the container (re-run this installer,
or docker rm -f litellm and run the docker command again with keys loaded);
a plain restart will NOT pick up new keys.
* Cold-boot autostart of the router needs lingering for your user. If it is
not already on, an admin runs once: loginctl enable-linger $SERVICE_USER
DONE
if [ "$CCR_OK" != "yes" ] || [ "$GW_OK" != "yes" ]; then
warn "One or more services were not confirmed healthy above; review the notes before relying on it."
fi
exit 0