"""Start Routstr, then make the node's choices win over its saved settings. Routstr seeds settings from the environment on first start only; afterwards its database copy wins. The node wallet owns the mints and payout address (CASHU_MINTS, RECEIVE_LN_ADDRESS, rendered by the orchestrator from the node wallet), so they are re-applied through the admin API on every start. The model entry's context is re-stated on every start, so buyers see the context size llama-server really has. Its name, answer limit, price and on/off choice belong to the operator (Settings, "What you sell"): they are set at install defaults only when there is no entry yet, and priced only while unpriced (Routstr refuses to serve an unpriced model). Failures here are logged and never stop Routstr: unconfigured, it serves no paid inference rather than the wrong mint. """ import json import os import signal import subprocess import sys import time import urllib.error import urllib.request API = "http://127.0.0.1:8000" UVICORN = [ "/opt/routstr/.venv/bin/python", "-m", "uvicorn", "routstr:fastapi_app", "--host", "0.0.0.0", "--port", "8000", "--no-access-log", ] def log(message): print(f"archy-start: {message}", file=sys.stderr, flush=True) def call(method, path, body=None, token=None): headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" data = json.dumps(body).encode() if body is not None else None request = urllib.request.Request(API + path, data, headers, method=method) with urllib.request.urlopen(request, timeout=30) as response: return json.loads(response.read() or b"null") def wait_ready(child, deadline): while time.monotonic() < deadline and child.poll() is None: try: call("GET", "/v1/info") return True except (urllib.error.URLError, OSError, ValueError): time.sleep(2) return False def sync_settings(token): update = {} mints = [m.strip() for m in os.environ.get("CASHU_MINTS", "").split(",") if m.strip()] if mints: update["cashu_mints"] = mints update["primary_mint"] = mints[0] address = os.environ.get("RECEIVE_LN_ADDRESS", "").strip() if address: update["receive_ln_address"] = address if update: call("PATCH", "/admin/api/settings", update, token) log(f"applied node wallet settings: {sorted(update)}") def priced(model): pricing = model.get("pricing") or {} return any(float(pricing.get(k) or 0) > 0 for k in ("prompt", "completion", "request")) def sync_model(token): model_id = os.environ.get("ARCHY_MODEL_ID", "").strip() if not model_id: return path = f"/admin/api/upstream-providers/custom/models/{model_id}" try: existing = call("GET", path, token=token) except urllib.error.HTTPError as error: if error.code != 404: raise existing = None keep = existing if existing and priced(existing) else None pricing = keep["pricing"] if keep else { "prompt": os.environ["ARCHY_DEFAULT_PROMPT_USD_PER_TOKEN"], "completion": os.environ["ARCHY_DEFAULT_COMPLETION_USD_PER_TOKEN"], "request": "0", "image": "0", "web_search": "0", "internal_reasoning": "0", } context = int(os.environ.get("ARCHY_MODEL_CONTEXT", "4096")) max_completion = int(os.environ.get("ARCHY_MODEL_MAX_COMPLETION", "1024")) name = os.environ.get("ARCHY_MODEL_NAME", model_id) if existing: name = existing.get("name") or name kept = (existing.get("top_provider") or {}).get("max_completion_tokens") if isinstance(kept, int) and kept > 0: max_completion = kept max_completion = min(max_completion, context) call("POST", "/admin/api/upstream-providers/custom/models", { "id": model_id, "name": name, "description": "Local llama-server model", "created": 0, "context_length": context, "architecture": { "modality": "text->text", "input_modalities": ["text"], "output_modalities": ["text"], "tokenizer": "Other", "instruct_type": None, }, "pricing": pricing, "top_provider": { "context_length": context, "max_completion_tokens": max_completion, "is_moderated": False, }, "enabled": keep.get("enabled", True) if keep else True, }, token) log(f"{model_id}: context {context}, " + ("operator price kept" if keep else "priced at install defaults")) def configure(child): if not wait_ready(child, time.monotonic() + 300): log("Routstr did not become ready; node settings not applied") return try: password = os.environ["ADMIN_PASSWORD"] token = call("POST", "/admin/api/login", {"password": password})["token"] sync_settings(token) sync_model(token) except Exception as error: # never take Routstr down over configuration log(f"configuration failed ({type(error).__name__}): {error}") def main(): child = subprocess.Popen(UVICORN) stopping = [] def forward(signum, _frame): stopping.append(signum) child.send_signal(signum) for sig in (signal.SIGTERM, signal.SIGINT): signal.signal(sig, forward) configure(child) code = child.wait() # uvicorn re-raises the stop signal after a graceful shutdown; a stop we # were asked for is a clean exit, not a crash for the orchestrator. if code < 0 and -code in stopping: code = 0 sys.exit(code if code >= 0 else 128 - code) if __name__ == "__main__": main()