synderesis-api / space_start.py
cuivienen's picture
Deploy Synderesis API Docker Space
35c027e verified
Raw
History Blame Contribute Delete
8.43 kB
#!/usr/bin/env python3
"""Start the Synderesis proxy inside a Hugging Face Docker Space."""
from __future__ import annotations
import json
import os
import sys
from decimal import InvalidOperation, Decimal
from pathlib import Path
from huggingface_hub import hf_hub_download
DEFAULT_TINKER_MODEL_PATH = (
"tinker://69e3ecac-e413-51b3-b3ff-2eb20d22c878:train:1/"
"sampler_weights/synderesis-nemotron-super-catholic-v2-style-20260616T170321Z"
)
DEFAULT_BACKEND = "llm-synod"
DEPLOYMENT_METADATA_PATH = Path("synderesis_deployment.json")
PERSISTENT_STATE_DIR = Path("/data")
FALLBACK_RUNTIME_DIR = Path("/tmp/synderesis")
REQUIRE_PERSISTENT_STATE_ENV = "SYNDERESIS_REQUIRE_PERSISTENT_STATE"
def load_deployment_metadata() -> dict[str, object]:
"""Read build metadata generated by the deploy script."""
if not DEPLOYMENT_METADATA_PATH.exists():
return {}
try:
payload = json.loads(DEPLOYMENT_METADATA_PATH.read_text(encoding="utf-8"))
except Exception as exc:
print(f"Could not read deployment metadata: {exc}", file=sys.stderr, flush=True)
return {}
return payload if isinstance(payload, dict) else {}
def configure_deployment_env(metadata: dict[str, object]) -> None:
"""Expose safe deployment metadata to the API process."""
source_db = metadata.get("source_db") if isinstance(metadata.get("source_db"), dict) else {}
website_variant = str(metadata.get("website_variant") or "").strip().lower()
env_values = {
"SYNDERESIS_GIT_COMMIT": metadata.get("git_commit", ""),
"SYNDERESIS_GIT_BRANCH": metadata.get("git_branch", ""),
"SYNDERESIS_BUILD_GENERATED_AT": metadata.get("generated_at", ""),
"SYNDERESIS_SOURCE_DB_SHA256": source_db.get("sha256", "") if isinstance(source_db, dict) else "",
"SYNDERESIS_SOURCE_DB_EMBEDDER_SIGNATURE": source_db.get("embedder_signature", "") if isinstance(source_db, dict) else "",
}
for key, value in env_values.items():
if value:
os.environ.setdefault(key, str(value))
# The UI and API mode come from the same uploaded bundle, including upload-only deploys.
os.environ["SYNDERESIS_WEBSITE_VARIANT"] = (
website_variant if website_variant in {"catholic", "research"} else "catholic"
)
def log_deployment_metadata(metadata: dict[str, object]) -> None:
"""Emit structured non-secret deployment metadata once at startup."""
if not metadata:
print("SYNDERESIS_DEPLOYMENT_INFO {}", file=sys.stderr, flush=True)
return
print(f"SYNDERESIS_DEPLOYMENT_INFO {json.dumps(metadata, sort_keys=True, separators=(',', ':'))}", file=sys.stderr, flush=True)
def persistent_state_required() -> bool:
"""Return whether this deployment must use the mounted /data volume."""
raw_value = os.getenv(REQUIRE_PERSISTENT_STATE_ENV, "").strip().lower()
if not raw_value:
return False
if raw_value in {"1", "true", "yes", "on"}:
return True
if raw_value in {"0", "false", "no", "off"}:
return False
raise RuntimeError(
f"{REQUIRE_PERSISTENT_STATE_ENV} must be a boolean flag"
)
def writable_runtime_dir() -> Path:
"""Return a writable directory for SQLite state."""
if (
PERSISTENT_STATE_DIR.is_dir()
and os.access(PERSISTENT_STATE_DIR, os.W_OK)
):
return PERSISTENT_STATE_DIR
if persistent_state_required():
raise RuntimeError(
"Persistent API state requires a writable /data volume"
)
FALLBACK_RUNTIME_DIR.mkdir(parents=True, exist_ok=True)
return FALLBACK_RUNTIME_DIR
def configure_source_db() -> None:
"""Download the retrieval database from a Hub dataset when configured."""
if os.getenv("SYNDERESIS_SOURCE_DB"):
return
repo_id = os.getenv("SYNDERESIS_SOURCE_DB_REPO", "").strip()
if not repo_id:
return
filename = os.getenv("SYNDERESIS_SOURCE_DB_FILENAME", "official_sources.sqlite3").strip()
repo_type = os.getenv("SYNDERESIS_SOURCE_DB_REPO_TYPE", "dataset").strip()
token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN") or None
downloaded_path = hf_hub_download(repo_id=repo_id, filename=filename, repo_type=repo_type, token=token)
os.environ["SYNDERESIS_SOURCE_DB"] = downloaded_path
print(f"Using source retrieval DB: {downloaded_path}", file=sys.stderr)
def configure_defaults() -> None:
"""Set Space-safe defaults without overriding explicit variables."""
runtime_dir = writable_runtime_dir()
os.environ.setdefault("SYNDERESIS_API_DB", str(runtime_dir / "synderesis_api.sqlite3"))
if persistent_state_required():
ledger = Path(os.environ["SYNDERESIS_API_DB"]).resolve()
if not ledger.is_relative_to(PERSISTENT_STATE_DIR.resolve()):
raise RuntimeError(
"Persistent API state requires SQLite under /data"
)
os.environ.setdefault("SYNDERESIS_SQLITE_JOURNAL_MODE", "DELETE")
os.environ.setdefault("SYNDERESIS_API_HOST", "0.0.0.0")
os.environ.setdefault("SYNDERESIS_API_PORT", os.getenv("PORT", "7860"))
os.environ.setdefault("SYNDERESIS_BACKEND", os.getenv("SYNDERESIS_MODEL_BACKEND", DEFAULT_BACKEND))
os.environ.setdefault("SYNDERESIS_MODEL_BACKEND", os.getenv("SYNDERESIS_BACKEND", DEFAULT_BACKEND))
if os.environ["SYNDERESIS_MODEL_BACKEND"] == "tinker":
os.environ.setdefault(
"SYNDERESIS_TINKER_MODEL_PATH",
DEFAULT_TINKER_MODEL_PATH,
)
os.environ.setdefault("SYNDERESIS_RETRIEVAL_LIMIT", "6")
os.environ.setdefault("SYNDERESIS_CHAT_CONTEXT_MESSAGE_LIMIT", "12")
os.environ.setdefault("SYNDERESIS_CHAT_CONTEXT_CHAR_LIMIT", "12000")
os.environ.setdefault("SYNDERESIS_SOURCE_EMBEDDER", "harrier")
os.environ.setdefault("NOLEGRAPH_AUTOLOAD_EMBEDDER", "1")
stripe_names = (
"SYNDERESIS_STRIPE_SECRET_KEY",
"SYNDERESIS_STRIPE_WEBHOOK_SECRET",
"SYNDERESIS_STRIPE_FIXED_PRICE_ID",
"SYNDERESIS_STRIPE_METERED_PRICE_ID",
"SYNDERESIS_STRIPE_METER_EVENT_NAME",
"SYNDERESIS_STRIPE_PUBLIC_ORIGIN",
"SYNDERESIS_STRIPE_INCLUDED_RETAIL_MICRO_USD",
"SYNDERESIS_STRIPE_FIXED_MONTHLY_CENTS",
"SYNDERESIS_STRIPE_EXPECTED_LIVEMODE",
"SYNDERESIS_STRIPE_REQUIRE_PERSISTENT_LEDGER",
)
byok_fee_name = "SYNDERESIS_STRIPE_BYOK_PLATFORM_FEE_RATE"
configured_byok_fee = os.getenv(byok_fee_name, "").strip()
present = {name for name in stripe_names if os.getenv(name, "").strip()}
if not present and configured_byok_fee:
raise RuntimeError(
"BYOK platform fee configuration requires Stripe billing"
)
if present and present != set(stripe_names):
raise RuntimeError("Stripe billing configuration is incomplete")
if present:
byok_fee = configured_byok_fee or "0.25"
try:
parsed_fee = Decimal(byok_fee)
except (InvalidOperation, ValueError) as exc:
raise RuntimeError(
"Stripe BYOK platform fee rate must be a finite decimal"
) from exc
if (
not parsed_fee.is_finite()
or parsed_fee <= 0
or parsed_fee >= 1
):
raise RuntimeError(
"Stripe BYOK platform fee rate must be greater than zero and less than one"
)
os.environ[byok_fee_name] = byok_fee
if os.environ["SYNDERESIS_STRIPE_REQUIRE_PERSISTENT_LEDGER"].lower() != "true":
raise RuntimeError("Stripe billing requires persistent ledger enforcement")
ledger = Path(os.environ["SYNDERESIS_API_DB"]).resolve()
if not ledger.is_relative_to(Path("/data")):
raise RuntimeError("Stripe billing requires persistent SQLite state under /data")
def main() -> None:
"""Configure the container and replace this process with the API server."""
deployment_metadata = load_deployment_metadata()
configure_deployment_env(deployment_metadata)
log_deployment_metadata(deployment_metadata)
configure_defaults()
configure_source_db()
port = os.environ["SYNDERESIS_API_PORT"]
cmd = [
sys.executable,
"scripts/serve_huggingface_api.py",
"--host",
os.environ["SYNDERESIS_API_HOST"],
"--port",
port,
]
os.execvpe(sys.executable, cmd, os.environ)
if __name__ == "__main__":
main()