peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
12 kB
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
PROJECT_ROOT="${PROJECT_ROOT:-$DEFAULT_PROJECT_ROOT}"
PROFILE_DIR="${PROFILE_DIR:-$PROJECT_ROOT/auth_profiles/saved}"
DOCKER_ENV_FILE="${DOCKER_ENV_FILE:-$PROJECT_ROOT/docker/.env}"
DEFAULT_IMAGE_NAME="ai-studio-proxy:latest"
COMPOSE_DEFAULT_IMAGE_NAME="docker-ai-studio-proxy:latest"
IMAGE_NAME="${IMAGE_NAME:-$DEFAULT_IMAGE_NAME}"
IMAGE_NAME_SOURCE="default"
CONTAINER_PREFIX="${CONTAINER_PREFIX:-ai-proxy}"
API_KEY="${API_KEY:-sk-dummy}"
SERVER_LOG_LEVEL="${SERVER_LOG_LEVEL:-INFO}"
BASE_API_PORT="${BASE_API_PORT:-2048}"
BASE_STREAM_PORT="${BASE_STREAM_PORT:-3120}"
MEMORY_PER_INSTANCE_GB="${MEMORY_PER_INSTANCE_GB:-1}"
AUTO_CONFIRM="${AUTO_CONFIRM:-false}"
DRY_RUN="${DRY_RUN:-false}"
MOUNT_DOCKER_ENV="${MOUNT_DOCKER_ENV:-true}"
DISABLE_AUTH_ROTATION="${DISABLE_AUTH_ROTATION:-true}"
EXTRA_DOCKER_ARGS="${EXTRA_DOCKER_ARGS:-}"
RUNTIME_STATE_DIR="${RUNTIME_STATE_DIR:-$PROJECT_ROOT/.multi-instance-runtime}"
usage() {
cat <<'EOF'
Usage:
bash scripts/multi-instance-manager/run-multi-instance.sh [options]
Options:
--project-root PATH Path to repository root
--profile-dir PATH Directory with saved auth profiles
--docker-env-file PATH Path to docker/.env file to mount into containers
--image NAME Docker image name to run
--container-prefix PREFIX Prefix for container names
--api-key KEY Shared API key for launched containers
--log-level LEVEL SERVER_LOG_LEVEL value
--base-api-port PORT Starting host port for API service
--base-stream-port PORT Starting host port for stream service
--memory-per-instance-gb N Estimated RAM usage per instance
--auto-confirm Skip interactive confirmation
--dry-run Print actions without running docker
--no-docker-env Do not mount docker/.env into containers
--enable-auth-rotation Allow containers to rotate into other profiles
--help Show this help
Environment variables can also be used:
PROJECT_ROOT PROFILE_DIR DOCKER_ENV_FILE IMAGE_NAME CONTAINER_PREFIX API_KEY
SERVER_LOG_LEVEL BASE_API_PORT BASE_STREAM_PORT MEMORY_PER_INSTANCE_GB
AUTO_CONFIRM DRY_RUN MOUNT_DOCKER_ENV DISABLE_AUTH_ROTATION EXTRA_DOCKER_ARGS
RUNTIME_STATE_DIR
EOF
}
log() {
printf '%s\n' "$*"
}
warn() {
printf '⚠️ %s\n' "$*" >&2
}
die() {
printf '❌ %s\n' "$*" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
trim() {
local value="$1"
value="${value#${value%%[![:space:]]*}}"
value="${value%${value##*[![:space:]]}}"
printf '%s' "$value"
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--project-root)
PROJECT_ROOT="$2"
shift 2
;;
--profile-dir)
PROFILE_DIR="$2"
shift 2
;;
--docker-env-file)
DOCKER_ENV_FILE="$2"
shift 2
;;
--image)
IMAGE_NAME="$2"
IMAGE_NAME_SOURCE="cli"
shift 2
;;
--container-prefix)
CONTAINER_PREFIX="$2"
shift 2
;;
--api-key)
API_KEY="$2"
shift 2
;;
--log-level)
SERVER_LOG_LEVEL="$2"
shift 2
;;
--base-api-port)
BASE_API_PORT="$2"
shift 2
;;
--base-stream-port)
BASE_STREAM_PORT="$2"
shift 2
;;
--memory-per-instance-gb)
MEMORY_PER_INSTANCE_GB="$2"
shift 2
;;
--auto-confirm)
AUTO_CONFIRM="true"
shift
;;
--dry-run)
DRY_RUN="true"
shift
;;
--no-docker-env)
MOUNT_DOCKER_ENV="false"
shift
;;
--enable-auth-rotation)
DISABLE_AUTH_ROTATION="false"
shift
;;
--help|-h)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
done
}
require_integer() {
local name="$1"
local value="$2"
[[ "$value" =~ ^[0-9]+$ ]] || die "$name must be a non-negative integer, got: $value"
}
validate_paths() {
[[ -d "$PROJECT_ROOT" ]] || die "Project root not found: $PROJECT_ROOT"
[[ -d "$PROFILE_DIR" ]] || die "Profile directory not found: $PROFILE_DIR"
if [[ "$MOUNT_DOCKER_ENV" == "true" && ! -f "$DOCKER_ENV_FILE" ]]; then
die "docker env file not found: $DOCKER_ENV_FILE"
fi
mkdir -p "$RUNTIME_STATE_DIR/active"
}
resolve_image_name() {
if docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
return 0
fi
if [[ "$IMAGE_NAME_SOURCE" == "default" && "$IMAGE_NAME" == "$DEFAULT_IMAGE_NAME" ]] \
&& docker image inspect "$COMPOSE_DEFAULT_IMAGE_NAME" >/dev/null 2>&1; then
warn "Docker image $DEFAULT_IMAGE_NAME not found; falling back to Compose-built image $COMPOSE_DEFAULT_IMAGE_NAME"
IMAGE_NAME="$COMPOSE_DEFAULT_IMAGE_NAME"
return 0
fi
die "docker image not found: $IMAGE_NAME"
}
check_docker() {
command_exists docker || die "docker command not found"
docker info >/dev/null 2>&1 || die "docker daemon is not available"
resolve_image_name
}
discover_profiles() {
shopt -s nullglob
PROFILE_FILES=("$PROFILE_DIR"/*.json)
shopt -u nullglob
[[ ${#PROFILE_FILES[@]} -gt 0 ]] || die "No .json auth profiles found in $PROFILE_DIR"
}
print_memory_warning() {
local instance_count="${#PROFILE_FILES[@]}"
local estimated_total_gb=$((instance_count * MEMORY_PER_INSTANCE_GB))
log "============================================================"
warn "Multi-instance mode launches one isolated Docker container per auth profile."
warn "Each instance may consume about ${MEMORY_PER_INSTANCE_GB} GB RAM or more."
warn "Planned instances: ${instance_count}"
warn "Estimated RAM requirement: ~${estimated_total_gb} GB"
warn "Running too many instances can trigger memory pressure or OOM kills."
log "============================================================"
}
confirm_launch() {
[[ "$AUTO_CONFIRM" == "true" ]] && return 0
local answer
printf 'Continue launching %s containers? [y/N]: ' "${#PROFILE_FILES[@]}"
read -r answer
answer="$(trim "$answer")"
[[ "$answer" =~ ^([yY][eE][sS]|[yY])$ ]] || die "Launch cancelled by user"
}
port_in_use() {
local port="$1"
if command_exists lsof; then
lsof -nP -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
return $?
fi
docker ps --format '{{.Ports}}' | grep -E "(^|[ ,])0\.0\.0\.0:${port}->|(^|[ ,])127\.0\.0\.1:${port}->|(^|[ ,])\[::\]:${port}->" >/dev/null 2>&1
}
container_exists() {
local container_name="$1"
docker ps -a --format '{{.Names}}' | grep -Fx "$container_name" >/dev/null 2>&1
}
container_uses_port() {
local container_name="$1"
local port="$2"
docker ps --filter "name=^${container_name}$" --format '{{.Ports}}' | grep -E "(^|[ ,])127\.0\.0\.1:${port}->|(^|[ ,])0\.0\.0\.0:${port}->|(^|[ ,])\[::\]:${port}->" >/dev/null 2>&1
}
check_port_available() {
local port="$1"
local port_role="$2"
local container_name="$3"
if port_in_use "$port"; then
if [[ -n "$container_name" ]] && container_uses_port "$container_name" "$port"; then
return 0
fi
die "Host ${port_role} port already in use by another listener: $port"
fi
}
sanitize_name() {
local value="$1"
value="$(printf '%s' "$value" | tr '[:upper:]' '[:lower:]')"
value="${value//[^a-z0-9_.-]/-}"
value="${value#-}"
value="${value%-}"
printf '%s' "$value"
}
stop_existing_container() {
local container_name="$1"
if container_exists "$container_name"; then
log "♻️ Removing existing container: $container_name"
if [[ "$DRY_RUN" != "true" ]]; then
docker rm -f "$container_name" >/dev/null
fi
fi
}
prepare_container_ports() {
local container_name="$1"
local api_port="$2"
local stream_port="$3"
check_port_available "$api_port" "API" "$container_name"
check_port_available "$stream_port" "stream" "$container_name"
stop_existing_container "$container_name"
check_port_available "$api_port" "API" ""
check_port_available "$stream_port" "stream" ""
}
launch_container() {
local profile_path="$1"
local profile_name="$2"
local api_port="$3"
local stream_port="$4"
local container_name="$5"
local container_profile_path="/app/auth_profiles/saved/${profile_name}.json"
local runtime_active_host_path="$RUNTIME_STATE_DIR/active/${container_name}.json"
local container_active_profile_path="/app/auth_profiles/active/${profile_name}.json"
cp "$profile_path" "$runtime_active_host_path"
local -a docker_args=(
run -d
--name "$container_name"
--restart unless-stopped
-p "127.0.0.1:${api_port}:2048"
-p "127.0.0.1:${stream_port}:3120"
-v "$PROJECT_ROOT/auth_profiles:/app/auth_profiles"
-v "$runtime_active_host_path:${container_active_profile_path}:ro"
-e "ACTIVE_AUTH_JSON_PATH=${container_profile_path}"
-e "API_KEY=${API_KEY}"
-e "SERVER_LOG_LEVEL=${SERVER_LOG_LEVEL}"
)
if [[ "$MOUNT_DOCKER_ENV" == "true" ]]; then
docker_args+=( -v "$DOCKER_ENV_FILE:/app/.env:ro" )
fi
if [[ "$DISABLE_AUTH_ROTATION" == "true" ]]; then
docker_args+=(
-e "AUTO_AUTH_ROTATION_ON_STARTUP=false"
-e "AUTO_ROTATE_AUTH_PROFILE=false"
)
fi
if [[ -n "$EXTRA_DOCKER_ARGS" ]]; then
# shellcheck disable=SC2206
local -a extra_args=( $EXTRA_DOCKER_ARGS )
docker_args+=( "${extra_args[@]}" )
fi
docker_args+=( "$IMAGE_NAME" )
log "------------------------------------------------------------"
log "🚀 Launching profile: $profile_name"
log " Container: $container_name"
log " API port: $api_port"
log " Stream: $stream_port"
log " Saved auth: $container_profile_path"
log " Active mount: $container_active_profile_path"
log " Startup mode: dual-path (ACTIVE_AUTH_JSON_PATH + active-file mount)"
if [[ "$DISABLE_AUTH_ROTATION" == "true" ]]; then
log " Rotation: disabled for strict profile isolation"
fi
if [[ "$DRY_RUN" == "true" ]]; then
printf 'DRY RUN: docker'
printf ' %q' "${docker_args[@]}"
printf '\n'
else
docker "${docker_args[@]}" >/dev/null
fi
}
print_summary() {
log ""
log "✅ Done. Processed profiles: ${#PROFILE_FILES[@]}"
log "🔑 Shared API key: ${API_KEY}"
log "🐳 Container prefix: ${CONTAINER_PREFIX}"
if [[ "$DISABLE_AUTH_ROTATION" == "true" ]]; then
log "🔒 Auth rotation: disabled"
else
log "🔁 Auth rotation: enabled"
fi
if [[ "$DRY_RUN" == "true" ]]; then
log "🧪 Dry run mode was enabled. No containers were started."
else
docker ps --filter "name=^${CONTAINER_PREFIX}-" --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
fi
}
main() {
parse_args "$@"
if [[ -n "${IMAGE_NAME:-}" && "$IMAGE_NAME" != "$DEFAULT_IMAGE_NAME" && "$IMAGE_NAME_SOURCE" == "default" ]]; then
IMAGE_NAME_SOURCE="env"
fi
require_integer "BASE_API_PORT" "$BASE_API_PORT"
require_integer "BASE_STREAM_PORT" "$BASE_STREAM_PORT"
require_integer "MEMORY_PER_INSTANCE_GB" "$MEMORY_PER_INSTANCE_GB"
validate_paths
check_docker
discover_profiles
log "🔍 Found profiles: ${#PROFILE_FILES[@]} in $PROFILE_DIR"
print_memory_warning
confirm_launch
local idx profile_path profile_basename profile_name container_name api_port stream_port
for idx in "${!PROFILE_FILES[@]}"; do
profile_path="${PROFILE_FILES[$idx]}"
profile_basename="$(basename "$profile_path")"
profile_name="${profile_basename%.json}"
container_name="$(sanitize_name "${CONTAINER_PREFIX}-${profile_name}")"
api_port=$((BASE_API_PORT + idx))
stream_port=$((BASE_STREAM_PORT + idx))
prepare_container_ports "$container_name" "$api_port" "$stream_port"
launch_container "$profile_path" "$profile_name" "$api_port" "$stream_port" "$container_name"
done
print_summary
}
main "$@"