rosdiff / api /config.py
Chandrakiran-08
Deploy to HF Spaces
8501103
Raw History Blame Contribute Delete
8.29 kB
"""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", ""),
)