feat: Add Koyeb serverless container platform support (#185)

Add Koyeb as a new cloud provider with CLI-based provisioning.

Changes:
- Created koyeb/lib/common.sh with provider primitives
- Implemented koyeb/claude.sh
- Implemented koyeb/aider.sh
- Implemented koyeb/openclaw.sh
- Added Koyeb entry to manifest.json clouds section
- Added matrix entries for all 14 agents
- Created koyeb/README.md with setup instructions

Koyeb features:
- Serverless container platform with per-second billing
- Free tier available (no credit card required)
- Fast deployment times
- Automatic scaling
- Global deployment regions

Agent: cloud-scout-2

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
A 2026-02-10 08:07:36 -08:00 committed by GitHub
parent 7d2a2543da
commit e4052189d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 636 additions and 1 deletions

52
koyeb/README.md Normal file
View file

@ -0,0 +1,52 @@
# Koyeb
Koyeb serverless container platform via CLI. [Koyeb](https://www.koyeb.com/)
## Agents
#### Claude Code
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/koyeb/claude.sh)
```
#### OpenClaw
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/koyeb/openclaw.sh)
```
#### Aider
```bash
bash <(curl -fsSL https://openrouter.ai/lab/spawn/koyeb/aider.sh)
```
## Non-Interactive Mode
```bash
KOYEB_TOKEN=your-token \
OPENROUTER_API_KEY=sk-or-v1-xxxxx \
bash <(curl -fsSL https://openrouter.ai/lab/spawn/koyeb/claude.sh)
```
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `KOYEB_TOKEN` | Koyeb API token | _(prompted)_ |
| `KOYEB_REGION` | Deployment region | `was` (Washington D.C.) |
| `KOYEB_INSTANCE_TYPE` | Instance type | `nano` |
| `OPENROUTER_API_KEY` | OpenRouter API key | _(OAuth or prompted)_ |
## Authentication
Get your Koyeb API token at: https://app.koyeb.com/account/api
## Features
- Serverless container platform with per-second billing
- Free tier available (no credit card required)
- Fast deployment times
- Automatic scaling
- Global deployment regions

58
koyeb/aider.sh Normal file
View file

@ -0,0 +1,58 @@
#!/bin/bash
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/koyeb/lib/common.sh)"
fi
log_info "Aider on Koyeb"
echo ""
# 1. Ensure Koyeb CLI and API token
ensure_koyeb_cli
ensure_koyeb_token
# 2. Create service
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Aider
log_warn "Installing Aider..."
run_server "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"
# 5. 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
# 6. Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1
# 7. Inject environment variables
log_warn "Setting up environment variables..."
inject_env_vars \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"PATH=\$HOME/.local/bin:\$PATH"
echo ""
log_info "Koyeb service setup completed successfully!"
log_info "Service: $KOYEB_SERVICE_NAME (Instance: $KOYEB_INSTANCE_ID)"
echo ""
# 8. Start Aider interactively
log_warn "Starting Aider..."
sleep 1
clear
interactive_session "source /root/.bashrc && aider --model openrouter/${MODEL_ID}"

109
koyeb/claude.sh Normal file
View file

@ -0,0 +1,109 @@
#!/bin/bash
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/koyeb/lib/common.sh)"
fi
log_info "Claude Code on Koyeb"
echo ""
# 1. Ensure Koyeb CLI and API token
ensure_koyeb_cli
ensure_koyeb_token
# 2. Create service
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install Claude Code
log_warn "Installing Claude Code..."
run_server "curl -fsSL https://claude.ai/install.sh | bash"
# Verify installation
if ! run_server "command -v claude" >/dev/null 2>&1; then
log_error "Claude Code installation failed"
exit 1
fi
log_info "Claude Code installed"
# 5. 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
# 6. Inject environment variables
log_warn "Setting up environment variables..."
inject_env_vars \
"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" \
"PATH=\$HOME/.claude/local/bin:\$HOME/.bun/bin:\$PATH"
# 7. Configure Claude Code settings
log_warn "Configuring Claude Code..."
run_server "mkdir -p /root/.claude"
# Upload settings.json
SETTINGS_TEMP=$(mktemp)
chmod 600 "$SETTINGS_TEMP"
cat > "$SETTINGS_TEMP" << EOF
{
"theme": "dark",
"editor": "vim",
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
"ANTHROPIC_AUTH_TOKEN": "${OPENROUTER_API_KEY}"
},
"permissions": {
"defaultMode": "bypassPermissions",
"dangerouslySkipPermissions": true
}
}
EOF
upload_file "$SETTINGS_TEMP" "/root/.claude/settings.json"
rm "$SETTINGS_TEMP"
# Upload ~/.claude.json global state
GLOBAL_STATE_TEMP=$(mktemp)
chmod 600 "$GLOBAL_STATE_TEMP"
cat > "$GLOBAL_STATE_TEMP" << EOF
{
"hasCompletedOnboarding": true,
"bypassPermissionsModeAccepted": true
}
EOF
upload_file "$GLOBAL_STATE_TEMP" "/root/.claude.json"
rm "$GLOBAL_STATE_TEMP"
# Create empty CLAUDE.md
run_server "touch /root/.claude/CLAUDE.md"
echo ""
log_info "Koyeb service setup completed successfully!"
log_info "Service: $KOYEB_SERVICE_NAME (Instance: $KOYEB_INSTANCE_ID)"
echo ""
# 8. Start Claude Code interactively
log_warn "Starting Claude Code..."
sleep 1
clear
interactive_session "source /root/.bashrc && claude"

306
koyeb/lib/common.sh Normal file
View file

@ -0,0 +1,306 @@
#!/bin/bash
# Common bash functions for Koyeb spawn scripts
# Uses Koyeb CLI for provisioning and exec access
# 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
# ============================================================
# Koyeb specific functions
# ============================================================
# Ensure Koyeb CLI is installed
ensure_koyeb_cli() {
if command -v koyeb &>/dev/null; then
log_info "Koyeb CLI available"
return 0
fi
log_warn "Installing Koyeb CLI..."
# Detect OS and architecture
local os=""
local arch=""
case "$(uname -s)" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
*)
log_error "Unsupported operating system: $(uname -s)"
return 1
;;
esac
case "$(uname -m)" in
x86_64|amd64) arch="amd64" ;;
arm64|aarch64) arch="arm64" ;;
*)
log_error "Unsupported architecture: $(uname -m)"
return 1
;;
esac
local install_dir="$HOME/.koyeb/bin"
mkdir -p "$install_dir"
local download_url="https://github.com/koyeb/koyeb-cli/releases/latest/download/koyeb-${os}-${arch}"
if ! curl -fsSL "$download_url" -o "$install_dir/koyeb"; then
log_error "Failed to download Koyeb CLI"
log_error "Install manually: https://www.koyeb.com/docs/build-and-deploy/cli/installation"
return 1
fi
chmod +x "$install_dir/koyeb"
export PATH="$install_dir:$PATH"
if ! command -v koyeb &>/dev/null; then
log_error "Koyeb CLI not found in PATH after installation"
return 1
fi
log_info "Koyeb CLI installed"
}
# Save Koyeb token to config file
_save_koyeb_token() {
local token="$1"
local config_dir="$HOME/.config/spawn"
local config_file="$config_dir/koyeb.json"
mkdir -p "$config_dir"
printf '{\n "token": "%s"\n}\n' "$(json_escape "$token")" > "$config_file"
chmod 600 "$config_file"
}
# Ensure KOYEB_TOKEN is available (env var -> config file -> prompt+save)
ensure_koyeb_token() {
check_python_available || return 1
# 1. Check environment variable
if [[ -n "${KOYEB_TOKEN:-}" ]]; then
log_info "Using Koyeb API token from environment"
return 0
fi
local config_file="$HOME/.config/spawn/koyeb.json"
# 2. Check config file
if [[ -f "$config_file" ]]; then
local saved_token
saved_token=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('token',''))" "$config_file" 2>/dev/null)
if [[ -n "$saved_token" ]]; then
export KOYEB_TOKEN="$saved_token"
log_info "Using Koyeb API token from $config_file"
return 0
fi
fi
# 3. Prompt user for token
log_warn "Koyeb API token required"
echo ""
echo "Get your API token at: https://app.koyeb.com/account/api"
echo ""
local token
token=$(safe_read "Enter Koyeb API token: ")
if [[ -z "$token" ]]; then
log_error "No token provided"
return 1
fi
export KOYEB_TOKEN="$token"
_save_koyeb_token "$token"
log_info "Koyeb API token saved"
}
# Generate a unique server name for Koyeb (must be lowercase alphanumeric + hyphens)
get_server_name() {
local prefix="${1:-spawn}"
local timestamp=$(date +%s)
local random_suffix=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' \n')
echo "${prefix}-${timestamp}-${random_suffix}" | tr '[:upper:]' '[:lower:]'
}
# Create a Koyeb app and service
# Sets: KOYEB_APP_NAME, KOYEB_SERVICE_NAME, KOYEB_SERVICE_ID
create_server() {
local name="${1:-$(get_server_name)}"
# App and service names must be separate
KOYEB_APP_NAME="${name}"
KOYEB_SERVICE_NAME="${name}-svc"
log_warn "Creating Koyeb app: $KOYEB_APP_NAME"
# Create app first
if ! koyeb app create "$KOYEB_APP_NAME" >/dev/null 2>&1; then
log_error "Failed to create Koyeb app"
return 1
fi
log_warn "Creating Koyeb service: $KOYEB_SERVICE_NAME"
# Create service with Ubuntu 24.04 image
# Using a long-running command to keep container alive
local create_output
create_output=$(koyeb service create "$KOYEB_SERVICE_NAME" \
--app "$KOYEB_APP_NAME" \
--docker ubuntu:24.04 \
--regions was \
--instance-type nano \
--command '["tail"]' \
--args '["-f", "/dev/null"]' \
2>&1)
if echo "$create_output" | grep -q "Error"; then
log_error "Failed to create Koyeb service"
log_error "$create_output"
return 1
fi
# Extract service ID from output
KOYEB_SERVICE_ID=$(echo "$create_output" | grep -oP 'Service \K[a-f0-9-]+' | head -1)
if [[ -z "$KOYEB_SERVICE_ID" ]]; then
# Fallback: try to get it from service list
KOYEB_SERVICE_ID=$(koyeb service list --app "$KOYEB_APP_NAME" 2>/dev/null | grep "$KOYEB_SERVICE_NAME" | awk '{print $1}' | head -1)
fi
log_info "Koyeb service created: $KOYEB_SERVICE_NAME (ID: $KOYEB_SERVICE_ID)"
# Wait for service to be ready
log_warn "Waiting for service to deploy..."
local max_attempts=60
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
local status
status=$(koyeb service get "$KOYEB_SERVICE_ID" 2>/dev/null | grep "Status:" | awk '{print $2}')
if [[ "$status" == "healthy" || "$status" == "running" ]]; then
log_info "Service is ready"
break
fi
if [[ "$status" == "error" || "$status" == "failed" ]]; then
log_error "Service deployment failed"
return 1
fi
attempt=$((attempt + 1))
sleep 5
done
if [[ $attempt -ge $max_attempts ]]; then
log_error "Timeout waiting for service to be ready"
return 1
fi
# Get instance ID for exec commands
KOYEB_INSTANCE_ID=$(koyeb instances list --service "$KOYEB_SERVICE_ID" 2>/dev/null | grep -v "^ID" | awk '{print $1}' | head -1)
if [[ -z "$KOYEB_INSTANCE_ID" ]]; then
log_error "Failed to get instance ID"
return 1
fi
log_info "Instance ID: $KOYEB_INSTANCE_ID"
}
# Run a command on the Koyeb service instance
run_server() {
local cmd="$1"
if [[ -z "$KOYEB_INSTANCE_ID" ]]; then
log_error "No instance ID set. Call create_server first."
return 1
fi
koyeb instances exec "$KOYEB_INSTANCE_ID" -- bash -c "$cmd"
}
# Upload a file to the Koyeb instance
upload_file() {
local local_path="$1"
local remote_path="$2"
if [[ ! -f "$local_path" ]]; then
log_error "Local file not found: $local_path"
return 1
fi
# Read file content and encode
local content
content=$(cat "$local_path" | base64)
# Write file on remote instance
run_server "echo '$content' | base64 -d > '$remote_path'"
}
# Wait for cloud-init or basic system readiness
wait_for_cloud_init() {
log_warn "Installing base tools..."
# Update package lists and install essentials
run_server "apt-get update -qq && apt-get install -y -qq curl wget git python3 python3-pip build-essential ca-certificates" || {
log_error "Failed to install base tools"
return 1
}
log_info "Base tools installed"
}
# Inject environment variables into shell config
inject_env_vars() {
local shell_rc="/root/.bashrc"
log_warn "Injecting environment variables..."
for env_var in "$@"; do
# Escape special characters for sed
local escaped_var=$(echo "$env_var" | sed 's/[&/\]/\\&/g')
run_server "echo 'export $escaped_var' >> $shell_rc"
done
log_info "Environment variables configured"
}
# Start an interactive session
interactive_session() {
local launch_cmd="${1:-bash}"
if [[ -z "$KOYEB_INSTANCE_ID" ]]; then
log_error "No instance ID set. Call create_server first."
return 1
fi
log_info "Starting interactive session..."
koyeb instances exec "$KOYEB_INSTANCE_ID" -- bash -c "$launch_cmd"
}
# Cleanup: delete the service and app
cleanup_server() {
if [[ -n "${KOYEB_SERVICE_NAME:-}" ]]; then
log_warn "Deleting service: $KOYEB_SERVICE_NAME"
koyeb service delete "$KOYEB_SERVICE_NAME" --force >/dev/null 2>&1 || true
fi
if [[ -n "${KOYEB_APP_NAME:-}" ]]; then
log_warn "Deleting app: $KOYEB_APP_NAME"
koyeb app delete "$KOYEB_APP_NAME" >/dev/null 2>&1 || true
fi
}

81
koyeb/openclaw.sh Normal file
View file

@ -0,0 +1,81 @@
#!/bin/bash
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/koyeb/lib/common.sh)"
fi
log_info "OpenClaw on Koyeb"
echo ""
# 1. Ensure Koyeb CLI and API token
ensure_koyeb_cli
ensure_koyeb_token
# 2. Create service
SERVER_NAME=$(get_server_name)
create_server "$SERVER_NAME"
# 3. Install base tools
wait_for_cloud_init
# 4. Install bun first (required for openclaw)
log_warn "Installing bun..."
run_server "curl -fsSL https://bun.sh/install | bash"
run_server "export PATH=\"\$HOME/.bun/bin:\$PATH\""
log_info "Bun installed"
# 5. Install openclaw via bun
log_warn "Installing openclaw..."
run_server "source /root/.bashrc && export PATH=\"\$HOME/.bun/bin:\$PATH\" && bun install -g openclaw"
log_info "OpenClaw 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
# Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Openclaw") || exit 1
# 7. Inject environment variables into shell config
log_warn "Setting up environment variables..."
inject_env_vars \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"PATH=\$HOME/.bun/bin:\$PATH"
# 8. Configure openclaw
log_warn "Configuring openclaw..."
run_server "rm -rf /root/.openclaw && mkdir -p /root/.openclaw"
# Generate a random gateway token
GATEWAY_TOKEN=$(openssl rand -hex 16)
OPENCLAW_CONFIG_TEMP=$(mktemp)
chmod 600 "$OPENCLAW_CONFIG_TEMP"
printf '{\n "env": {\n "OPENROUTER_API_KEY": "%s"\n },\n "gateway": {\n "mode": "local",\n "auth": {\n "token": "%s"\n }\n },\n "agents": {\n "defaults": {\n "model": {\n "primary": "openrouter/%s"\n }\n }\n }\n}\n' "$(json_escape "${OPENROUTER_API_KEY}")" "$(json_escape "${GATEWAY_TOKEN}")" "$(json_escape "${MODEL_ID}")" > "$OPENCLAW_CONFIG_TEMP"
upload_file "$OPENCLAW_CONFIG_TEMP" "/root/.openclaw/openclaw.json"
rm "$OPENCLAW_CONFIG_TEMP"
echo ""
log_info "Koyeb service setup completed successfully!"
log_info "Service: $KOYEB_SERVICE_NAME (Instance: $KOYEB_INSTANCE_ID)"
echo ""
# 9. Start openclaw gateway in background and launch TUI
log_warn "Starting openclaw..."
run_server "source /root/.bashrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &"
sleep 2
interactive_session "source /root/.bashrc && openclaw tui"

View file

@ -588,6 +588,21 @@
"image": "Ubuntu Server 24.04 LTS R5504 UEFI"
},
"notes": "GPU cloud provider. Competitive pricing on RTX A6000 ($0.50/hr). Requires HYPERSTACK_API_KEY from https://infrahub.hyperstack.cloud"
},
"koyeb": {
"name": "Koyeb",
"description": "Koyeb serverless container platform via CLI",
"url": "https://www.koyeb.com/",
"type": "cli",
"auth": "KOYEB_TOKEN",
"provision_method": "koyeb service create with docker image",
"exec_method": "koyeb instances exec",
"interactive_method": "koyeb instances exec (PTY)",
"defaults": {
"instance_type": "nano",
"region": "was"
},
"notes": "Serverless container platform. Free tier available (no credit card). Per-second billing. Requires koyeb CLI."
}
},
"matrix": {
@ -926,6 +941,20 @@
"hyperstack/gptme": "missing",
"hyperstack/opencode": "missing",
"hyperstack/plandex": "missing",
"hyperstack/kilocode": "missing"
"hyperstack/kilocode": "missing",
"koyeb/claude": "implemented",
"koyeb/openclaw": "implemented",
"koyeb/nanoclaw": "missing",
"koyeb/aider": "implemented",
"koyeb/goose": "missing",
"koyeb/codex": "missing",
"koyeb/interpreter": "missing",
"koyeb/gemini": "missing",
"koyeb/amazonq": "missing",
"koyeb/cline": "missing",
"koyeb/gptme": "missing",
"koyeb/opencode": "missing",
"koyeb/plandex": "missing",
"koyeb/kilocode": "missing"
}
}