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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ classifiers = [
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.10,<3.15"
requires-python = ">=3.10,<3.13"
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Narrowing requires-python to <3.13 will likely break the existing CI matrix entries that run on Python 3.13 and 3.14 (see .github/workflows/ci.yml). Either update the workflow matrix in this PR (preferred) or keep requires-python compatible with the versions CI is still running.

Suggested change
requires-python = ">=3.10,<3.13"
requires-python = ">=3.10,<3.15"

Copilot uses AI. Check for mistakes.

dependencies = [
"cloudpickle>=3.1.1",
Expand Down
34 changes: 29 additions & 5 deletions src/runpod_flash/cli/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
except ImportError:
import tomli as tomllib # Python 3.9-3.10

from runpod_flash.core.resources.constants import MAX_TARBALL_SIZE_MB
from runpod_flash.core.resources.constants import (
MAX_TARBALL_SIZE_MB,
SUPPORTED_PYTHON_VERSIONS,
validate_python_version,
)

from ..utils.ignore import get_file_tree, load_ignore_patterns
from .build_utils.handler_generator import HandlerGenerator
Expand Down Expand Up @@ -233,6 +237,24 @@ def run_build(
spec = load_ignore_patterns(project_dir)
files = get_file_tree(project_dir, spec)

# Validate Python version unconditionally — even projects with no dependencies
# must build on a supported Python to avoid runtime ABI mismatches.
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
try:
validate_python_version(python_version)
except ValueError:
console.print(
f"\n[red]Python {python_version} is not supported for Flash deployment.[/red]"
)
console.print(
f"[yellow]Supported versions: {', '.join(SUPPORTED_PYTHON_VERSIONS)}[/yellow]"
)
console.print(
"[yellow]Please switch your local Python interpreter to a supported "
"version, or build inside a virtual environment that uses one.[/yellow]"
)
raise typer.Exit(1)

try:
copy_project_files(files, project_dir, build_dir)

Expand All @@ -241,7 +263,11 @@ def run_build(
remote_functions = scanner.discover_remote_functions()

manifest_builder = ManifestBuilder(
app_name, remote_functions, scanner, build_dir=build_dir
app_name,
remote_functions,
scanner,
build_dir=build_dir,
python_version=python_version,
)
manifest = manifest_builder.build()
manifest_path = build_dir / "flash_manifest.json"
Expand Down Expand Up @@ -792,13 +818,11 @@ def install_dependencies(
console.print(f" • {UV_COMMAND} {PIP_MODULE} install {PIP_MODULE}")
return False

# Get current Python version for compatibility
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"

# Determine if using uv pip or standard pip (different flag formats)
is_uv_pip = pip_cmd[0] == UV_COMMAND

# Build pip command with platform-specific flags for RunPod serverless
python_version = f"{sys.version_info.major}.{sys.version_info.minor}"
cmd = pip_cmd + [
"install",
"--target",
Expand Down
5 changes: 5 additions & 0 deletions src/runpod_flash/cli/commands/build_utils/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,17 @@ def __init__(
remote_functions: List[RemoteFunctionMetadata],
scanner=None,
build_dir: Optional[Path] = None,
python_version: Optional[str] = None,
):
self.project_name = project_name
self.remote_functions = remote_functions
self.scanner = (
scanner # Optional: RemoteDecoratorScanner with resource config info
)
self.build_dir = build_dir
self.python_version = (
python_version or f"{sys.version_info.major}.{sys.version_info.minor}"
)

def _import_module(self, file_path: Path):
"""Import a module from file path, returning (module, cleanup_fn).
Expand Down Expand Up @@ -406,6 +410,7 @@ def build(self) -> Dict[str, Any]:

manifest = {
"version": "1.0",
"python_version": self.python_version,
"generated_at": datetime.now(timezone.utc)
.isoformat()
.replace("+00:00", "Z"),
Expand Down
5 changes: 5 additions & 0 deletions src/runpod_flash/cli/utils/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,12 @@ async def provision_resources_for_build(
resources_to_provision = []

# Create resource configs from manifest
manifest_python_version = manifest.get("python_version")
for resource_name, resource_config in manifest["resources"].items():
resource = create_resource_from_manifest(
resource_name,
resource_config,
python_version=manifest_python_version,
)
resources_to_provision.append((resource_name, resource))

Expand Down Expand Up @@ -236,6 +238,7 @@ async def reconcile_and_provision_resources(
# Create resource manager
manager = ResourceManager()
actions = []
manifest_python_version = local_manifest.get("python_version")

# Provision new resources
for resource_name in sorted(to_provision):
Expand All @@ -244,6 +247,7 @@ async def reconcile_and_provision_resources(
resource_name,
resource_config,
flash_environment_id=environment_id,
python_version=manifest_python_version,
)
actions.append(
("provision", resource_name, manager.get_or_deploy_resource(resource))
Expand All @@ -267,6 +271,7 @@ async def reconcile_and_provision_resources(
resource_name,
local_config,
flash_environment_id=environment_id,
python_version=manifest_python_version,
)
actions.append(
("update", resource_name, manager.get_or_deploy_resource(resource))
Expand Down
114 changes: 110 additions & 4 deletions src/runpod_flash/core/resources/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,121 @@ def _endpoint_domain_from_base_url(base_url: str) -> str:
ENDPOINT_DOMAIN = _endpoint_domain_from_base_url(runpod.endpoint_url_base)


# Python version support
SUPPORTED_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")
GPU_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")
CPU_PYTHON_VERSIONS: tuple[str, ...] = ("3.10", "3.11", "3.12")
Comment on lines +23 to +25
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GPU_PYTHON_VERSIONS currently includes "3.10", but the PR description states GPU images require Python 3.11+ (no CUDA 12.8 support for 3.10). If GPU images truly start at 3.11, this constant (and related tests/logic) should be updated to exclude 3.10; otherwise the PR description (and any docs) should be corrected to avoid misleading users and selecting non-existent/unsupported images.

Copilot uses AI. Check for mistakes.
DEFAULT_PYTHON_VERSION: str = "3.11"


def local_python_version() -> str:
"""Return the running interpreter's major.minor version string."""
import sys

return f"{sys.version_info.major}.{sys.version_info.minor}"


# Image type to repository mapping
_IMAGE_REPOS: dict[str, str] = {
"gpu": "runpod/flash",
"cpu": "runpod/flash-cpu",
"lb": "runpod/flash-lb",
"lb-cpu": "runpod/flash-lb-cpu",
}

# Image types that require GPU-compatible Python versions
_GPU_IMAGE_TYPES: frozenset[str] = frozenset({"gpu", "lb"})

# Image type to environment variable override mapping
_IMAGE_ENV_VARS: dict[str, str] = {
"gpu": "FLASH_GPU_IMAGE",
"cpu": "FLASH_CPU_IMAGE",
"lb": "FLASH_LB_IMAGE",
"lb-cpu": "FLASH_CPU_LB_IMAGE",
}


def validate_python_version(version: str) -> str:
"""Validate that a Python version string is supported.

Args:
version: Python version string (e.g. "3.11").

Returns:
The validated version string.

Raises:
ValueError: If version is not in SUPPORTED_PYTHON_VERSIONS.
"""
if version not in SUPPORTED_PYTHON_VERSIONS:
supported = ", ".join(SUPPORTED_PYTHON_VERSIONS)
raise ValueError(
f"Python {version} is not supported. Supported versions: {supported}"
)
return version


def get_image_name(
image_type: str,
python_version: str,
*,
tag: str | None = None,
) -> str:
"""Resolve a versioned Docker image name for the given type and Python version.

Args:
image_type: One of 'gpu', 'cpu', 'lb', 'lb-cpu'.
python_version: Python version string (e.g. "3.11", "3.12").
tag: Image tag suffix. Defaults to FLASH_IMAGE_TAG env var or "latest".

Returns:
Fully qualified image name, e.g. "runpod/flash:py3.12-latest".

Raises:
ValueError: If image_type is unknown, python_version is unsupported,
or a GPU image type is requested with a CPU-only Python version.
"""
if image_type not in _IMAGE_REPOS:
raise ValueError(
f"Unknown image type '{image_type}'. "
f"Valid types: {', '.join(sorted(_IMAGE_REPOS))}"
)

# Environment variable override takes precedence, bypassing version validation
env_var = _IMAGE_ENV_VARS[image_type]
override = os.environ.get(env_var)
if override:
return override

validate_python_version(python_version)

if image_type in _GPU_IMAGE_TYPES and python_version not in GPU_PYTHON_VERSIONS:
gpu_versions = ", ".join(GPU_PYTHON_VERSIONS)
raise ValueError(
f"GPU endpoints require Python {gpu_versions}. Got Python {python_version}."
)

resolved_tag = tag or os.environ.get("FLASH_IMAGE_TAG", "latest")
repo = _IMAGE_REPOS[image_type]
return f"{repo}:py{python_version}-{resolved_tag}"


# Docker image configuration
FLASH_IMAGE_TAG = os.environ.get("FLASH_IMAGE_TAG", "latest")
_RESOLVED_TAG = FLASH_IMAGE_TAG

FLASH_GPU_IMAGE = os.environ.get("FLASH_GPU_IMAGE", f"runpod/flash:{_RESOLVED_TAG}")
FLASH_CPU_IMAGE = os.environ.get("FLASH_CPU_IMAGE", f"runpod/flash-cpu:{_RESOLVED_TAG}")
FLASH_LB_IMAGE = os.environ.get("FLASH_LB_IMAGE", f"runpod/flash-lb:{_RESOLVED_TAG}")
FLASH_GPU_IMAGE = os.environ.get(
"FLASH_GPU_IMAGE", f"runpod/flash:py{DEFAULT_PYTHON_VERSION}-{_RESOLVED_TAG}"
)
FLASH_CPU_IMAGE = os.environ.get(
"FLASH_CPU_IMAGE", f"runpod/flash-cpu:py{DEFAULT_PYTHON_VERSION}-{_RESOLVED_TAG}"
)
FLASH_LB_IMAGE = os.environ.get(
"FLASH_LB_IMAGE", f"runpod/flash-lb:py{DEFAULT_PYTHON_VERSION}-{_RESOLVED_TAG}"
)
FLASH_CPU_LB_IMAGE = os.environ.get(
"FLASH_CPU_LB_IMAGE", f"runpod/flash-lb-cpu:{_RESOLVED_TAG}"
"FLASH_CPU_LB_IMAGE",
f"runpod/flash-lb-cpu:py{DEFAULT_PYTHON_VERSION}-{_RESOLVED_TAG}",
)

# Worker configuration defaults
Expand Down
Loading
Loading