Spaces:
Paused
Paused
File size: 8,293 Bytes
f850954 da839c2 4508afe f850954 da839c2 5dbd0bf 1a36c6b 52ba0e1 115459a 1a36c6b 5b063c2 b357d7d 04ad412 52ba0e1 536e671 8501103 52ba0e1 5dbd0bf f850954 1a36c6b f850954 da839c2 536e671 a4ef305 f850954 536e671 da839c2 5dbd0bf 52ba0e1 37dfe11 1a36c6b 916d469 b357d7d 5b063c2 04ad412 52ba0e1 536e671 8501103 f850954 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """Settings, read from environment variables (see .env.example)."""
from __future__ import annotations
import os
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
def _env(name: str, default: str) -> str:
value = os.environ.get(name)
return value if value not in (None, "") else default
@dataclass(frozen=True)
class Settings:
# Model: one model for every tester in Stage 1, via any OpenAI-compatible endpoint (OpenRouter by default).
llm_api_key: str
llm_base_url: str
llm_model: str # scene generation (/simulate/generate)
code_llm_model: str # ROS 2 code generation: OpenCode's model, or the direct backend's
llm_timeout: float
# Storage
data_dir: Path
testers_file: Path
log_db: Path
# MuJoCo generation
menagerie_dir: Path
mjcf_max_attempts: int
mjcf_sim_seconds: float
validate_timeout: float
# ROS 2 generation
codegen_backend: str # "opencode" or "direct"
opencode_bin: str
opencode_timeout: float
code_max_attempts: int
# Simulation runs (Modal or RunPod serverless, results in Cloudflare R2)
sim_backend: str # "modal" or "runpod"
modal_app: str
runpod_api_key: str
runpod_endpoint_id: str
run_execution_timeout_s: int
run_max_duration_s: float
run_burst_warn: int # log a warning when a tester starts more runs than this within 10 minutes
report_secret: str # signs report links
public_base_url: str # how testers reach this API (for report links); empty = use the request's host
# Spend: estimated from each job's run time; new jobs are refused once the month's estimate reaches the limit.
monthly_credit_usd: float # 0 = no limit
# ...and each tester gets an equal share of it, enforced before every job against the job's worst case.
compute_testers: int # how many testers share the compute credit
compute_per_tester_usd: float # 0 = monthly_credit_usd / compute_testers
storage_limit_gb: float # new jobs are refused while the R2 bucket holds more than this (0 = no limit)
job_prices: dict # USD per second by hardware: t4, l4, cpu, runpod
job_overhead_s: float # billed seconds per job beyond the worker's own timing (start-up, idle shutdown)
isaac_daily_limit: int # Isaac Sim renders per day, across all testers
auto_review: bool # review every finished simulation video with Cosmos Reason
# AI budget in rupees for the test group: one shared pool, an equal allocation per tester. Only billable
# AI calls count (OpenRouter-exempt ones are recorded but not deducted); GPU/CPU jobs have their own
# dollar limit above (Modal's free credit).
usd_inr: float
budget_total_inr: float
budget_per_user_inr: float
confirm_above_inr: float # ask before running a task estimated to cost more than this
admin_testers: tuple # testers who see the team budget view
hf_namespace: str # Hugging Face user/org that dataset exports are pushed to (with the worker's HF_TOKEN)
foxglove_org: str # optional Foxglove organization slug for the embedded viewer
# Prompt enhancement through OpenCode with a free OpenCode Zen model (never OpenRouter, never billed).
prompt_enhance: bool
enhance_model: str # "<opencode provider>/<model>"
enhance_timeout: float
zen_api_key: str # OpenCode Zen key, if the free model asks for one
# Debugging: the first repair goes to a free OpenCode Zen model first (paid tier only if that call fails).
free_debug: bool
debug_model: str # "opencode/<model>"
owner_api_key: str # persistent key for the owner (bypasses budgets, doesn't count as tester)
@property
def compute_share_usd(self) -> float:
"""Each tester's monthly GPU/CPU allowance in dollars (0 = no per-tester limit)."""
if self.compute_per_tester_usd > 0:
return self.compute_per_tester_usd
if self.monthly_credit_usd > 0 and self.compute_testers > 0:
return round(self.monthly_credit_usd / self.compute_testers, 6)
return 0.0
@property
def interfaces_file(self) -> Path:
return self.data_dir / "ros2_interfaces.json"
@property
def mjcf_schema_file(self) -> Path:
return self.data_dir / "mjcf_schema.json"
# Estimated USD per second of container time, GPU plus the CPU and memory that come with it
# (Modal list prices, Sept 2026; override with JOB_PRICES="t4=...,l4=...,cpu=...,runpod=...").
DEFAULT_PRICES = {"t4": 0.00019, "l4": 0.00025, "cpu": 0.00004, "runpod": 0.00031}
def _prices(spec: str) -> dict:
prices = dict(DEFAULT_PRICES)
for part in filter(None, (p.strip() for p in spec.split(","))):
key, _, value = part.partition("=")
prices[key.strip()] = float(value)
return prices
@lru_cache
def get_settings() -> Settings:
data_dir = Path(_env("ROSDIFF_DATA_DIR", str(REPO_ROOT / "data"))).resolve()
return Settings(
llm_api_key=_env("LLM_API_KEY", ""),
llm_base_url=_env("LLM_BASE_URL", "https://openrouter.ai/api/v1").rstrip("/"),
llm_model=_env("LLM_MODEL", "z-ai/glm-5.2"),
# DeepSeek V4 Flash: 79.0% on SWE-bench Verified (V4 Pro: 80.6%) at about $0.10/$0.20 per M tokens on
# OpenRouter (Sept 2026) - enough for a test phase. For more quality: deepseek/deepseek-v4-pro.
code_llm_model=_env("CODE_LLM_MODEL", "deepseek/deepseek-v4-flash"),
llm_timeout=float(_env("LLM_TIMEOUT", "180")),
data_dir=data_dir,
testers_file=Path(_env("ROSDIFF_TESTERS_FILE", str(REPO_ROOT / "testers.yaml"))),
log_db=Path(_env("ROSDIFF_LOG_DB", str(data_dir / "generations.sqlite3"))),
menagerie_dir=Path(_env("MENAGERIE_DIR", str(data_dir / "sources" / "mujoco_menagerie"))),
mjcf_max_attempts=int(_env("MJCF_MAX_ATTEMPTS", "3")),
mjcf_sim_seconds=float(_env("MJCF_SIM_SECONDS", "2.0")),
validate_timeout=float(_env("VALIDATE_TIMEOUT", "60")),
codegen_backend=_env("CODEGEN_BACKEND", "opencode"),
opencode_bin=_env("OPENCODE_BIN", "opencode"),
opencode_timeout=float(_env("OPENCODE_TIMEOUT", "600")),
code_max_attempts=int(_env("CODE_MAX_ATTEMPTS", "3")),
sim_backend=_env("SIM_BACKEND", "modal").lower(),
modal_app=_env("MODAL_APP", "rosdiff-sim"),
runpod_api_key=_env("RUNPOD_API_KEY", ""),
runpod_endpoint_id=_env("RUNPOD_ENDPOINT_ID", ""),
run_execution_timeout_s=int(_env("RUN_EXECUTION_TIMEOUT_S", "600")),
run_max_duration_s=float(_env("RUN_MAX_DURATION_S", "60")),
run_burst_warn=int(_env("RUN_BURST_WARN", "10")),
report_secret=_env("REPORT_SECRET", ""),
public_base_url=_env("PUBLIC_BASE_URL", "").rstrip("/"),
monthly_credit_usd=float(_env("MONTHLY_CREDIT_LIMIT_USD", "29.98")),
compute_testers=int(_env("COMPUTE_TESTERS", "4")),
compute_per_tester_usd=float(_env("COMPUTE_PER_TESTER_USD", "0")),
storage_limit_gb=float(_env("STORAGE_LIMIT_GB", "9")),
job_prices=_prices(_env("JOB_PRICES", "")),
job_overhead_s=float(_env("JOB_OVERHEAD_S", "45")),
isaac_daily_limit=int(_env("ISAAC_DAILY_LIMIT", "10")),
auto_review=_env("AUTO_REVIEW", "false").lower() in ("1", "true", "yes"),
hf_namespace=_env("HF_NAMESPACE", "rosdiff"),
usd_inr=float(_env("USD_INR", "88")),
budget_total_inr=float(_env("BUDGET_TOTAL_INR", "500")),
budget_per_user_inr=float(_env("BUDGET_PER_USER_INR", "125")),
confirm_above_inr=float(_env("CONFIRM_ABOVE_INR", "10")),
admin_testers=tuple(n.strip() for n in _env("ADMIN_TESTERS", "").split(",") if n.strip()),
foxglove_org=_env("FOXGLOVE_ORG", ""),
prompt_enhance=_env("PROMPT_ENHANCE", "true").lower() in ("1", "true", "yes"),
enhance_model=_env("ENHANCE_MODEL", "opencode/deepseek-v4-flash-free"),
enhance_timeout=float(_env("ENHANCE_TIMEOUT", "90")),
zen_api_key=_env("ZEN_API_KEY", ""),
free_debug=_env("FREE_DEBUG", "true").lower() in ("1", "true", "yes"),
debug_model=_env("DEBUG_MODEL", "opencode/deepseek-v4-flash-free"),
owner_api_key=_env("OWNER_API_KEY", ""),
)
|