Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6079274
docs: add PRD for zero-boilerplate flash run experience
deanq Feb 18, 2026
13a6757
feat(client,scanner): LB route handler passthrough and path-aware dis…
deanq Feb 18, 2026
0782671
refactor(manifest): remove mothership dead code, flat resource structure
deanq Feb 18, 2026
9b38bd8
feat(run): file-system-as-namespace dev server generation
deanq Feb 18, 2026
098aafc
feat(build,lb_handler_generator): invoke LB handler generator, rglob …
deanq Feb 18, 2026
ceb099f
fix(serverless): resolve flash run runtime bugs
deanq Feb 18, 2026
c4065f3
fix(run): hot-reload regenerates server.py on route changes
deanq Feb 18, 2026
d8e14b7
fix(run): suppress watchfiles debug logs from flash run output
deanq Feb 18, 2026
dd2491d
fix(run): omit body param from GET/HEAD route handlers
deanq Feb 18, 2026
9896c46
feat(run): proxy LB routes to deployed endpoints, restore --auto-prov…
deanq Feb 19, 2026
440b4b3
fix(run): add project root to sys.path during resource discovery
deanq Feb 19, 2026
1c285ce
feat(run): show resource count and elapsed time during cleanup
deanq Feb 19, 2026
98d471f
fix(run): route LB calls through LoadBalancerSlsStub instead of HTTP …
deanq Feb 19, 2026
76e6927
fix(run): revert LB to remote dispatch, remove QB /run route
deanq Feb 19, 2026
e0874e0
refactor(init): simplify skeleton to flat worker files for flash run
deanq Feb 19, 2026
bed42ef
fix(run): handle numeric-prefixed directories in server codegen
deanq Feb 19, 2026
5480404
fix(ci): update validate-wheel.sh for flat skeleton template
deanq Feb 20, 2026
8b1e19f
fix: address PR 208 review feedback
deanq Feb 20, 2026
277cd7a
fix(run): match generated QB/LB route calls to @remote function signa…
deanq Feb 20, 2026
6881489
feat(run): replace body: dict with Pydantic models for typed Swagger UI
deanq Feb 20, 2026
ef92813
feat(stubs): inject remote dispatch stubs for stacked @remote execution
deanq Feb 20, 2026
f262f65
refactor(run): group Swagger UI tags by project directory
deanq Feb 20, 2026
ea426c5
fix: address PR 208 review feedback
deanq Feb 20, 2026
9f1928d
fix(serverless): prevent ValueError when deploy mutates config with b…
deanq Feb 20, 2026
35af650
chore: remove PRD.md from branch
deanq Feb 20, 2026
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
21 changes: 5 additions & 16 deletions scripts/validate-wheel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,12 @@ REQUIRED_TEMPLATE_FILES=(
"runpod_flash/cli/utils/skeleton_template/.env.example"
"runpod_flash/cli/utils/skeleton_template/.gitignore"
"runpod_flash/cli/utils/skeleton_template/.flashignore"
"runpod_flash/cli/utils/skeleton_template/main.py"
"runpod_flash/cli/utils/skeleton_template/cpu_worker.py"
"runpod_flash/cli/utils/skeleton_template/gpu_worker.py"
"runpod_flash/cli/utils/skeleton_template/lb_worker.py"
"runpod_flash/cli/utils/skeleton_template/pyproject.toml"
"runpod_flash/cli/utils/skeleton_template/README.md"
"runpod_flash/cli/utils/skeleton_template/requirements.txt"
"runpod_flash/cli/utils/skeleton_template/workers/__init__.py"
"runpod_flash/cli/utils/skeleton_template/workers/cpu/__init__.py"
"runpod_flash/cli/utils/skeleton_template/workers/cpu/endpoint.py"
"runpod_flash/cli/utils/skeleton_template/workers/gpu/__init__.py"
"runpod_flash/cli/utils/skeleton_template/workers/gpu/endpoint.py"
)

MISSING_IN_WHEEL=0
Expand Down Expand Up @@ -77,7 +75,7 @@ flash init test_project > /dev/null 2>&1
# Verify critical files exist
echo ""
echo "Verifying created files..."
REQUIRED_FILES=(".env.example" ".gitignore" ".flashignore" "main.py" "README.md" "requirements.txt")
REQUIRED_FILES=(".env.example" ".gitignore" ".flashignore" "cpu_worker.py" "gpu_worker.py" "lb_worker.py" "pyproject.toml" "README.md" "requirements.txt")
MISSING_IN_OUTPUT=0

for file in "${REQUIRED_FILES[@]}"; do
Expand All @@ -94,15 +92,6 @@ for file in "${REQUIRED_FILES[@]}"; do
fi
done

# Verify workers directory structure
if [ -d "test_project/workers/cpu" ] && [ -d "test_project/workers/gpu" ]; then
echo "[OK] workers/cpu/"
echo "[OK] workers/gpu/"
else
echo "[MISSING] workers directory structure"
MISSING_IN_OUTPUT=$((MISSING_IN_OUTPUT + 1))
fi

# Cleanup
deactivate
cd - > /dev/null
Expand Down
110 changes: 110 additions & 0 deletions src/runpod_flash/cli/commands/_run_server_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Helpers for the flash run dev server — loaded inside the generated server.py."""

import inspect
from typing import Any, get_type_hints

from fastapi import HTTPException
from pydantic import create_model

from runpod_flash.core.resources.resource_manager import ResourceManager
from runpod_flash.stubs.load_balancer_sls import LoadBalancerSlsStub

_resource_manager = ResourceManager()


def _map_body_to_params(func, body):
"""Map an HTTP request body to function parameters.

If the body is a dict whose keys match the function's parameter names,
spread it as kwargs. Otherwise pass the whole body as the value of the
first parameter (mirrors how FastAPI maps a JSON body to a single param).
"""
sig = inspect.signature(func)
param_names = set(sig.parameters.keys())

if isinstance(body, dict) and body.keys() <= param_names:
return body

first_param = next(iter(sig.parameters), None)
if first_param is None:
return {}
return {first_param: body}


def make_input_model(name: str, func) -> type | None:
"""Create a Pydantic model from a function's signature for FastAPI body typing.

Returns None for zero-param functions or on failure (caller uses ``or dict``).
"""
try:
sig = inspect.signature(func)
hints = get_type_hints(func)
except (ValueError, TypeError):
return None

_SKIP_KINDS = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
fields: dict[str, Any] = {}
for param_name, param in sig.parameters.items():
if param_name == "self" or param.kind in _SKIP_KINDS:
continue
annotation = hints.get(param_name, Any)
if param.default is not inspect.Parameter.empty:
fields[param_name] = (annotation, param.default)
else:
fields[param_name] = (annotation, ...)

if not fields:
return None

return create_model(name, **fields)


async def call_with_body(func, body):
"""Call func with body kwargs, handling Pydantic models and dicts."""
if hasattr(body, "model_dump"):
return await func(**body.model_dump())
raw = body.get("input", body) if isinstance(body, dict) else body
kwargs = _map_body_to_params(func, raw)
return await func(**kwargs)


def to_dict(body) -> dict:
"""Convert Pydantic model or dict to plain dict."""
return body.model_dump() if hasattr(body, "model_dump") else body


async def lb_execute(resource_config, func, body: dict):
"""Dispatch an LB route to the deployed endpoint via LoadBalancerSlsStub.

Provisions the endpoint via ResourceManager, maps the HTTP body to
function kwargs, then dispatches through the stub's /execute path
which serializes the function via cloudpickle to the remote container.

Args:
resource_config: The resource config object (e.g. LiveLoadBalancer instance).
func: The @remote LB route handler function.
body: Parsed request body (from FastAPI's automatic JSON parsing).
"""
try:
deployed = await _resource_manager.get_or_deploy_resource(resource_config)
except Exception as e:
raise HTTPException(
status_code=503,
detail=f"Failed to provision '{resource_config.name}': {e}",
)

stub = LoadBalancerSlsStub(deployed)
kwargs = _map_body_to_params(func, body)

try:
return await stub(func, None, None, False, **kwargs)
except TimeoutError as e:
raise HTTPException(status_code=504, detail=str(e))
except ConnectionError as e:
raise HTTPException(status_code=502, detail=str(e))
except HTTPException:
raise
except (ValueError, KeyError, TypeError) as e:
raise HTTPException(status_code=422, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
25 changes: 10 additions & 15 deletions src/runpod_flash/cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from runpod_flash.core.resources.constants import MAX_TARBALL_SIZE_MB

from ..utils.ignore import get_file_tree, load_ignore_patterns
from .build_utils.lb_handler_generator import LBHandlerGenerator
from .build_utils.manifest import ManifestBuilder
from .build_utils.scanner import RemoteDecoratorScanner

Expand Down Expand Up @@ -240,6 +241,9 @@ def run_build(
manifest_path = build_dir / "flash_manifest.json"
manifest_path.write_text(json.dumps(manifest, indent=2))

lb_generator = LBHandlerGenerator(manifest, build_dir)
lb_generator.generate_handlers()

flash_dir = project_dir / ".flash"
deployment_manifest_path = flash_dir / "flash_manifest.json"
shutil.copy2(manifest_path, deployment_manifest_path)
Expand Down Expand Up @@ -426,28 +430,19 @@ def validate_project_structure(project_dir: Path) -> bool:
"""
Validate that directory is a Flash project.

A Flash project is any directory containing Python files. The
RemoteDecoratorScanner validates that @remote functions exist.

Args:
project_dir: Directory to validate

Returns:
True if valid Flash project
"""
main_py = project_dir / "main.py"

if not main_py.exists():
console.print(f"[red]Error:[/red] main.py not found in {project_dir}")
py_files = list(project_dir.rglob("*.py"))
if not py_files:
console.print(f"[red]Error:[/red] No Python files found in {project_dir}")
return False

# Check if main.py has FastAPI app
try:
content = main_py.read_text(encoding="utf-8")
if "FastAPI" not in content:
console.print(
"[yellow]Warning:[/yellow] main.py does not appear to have a FastAPI app"
)
except Exception:
pass

return True


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,10 @@
- Real-time communication patterns
"""

import asyncio
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Optional

from fastapi import FastAPI, Request
from fastapi import FastAPI
from runpod_flash.runtime.lb_handler import create_lb_handler

logger = logging.getLogger(__name__)
Expand All @@ -45,57 +42,8 @@
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Handle application startup and shutdown."""
# Startup
logger.info("Starting {resource_name} endpoint")

# Check if this is the mothership and run reconciliation
# Note: Resources are now provisioned upfront by the CLI during deployment.
# This background task runs reconciliation on mothership startup to ensure
# all resources are still deployed and in sync with the manifest.
try:
from runpod_flash.runtime.mothership_provisioner import (
is_mothership,
reconcile_children,
get_mothership_url,
)
from runpod_flash.runtime.state_manager_client import StateManagerClient

if is_mothership():
logger.info("=" * 60)
logger.info("Mothership detected - Starting reconciliation task")
logger.info("Resources are provisioned upfront by the CLI")
logger.info("This task ensures all resources remain in sync")
logger.info("=" * 60)
try:
mothership_url = get_mothership_url()
logger.info(f"Mothership URL: {{mothership_url}}")

# Initialize State Manager client for reconciliation
state_client = StateManagerClient()

# Spawn background reconciliation task (non-blocking)
# This will verify all resources from manifest are deployed
manifest_path = Path(__file__).parent / "flash_manifest.json"
task = asyncio.create_task(
reconcile_children(manifest_path, mothership_url, state_client)
)
# Add error callback to catch and log background task exceptions
task.add_done_callback(
lambda t: logger.error(f"Reconciliation task failed: {{t.exception()}}")
if t.exception()
else None
)

except Exception as e:
logger.error(f"Failed to start reconciliation task: {{e}}")
# Don't fail startup - continue serving traffic

except ImportError:
logger.debug("Mothership provisioning modules not available")

yield

# Shutdown
logger.info("Shutting down {resource_name} endpoint")


Expand Down
Loading
Loading