Skip to content
Draft
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 backend/tests/test_default_cloud_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_override_tpu_defaults_to_gke() -> None:

def test_override_gpus_does_not_default_to_gke() -> None:
submission = _submission(override_gpus=1)
assert get_default_cloud_environment(submission) != EnvironmentType.GKE
assert get_default_cloud_environment(submission) == EnvironmentType.DAYTONA


def test_plain_submission_does_not_default_to_gke() -> None:
Expand Down
5 changes: 2 additions & 3 deletions oddish/alembic/versions/modal_costs_002_seed_daytona_rates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
Create Date: 2026-07-22 00:00:00.000000

Adds daytona rows to the ``modal_rates`` card so daytona sandbox spans price
instead of recording as ``no_rate``. Only the sandbox class exists for daytona
(the oddish worker always runs on Modal); GPU trials route to Modal, so the
daytona GPU rows are defensive.
instead of recording as ``no_rate``. Only the sandbox class exists for daytona;
the oddish worker always runs on Modal.

Public list prices from daytona.io/pricing, converted per-hour / 3600 to
per-second. Free-tier allowances (20 vCPU-h + 40 GiB-h/day, $200 credit) are
Expand Down
72 changes: 72 additions & 0 deletions oddish/alembic/versions/modal_costs_003_seed_daytona_rtx_rates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""seed new daytona gpu rate rows

Revision ID: modal_costs_003
Revises: trajgraph_002
Create Date: 2026-08-03 00:00:00.000000
"""

from datetime import datetime, timezone
from decimal import Decimal
from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

from oddish.db import generate_id

revision: str = "modal_costs_003"
down_revision: Union[str, Sequence[str], None] = "trajgraph_002"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None

SEED_EFFECTIVE_AT = datetime(2025, 1, 1, tzinfo=timezone.utc)
SEED_NOTE = "daytona.io/pricing 2026-08-03"
SEED_RATES: tuple[tuple[str, str, str], ...] = (
("daytona", "gpu:RTX_5090", "0.0003583333"),
("daytona", "gpu:RTX_4090", "0.0002750000"),
)


def upgrade() -> None:
bind = op.get_bind()
if not sa.inspect(bind).has_table("modal_rates"):
return
stmt = sa.text(
"""
INSERT INTO modal_rates
(id, provider, sku, usd_per_sec, effective_at, note,
created_at, updated_at)
VALUES
(:id, :provider, :sku, :usd_per_sec, :effective_at, :note,
NOW(), NOW())
ON CONFLICT (provider, sku, effective_at) DO NOTHING
"""
)
for provider, sku, usd_per_sec in SEED_RATES:
bind.execute(
stmt,
{
"id": generate_id(),
"provider": provider,
"sku": sku,
"usd_per_sec": Decimal(usd_per_sec),
"effective_at": SEED_EFFECTIVE_AT,
"note": SEED_NOTE,
},
)


def downgrade() -> None:
bind = op.get_bind()
if not sa.inspect(bind).has_table("modal_rates"):
return
bind.execute(
sa.text(
"""
DELETE FROM modal_rates
WHERE provider = 'daytona' AND effective_at = :effective_at
AND sku IN ('gpu:RTX_5090', 'gpu:RTX_4090')
"""
),
{"effective_at": SEED_EFFECTIVE_AT},
)
3 changes: 1 addition & 2 deletions oddish/src/oddish/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,7 @@ def run(
"-e",
help=(
"Execution environment (docker, daytona, e2b, modal, runloop, gke). "
"Defaults: daytona for CPU-only hosted tasks, modal for GPU hosted "
"tasks, docker otherwise."
"Defaults: daytona for CPU/GPU hosted tasks, docker otherwise."
),
),
] = None,
Expand Down
8 changes: 6 additions & 2 deletions oddish/src/oddish/costs/modal_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,19 @@ class RateRow:
("modal", "gpu:A10", "0.000306"),
("modal", "gpu:L4", "0.000222"),
("modal", "gpu:T4", "0.000164"),
# Daytona (seeded by modal_costs_002). Public list prices from
# Daytona (seeded by modal_costs_002+). Public list prices from
# daytona.io/pricing, converted per-hour / 3600 to per-second. Only the
# sandbox class exists for daytona (the oddish worker always runs on
# Modal); GPU trials route to Modal, so daytona GPU rows are defensive.
# Modal).
# Free-tier allowances (20 vCPU-h + 40 GiB-h/day, $200 credit) are not
# modeled: this is a gross list-price estimate, like the Modal rows.
("daytona", "sandbox:cpu_core_sec", "0.0000140"),
("daytona", "sandbox:mem_gib_sec", "0.0000045"),
("daytona", "gpu:H200", "0.0012611111"),
("daytona", "gpu:H100", "0.0010972222"),
("daytona", "gpu:RTX_PRO_6000", "0.0008416667"),
("daytona", "gpu:RTX_5090", "0.0003583333"),
("daytona", "gpu:RTX_4090", "0.0002750000"),
)
)

Expand Down Expand Up @@ -149,6 +151,8 @@ class SpanResources:
"H200",
"H100",
"RTX_PRO_6000",
"RTX_5090",
"RTX_4090",
"A100-80GB",
"A100-40GB",
"L40S",
Expand Down
7 changes: 5 additions & 2 deletions oddish/src/oddish/runtime/backends/daytona.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Any, Iterator

from oddish.config import settings
from oddish.runtime.ports import Capabilities, ExecutionBackend
from oddish.runtime.ports import Capabilities, ExecutionBackend, GpuSupport

logger = logging.getLogger(__name__)

Expand All @@ -19,7 +19,10 @@ class DaytonaBackend:

def capabilities(self) -> Capabilities:
return Capabilities(
gpu=None,
gpu=GpuSupport(
accelerators=("H200", "H100", "RTX_PRO_6000", "RTX_5090", "RTX_4090"),
max_count=1,
),
private_registry_pull=False,
network_egress="allow",
persistent_volumes=False,
Expand Down
12 changes: 6 additions & 6 deletions oddish/src/oddish/runtime/registry.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Name → ExecutionBackend resolution + cheap-first ordering.

``ordered_backends()`` returns Daytona before Modal so capability negotiation
(routing.py) picks the cheap CPU backend by default and only escalates to
Modal when a capability (GPU, private-registry pull) requires it. GKE joins
last, only when a cluster is configured, so cheap-first negotiation hands it
only the TPU work nothing cheaper satisfies."""
(routing.py) picks the cheap CPU/GPU backend by default and only escalates to
Modal when a capability (private-registry pull) requires it. GKE joins last,
only when a cluster is configured, so cheap-first negotiation hands it only the
TPU work nothing cheaper satisfies."""

from __future__ import annotations

Expand Down Expand Up @@ -39,8 +39,8 @@ def get_backend(name: str | None) -> ExecutionBackend | None:


def ordered_backends() -> list[ExecutionBackend]:
"""Backends in cheap-first order: Daytona (CPU), Modal (GPU/private), then
GKE (TPU) when a cluster is configured.
"""Backends in cheap-first order: Daytona (CPU/GPU), Modal (private
registry), then GKE (TPU) when a cluster is configured.

Sourced from ``REGISTERED_BACKENDS`` (insertion-ordered cheap-first) so the
resolution set and the routing order never desync."""
Expand Down
11 changes: 5 additions & 6 deletions oddish/src/oddish/runtime/routing.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""Capability negotiation + the cloud-environment default.

The negotiation reproduces today's outcome (GPU/private-registry → Modal,
plain CPU → Daytona) by iterating ``ordered_backends()`` (cheap-first) and
returning the first backend whose capabilities satisfy the requirements.
``default_cloud_environment`` is the behavior-preserving facade the CLI and
the backend cloud policy call."""
The negotiation chooses the cheapest registered backend whose capabilities
satisfy the requirements. Daytona is first in the order, so CPU and GPU default
there; private-registry pulls still require Modal. ``default_cloud_environment``
is the facade the CLI and backend cloud policy call."""

from __future__ import annotations

Expand Down Expand Up @@ -47,7 +46,7 @@ def select_backend(
def default_cloud_environment(
*, requires_gpu: bool = False, requires_tpu: bool = False
) -> EnvironmentType:
"""The cloud default via capability negotiation: TPU → GKE, GPU → Modal,
"""The cloud default via capability negotiation: TPU → GKE, GPU → Daytona,
else Daytona."""
return EnvironmentType(
select_backend(requires_gpu=requires_gpu, requires_tpu=requires_tpu).name
Expand Down
32 changes: 32 additions & 0 deletions oddish/src/oddish/workers/harbor/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,44 @@ def apply_harbor_patches() -> None:
global _PATCHED
if _PATCHED:
return
_patch_daytona_gpu_types()
_patch_restricted_network_runtime_fields()
_patch_daytona_dind()
_patch_modal_dind()
_PATCHED = True


def _patch_daytona_gpu_types() -> None:
"""Extend older Harbor Daytona GPU aliases to the current Daytona set."""
try:
module = importlib.import_module("harbor.environments.daytona.environment")
except Exception:
logger.debug("Harbor Daytona environment unavailable; skipping GPU type patch")
return

gpu_map = getattr(module, "DAYTONA_GPU_TYPE_MAP", None)
if not isinstance(gpu_map, dict):
logger.debug("Harbor Daytona GPU type map unavailable; skipping patch")
return

gpu_map.update(
{
"h200": "H200",
"nvidia-h200": "H200",
"nvidia-h200-141gb": "H200",
"rtx-pro-6000": "RTX-PRO-6000",
"rtx_pro_6000": "RTX-PRO-6000",
"nvidia-rtx-pro-6000": "RTX-PRO-6000",
"rtx-4090": "RTX-4090",
"rtx_4090": "RTX-4090",
"nvidia-rtx-4090": "RTX-4090",
"rtx-5090": "RTX-5090",
"rtx_5090": "RTX-5090",
"nvidia-rtx-5090": "RTX-5090",
}
)


def _patch_restricted_network_runtime_fields() -> None:
"""Teach Harbor to consume Oddish's non-serialized runtime fields.

Expand Down
4 changes: 2 additions & 2 deletions oddish/tests/test_cli_run_tpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,13 @@ def test_override_gpus_on_tpu_task_raises_clear_error(tmp_path, monkeypatch):
run._default_cloud_environment_for_task(tmp_path, override_gpus=4)


def test_gpu_task_stays_modal_even_with_gke_available(tmp_path, monkeypatch):
def test_gpu_task_routes_to_daytona_even_with_gke_available(tmp_path, monkeypatch):
(tmp_path / "task.toml").write_text("x")
_patch_env(monkeypatch, gpus=2, tpu=None)
_stub_registry_with_gke(monkeypatch)
assert (
run._default_cloud_environment_for_task(tmp_path, override_gpus=None)
== EnvironmentType.MODAL
== EnvironmentType.DAYTONA
)


Expand Down
27 changes: 25 additions & 2 deletions oddish/tests/test_harbor_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,13 @@ async def test_login_nonzero_log_redacts_token(creds, caplog):


def test_apply_harbor_patches_is_idempotent(monkeypatch):
calls = {"daytona": 0, "modal": 0}
calls = {"gpu": 0, "daytona": 0, "modal": 0}
monkeypatch.setattr(harbor_patches, "_PATCHED", False)
monkeypatch.setattr(
harbor_patches,
"_patch_daytona_gpu_types",
lambda: calls.__setitem__("gpu", calls["gpu"] + 1),
)
monkeypatch.setattr(
harbor_patches,
"_patch_daytona_dind",
Expand All @@ -395,7 +400,25 @@ def test_apply_harbor_patches_is_idempotent(monkeypatch):
harbor_patches.apply_harbor_patches()
harbor_patches.apply_harbor_patches()

assert calls == {"daytona": 1, "modal": 1}
assert calls == {"gpu": 1, "daytona": 1, "modal": 1}


def test_daytona_gpu_type_patch_adds_current_aliases():
module = importlib.import_module("harbor.environments.daytona.environment")
original = dict(module.DAYTONA_GPU_TYPE_MAP)
try:
module.DAYTONA_GPU_TYPE_MAP.clear()
module.DAYTONA_GPU_TYPE_MAP.update({"h100": "H100"})

harbor_patches._patch_daytona_gpu_types()

assert module.DAYTONA_GPU_TYPE_MAP["h200"] == "H200"
assert module.DAYTONA_GPU_TYPE_MAP["nvidia-h200-141gb"] == "H200"
assert module.DAYTONA_GPU_TYPE_MAP["rtx-4090"] == "RTX-4090"
assert module.DAYTONA_GPU_TYPE_MAP["rtx_5090"] == "RTX-5090"
finally:
module.DAYTONA_GPU_TYPE_MAP.clear()
module.DAYTONA_GPU_TYPE_MAP.update(original)


def test_daytona_mirror_patch_targets_dind_only(monkeypatch):
Expand Down
20 changes: 20 additions & 0 deletions oddish/tests/test_modal_cost_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ def test_normalize_gpu_type() -> None:
assert normalize_gpu_type("A100_80GB") == "A100-80GB"
assert normalize_gpu_type("L40S") == "L40S"
assert normalize_gpu_type("rtx pro 6000") == "RTX_PRO_6000"
assert normalize_gpu_type("rtx-5090") == "RTX_5090"
assert normalize_gpu_type("RTX_4090") == "RTX_4090"
assert normalize_gpu_type("any") is None
assert normalize_gpu_type("ANY") is None
assert normalize_gpu_type("warpcore9000") is None
Expand Down Expand Up @@ -223,6 +225,23 @@ def test_daytona_sandbox_prices_at_daytona_rates() -> None:
)


def test_daytona_rtx_gpu_prices_at_daytona_rates() -> None:
sel = select_rates(DEFAULT_RATES, "daytona", "sandbox", "RTX_4090", T0)
assert sel.gpu is not None
assert sel.gpu.sku == "gpu:RTX_4090"
assert sel.gpu.usd_per_sec == Decimal("0.0002750000")

res = estimate_span_cost(
T0,
T0 + timedelta(seconds=10),
_sandbox(gpu_type="RTX_4090", gpu_count=1),
sel,
)
assert res.cost_usd == Decimal(10) * (
Decimal(2) * DAYTONA_CPU + Decimal(4) * DAYTONA_MEM + Decimal("0.0002750000")
)


def test_mem_mib_to_gib_is_exact_divide_by_1024() -> None:
sel = select_rates(DEFAULT_RATES, "modal", "sandbox", None, T0)
res = estimate_span_cost(
Expand Down Expand Up @@ -466,6 +485,7 @@ def test_default_rates_mirror_migration_seed_exactly() -> None:
# migration can append rows without pinning DEFAULT_RATES' ordering.
seed = _migration_seed_rates("modal_costs_001_add_modal_costs.py")
seed += _migration_seed_rates("modal_costs_002_seed_daytona_rates.py")
seed += _migration_seed_rates("modal_costs_003_seed_daytona_rtx_rates.py")
expected = {(row.provider, row.sku, str(row.usd_per_sec)) for row in DEFAULT_RATES}
assert set(seed) == expected
assert len(seed) == len(DEFAULT_RATES), "duplicate or missing seed row"
Expand Down
6 changes: 3 additions & 3 deletions oddish/tests/test_offline_task_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ def test_online_cpu_task_keeps_the_cheap_default(tmp_path) -> None:
)


def test_gpu_task_still_routes_to_modal(tmp_path) -> None:
# The GPU rule is untouched by the revert.
def test_gpu_task_routes_to_daytona(tmp_path) -> None:
# GPU now uses the cheap-first default too.
task_dir = _write_task(
tmp_path,
"""
Expand All @@ -166,5 +166,5 @@ def test_gpu_task_still_routes_to_modal(tmp_path) -> None:
)
assert (
_default_cloud_environment_for_task(task_dir, override_gpus=None)
== EnvironmentType.MODAL
== EnvironmentType.DAYTONA
)
12 changes: 10 additions & 2 deletions oddish/tests/test_runtime_daytona_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,17 @@ def test_daytona_backend_name_matches_environment_value() -> None:
assert DaytonaBackend().name == "daytona"


def test_daytona_capabilities_are_cpu_only_no_private_registry() -> None:
def test_daytona_capabilities_include_gpu_no_private_registry() -> None:
caps = DaytonaBackend().capabilities()
assert caps.gpu is None
assert caps.gpu is not None
assert caps.gpu.accelerators == (
"H200",
"H100",
"RTX_PRO_6000",
"RTX_5090",
"RTX_4090",
)
assert caps.gpu.max_count == 1
assert caps.private_registry_pull is False
assert caps.cold_start == "seconds"

Expand Down
Loading
Loading