feat: cloud adapters E2B/Modal and billing export (SAND-WP-0010)

Add credentialed E2B and Modal extensions, burst routing fallback,
fin-hub meter export hook, BYOK docs, and 77 tests.
This commit is contained in:
tegwick 2026-06-24 12:50:19 +02:00
parent 6d0a1a8b1e
commit 15f031fd65
26 changed files with 859 additions and 75 deletions

View file

@ -4,6 +4,7 @@ from __future__ import annotations
import os
from sandboxer.extensions.credentials import credentials_available
from sandboxer.extensions.registry import load_extension
from sandboxer.models import Extension, Profile, RouteStrategy
from sandboxer.payments.metering import estimate_cost
@ -20,6 +21,49 @@ def _is_metered(ext: Extension) -> bool:
return ext.capabilities.pricing_model == "metered"
def _metered_available(ext: Extension) -> bool:
if ext.id == "ext.saas-stub":
return True
return credentials_available(ext.id, ext.config)
def _select_metered_fallback(
loaded: list[Extension],
profile: Profile,
inputs: dict[str, str],
*,
duration_s: int,
) -> Extension | None:
"""Pick cheapest credentialed metered extension; stub is always last resort."""
available = [ext for ext in loaded if _is_metered(ext) and _metered_available(ext)]
if not available:
return None
strategy = profile.route.strategy if profile.route else RouteStrategy.EXPLICIT
if strategy == RouteStrategy.LOWEST_COST:
best: Extension | None = None
best_cost: float | None = None
for ext in available:
if not _metered_available(ext):
continue
cost = _quote_cost(ext, profile, inputs, duration_s)
if cost is None:
continue
max_hour = profile.route.max_cost_per_hour_usd if profile.route else None
if max_hour is not None and cost > max_hour:
continue
if best is None or cost < (best_cost or float("inf")):
best, best_cost = ext, cost
if best:
return best
for ext_id in ("ext.e2b", "ext.modal", "ext.saas-stub"):
for ext in available:
if ext.id == ext_id:
return ext
return available[0]
def _self_hosted_available(profile: Profile, ext: Extension, host_override: str | None) -> bool:
if _is_metered(ext):
return True
@ -65,6 +109,11 @@ def resolve_extension(
for ext in loaded:
if not _is_metered(ext) and _self_hosted_available(profile, ext, host_override):
return ext
fallback = _select_metered_fallback(
loaded, profile, inputs, duration_s=duration_s
)
if fallback:
return fallback
for ext in loaded:
if _is_metered(ext):
return ext
@ -76,6 +125,8 @@ def resolve_extension(
for ext in loaded:
if not _is_metered(ext) and _self_hosted_available(profile, ext, host_override):
return ext
if not _metered_available(ext):
continue
cost = _quote_cost(ext, profile, inputs, duration_s)
if cost is None:
continue
@ -92,6 +143,11 @@ def resolve_extension(
for ext in loaded:
if not _is_metered(ext) and _self_hosted_available(profile, ext, host_override):
return ext
fallback = _select_metered_fallback(
loaded, profile, inputs, duration_s=duration_s
)
if fallback:
return fallback
return loaded[-1]
return load_extension(profile.extension)