mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-07 09:10:55 +00:00
feat: Add FluidStack GPU cloud provider with claude, aider, and gptme agents (#247)
FluidStack is a cost-effective GPU cloud provider offering A100s and H100s starting at $1.35/hr (up to 70% cheaper than hyperscalers). Features: - Simple REST API with Python SDK - SSH access with automatic key registration - Zero egress fees - GPU types: RTX_4090 (default), A100, H100 Implemented agents: - Claude Code (fluidstack/claude.sh) - Aider (fluidstack/aider.sh) - gptme (fluidstack/gptme.sh) Added fluidstack/lib/common.sh with provisioning primitives. Updated manifest.json with cloud entry and matrix entries (3 implemented, 11 missing). Agent: cloud-scout Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
cde9f586d8
commit
efec4ac8ca
6 changed files with 570 additions and 0 deletions
56
fluidstack/README.md
Normal file
56
fluidstack/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# FluidStack
|
||||
|
||||
FluidStack GPU cloud via REST API. [FluidStack](https://www.fluidstack.io/)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A FluidStack account with API key from [Dashboard API Keys](https://platform.fluidstack.io/dashboard/api-keys)
|
||||
2. SSH public key (will be registered automatically)
|
||||
|
||||
## Agents
|
||||
|
||||
#### Claude Code
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fluidstack/claude.sh)
|
||||
```
|
||||
|
||||
#### Aider
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fluidstack/aider.sh)
|
||||
```
|
||||
|
||||
#### gptme
|
||||
|
||||
```bash
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fluidstack/gptme.sh)
|
||||
```
|
||||
|
||||
## Non-Interactive Mode
|
||||
|
||||
```bash
|
||||
FLUIDSTACK_SERVER_NAME=dev-gpu \
|
||||
FLUIDSTACK_API_KEY=your-api-key \
|
||||
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
|
||||
bash <(curl -fsSL https://openrouter.ai/lab/spawn/fluidstack/claude.sh)
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `FLUIDSTACK_API_KEY` | FluidStack API key | _(prompted)_ |
|
||||
| `FLUIDSTACK_SERVER_NAME` | Instance name | _(prompted)_ |
|
||||
| `FLUIDSTACK_GPU_TYPE` | GPU type (e.g., `RTX_4090`, `A100`, `H100`) | `RTX_4090` |
|
||||
| `FLUIDSTACK_SSH_KEY_NAME` | SSH key name | `spawn-${USER}` |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter API key | _(prompted via OAuth)_ |
|
||||
|
||||
## Notes
|
||||
|
||||
- FluidStack is a GPU cloud provider with A100s and H100s starting at $1.35/hr
|
||||
- Up to 70% cheaper than traditional hyperscalers
|
||||
- Zero egress fees for data transfer
|
||||
- Simple REST API with Python SDK available
|
||||
- SSH keys are automatically registered via the API
|
||||
- Instances use the `root` user for SSH access
|
||||
55
fluidstack/aider.sh
Executable file
55
fluidstack/aider.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
# shellcheck disable=SC2154
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=fluidstack/lib/common.sh
|
||||
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
else
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/fluidstack/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Aider on FluidStack"
|
||||
echo ""
|
||||
|
||||
ensure_fluidstack_token
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity
|
||||
install_base_tools
|
||||
|
||||
log_warn "Installing Aider..."
|
||||
run_server "${FLUIDSTACK_SERVER_IP}" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
|
||||
|
||||
if ! run_server "${FLUIDSTACK_SERVER_IP}" "command -v aider &> /dev/null && aider --version &> /dev/null"; then
|
||||
log_error "Aider installation verification failed"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Aider installation verified successfully"
|
||||
|
||||
echo ""
|
||||
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
|
||||
log_info "Using OpenRouter API key from environment"
|
||||
else
|
||||
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
|
||||
fi
|
||||
|
||||
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1
|
||||
|
||||
log_warn "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${FLUIDSTACK_SERVER_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
|
||||
|
||||
echo ""
|
||||
log_info "FluidStack instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (IP: ${FLUIDSTACK_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
log_warn "Starting Aider..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${FLUIDSTACK_SERVER_IP}" "source ~/.zshrc && aider --model openrouter/${MODEL_ID}"
|
||||
71
fluidstack/claude.sh
Executable file
71
fluidstack/claude.sh
Executable file
|
|
@ -0,0 +1,71 @@
|
|||
#!/bin/bash
|
||||
# shellcheck disable=SC2154
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
# shellcheck source=fluidstack/lib/common.sh
|
||||
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
else
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/fluidstack/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "Claude Code on FluidStack"
|
||||
echo ""
|
||||
|
||||
# 1. Ensure FluidStack API key is configured
|
||||
ensure_fluidstack_token
|
||||
|
||||
# 2. Generate + register SSH key
|
||||
ensure_ssh_key
|
||||
|
||||
# 3. Get instance name and create instance
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
|
||||
# 4. Wait for SSH connectivity and install base tools
|
||||
verify_server_connectivity
|
||||
install_base_tools
|
||||
|
||||
# 5. Verify Claude Code is installed (fallback to manual install)
|
||||
log_warn "Verifying Claude Code installation..."
|
||||
if ! run_server "${FLUIDSTACK_SERVER_IP}" "command -v claude" >/dev/null 2>&1; then
|
||||
log_warn "Claude Code not found, installing manually..."
|
||||
run_server "${FLUIDSTACK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
|
||||
fi
|
||||
log_info "Claude Code is installed"
|
||||
|
||||
# 6. Get OpenRouter API key
|
||||
echo ""
|
||||
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
|
||||
log_info "Using OpenRouter API key from environment"
|
||||
else
|
||||
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
|
||||
fi
|
||||
|
||||
# 7. Inject environment variables
|
||||
log_warn "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${FLUIDSTACK_SERVER_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
|
||||
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
|
||||
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
|
||||
"ANTHROPIC_API_KEY=" \
|
||||
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
|
||||
"CLAUDE_CODE_ENABLE_TELEMETRY=0"
|
||||
|
||||
# 8. Configure Claude Code settings
|
||||
setup_claude_code_config "${OPENROUTER_API_KEY}" \
|
||||
"upload_file ${FLUIDSTACK_SERVER_IP}" \
|
||||
"run_server ${FLUIDSTACK_SERVER_IP}"
|
||||
|
||||
echo ""
|
||||
log_info "FluidStack instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (IP: ${FLUIDSTACK_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
# 9. Start Claude Code interactively
|
||||
log_warn "Starting Claude Code..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${FLUIDSTACK_SERVER_IP}" "source ~/.zshrc && claude"
|
||||
54
fluidstack/gptme.sh
Executable file
54
fluidstack/gptme.sh
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# shellcheck disable=SC2154
|
||||
set -eo pipefail
|
||||
|
||||
# Source common functions - try local file first, fall back to remote
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
else
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/fluidstack/lib/common.sh)"
|
||||
fi
|
||||
|
||||
log_info "gptme on FluidStack"
|
||||
echo ""
|
||||
|
||||
ensure_fluidstack_token
|
||||
ensure_ssh_key
|
||||
|
||||
SERVER_NAME=$(get_server_name)
|
||||
create_server "${SERVER_NAME}"
|
||||
verify_server_connectivity
|
||||
install_base_tools
|
||||
|
||||
log_warn "Installing gptme..."
|
||||
run_server "${FLUIDSTACK_SERVER_IP}" "pip install gptme 2>/dev/null || pip3 install gptme"
|
||||
|
||||
if ! run_server "${FLUIDSTACK_SERVER_IP}" "command -v gptme &> /dev/null && gptme --version &> /dev/null"; then
|
||||
log_error "gptme installation verification failed"
|
||||
exit 1
|
||||
fi
|
||||
log_info "gptme installation verified successfully"
|
||||
|
||||
echo ""
|
||||
if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then
|
||||
log_info "Using OpenRouter API key from environment"
|
||||
else
|
||||
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
|
||||
fi
|
||||
|
||||
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
|
||||
|
||||
log_warn "Setting up environment variables..."
|
||||
inject_env_vars_ssh "${FLUIDSTACK_SERVER_IP}" upload_file run_server \
|
||||
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
|
||||
|
||||
echo ""
|
||||
log_info "FluidStack instance setup completed successfully!"
|
||||
log_info "Instance: ${SERVER_NAME} (IP: ${FLUIDSTACK_SERVER_IP})"
|
||||
echo ""
|
||||
|
||||
log_warn "Starting gptme..."
|
||||
sleep 1
|
||||
clear
|
||||
interactive_session "${FLUIDSTACK_SERVER_IP}" "source ~/.zshrc && gptme -m openrouter/${MODEL_ID}"
|
||||
306
fluidstack/lib/common.sh
Normal file
306
fluidstack/lib/common.sh
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
#!/bin/bash
|
||||
# Common bash functions for FluidStack spawn scripts
|
||||
# Uses FluidStack REST API — https://docs.fluidstack.io/
|
||||
|
||||
# Bash safety flags
|
||||
set -eo pipefail
|
||||
|
||||
# ============================================================
|
||||
# Provider-agnostic functions
|
||||
# ============================================================
|
||||
|
||||
# Source shared provider-agnostic functions (local or remote fallback)
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
||||
if [[ -n "${SCRIPT_DIR}" && -f "${SCRIPT_DIR}/../../shared/common.sh" ]]; then
|
||||
source "${SCRIPT_DIR}/../../shared/common.sh"
|
||||
else
|
||||
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)"
|
||||
fi
|
||||
|
||||
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
|
||||
|
||||
# ============================================================
|
||||
# FluidStack specific functions
|
||||
# ============================================================
|
||||
|
||||
readonly FLUIDSTACK_API_BASE="https://platform.fluidstack.io/v1"
|
||||
# SSH_OPTS is defined in shared/common.sh
|
||||
|
||||
# Configurable timeout/delay constants
|
||||
INSTANCE_STATUS_POLL_DELAY=${INSTANCE_STATUS_POLL_DELAY:-10} # Delay between instance status checks
|
||||
SSH_RETRY_DELAY=${SSH_RETRY_DELAY:-5} # Delay between SSH connection retry attempts
|
||||
|
||||
# FluidStack API wrapper
|
||||
# Usage: fluidstack_api METHOD ENDPOINT [BODY]
|
||||
fluidstack_api() {
|
||||
local method="${1}"
|
||||
local endpoint="${2}"
|
||||
local body="${3:-}"
|
||||
|
||||
if [[ -n "${body}" ]]; then
|
||||
curl -s -X "${method}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "api-key: ${FLUIDSTACK_API_KEY}" \
|
||||
"${FLUIDSTACK_API_BASE}${endpoint}" \
|
||||
-d "${body}"
|
||||
else
|
||||
curl -s -X "${method}" \
|
||||
-H "api-key: ${FLUIDSTACK_API_KEY}" \
|
||||
"${FLUIDSTACK_API_BASE}${endpoint}"
|
||||
fi
|
||||
}
|
||||
|
||||
test_fluidstack_token() {
|
||||
local response
|
||||
response=$(fluidstack_api GET "/ssh_keys")
|
||||
if echo "${response}" | grep -q '"ssh_keys"'; then
|
||||
log_info "API key validated"
|
||||
return 0
|
||||
else
|
||||
local error_msg
|
||||
error_msg=$(echo "${response}" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','No details available'))" 2>/dev/null || echo "Unable to parse error")
|
||||
log_error "API Error: ${error_msg}"
|
||||
log_warn "Remediation steps:"
|
||||
log_warn " 1. Verify API key at: https://platform.fluidstack.io/dashboard/api-keys"
|
||||
log_warn " 2. Ensure the key has appropriate permissions"
|
||||
log_warn " 3. Check key hasn't been revoked"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Ensure FLUIDSTACK_API_KEY is available (env var -> config file -> prompt+save)
|
||||
ensure_fluidstack_token() {
|
||||
ensure_api_token_with_provider \
|
||||
"FluidStack" \
|
||||
"FLUIDSTACK_API_KEY" \
|
||||
"${HOME}/.config/spawn/fluidstack.json" \
|
||||
"https://platform.fluidstack.io/dashboard/api-keys" \
|
||||
"test_fluidstack_token"
|
||||
}
|
||||
|
||||
# Check if SSH key is registered with FluidStack
|
||||
fluidstack_check_ssh_key() {
|
||||
local fingerprint="${1}"
|
||||
local existing_keys
|
||||
existing_keys=$(fluidstack_api GET "/ssh_keys")
|
||||
# FluidStack returns SSH key fingerprints in MD5 format in "public_key_fingerprint" field
|
||||
echo "${existing_keys}" | python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
for key in data.get('ssh_keys', []):
|
||||
if '${fingerprint}' in key.get('public_key_fingerprint', '') or '${fingerprint}' in key.get('name', ''):
|
||||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
"
|
||||
}
|
||||
|
||||
# Register SSH key with FluidStack
|
||||
fluidstack_register_ssh_key() {
|
||||
local key_name="${1}"
|
||||
local pub_path="${2}"
|
||||
local pub_key
|
||||
pub_key=$(cat "${pub_path}")
|
||||
|
||||
local register_body
|
||||
register_body=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'name': '${key_name}',
|
||||
'public_key': '''${pub_key}'''
|
||||
}))
|
||||
")
|
||||
|
||||
local register_response
|
||||
register_response=$(fluidstack_api POST "/ssh_keys" "${register_body}")
|
||||
|
||||
if echo "${register_response}" | grep -q '"ssh_key_name"'; then
|
||||
return 0
|
||||
else
|
||||
local error_msg
|
||||
error_msg=$(echo "${register_response}" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','Unknown error'))" 2>/dev/null || echo "${register_response}")
|
||||
log_error "API Error: ${error_msg}"
|
||||
log_warn "Common causes:"
|
||||
log_warn " - SSH key already registered"
|
||||
log_warn " - Invalid SSH key format"
|
||||
log_warn " - API key lacks write permissions"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_ssh_key() {
|
||||
ensure_ssh_key_with_provider fluidstack_check_ssh_key fluidstack_register_ssh_key "FluidStack"
|
||||
}
|
||||
|
||||
get_server_name() {
|
||||
local server_name
|
||||
server_name=$(get_resource_name "FLUIDSTACK_SERVER_NAME" "Enter instance name: ") || return 1
|
||||
|
||||
if ! validate_server_name "${server_name}"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "${server_name}"
|
||||
}
|
||||
|
||||
# Wait for FluidStack instance to become running and get its IP
|
||||
# Sets: FLUIDSTACK_SERVER_IP
|
||||
# Usage: wait_for_instance_ready INSTANCE_ID [MAX_ATTEMPTS]
|
||||
wait_for_instance_ready() {
|
||||
local instance_id="${1}"
|
||||
local max_attempts=${2:-60}
|
||||
local attempt=1
|
||||
|
||||
log_warn "Waiting for instance to become ready..."
|
||||
while [[ "${attempt}" -le "${max_attempts}" ]]; do
|
||||
local status_response
|
||||
status_response=$(fluidstack_api GET "/instances/${instance_id}")
|
||||
|
||||
local status
|
||||
status=$(echo "${status_response}" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
||||
|
||||
if [[ "${status}" == "running" ]]; then
|
||||
FLUIDSTACK_SERVER_IP=$(echo "${status_response}" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('ip_address',''))" 2>/dev/null || echo "")
|
||||
export FLUIDSTACK_SERVER_IP
|
||||
|
||||
if [[ -z "${FLUIDSTACK_SERVER_IP}" ]]; then
|
||||
log_error "Instance running but no IP address assigned"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_info "Instance ready: ${FLUIDSTACK_SERVER_IP}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_warn "Instance status: ${status} (${attempt}/${max_attempts})"
|
||||
sleep "${INSTANCE_STATUS_POLL_DELAY}"
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
log_error "Instance did not become ready in time"
|
||||
return 1
|
||||
}
|
||||
|
||||
create_server() {
|
||||
local name="${1}"
|
||||
local gpu_type="${FLUIDSTACK_GPU_TYPE:-RTX_4090}"
|
||||
local ssh_key_name="${FLUIDSTACK_SSH_KEY_NAME:-spawn-${USER}}"
|
||||
|
||||
# Block injection chars in string values (quotes, backslashes)
|
||||
if [[ "${gpu_type}" =~ [\"\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_GPU_TYPE: contains unsafe characters"; return 1; fi
|
||||
if [[ "${ssh_key_name}" =~ [\"\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_SSH_KEY_NAME: contains unsafe characters"; return 1; fi
|
||||
|
||||
log_warn "Creating FluidStack instance '${name}' (GPU: ${gpu_type})..."
|
||||
|
||||
# Build instance creation request
|
||||
local create_body
|
||||
create_body=$(python3 -c "
|
||||
import json
|
||||
print(json.dumps({
|
||||
'gpu_type': '${gpu_type}',
|
||||
'ssh_key': '${ssh_key_name}'
|
||||
}))
|
||||
")
|
||||
|
||||
local response
|
||||
response=$(fluidstack_api POST "/instances" "${create_body}")
|
||||
|
||||
if echo "${response}" | grep -q '"instance_id"'; then
|
||||
FLUIDSTACK_INSTANCE_ID=$(echo "${response}" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('instance_id',''))")
|
||||
export FLUIDSTACK_INSTANCE_ID
|
||||
log_info "Instance created: ID=${FLUIDSTACK_INSTANCE_ID}"
|
||||
|
||||
wait_for_instance_ready "${FLUIDSTACK_INSTANCE_ID}"
|
||||
else
|
||||
log_error "Failed to create FluidStack instance"
|
||||
local error_msg
|
||||
error_msg=$(echo "${response}" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error','Unknown error'))" 2>/dev/null || echo "${response}")
|
||||
log_error "API Error: ${error_msg}"
|
||||
log_warn "Common issues:"
|
||||
log_warn " - Insufficient account balance"
|
||||
log_warn " - GPU type unavailable (try different FLUIDSTACK_GPU_TYPE)"
|
||||
log_warn " - SSH key not found (check FLUIDSTACK_SSH_KEY_NAME)"
|
||||
log_warn "Remediation: Check https://platform.fluidstack.io/dashboard"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
verify_server_connectivity() {
|
||||
local max_attempts=${1:-30}
|
||||
generic_ssh_wait "${FLUIDSTACK_SERVER_IP}" "root" "${max_attempts}"
|
||||
}
|
||||
|
||||
# Install base tools via SSH
|
||||
install_base_tools() {
|
||||
log_warn "Installing base tools..."
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "root@${FLUIDSTACK_SERVER_IP}" "apt-get update -y && apt-get install -y curl unzip git zsh npm" >/dev/null 2>&1 || true
|
||||
|
||||
# Install Bun
|
||||
log_warn "Installing Bun..."
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "root@${FLUIDSTACK_SERVER_IP}" "curl -fsSL https://bun.sh/install | bash" >/dev/null 2>&1 || true
|
||||
|
||||
# Install Claude Code
|
||||
log_warn "Installing Claude Code..."
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "root@${FLUIDSTACK_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" >/dev/null 2>&1 || true
|
||||
|
||||
# Configure PATH in .bashrc and .zshrc
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "root@${FLUIDSTACK_SERVER_IP}" "grep -q '.bun/bin' ~/.bashrc 2>/dev/null || printf '%s\n' 'export PATH=\"\${HOME}/.claude/local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.bashrc; grep -q '.bun/bin' ~/.zshrc 2>/dev/null || printf '%s\n' 'export PATH=\"\${HOME}/.claude/local/bin:\${HOME}/.bun/bin:\${PATH}\"' >> ~/.zshrc" >/dev/null 2>&1 || true
|
||||
|
||||
log_info "Base tools installed"
|
||||
}
|
||||
|
||||
# FluidStack uses root user for SSH access
|
||||
run_server() {
|
||||
local ip="${1}"
|
||||
local cmd="${2}"
|
||||
# shellcheck disable=SC2086
|
||||
ssh ${SSH_OPTS} "root@${ip}" "${cmd}"
|
||||
}
|
||||
|
||||
upload_file() {
|
||||
local ip="${1}"
|
||||
local local_path="${2}"
|
||||
local remote_path="${3}"
|
||||
# shellcheck disable=SC2086
|
||||
scp ${SSH_OPTS} "${local_path}" "root@${ip}:${remote_path}"
|
||||
}
|
||||
|
||||
interactive_session() {
|
||||
local ip="${1}"
|
||||
local cmd="${2}"
|
||||
# shellcheck disable=SC2086
|
||||
ssh -t ${SSH_OPTS} "root@${ip}" "${cmd}"
|
||||
}
|
||||
|
||||
destroy_server() {
|
||||
local instance_id="${1}"
|
||||
log_warn "Terminating instance ${instance_id}..."
|
||||
fluidstack_api DELETE "/instances/${instance_id}" >/dev/null
|
||||
log_info "Instance ${instance_id} terminated"
|
||||
}
|
||||
|
||||
list_servers() {
|
||||
local response
|
||||
response=$(fluidstack_api GET "/instances")
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
data = json.loads(sys.stdin.read())
|
||||
instances = data.get('instances', [])
|
||||
if not instances:
|
||||
print('No instances found')
|
||||
sys.exit(0)
|
||||
print(f\"{'NAME':<25} {'ID':<30} {'STATUS':<12} {'IP':<15} {'GPU':<15}\")
|
||||
print('-' * 97)
|
||||
for inst in instances:
|
||||
name = inst.get('hostname', inst.get('instance_id', 'N/A'))[:24]
|
||||
iid = inst.get('instance_id', 'N/A')[:29]
|
||||
status = inst.get('status', 'N/A')[:11]
|
||||
ip = inst.get('ip_address', 'N/A')[:14]
|
||||
gpu = inst.get('gpu_type', 'N/A')[:14]
|
||||
print(f'{name:<25} {iid:<30} {status:<12} {ip:<15} {gpu:<15}')
|
||||
" <<< "${response}"
|
||||
}
|
||||
|
|
@ -396,6 +396,20 @@
|
|||
},
|
||||
"notes": "Uses Machines API for provisioning and flyctl SSH for exec. Docker-based, pay-per-second pricing. Requires flyctl CLI."
|
||||
},
|
||||
"fluidstack": {
|
||||
"name": "FluidStack",
|
||||
"description": "FluidStack GPU cloud via REST API",
|
||||
"url": "https://www.fluidstack.io/",
|
||||
"type": "api",
|
||||
"auth": "FLUIDSTACK_API_KEY",
|
||||
"provision_method": "POST /instances with gpu_type and ssh_key",
|
||||
"exec_method": "ssh root@IP",
|
||||
"interactive_method": "ssh -t root@IP",
|
||||
"defaults": {
|
||||
"gpu_type": "RTX_4090"
|
||||
},
|
||||
"notes": "GPU cloud provider with A100s and H100s starting at $1.35/hr. Up to 70% cheaper than hyperscalers. Zero egress fees. Simple REST API with Python SDK available."
|
||||
},
|
||||
"civo": {
|
||||
"name": "Civo",
|
||||
"description": "Civo cloud-native instances via REST API",
|
||||
|
|
@ -800,6 +814,20 @@
|
|||
"fly/amazonq": "implemented",
|
||||
"fly/cline": "implemented",
|
||||
"fly/gptme": "implemented",
|
||||
"fluidstack/claude": "implemented",
|
||||
"fluidstack/openclaw": "missing",
|
||||
"fluidstack/nanoclaw": "missing",
|
||||
"fluidstack/aider": "implemented",
|
||||
"fluidstack/goose": "missing",
|
||||
"fluidstack/codex": "missing",
|
||||
"fluidstack/interpreter": "missing",
|
||||
"fluidstack/gemini": "missing",
|
||||
"fluidstack/amazonq": "missing",
|
||||
"fluidstack/cline": "missing",
|
||||
"fluidstack/gptme": "implemented",
|
||||
"fluidstack/opencode": "missing",
|
||||
"fluidstack/plandex": "missing",
|
||||
"fluidstack/kilocode": "missing",
|
||||
"civo/claude": "implemented",
|
||||
"civo/aider": "implemented",
|
||||
"civo/codex": "implemented",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue