Created
March 20, 2026 19:52
-
-
Save secemp9/3ef68c1539e979611d1bb44490ff1513 to your computer and use it in GitHub Desktop.
script for clipproxyapi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # setup_proxy.sh - CLIProxyAPI Setup and Management Script | |
| # | |
| # This script provides comprehensive setup and management capabilities for CLIProxyAPI, | |
| # a proxy server that provides OpenAI/Gemini/Claude compatible API interfaces. | |
| # | |
| # Features: | |
| # - Automatic Go 1.26+ installation check | |
| # - CLIProxyAPI build and setup | |
| # - OAuth-based account login (Claude, Codex) | |
| # - API key management | |
| # - Proxy server lifecycle management | |
| # | |
| # Usage: ./setup_proxy.sh [OPTIONS] | |
| # | |
| # Author: Generated for round_robin_subscription project | |
| # License: MIT | |
| # | |
| set -euo pipefail | |
| # ============================================================================= | |
| # CONFIGURATION | |
| # ============================================================================= | |
| # Paths | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| CLIPROXYAPI_DIR="${SCRIPT_DIR}/CLIProxyAPI" | |
| CONFIG_FILE="${CLIPROXYAPI_DIR}/config.yaml" | |
| CONFIG_EXAMPLE="${CLIPROXYAPI_DIR}/config.example.yaml" | |
| AUTH_DIR="${HOME}/.cli-proxy-api" | |
| BINARY_NAME="cli-proxy-api" | |
| BINARY_PATH="${CLIPROXYAPI_DIR}/${BINARY_NAME}" | |
| PID_FILE="${CLIPROXYAPI_DIR}/.proxy.pid" | |
| LOG_FILE="${CLIPROXYAPI_DIR}/proxy.log" | |
| # Required Go version | |
| REQUIRED_GO_VERSION="1.26" | |
| # Default proxy port | |
| DEFAULT_PORT=8317 | |
| # ============================================================================= | |
| # COLOR DEFINITIONS | |
| # ============================================================================= | |
| # Check if terminal supports colors | |
| if [[ -t 1 ]] && command -v tput &>/dev/null && [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then | |
| RED=$(tput setaf 1) | |
| GREEN=$(tput setaf 2) | |
| YELLOW=$(tput setaf 3) | |
| BLUE=$(tput setaf 4) | |
| MAGENTA=$(tput setaf 5) | |
| CYAN=$(tput setaf 6) | |
| WHITE=$(tput setaf 7) | |
| BOLD=$(tput bold) | |
| RESET=$(tput sgr0) | |
| else | |
| RED="" | |
| GREEN="" | |
| YELLOW="" | |
| BLUE="" | |
| MAGENTA="" | |
| CYAN="" | |
| WHITE="" | |
| BOLD="" | |
| RESET="" | |
| fi | |
| # ============================================================================= | |
| # LOGGING FUNCTIONS | |
| # ============================================================================= | |
| log_info() { | |
| echo "${BLUE}[INFO]${RESET} $*" | |
| } | |
| log_success() { | |
| echo "${GREEN}[SUCCESS]${RESET} $*" | |
| } | |
| log_warning() { | |
| echo "${YELLOW}[WARNING]${RESET} $*" | |
| } | |
| log_error() { | |
| echo "${RED}[ERROR]${RESET} $*" >&2 | |
| } | |
| log_header() { | |
| echo "" | |
| echo "${BOLD}${CYAN}=== $* ===${RESET}" | |
| echo "" | |
| } | |
| # ============================================================================= | |
| # UTILITY FUNCTIONS | |
| # ============================================================================= | |
| # Check if a command exists | |
| command_exists() { | |
| command -v "$1" &>/dev/null | |
| } | |
| # Compare semantic versions (returns 0 if $1 >= $2) | |
| version_ge() { | |
| local ver1="$1" | |
| local ver2="$2" | |
| # Remove 'go' prefix if present | |
| ver1="${ver1#go}" | |
| ver2="${ver2#go}" | |
| # Compare using sort -V | |
| printf '%s\n%s\n' "$ver2" "$ver1" | sort -V -C | |
| } | |
| # Generate a random API key (32 hex characters) | |
| generate_api_key() { | |
| if command_exists openssl; then | |
| openssl rand -hex 16 | |
| elif [[ -r /dev/urandom ]]; then | |
| head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' | |
| else | |
| # Fallback: use date and process info | |
| echo "$(date +%s%N)$$" | sha256sum | head -c 32 | |
| fi | |
| } | |
| # Mask an API key showing only last 4 characters | |
| mask_api_key() { | |
| local key="$1" | |
| local len=${#key} | |
| if [[ $len -le 4 ]]; then | |
| echo "****" | |
| else | |
| local masked_len=$((len - 4)) | |
| printf '%*s' "$masked_len" '' | tr ' ' '*' | |
| echo -n "${key: -4}" | |
| echo "" | |
| fi | |
| } | |
| # Check if config.yaml exists | |
| config_exists() { | |
| [[ -f "$CONFIG_FILE" ]] | |
| } | |
| # Backup config file | |
| backup_config() { | |
| if config_exists; then | |
| local backup_file="${CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)" | |
| cp "$CONFIG_FILE" "$backup_file" | |
| log_info "Config backed up to: $backup_file" | |
| fi | |
| } | |
| # Validate API key format (basic validation) | |
| validate_api_key() { | |
| local key="$1" | |
| local provider="$2" | |
| if [[ -z "$key" ]]; then | |
| log_error "API key cannot be empty" | |
| return 1 | |
| fi | |
| case "$provider" in | |
| claude) | |
| # Claude API keys typically start with sk-ant- | |
| if [[ ! "$key" =~ ^sk-ant- ]] && [[ ! "$key" =~ ^sk- ]]; then | |
| log_warning "Claude API key may not be in the expected format (should start with sk-ant- or sk-)" | |
| fi | |
| ;; | |
| codex) | |
| # Codex/OpenAI keys start with sk- | |
| if [[ ! "$key" =~ ^sk- ]]; then | |
| log_warning "Codex API key may not be in the expected format (should start with sk-)" | |
| fi | |
| ;; | |
| esac | |
| return 0 | |
| } | |
| # ============================================================================= | |
| # GO INSTALLATION FUNCTIONS | |
| # ============================================================================= | |
| # Get installed Go version | |
| get_go_version() { | |
| if command_exists go; then | |
| go version | grep -oP 'go\d+\.\d+(\.\d+)?' | head -1 | |
| else | |
| echo "" | |
| fi | |
| } | |
| # Check if Go version meets requirements | |
| check_go_version() { | |
| local current_version | |
| current_version=$(get_go_version) | |
| if [[ -z "$current_version" ]]; then | |
| return 1 | |
| fi | |
| version_ge "$current_version" "go${REQUIRED_GO_VERSION}" | |
| } | |
| # Install Go | |
| install_go() { | |
| log_header "Installing Go ${REQUIRED_GO_VERSION}" | |
| local arch | |
| local os_type="linux" | |
| # Detect architecture | |
| case "$(uname -m)" in | |
| x86_64) | |
| arch="amd64" | |
| ;; | |
| aarch64|arm64) | |
| arch="arm64" | |
| ;; | |
| armv7l|armv6l) | |
| arch="armv6l" | |
| ;; | |
| *) | |
| log_error "Unsupported architecture: $(uname -m)" | |
| return 1 | |
| ;; | |
| esac | |
| local go_version="${REQUIRED_GO_VERSION}.0" | |
| local go_tarball="go${go_version}.${os_type}-${arch}.tar.gz" | |
| local download_url="https://go.dev/dl/${go_tarball}" | |
| local install_dir="$HOME/.local" | |
| local tmp_dir | |
| tmp_dir=$(mktemp -d) | |
| # Create install directory if it doesn't exist | |
| if [[ ! -d "$install_dir" ]]; then | |
| mkdir -p "$install_dir" | |
| fi | |
| log_info "Downloading Go ${go_version} for ${os_type}/${arch}..." | |
| if command_exists curl; then | |
| curl -fsSL -o "${tmp_dir}/${go_tarball}" "$download_url" | |
| elif command_exists wget; then | |
| wget -q -O "${tmp_dir}/${go_tarball}" "$download_url" | |
| else | |
| log_error "Neither curl nor wget found. Please install one of them." | |
| rm -rf "$tmp_dir" | |
| return 1 | |
| fi | |
| log_info "Installing Go to ${install_dir}..." | |
| # Remove existing Go installation | |
| if [[ -d "${install_dir}/go" ]]; then | |
| log_warning "Removing existing Go installation..." | |
| rm -rf "${install_dir}/go" | |
| fi | |
| # Extract new Go installation | |
| tar -C "$install_dir" -xzf "${tmp_dir}/${go_tarball}" | |
| # Clean up | |
| rm -rf "$tmp_dir" | |
| # Update PATH if needed | |
| if [[ ":$PATH:" != *":$HOME/.local/go/bin:"* ]]; then | |
| export PATH="$PATH:$HOME/.local/go/bin" | |
| # Add to shell profile | |
| local profile_file="" | |
| if [[ -f "${HOME}/.bashrc" ]]; then | |
| profile_file="${HOME}/.bashrc" | |
| elif [[ -f "${HOME}/.zshrc" ]]; then | |
| profile_file="${HOME}/.zshrc" | |
| elif [[ -f "${HOME}/.profile" ]]; then | |
| profile_file="${HOME}/.profile" | |
| fi | |
| if [[ -n "$profile_file" ]]; then | |
| if ! grep -q '\$HOME/.local/go/bin' "$profile_file" 2>/dev/null; then | |
| echo 'export PATH="$PATH:$HOME/.local/go/bin"' >> "$profile_file" | |
| log_info "Added Go to PATH in $profile_file" | |
| fi | |
| fi | |
| fi | |
| # Verify installation | |
| if check_go_version; then | |
| log_success "Go $(get_go_version) installed successfully" | |
| else | |
| log_error "Go installation verification failed" | |
| return 1 | |
| fi | |
| } | |
| # ============================================================================= | |
| # BUILD FUNCTIONS | |
| # ============================================================================= | |
| # Build CLIProxyAPI binary | |
| build_binary() { | |
| log_header "Building CLIProxyAPI" | |
| if [[ ! -d "$CLIPROXYAPI_DIR" ]]; then | |
| log_error "CLIProxyAPI directory not found at: $CLIPROXYAPI_DIR" | |
| return 1 | |
| fi | |
| cd "$CLIPROXYAPI_DIR" | |
| log_info "Downloading dependencies..." | |
| go mod download | |
| log_info "Building binary..." | |
| # Build with version info | |
| local version commit build_date | |
| version="dev" | |
| commit="local" | |
| build_date=$(date -u +"%Y-%m-%dT%H:%M:%SZ") | |
| # Check if we're in a git repo and get version info | |
| if command_exists git && [[ -d .git ]]; then | |
| commit=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") | |
| version=$(git describe --tags --always 2>/dev/null || echo "dev") | |
| fi | |
| go build -ldflags "-X main.Version=${version} -X main.Commit=${commit} -X main.BuildDate=${build_date}" \ | |
| -o "$BINARY_NAME" ./cmd/server | |
| chmod +x "$BINARY_NAME" | |
| log_success "Binary built successfully: ${BINARY_PATH}" | |
| } | |
| # ============================================================================= | |
| # CONFIG MANAGEMENT FUNCTIONS | |
| # ============================================================================= | |
| # Create default config.yaml if it doesn't exist | |
| create_default_config() { | |
| log_header "Creating Default Configuration" | |
| if config_exists; then | |
| log_info "Config file already exists at: $CONFIG_FILE" | |
| return 0 | |
| fi | |
| if [[ ! -f "$CONFIG_EXAMPLE" ]]; then | |
| log_error "Example config not found at: $CONFIG_EXAMPLE" | |
| return 1 | |
| fi | |
| # Generate a random API key for client authentication | |
| local local_api_key | |
| local_api_key=$(generate_api_key) | |
| log_info "Creating config.yaml with round-robin routing enabled..." | |
| # Create a minimal config based on the example | |
| cat > "$CONFIG_FILE" << EOF | |
| # CLIProxyAPI Configuration | |
| # Generated by setup_proxy.sh on $(date) | |
| # Server host/interface to bind to | |
| host: "" | |
| # Server port | |
| port: ${DEFAULT_PORT} | |
| # Authentication directory | |
| auth-dir: "${AUTH_DIR}" | |
| # API keys for client authentication | |
| api-keys: | |
| - "${local_api_key}" | |
| # Enable debug logging | |
| debug: false | |
| # Routing strategy for selecting credentials when multiple match | |
| routing: | |
| strategy: "round-robin" | |
| # Number of times to retry a request | |
| request-retry: 3 | |
| # Quota exceeded behavior | |
| quota-exceeded: | |
| switch-project: true | |
| switch-preview-model: true | |
| # Management API settings (enable via --panel) | |
| remote-management: | |
| allow-remote: false | |
| secret-key: "" | |
| disable-control-panel: false | |
| # Claude API keys (add via --add-claude-key) | |
| # claude-api-key: | |
| # - api-key: "sk-ant-..." | |
| # Codex API keys (add via --add-codex-key) | |
| # codex-api-key: | |
| # - api-key: "sk-..." | |
| EOF | |
| log_success "Config created at: $CONFIG_FILE" | |
| log_info "Your local API key for client authentication: ${local_api_key}" | |
| log_warning "Save this key! It's required for connecting clients to the proxy." | |
| } | |
| # Setup auth directory | |
| setup_auth_dir() { | |
| log_header "Setting up Authentication Directory" | |
| if [[ ! -d "$AUTH_DIR" ]]; then | |
| mkdir -p "$AUTH_DIR" | |
| chmod 700 "$AUTH_DIR" | |
| log_success "Auth directory created: $AUTH_DIR" | |
| else | |
| log_info "Auth directory already exists: $AUTH_DIR" | |
| fi | |
| } | |
| # Add Claude API key to config | |
| add_claude_api_key() { | |
| local api_key="$1" | |
| log_header "Adding Claude API Key" | |
| if ! validate_api_key "$api_key" "claude"; then | |
| return 1 | |
| fi | |
| if ! config_exists; then | |
| log_error "Config file not found. Run --install first." | |
| return 1 | |
| fi | |
| backup_config | |
| # Check if claude-api-key section exists (not commented) | |
| if grep -q "^claude-api-key:" "$CONFIG_FILE"; then | |
| # Section exists, append to it | |
| # Find the line number of claude-api-key and add after the last entry | |
| local insert_line | |
| insert_line=$(grep -n "^claude-api-key:" "$CONFIG_FILE" | cut -d: -f1) | |
| # Find the next section or end of file | |
| local next_section_line | |
| next_section_line=$(tail -n +$((insert_line + 1)) "$CONFIG_FILE" | grep -n "^[a-z]" | head -1 | cut -d: -f1) | |
| if [[ -n "$next_section_line" ]]; then | |
| insert_line=$((insert_line + next_section_line - 1)) | |
| else | |
| insert_line=$(wc -l < "$CONFIG_FILE") | |
| fi | |
| # Insert the new key | |
| sed -i "${insert_line}a\\ - api-key: \"${api_key}\"" "$CONFIG_FILE" | |
| else | |
| # Section doesn't exist, create it | |
| # Remove commented claude-api-key section if present | |
| sed -i '/^# claude-api-key:/,/^[^#]/{ /^# claude-api-key:/d; /^#.*api-key:/d; }' "$CONFIG_FILE" 2>/dev/null || true | |
| # Append new section | |
| cat >> "$CONFIG_FILE" << EOF | |
| # Claude API keys | |
| claude-api-key: | |
| - api-key: "${api_key}" | |
| EOF | |
| fi | |
| log_success "Claude API key added (masked): $(mask_api_key "$api_key")" | |
| } | |
| # Add Codex API key to config | |
| add_codex_api_key() { | |
| local api_key="$1" | |
| log_header "Adding Codex API Key" | |
| if ! validate_api_key "$api_key" "codex"; then | |
| return 1 | |
| fi | |
| if ! config_exists; then | |
| log_error "Config file not found. Run --install first." | |
| return 1 | |
| fi | |
| backup_config | |
| # Check if codex-api-key section exists (not commented) | |
| if grep -q "^codex-api-key:" "$CONFIG_FILE"; then | |
| # Section exists, append to it | |
| local insert_line | |
| insert_line=$(grep -n "^codex-api-key:" "$CONFIG_FILE" | cut -d: -f1) | |
| # Find the next section or end of file | |
| local next_section_line | |
| next_section_line=$(tail -n +$((insert_line + 1)) "$CONFIG_FILE" | grep -n "^[a-z]" | head -1 | cut -d: -f1) | |
| if [[ -n "$next_section_line" ]]; then | |
| insert_line=$((insert_line + next_section_line - 1)) | |
| else | |
| insert_line=$(wc -l < "$CONFIG_FILE") | |
| fi | |
| # Insert the new key | |
| sed -i "${insert_line}a\\ - api-key: \"${api_key}\"" "$CONFIG_FILE" | |
| else | |
| # Section doesn't exist, create it | |
| # Remove commented codex-api-key section if present | |
| sed -i '/^# codex-api-key:/,/^[^#]/{ /^# codex-api-key:/d; /^#.*api-key:/d; }' "$CONFIG_FILE" 2>/dev/null || true | |
| # Append new section | |
| cat >> "$CONFIG_FILE" << EOF | |
| # Codex API keys | |
| codex-api-key: | |
| - api-key: "${api_key}" | |
| EOF | |
| fi | |
| log_success "Codex API key added (masked): $(mask_api_key "$api_key")" | |
| } | |
| # ============================================================================= | |
| # MANAGEMENT PANEL FUNCTIONS | |
| # ============================================================================= | |
| # Enable and configure the Management Center panel | |
| setup_management_panel() { | |
| local allow_remote="${1:-false}" | |
| log_header "Management Center Setup" | |
| if ! config_exists; then | |
| log_error "Config file not found. Run --install first." | |
| return 1 | |
| fi | |
| backup_config | |
| # Generate a management secret key | |
| local mgmt_key | |
| mgmt_key="mgmt-$(generate_api_key)" | |
| log_info "Configuring Management Center..." | |
| # Check if remote-management section already has a non-empty secret-key | |
| local existing_key | |
| existing_key=$(sed -n '/^remote-management:/,/^[a-z]/{ | |
| /secret-key:/{ s/.*secret-key:[[:space:]]*"\?\([^"]*\)"\?/\1/; /^$/!p; } | |
| }' "$CONFIG_FILE" 2>/dev/null || true) | |
| if [[ -n "$existing_key" ]]; then | |
| log_info "Management panel is already configured with an existing key." | |
| mgmt_key="$existing_key" | |
| else | |
| # Update or create the remote-management section | |
| if grep -q "^remote-management:" "$CONFIG_FILE"; then | |
| # Section exists, update the secret-key | |
| sed -i "/^remote-management:/,/^[a-z]/{ | |
| s|^\([[:space:]]*\)secret-key:.*|\1secret-key: \"${mgmt_key}\"| | |
| }" "$CONFIG_FILE" | |
| else | |
| # Section doesn't exist, append it | |
| cat >> "$CONFIG_FILE" << EOF | |
| # Management API settings | |
| remote-management: | |
| allow-remote: ${allow_remote} | |
| secret-key: "${mgmt_key}" | |
| disable-control-panel: false | |
| EOF | |
| fi | |
| fi | |
| # Update allow-remote if requested | |
| if [[ "$allow_remote" == "true" ]]; then | |
| sed -i "/^remote-management:/,/^[a-z]/{ | |
| s|^\([[:space:]]*\)allow-remote:.*|\1allow-remote: true| | |
| }" "$CONFIG_FILE" | |
| log_warning "Remote management access is ENABLED. Only enable this on trusted networks." | |
| fi | |
| # Read port from config | |
| local port | |
| port=$(grep "^port:" "$CONFIG_FILE" | head -1 | awk '{print $2}' || echo "$DEFAULT_PORT") | |
| log_success "Management Center configured!" | |
| echo "" | |
| echo "${BOLD}Access the Management Center:${RESET}" | |
| echo " URL: ${CYAN}http://localhost:${port}/management.html${RESET}" | |
| echo "" | |
| echo "${BOLD}Management Key (NOT the same as proxy API keys):${RESET}" | |
| echo " ${CYAN}${mgmt_key}${RESET}" | |
| echo "" | |
| log_warning "Save this management key! You need it to log into the panel." | |
| echo "" | |
| echo "${BOLD}How to use:${RESET}" | |
| echo " 1. Start the proxy: ${CYAN}$0 --start${RESET}" | |
| echo " 2. Open the URL above in your browser" | |
| echo " 3. Enter the management key to connect" | |
| echo "" | |
| if [[ "$allow_remote" != "true" ]]; then | |
| echo "${BOLD}Note:${RESET} Remote access is disabled (localhost only)." | |
| echo " To enable: ${CYAN}$0 --panel --remote${RESET}" | |
| fi | |
| } | |
| # ============================================================================= | |
| # OAUTH LOGIN FUNCTIONS | |
| # ============================================================================= | |
| # Login to Claude via OAuth | |
| login_claude() { | |
| log_header "Claude OAuth Login" | |
| if [[ ! -x "$BINARY_PATH" ]]; then | |
| log_error "Binary not found. Run --install first." | |
| return 1 | |
| fi | |
| cd "$CLIPROXYAPI_DIR" | |
| log_info "Starting Claude OAuth login flow..." | |
| log_info "A browser window will open for authentication." | |
| echo "" | |
| ./"$BINARY_NAME" -claude-login -config "$CONFIG_FILE" | |
| log_success "Claude OAuth login completed" | |
| } | |
| # Login to Codex via OAuth | |
| login_codex() { | |
| log_header "Codex OAuth Login" | |
| if [[ ! -x "$BINARY_PATH" ]]; then | |
| log_error "Binary not found. Run --install first." | |
| return 1 | |
| fi | |
| cd "$CLIPROXYAPI_DIR" | |
| log_info "Starting Codex OAuth login flow..." | |
| log_info "A browser window will open for authentication." | |
| echo "" | |
| ./"$BINARY_NAME" -codex-login -config "$CONFIG_FILE" | |
| log_success "Codex OAuth login completed" | |
| } | |
| # ============================================================================= | |
| # ACCOUNT LISTING FUNCTIONS | |
| # ============================================================================= | |
| # List all configured accounts | |
| list_accounts() { | |
| log_header "Configured Accounts" | |
| local claude_api_count=0 | |
| local codex_api_count=0 | |
| local claude_oauth_count=0 | |
| local codex_oauth_count=0 | |
| local gemini_oauth_count=0 | |
| local other_oauth_count=0 | |
| # Count and display API keys from config | |
| if config_exists; then | |
| echo "${BOLD}API Keys from config.yaml:${RESET}" | |
| echo "" | |
| # Claude API keys | |
| if grep -q "^claude-api-key:" "$CONFIG_FILE"; then | |
| echo " ${CYAN}Claude API Keys:${RESET}" | |
| while IFS= read -r line; do | |
| if [[ "$line" =~ api-key:\ *[\"\']?([^\"\']+)[\"\']? ]]; then | |
| local key="${BASH_REMATCH[1]}" | |
| echo " - $(mask_api_key "$key")" | |
| ((claude_api_count++)) || true | |
| fi | |
| done < <(sed -n '/^claude-api-key:/,/^[a-z]/p' "$CONFIG_FILE" | grep "api-key:") | |
| if [[ $claude_api_count -eq 0 ]]; then | |
| echo " (none configured)" | |
| fi | |
| echo "" | |
| fi | |
| # Codex API keys | |
| if grep -q "^codex-api-key:" "$CONFIG_FILE"; then | |
| echo " ${CYAN}Codex API Keys:${RESET}" | |
| while IFS= read -r line; do | |
| if [[ "$line" =~ api-key:\ *[\"\']?([^\"\']+)[\"\']? ]]; then | |
| local key="${BASH_REMATCH[1]}" | |
| echo " - $(mask_api_key "$key")" | |
| ((codex_api_count++)) || true | |
| fi | |
| done < <(sed -n '/^codex-api-key:/,/^[a-z]/p' "$CONFIG_FILE" | grep "api-key:") | |
| if [[ $codex_api_count -eq 0 ]]; then | |
| echo " (none configured)" | |
| fi | |
| echo "" | |
| fi | |
| # Local API keys (for client auth) | |
| if grep -q "^api-keys:" "$CONFIG_FILE"; then | |
| echo " ${CYAN}Local Client API Keys:${RESET}" | |
| local local_key_count=0 | |
| while IFS= read -r line; do | |
| line=$(echo "$line" | sed 's/^[[:space:]]*-[[:space:]]*//' | tr -d '"' | tr -d "'") | |
| if [[ -n "$line" && ! "$line" =~ ^# ]]; then | |
| echo " - $(mask_api_key "$line")" | |
| ((local_key_count++)) || true | |
| fi | |
| done < <(sed -n '/^api-keys:/,/^[a-z]/{ /^[[:space:]]*-/p }' "$CONFIG_FILE") | |
| if [[ $local_key_count -eq 0 ]]; then | |
| echo " (none configured)" | |
| fi | |
| echo "" | |
| fi | |
| else | |
| echo " ${YELLOW}Config file not found${RESET}" | |
| echo "" | |
| fi | |
| # List OAuth token files | |
| echo "${BOLD}OAuth Tokens in ${AUTH_DIR}:${RESET}" | |
| echo "" | |
| if [[ -d "$AUTH_DIR" ]]; then | |
| # Find all token files | |
| local found_tokens=false | |
| while IFS= read -r -d '' token_file; do | |
| found_tokens=true | |
| local filename | |
| filename=$(basename "$token_file") | |
| local provider="unknown" | |
| local account_info="" | |
| # Determine provider from filename | |
| case "$filename" in | |
| *claude* | *anthropic*) | |
| provider="Claude" | |
| ((claude_oauth_count++)) || true | |
| ;; | |
| *codex* | *openai*) | |
| provider="Codex/OpenAI" | |
| ((codex_oauth_count++)) || true | |
| ;; | |
| *gemini* | *google*) | |
| provider="Gemini/Google" | |
| ((gemini_oauth_count++)) || true | |
| ;; | |
| *) | |
| ((other_oauth_count++)) || true | |
| ;; | |
| esac | |
| # Try to extract account info (email) from token file if it's JSON | |
| if command_exists jq && [[ -f "$token_file" ]]; then | |
| account_info=$(jq -r '.email // .account // .user // empty' "$token_file" 2>/dev/null || true) | |
| fi | |
| echo " ${CYAN}${provider}:${RESET}" | |
| echo " File: $filename" | |
| if [[ -n "$account_info" ]]; then | |
| echo " Account: $account_info" | |
| fi | |
| local mtime | |
| mtime=$(stat -c %y "$token_file" 2>/dev/null | cut -d' ' -f1 || stat -f %Sm -t %Y-%m-%d "$token_file" 2>/dev/null || echo "unknown") | |
| echo " Last modified: $mtime" | |
| echo "" | |
| done < <(find "$AUTH_DIR" -type f \( -name "*.json" -o -name "*.token" -o -name "token*" \) -print0 2>/dev/null) | |
| if [[ "$found_tokens" == "false" ]]; then | |
| echo " ${YELLOW}No OAuth tokens found${RESET}" | |
| echo "" | |
| fi | |
| else | |
| echo " ${YELLOW}Auth directory not found${RESET}" | |
| echo "" | |
| fi | |
| # Summary | |
| echo "${BOLD}Summary:${RESET}" | |
| echo " Claude: ${claude_api_count} API key(s), ${claude_oauth_count} OAuth account(s)" | |
| echo " Codex: ${codex_api_count} API key(s), ${codex_oauth_count} OAuth account(s)" | |
| echo " Gemini: ${gemini_oauth_count} OAuth account(s)" | |
| if [[ $other_oauth_count -gt 0 ]]; then | |
| echo " Other: ${other_oauth_count} OAuth account(s)" | |
| fi | |
| } | |
| # ============================================================================= | |
| # SERVER MANAGEMENT FUNCTIONS | |
| # ============================================================================= | |
| # Get proxy server PID | |
| get_proxy_pid() { | |
| if [[ -f "$PID_FILE" ]]; then | |
| local pid | |
| pid=$(cat "$PID_FILE") | |
| if kill -0 "$pid" 2>/dev/null; then | |
| echo "$pid" | |
| return 0 | |
| fi | |
| fi | |
| # Try to find by process name | |
| pgrep -f "${BINARY_NAME}.*-config" 2>/dev/null | head -1 || true | |
| } | |
| # Check if proxy is running | |
| is_proxy_running() { | |
| local pid | |
| pid=$(get_proxy_pid) | |
| [[ -n "$pid" ]] | |
| } | |
| # Start the proxy server | |
| start_proxy() { | |
| log_header "Starting Proxy Server" | |
| if is_proxy_running; then | |
| local pid | |
| pid=$(get_proxy_pid) | |
| log_warning "Proxy server is already running (PID: $pid)" | |
| return 0 | |
| fi | |
| if [[ ! -x "$BINARY_PATH" ]]; then | |
| log_error "Binary not found. Run --install first." | |
| return 1 | |
| fi | |
| if ! config_exists; then | |
| log_error "Config file not found. Run --install first." | |
| return 1 | |
| fi | |
| cd "$CLIPROXYAPI_DIR" | |
| log_info "Starting proxy server..." | |
| # Start the server in the background | |
| nohup ./"$BINARY_NAME" -config "$CONFIG_FILE" > "$LOG_FILE" 2>&1 & | |
| local pid=$! | |
| # Save PID | |
| echo "$pid" > "$PID_FILE" | |
| # Wait a moment and check if it started successfully | |
| sleep 2 | |
| if kill -0 "$pid" 2>/dev/null; then | |
| log_success "Proxy server started (PID: $pid)" | |
| log_info "Log file: $LOG_FILE" | |
| log_info "Listening on port: ${DEFAULT_PORT}" | |
| # Show the API key for convenience | |
| if config_exists; then | |
| local api_key | |
| api_key=$(sed -n '/^api-keys:/,/^[a-z]/{ /^[[:space:]]*-/{ s/^[[:space:]]*-[[:space:]]*//; s/["\x27]//g; p; q; }}' "$CONFIG_FILE" 2>/dev/null || true) | |
| if [[ -n "$api_key" ]]; then | |
| echo "" | |
| log_info "Use this API key for client connections:" | |
| echo " ${CYAN}${api_key}${RESET}" | |
| fi | |
| fi | |
| else | |
| rm -f "$PID_FILE" | |
| log_error "Failed to start proxy server. Check logs at: $LOG_FILE" | |
| tail -20 "$LOG_FILE" 2>/dev/null || true | |
| return 1 | |
| fi | |
| } | |
| # Stop the proxy server | |
| stop_proxy() { | |
| log_header "Stopping Proxy Server" | |
| local pid | |
| pid=$(get_proxy_pid) | |
| if [[ -z "$pid" ]]; then | |
| log_warning "Proxy server is not running" | |
| rm -f "$PID_FILE" | |
| return 0 | |
| fi | |
| log_info "Stopping proxy server (PID: $pid)..." | |
| # Send SIGTERM first | |
| kill "$pid" 2>/dev/null || true | |
| # Wait for graceful shutdown (up to 10 seconds) | |
| local count=0 | |
| while kill -0 "$pid" 2>/dev/null && [[ $count -lt 10 ]]; do | |
| sleep 1 | |
| ((count++)) || true | |
| done | |
| # Force kill if still running | |
| if kill -0 "$pid" 2>/dev/null; then | |
| log_warning "Graceful shutdown failed, forcing termination..." | |
| kill -9 "$pid" 2>/dev/null || true | |
| sleep 1 | |
| fi | |
| rm -f "$PID_FILE" | |
| if kill -0 "$pid" 2>/dev/null; then | |
| log_error "Failed to stop proxy server" | |
| return 1 | |
| else | |
| log_success "Proxy server stopped" | |
| fi | |
| } | |
| # Show proxy status | |
| show_status() { | |
| log_header "Proxy Server Status" | |
| local pid | |
| pid=$(get_proxy_pid) | |
| if [[ -n "$pid" ]]; then | |
| echo "${GREEN}Status: Running${RESET}" | |
| echo "PID: $pid" | |
| # Get process info | |
| if command_exists ps; then | |
| echo "" | |
| echo "Process Info:" | |
| ps -p "$pid" -o pid,ppid,user,%cpu,%mem,etime,cmd --no-headers 2>/dev/null || true | |
| fi | |
| # Check if port is listening | |
| if command_exists ss; then | |
| echo "" | |
| echo "Listening Ports:" | |
| ss -tlnp 2>/dev/null | grep "$pid" || echo " (unable to determine)" | |
| elif command_exists netstat; then | |
| echo "" | |
| echo "Listening Ports:" | |
| netstat -tlnp 2>/dev/null | grep "$pid" || echo " (unable to determine)" | |
| fi | |
| # Show recent log entries | |
| if [[ -f "$LOG_FILE" ]]; then | |
| echo "" | |
| echo "Recent Log Entries:" | |
| tail -5 "$LOG_FILE" 2>/dev/null | sed 's/^/ /' | |
| fi | |
| else | |
| echo "${RED}Status: Not Running${RESET}" | |
| fi | |
| echo "" | |
| echo "Configuration:" | |
| echo " Config file: $CONFIG_FILE" | |
| echo " Auth directory: $AUTH_DIR" | |
| echo " Binary: $BINARY_PATH" | |
| echo " Log file: $LOG_FILE" | |
| } | |
| # ============================================================================= | |
| # FULL INSTALLATION | |
| # ============================================================================= | |
| do_install() { | |
| log_header "CLIProxyAPI Installation" | |
| # Check dependencies | |
| log_info "Checking dependencies..." | |
| local missing_deps=() | |
| if ! command_exists git; then | |
| missing_deps+=("git") | |
| fi | |
| if [[ ${#missing_deps[@]} -gt 0 ]]; then | |
| log_error "Missing required dependencies: ${missing_deps[*]}" | |
| log_info "Please install them and try again." | |
| return 1 | |
| fi | |
| # Check Go version | |
| log_info "Checking Go installation..." | |
| if ! command_exists go; then | |
| log_warning "Go is not installed" | |
| install_go | |
| elif ! check_go_version; then | |
| local current_version | |
| current_version=$(get_go_version) | |
| log_warning "Go ${current_version} is installed but Go ${REQUIRED_GO_VERSION}+ is required" | |
| install_go | |
| else | |
| log_success "Go $(get_go_version) is already installed" | |
| fi | |
| # Check if CLIProxyAPI directory exists | |
| if [[ ! -d "$CLIPROXYAPI_DIR" ]]; then | |
| log_error "CLIProxyAPI directory not found at: $CLIPROXYAPI_DIR" | |
| log_info "Please clone the repository first." | |
| return 1 | |
| fi | |
| # Build the binary | |
| build_binary | |
| # Setup auth directory | |
| setup_auth_dir | |
| # Create default config | |
| create_default_config | |
| log_header "Installation Complete" | |
| echo "Next steps:" | |
| echo " 1. Add accounts:" | |
| echo " ${CYAN}$0 --add-claude${RESET} # OAuth login for Claude" | |
| echo " ${CYAN}$0 --add-codex${RESET} # OAuth login for Codex" | |
| echo " ${CYAN}$0 --add-claude-key <key>${RESET} # Add Claude API key" | |
| echo " ${CYAN}$0 --add-codex-key <key>${RESET} # Add Codex API key" | |
| echo "" | |
| echo " 2. Enable the web Management Center (optional):" | |
| echo " ${CYAN}$0 --panel${RESET}" | |
| echo "" | |
| echo " 3. Start the proxy:" | |
| echo " ${CYAN}$0 --start${RESET}" | |
| echo "" | |
| echo " 4. Configure your client to use:" | |
| echo " URL: http://localhost:${DEFAULT_PORT}" | |
| echo "" | |
| } | |
| # ============================================================================= | |
| # HELP MESSAGE | |
| # ============================================================================= | |
| show_help() { | |
| cat << EOF | |
| ${BOLD}CLIProxyAPI Setup and Management Script${RESET} | |
| ${BOLD}USAGE:${RESET} | |
| $0 [OPTIONS] | |
| ${BOLD}OPTIONS:${RESET} | |
| ${CYAN}Installation & Setup:${RESET} | |
| -i, --install Full installation and setup | |
| (Go check/install, build, config creation) | |
| ${CYAN}Account Management:${RESET} | |
| -c, --add-claude Login to Claude/Anthropic via OAuth | |
| -x, --add-codex Login to OpenAI Codex via OAuth | |
| --add-claude-key <key> Add a Claude API key directly to config | |
| --add-codex-key <key> Add a Codex API key directly to config | |
| -l, --list List all configured accounts | |
| ${CYAN}Management Panel:${RESET} | |
| -p, --panel Enable the web Management Center | |
| (generates key, shows access URL) | |
| --panel --remote Enable with remote access allowed | |
| ${CYAN}Server Management:${RESET} | |
| -s, --start Start the proxy server | |
| --stop Stop the proxy server | |
| --status Show proxy server status | |
| ${CYAN}Help:${RESET} | |
| -h, --help Show this help message | |
| ${BOLD}EXAMPLES:${RESET} | |
| # Full installation | |
| $0 --install | |
| # Add Claude account via OAuth | |
| $0 --add-claude | |
| # Add API key directly | |
| $0 --add-claude-key "sk-ant-api03-..." | |
| # Enable Management Center web panel | |
| $0 --panel | |
| # Enable Management Center with remote access | |
| $0 --panel --remote | |
| # Start proxy server | |
| $0 --start | |
| # Check status | |
| $0 --status | |
| ${BOLD}CONFIGURATION:${RESET} | |
| Config file: ${CONFIG_FILE} | |
| Auth directory: ${AUTH_DIR} | |
| Default port: ${DEFAULT_PORT} | |
| ${BOLD}NOTES:${RESET} | |
| - The proxy uses round-robin routing by default for load balancing | |
| - Multiple accounts can be added for the same provider | |
| - OAuth tokens are stored in ${AUTH_DIR} | |
| - API keys are stored in config.yaml (masked when listed) | |
| EOF | |
| } | |
| # ============================================================================= | |
| # MAIN ENTRY POINT | |
| # ============================================================================= | |
| main() { | |
| # No arguments provided | |
| if [[ $# -eq 0 ]]; then | |
| show_help | |
| exit 0 | |
| fi | |
| # Parse arguments | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -i|--install) | |
| do_install | |
| exit $? | |
| ;; | |
| -c|--add-claude) | |
| login_claude | |
| exit $? | |
| ;; | |
| -x|--add-codex) | |
| login_codex | |
| exit $? | |
| ;; | |
| --add-claude-key) | |
| if [[ -z "${2:-}" ]]; then | |
| log_error "Missing API key argument" | |
| echo "Usage: $0 --add-claude-key <api-key>" | |
| exit 1 | |
| fi | |
| add_claude_api_key "$2" | |
| exit $? | |
| ;; | |
| --add-codex-key) | |
| if [[ -z "${2:-}" ]]; then | |
| log_error "Missing API key argument" | |
| echo "Usage: $0 --add-codex-key <api-key>" | |
| exit 1 | |
| fi | |
| add_codex_api_key "$2" | |
| exit $? | |
| ;; | |
| -p|--panel) | |
| local allow_remote="false" | |
| if [[ "${2:-}" == "--remote" ]]; then | |
| allow_remote="true" | |
| shift | |
| fi | |
| setup_management_panel "$allow_remote" | |
| exit $? | |
| ;; | |
| -l|--list) | |
| list_accounts | |
| exit $? | |
| ;; | |
| -s|--start) | |
| start_proxy | |
| exit $? | |
| ;; | |
| --stop) | |
| stop_proxy | |
| exit $? | |
| ;; | |
| --status) | |
| show_status | |
| exit $? | |
| ;; | |
| -h|--help) | |
| show_help | |
| exit 0 | |
| ;; | |
| *) | |
| log_error "Unknown option: $1" | |
| echo "Use --help for usage information" | |
| exit 1 | |
| ;; | |
| esac | |
| shift | |
| done | |
| } | |
| # Run main function | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment