Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 45 additions & 18 deletions .github/workflows/fal-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@ on:
- main
- prod
default: 'main'
app:
description: 'Which app to deploy'
required: true
type: choice
options:
- both
- scope-app
- scope-livepeer
default: 'both'
workflow_run:
workflows: ["Build and Push Docker Image"]
types:
Expand Down Expand Up @@ -51,10 +42,6 @@ jobs:
run: pip install fal

- name: Deploy Scope app to fal
if: >
github.event_name == 'workflow_run' ||
github.event.inputs.app == 'both' ||
github.event.inputs.app == 'scope-app'
env:
FAL_KEY: ${{ secrets.FAL_KEY }}
run: |
Expand All @@ -68,11 +55,7 @@ jobs:
echo "Deploying Scope app to $ENV"
fal deploy --env $ENV --auth public src/scope/cloud/fal_app.py

- name: Deploy Livepeer runner to fal
if: >
github.event_name == 'workflow_run' ||
github.event.inputs.app == 'both' ||
github.event.inputs.app == 'scope-livepeer'
- name: Deploy default Livepeer runner to fal
env:
FAL_KEY: ${{ secrets.FAL_KEY }}
run: |
Expand All @@ -84,8 +67,52 @@ jobs:
fi

echo "Deploying Livepeer runner to $ENV as scope-livepeer"
echo "Using default deploy GPU"

fal deploy \
--app-name scope-livepeer \
--env $ENV \
--auth private \
src/scope/cloud/livepeer_fal_app.py

- name: Deploy RTX 4090 Livepeer runner to fal
env:
FAL_KEY: ${{ secrets.FAL_KEY }}
SCOPE_DEPLOY_GPU: GPU-RTX4090
run: |
# Use input environment for manual dispatch, default to main for workflow_run
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
ENV="${{ github.event.inputs.environment }}"
else
ENV="main"
fi

echo "Deploying Livepeer runner to $ENV as scope-livepeer-rtx4090"
echo "Using deploy GPU override: ${SCOPE_DEPLOY_GPU}"

fal deploy \
--app-name scope-livepeer-rtx4090 \
--env $ENV \
--auth private \
src/scope/cloud/livepeer_fal_app.py

- name: Deploy RTX 5090 Livepeer runner to fal
env:
FAL_KEY: ${{ secrets.FAL_KEY }}
SCOPE_DEPLOY_GPU: GPU-RTX5090
run: |
# Use input environment for manual dispatch, default to main for workflow_run
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
ENV="${{ github.event.inputs.environment }}"
else
ENV="main"
fi

echo "Deploying Livepeer runner to $ENV as scope-livepeer-rtx5090"
echo "Using deploy GPU override: ${SCOPE_DEPLOY_GPU}"

fal deploy \
--app-name scope-livepeer-rtx5090 \
--env $ENV \
--auth private \
src/scope/cloud/livepeer_fal_app.py
35 changes: 34 additions & 1 deletion docs/livepeer.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ fal deploy --env main --auth public src/scope/cloud/livepeer_fal_app.py

This starts the Livepeer runner as a subprocess inside the Fal container and proxies `/ws` traffic to it.

Use `SCOPE_DEPLOY_GPU` to select the Fal machine type at deploy time:

```bash
# Default H100 deployment:
fal deploy --app-name scope-livepeer --env prod --auth private src/scope/cloud/livepeer_fal_app.py

# RTX 4090 deployment:
SCOPE_DEPLOY_GPU=GPU-RTX4090 \
fal deploy --app-name scope-livepeer-rtx4090 --env prod --auth private src/scope/cloud/livepeer_fal_app.py

# RTX 5090 deployment:
SCOPE_DEPLOY_GPU=GPU-RTX5090 \
fal deploy --app-name scope-livepeer-rtx5090 --env prod --auth private src/scope/cloud/livepeer_fal_app.py
```

Supported deploy values are `GPU-RTX4090` and `GPU-RTX5090`. When `SCOPE_DEPLOY_GPU` is unset, the wrapper keeps the current default `GPU-H100`.

## Start the Scope Server

Set environment variables and launch the server:
Expand Down Expand Up @@ -71,7 +88,22 @@ LIVEPEER_WS_URL=wss://fal.run/<app-id>/ws \
uv run daydream-scope
```

To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and `SCOPE_CLOUD_APP_ID`. In that case the runner URL uses the default Livepeer flow.
To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and `SCOPE_CLOUD_APP_ID`. In that case Scope uses the built-in default Livepeer app id.

```bash
# Default H100-backed Livepeer deployment:
SCOPE_CLOUD_MODE=livepeer \
LIVEPEER_TOKEN=<base64-json-token> \
uv run daydream-scope

# Select the RTX 4090 Livepeer deployment:
SCOPE_CLOUD_MODE=livepeer \
LIVEPEER_TOKEN=<base64-json-token> \
SCOPE_CLOUD_GPU=rtx4090 \
uv run daydream-scope
```

If `SCOPE_CLOUD_APP_ID` is set, Scope will use that app id as-is and will not rewrite it based on `SCOPE_CLOUD_GPU`.

### Environment Variables

Expand All @@ -82,6 +114,7 @@ To switch away from explicit runner overrides, unset both `LIVEPEER_WS_URL` and
| `LIVEPEER_SIGNER` | No | Override signer URL used for Livepeer payments. To disable payments, set to a falsy value such as `"off"`. |
| `LIVEPEER_WS_URL` | No | Explicit runner WebSocket URL (e.g. `ws://127.0.0.1:8001/ws`). |
| `SCOPE_CLOUD_APP_ID` | No | Fal app id used to construct `ws_url` as `wss://fal.run/<app-id>`. Must include `/ws` suffix. Used when `LIVEPEER_WS_URL` is not set. |
| `SCOPE_CLOUD_GPU` | No | Livepeer GPU selector. Supported values: `h100`, `rtx4090`, `rtx5090`. Default `h100`. Ignored when `LIVEPEER_WS_URL` or `SCOPE_CLOUD_APP_ID` is set. |
| `LIVEPEER_TOKEN` | No | Base64-encoded JSON token used to start the LV2V job. Can be used to override Livepeer orch / payments routing. |
| `LIVEPEER_DEBUG` | No | Enables debug logging for the Livepeer Gateway SDK and local Livepeer modules. |
| `LIVEPEER_DEV_MODE` | No | Used for developing against a local Livepeer orchestrator with self-signed certificates. |
Expand Down
23 changes: 22 additions & 1 deletion src/scope/cloud/livepeer_fal_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
RUNNER_MAX_FAILURES_PER_WINDOW = 20
RUNNER_FAILURE_WINDOW_SECONDS = 60.0
ASSETS_DIR_PATH = "/tmp/.daydream-scope/assets"
SCOPE_DEPLOY_GPU_ENV = "SCOPE_DEPLOY_GPU"

# Gates startup cleanup so only one cleanup run executes at a time.
_cleanup_event: asyncio.Event | None = None
Expand Down Expand Up @@ -129,6 +130,21 @@ def _get_git_sha() -> str:
)


def _get_livepeer_machine_type() -> str:
"""Return the Fal machine type selected by SCOPE_DEPLOY_GPU."""
deploy_gpu = os.getenv(SCOPE_DEPLOY_GPU_ENV, "").strip()
if not deploy_gpu:
return "GPU-H100"
if deploy_gpu in {"GPU-RTX4090", "GPU-RTX5090"}:
return deploy_gpu
raise ValueError(
"Invalid SCOPE_DEPLOY_GPU. Expected `GPU-RTX4090`, `GPU-RTX5090`, or unset."
)


LIVEPEER_MACHINE_TYPE = _get_livepeer_machine_type()


def _runner_is_ready() -> bool:
"""Return True when the local runner HTTP server responds."""
import urllib.error
Expand Down Expand Up @@ -224,7 +240,7 @@ class LivepeerScopeApp(fal.App, keep_alive=300):
"""fal entrypoint that runs and proxies the existing Livepeer Scope runner."""

image = custom_image
machine_type = "GPU-H100"
machine_type = LIVEPEER_MACHINE_TYPE
requirements = [
"websockets",
"httpx",
Expand All @@ -235,6 +251,11 @@ def setup(self):
import subprocess

print(f"Starting Livepeer runner wrapper setup... (version: {GIT_SHA})")
print(
"Resolved Livepeer deploy GPU: "
f"{os.getenv(SCOPE_DEPLOY_GPU_ENV, '').strip() or 'default'} "
f"-> {self.machine_type}"
)

try:
result = subprocess.run(
Expand Down
7 changes: 3 additions & 4 deletions src/scope/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from typing import TYPE_CHECKING

import click
import click.core
import uvicorn
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPException, Query, Request
from fastapi.middleware.cors import CORSMiddleware
Expand Down Expand Up @@ -3115,7 +3116,7 @@ async def connect_to_cloud(
# Use request body credentials if provided, otherwise fall back to CLI/env
app_id = request.app_id or os.environ.get("SCOPE_CLOUD_APP_ID")
api_key = request.api_key or os.environ.get("SCOPE_CLOUD_API_KEY")
if not app_id:
if not app_id and not is_livepeer_enabled():
raise HTTPException(
status_code=400,
detail="cloud credentials not configured. Use --cloud-app-id and --cloud-api-key CLI args, "
Expand Down Expand Up @@ -3425,7 +3426,7 @@ def run_server(reload: bool, host: str, port: int, no_browser: bool):
)
@click.option(
"--cloud-app-id",
default="Daydream/scope-app--prod/ws",
default=None,
envvar="SCOPE_CLOUD_APP_ID",
help="Cloud app ID for cloud mode (e.g., 'username/scope-app')",
)
Expand Down Expand Up @@ -3460,8 +3461,6 @@ def main(

# MCP mode: run the MCP stdio server instead of the HTTP server
if mcp:
import click.core

from .mcp_server import run_mcp_server

# Only pre-connect if --port was explicitly provided on the command line
Expand Down
2 changes: 1 addition & 1 deletion src/scope/server/livepeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

def is_livepeer_enabled() -> bool:
"""Check if Livepeer mode is enabled via environment variables."""
return os.getenv("SCOPE_CLOUD_MODE", "").lower() == "livepeer"
return os.getenv("SCOPE_CLOUD_MODE", "livepeer").lower() == "livepeer"


class LivepeerConnection:
Expand Down
36 changes: 29 additions & 7 deletions src/scope/server/livepeer_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@
TASK_DRAIN_TIMEOUT_S = 0.25
RUNNER_RESTART_TIMEOUT_S = 30.0
PAYMENT_SEND_INTERVAL_S = 10.0
SCOPE_CLOUD_GPU_ENV = "SCOPE_CLOUD_GPU"
DEFAULT_LIVEPEER_APP_ID = "daydream/scope-livepeer--prod/ws"


def _normalize_optional_string(value: str | None) -> str | None:
if value is None:
return None
normalized = value.strip()
return normalized or None


def _resolve_livepeer_app_id(
app_id: str | None,
) -> str | None:
normalized_app_id = _normalize_optional_string(app_id)
if normalized_app_id is not None:
return normalized_app_id.strip("/")

gpu = _normalize_optional_string(os.getenv(SCOPE_CLOUD_GPU_ENV))
if gpu not in {None, "h100", "rtx4090", "rtx5090"}:
raise ValueError(
"Invalid SCOPE_CLOUD_GPU. Expected `h100`, `rtx4090`, `rtx5090`, or unset."
)
gpu_suffix = "" if gpu in {None, "h100"} else f"-{gpu}"
return f"daydream/scope-livepeer{gpu_suffix}--prod/ws"


@dataclass(slots=True)
Expand Down Expand Up @@ -312,14 +337,11 @@ def _normalize_ws_url(value: str | None) -> str | None:

@staticmethod
def _ws_url_from_app_id(value: str | None) -> str | None:
# HACK: Ignore the default app_id to use the orchestrator's own config
if not value or value == "Daydream/scope-app--prod/ws":
resolved_app_id = _resolve_livepeer_app_id(value)
if not resolved_app_id:
return None
try:
trimmed = value.strip()
if not trimmed:
raise ValueError
app_id = trimmed.strip("/")
app_id = resolved_app_id.strip("/")
if not app_id.endswith("/ws"):
raise ValueError
ws_url = f"wss://fal.run/{app_id}"
Expand All @@ -329,7 +351,7 @@ def _ws_url_from_app_id(value: str | None) -> str | None:
except Exception:
raise ValueError(
"Invalid cloud app id. Expected a non-empty app id ending in "
"`/ws` (for example `daydream/scope-app/ws`)."
"`/ws` (for example `daydream/custom-runner--prod/ws`)."
) from None
if parsed.scheme not in {"ws", "wss"}:
raise ValueError("Invalid ws_url. Expected a valid ws:// or wss:// URL.")
Expand Down
Loading