{
  "manifest_version": "1.0.0",
  "template": {
    "id": "61571e20-995d-4204-b9b4-2522b4303eae",
    "slug": "Lm9gxI",
    "name": "LiteLLM",
    "description": "LiteLLM: LLM router, virtual keys, budgets, cost tracking, guardrails",
    "url": "https://railway.com/deploy/Lm9gxI",
    "upstream": {
      "image": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf"
    }
  },
  "services": [
    {
      "name": "LiteLLM",
      "source": {
        "image": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf"
      },
      "needs_volume": false,
      "http": true
    },
    {
      "name": "Postgres",
      "source": {
        "image": "ghcr.io/railwayapp-templates/postgres-ssl:18@sha256:469c779c7c57ec6bad4670a0a3cb5a830aa6e0ce4f3707137608de5223a5041c"
      },
      "needs_volume": true,
      "volume_mount_path": "/var/lib/postgresql/data",
      "http": false
    },
    {
      "name": "Redis",
      "source": {
        "image": "redis:8.2@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d"
      },
      "needs_volume": true,
      "volume_mount_path": "/data",
      "http": false
    }
  ],
  "required_inputs": [
    {
      "key": "HOST",
      "service": "LiteLLM",
      "description": "Public listen address. Keep 0.0.0.0; the startup helper rejects a binding Railway cannot reach.",
      "secret": false,
      "strategy": "default",
      "default": "0.0.0.0"
    },
    {
      "key": "PORT",
      "service": "LiteLLM",
      "description": "Port used by both LiteLLM and Railway readiness. Default 4000; the startup helper validates and passes this value to the server.",
      "secret": false,
      "strategy": "default",
      "default": "4000"
    },
    {
      "key": "REDIS_URL",
      "service": "LiteLLM",
      "description": "Enables response caching and coordinates rate limits across replicas.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "OR_SITE_URL",
      "service": "LiteLLM",
      "description": "Optional - Sent to OpenRouter as HTTP-Referer for attribution.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_domain"
    },
    {
      "key": "UI_USERNAME",
      "service": "LiteLLM",
      "description": "Optional - Admin UI username.",
      "secret": false,
      "strategy": "default",
      "default": "admin"
    },
    {
      "key": "DATABASE_URL",
      "service": "LiteLLM",
      "description": "Stores models, virtual keys, budgets and spend.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "LITELLM_SALT_KEY",
      "service": "LiteLLM",
      "description": "Encrypts provider credentials at rest. Generated once and NEVER changed - rotating it silently makes every stored provider key unreadable.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "STORE_MODEL_IN_DB",
      "service": "LiteLLM",
      "description": "Lets you add models from the Admin UI. Without it, Add Model returns HTTP 500.",
      "secret": false,
      "strategy": "default",
      "default": "True"
    },
    {
      "key": "LITELLM_MASTER_KEY",
      "service": "LiteLLM",
      "description": "Your API key AND the Admin UI password. Generated for you - copy it from this service's Variables tab after deploying.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "LITELLM_LOCAL_MODEL_COST_MAP",
      "service": "LiteLLM",
      "description": "Uses the bundled pricing table instead of fetching it at boot. Halves startup time.",
      "secret": false,
      "strategy": "default",
      "default": "True"
    },
    {
      "key": "ENFORCE_PRISMA_MIGRATION_CHECK",
      "service": "LiteLLM",
      "description": "Fails the deploy loudly on a bad migration instead of serving a half-migrated database.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "LiteLLM",
      "description": "Allows 120 seconds for the previous gateway deployment to drain. Long requests may exceed this; dependency redeploys still interrupt service.",
      "secret": false,
      "strategy": "default",
      "default": "120"
    },
    {
      "key": "PGDATA",
      "service": "Postgres",
      "description": "Postgres data directory, inside the mounted volume.",
      "secret": false,
      "strategy": "default",
      "default": "/var/lib/postgresql/data/pgdata"
    },
    {
      "key": "PGHOST",
      "service": "Postgres",
      "description": "Private-network hostname of the Postgres service.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "PGPORT",
      "service": "Postgres",
      "description": "Port Postgres listens on.",
      "secret": false,
      "strategy": "default",
      "default": "5432"
    },
    {
      "key": "PGUSER",
      "service": "Postgres",
      "description": "Postgres superuser name.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PGDATABASE",
      "service": "Postgres",
      "description": "Default database name used by psql and the Postgres client tools.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "PGPASSWORD",
      "service": "Postgres",
      "description": "Password for the Postgres superuser.",
      "secret": true,
      "strategy": "railway_provided",
      "railway_source": "reference_variable"
    },
    {
      "key": "POSTGRES_DB",
      "service": "Postgres",
      "description": "Database created on first boot.",
      "secret": false,
      "strategy": "default",
      "default": "railway"
    },
    {
      "key": "DATABASE_URL",
      "service": "Postgres",
      "description": "Private-network connection string. LiteLLM stores models, virtual keys, budgets and spend history here.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "POSTGRES_USER",
      "service": "Postgres",
      "description": "Superuser created on first boot.",
      "secret": false,
      "strategy": "default",
      "default": "postgres"
    },
    {
      "key": "SSL_CERT_DAYS",
      "service": "Postgres",
      "description": "Validity window, in days, for the self-signed TLS certificate this image generates.",
      "secret": false,
      "strategy": "default",
      "default": "820"
    },
    {
      "key": "POSTGRES_PASSWORD",
      "service": "Postgres",
      "description": "Superuser password, generated once at deploy time.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Postgres",
      "description": "Seconds Railway keeps the old container alive after the new one is live, so in-flight requests can finish.",
      "secret": false,
      "strategy": "default",
      "default": "60"
    },
    {
      "key": "REDISHOST",
      "service": "Redis",
      "description": "Private-network hostname of the Redis service.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDISPORT",
      "service": "Redis",
      "description": "Port Redis listens on.",
      "secret": false,
      "strategy": "default",
      "default": "6379"
    },
    {
      "key": "REDISUSER",
      "service": "Redis",
      "description": "Redis username for this image's default user.",
      "secret": false,
      "strategy": "default",
      "default": "default"
    },
    {
      "key": "REDIS_URL",
      "service": "Redis",
      "description": "Private-network connection string. LiteLLM uses Redis for response caching and cross-replica rate limiting.",
      "secret": false,
      "strategy": "railway_provided",
      "railway_source": "railway_private_domain"
    },
    {
      "key": "REDIS_PASSWORD",
      "service": "Redis",
      "description": "Redis password, generated once at deploy time.",
      "secret": true,
      "strategy": "generate",
      "generate": "strong_password"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Redis",
      "description": "Seconds Railway keeps the old container alive after the new one is live, so in-flight requests can finish.",
      "secret": false,
      "strategy": "default",
      "default": "60"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "Lm9gxI"
      }
    },
    "cli": "railway deploy --template Lm9gxI",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "61571e20-995d-4204-b9b4-2522b4303eae",
            "serializedConfig": {
              "services": {
                "00af68c0-dab6-4206-9b37-75db9a6f3b73": {
                  "icon": "https://bogusz.co/external/litellm-icon.png",
                  "name": "LiteLLM",
                  "deploy": {
                    "startCommand": "python -c 'exec('\"'\"'\"\"\"Embedded startup command for the official LiteLLM image (no LiteLLM imports).\\n\\npredeploy: validate, wait for Postgres, run `prisma migrate deploy` directly (~90 MiB).\\nserve:     validate, wait for Postgres, exec litellm with the Prisma CLI hidden so the\\n           proxy skips its own startup migration pass (~500 MiB of Node on top of Python).\\n\"\"\"\\n\\nimport importlib.util\\nimport os\\nimport re\\nimport signal\\nimport subprocess\\nimport sys\\nimport tempfile\\nimport time\\n\\nimport yaml\\n\\nMIB = 2**20\\n# Measured on the pinned image: serve peaks ~800 MiB at readiness, ~1.3 GiB with LiteLLM\\'\"'\"'s own\\n# startup migration pass. Below MIN_MEMORY nothing works; below COMFORTABLE_MEMORY it runs but\\n# has little headroom under load.\\nMIN_MEMORY = 900 * MIB\\nCOMFORTABLE_MEMORY = 1536 * MIB\\n\\n\\nclass StartupError(Exception):\\n    \"\"\"Safe-to-log startup failure; never include user values or driver messages.\"\"\"\\n\\n\\ndef validate_environment(environ):\\n    for name in (\"LITELLM_MASTER_KEY\", \"LITELLM_SALT_KEY\"):\\n        if not environ.get(name, \"\").strip():\\n            raise StartupError(f\"{name} must be nonblank\")\\n    port = environ.get(\"PORT\", \"4000\")\\n    if not re.fullmatch(r\"[0-9]{1,5}\", port) or not 1 <= int(port) <= 65535:\\n        raise StartupError(\"PORT must be an integer from 1 to 65535\")\\n    host = environ.get(\"HOST\", \"0.0.0.0\")\\n    if host != \"0.0.0.0\":\\n        raise StartupError(\"HOST must be 0.0.0.0 for the template public binding\")\\n    return host, str(int(port))\\n\\n\\ndef validate_config(raw):\\n    if not raw or not raw.strip():\\n        return None\\n    try:\\n        config = yaml.safe_load(raw)\\n    except yaml.YAMLError as exc:\\n        mark = getattr(exc, \"problem_mark\", None)\\n        location = f\" at line {mark.line + 1}, column {mark.column + 1}\" if mark else \"\"\\n        raise StartupError(f\"LITELLM_CONFIG_YAML is invalid YAML{location}\") from None\\n    if not isinstance(config, dict) or not config:\\n        raise StartupError(\"LITELLM_CONFIG_YAML root must be a nonempty mapping\")\\n    for key in (\"general_settings\", \"litellm_settings\", \"router_settings\"):\\n        if key in config and not isinstance(config[key], dict):\\n            raise StartupError(f\"LITELLM_CONFIG_YAML {key} must be a mapping\")\\n    if \"model_list\" in config and not isinstance(config[\"model_list\"], list):\\n        raise StartupError(\"LITELLM_CONFIG_YAML model_list must be a list\")\\n    return config\\n\\n\\ndef resolve_database_url(config, environ):\\n    \"\"\"Match LiteLLM\\'\"'\"'s YAML-over-environment database selection before probing.\"\"\"\\n    configured = (config or {}).get(\"general_settings\", {}).get(\"database_url\")\\n    value = configured if configured is not None else environ.get(\"DATABASE_URL\")\\n    if isinstance(value, str) and value.startswith(\"os.environ/\"):\\n        value = environ.get(value.removeprefix(\"os.environ/\"))\\n    if not isinstance(value, str) or not value.strip():\\n        raise StartupError(\"Database URL must be nonblank; check DATABASE_URL and general_settings.database_url\")\\n    return value\\n\\n\\ndef memory_limit(path=\"/sys/fs/cgroup/memory.max\"):\\n    \"\"\"Container memory limit in bytes, or None when unlimited/unknown.\"\"\"\\n    try:\\n        with open(path) as f:\\n            value = f.read().strip()\\n    except OSError:\\n        return None\\n    return int(value) if value.isdigit() else None\\n\\n\\ndef check_memory(limit):\\n    if limit is None:\\n        return\\n    if limit < MIN_MEMORY:\\n        raise StartupError(\\n            f\"this container allows {limit // MIB} MiB of memory; LiteLLM needs at least 1 GB to start \"\\n            \"(Railway Trial) and 2 GB or more for real use (Hobby plan or higher). Upgrade the plan or \"\\n            \"raise the service memory limit, then redeploy\"\\n        )\\n    if limit < COMFORTABLE_MEMORY:\\n        print(\\n            f\"LiteLLM startup: {limit // MIB} MiB memory limit is enough to start but leaves little headroom; \"\\n            \"expect restarts under load. 2 GB or more is recommended\",\\n            file=sys.stderr, flush=True,\\n        )\\n\\n\\ndef prisma_cli():\\n    cli = os.environ.get(\"PRISMA_CLI_PATH\")\\n    if not cli:\\n        raise StartupError(\"The pinned image must provide PRISMA_CLI_PATH\")\\n    return cli\\n\\n\\ndef database_probe(dsn, timeout):\\n    \"\"\"Use the image\\'\"'\"'s offline Prisma toolchain, not an extra Python DB driver.\"\"\"\\n    command = [prisma_cli(), \"db\", \"execute\", \"--url\", dsn, \"--stdin\"]\\n    with subprocess.Popen(\\n        command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,\\n        text=True, start_new_session=True,\\n    ) as process:\\n        try:\\n            _, error = process.communicate(\"SELECT 1;\", timeout=timeout)\\n        except subprocess.TimeoutExpired:\\n            # Retire the native schema-engine child as well as the CLI parent.\\n            os.killpg(process.pid, signal.SIGKILL)\\n            process.communicate()\\n            return False\\n        if process.returncode == 0:\\n            return True\\n    # Driver output can contain credentials and SQL. Classify it, never print it.\\n    code = re.search(r\"\\\\bP[0-9]{4}\\\\b\", error)\\n    if code and code.group() in {\"P1001\", \"P1002\", \"P1008\", \"P1017\", \"P2024\", \"P2037\"}:\\n        return False\\n    if \"database system is starting up\" in error.lower():\\n        return False\\n    raise StartupError(\"Database connection rejected; check DATABASE_URL and credentials\")\\n\\n\\ndef wait_for_database(dsn):\\n    deadline = time.monotonic() + 90\\n    waiting = False\\n    while (remaining := deadline - time.monotonic()) > 0:\\n        if database_probe(dsn, min(5, remaining)):\\n            return\\n        if not waiting:\\n            print(\"LiteLLM startup: waiting up to 90 seconds for database readiness\", file=sys.stderr, flush=True)\\n            waiting = True\\n        remaining = deadline - time.monotonic()\\n        if remaining > 0:\\n            time.sleep(min(2, remaining))\\n    raise StartupError(\"Database readiness timed out after 90 seconds\")\\n\\n\\ndef migrations_dir():\\n    \"\"\"Directory holding LiteLLM\\'\"'\"'s schema.prisma and migrations, found without importing it.\"\"\"\\n    spec = importlib.util.find_spec(\"litellm_proxy_extras\")\\n    if spec is None or not spec.submodule_search_locations:\\n        raise StartupError(\"The pinned image must provide litellm_proxy_extras\")\\n    return list(spec.submodule_search_locations)[0]\\n\\n\\ndef migrate(dsn):\\n    \"\"\"`prisma migrate deploy` exactly as LiteLLM\\'\"'\"'s v2 resolver runs it, minus a 1.3 GiB Python import.\"\"\"\\n    env = os.environ | {\"DATABASE_URL\": dsn, \"NPM_CONFIG_PREFER_OFFLINE\": \"true\", \"PRISMA_HIDE_UPDATE_MESSAGE\": \"1\"}\\n    result = subprocess.run([prisma_cli(), \"migrate\", \"deploy\"], cwd=migrations_dir(), env=env)\\n    if result.returncode:\\n        raise StartupError(\"database migration failed; see the Prisma output above\")\\n\\n\\ndef serve_path(environ):\\n    \"\"\"PATH with every venv tool except `prisma`: LiteLLM then skips its startup migration pass.\"\"\"\\n    venv_bin = os.path.dirname(sys.executable)\\n    shim = tempfile.mkdtemp(prefix=\"litellm-bin-\")\\n    for name in os.listdir(venv_bin):\\n        if name != \"prisma\":\\n            os.symlink(os.path.join(venv_bin, name), os.path.join(shim, name))\\n    rest = [p for p in environ.get(\"PATH\", \"\").split(os.pathsep) if p and p != venv_bin]\\n    return os.pathsep.join([shim, *rest])\\n\\n\\ndef fail(message):\\n    \"\"\"Print the reason, then linger briefly: Railway drops the logs of containers that exit within ~1 s.\"\"\"\\n    print(message, file=sys.stderr, flush=True)\\n    time.sleep(3)\\n    return 1\\n\\n\\ndef main():\\n    path = None\\n    try:\\n        if len(sys.argv) != 2 or sys.argv[1] not in (\"predeploy\", \"serve\"):\\n            raise StartupError(\"Expected mode: predeploy or serve\")\\n        host, port = validate_environment(os.environ)\\n        raw = os.environ.get(\"LITELLM_CONFIG_YAML\")\\n        parsed = validate_config(raw)\\n        check_memory(memory_limit())\\n        dsn = resolve_database_url(parsed, os.environ)\\n        wait_for_database(dsn)\\n        if sys.argv[1] == \"predeploy\":\\n            migrate(dsn)\\n            return 0\\n        command = [os.path.join(os.path.dirname(sys.executable), \"litellm\"), \"--host\", host, \"--port\", port]\\n        if parsed is not None:\\n            # NamedTemporaryFile creates mode 0600. Preserve the original YAML,\\n            # including environment references, rather than merging or dumping it.\\n            with tempfile.NamedTemporaryFile(mode=\"w\", encoding=\"utf-8\", suffix=\".yaml\", delete=False) as config:\\n                path = config.name\\n                config.write(raw)\\n            command.extend([\"--config\", path])\\n        os.execve(command[0], command, os.environ | {\"PATH\": serve_path(os.environ)})\\n    except StartupError as exc:\\n        return fail(f\"LiteLLM startup: {exc}\")\\n    except OSError:\\n        return fail(\"LiteLLM startup: unable to write config or execute litellm\")\\n    finally:\\n        # Successful exec leaves the private file available for LiteLLM to read.\\n        if path is not None:\\n            os.unlink(path)\\n\\n\\nif __name__ == \"__main__\":\\n    sys.exit(main())\\n'\"'\"')' serve",
                    "healthcheckPath": "/health/readiness",
                    "preDeployCommand": [
                      "python -c 'exec('\"'\"'\"\"\"Embedded startup command for the official LiteLLM image (no LiteLLM imports).\\n\\npredeploy: validate, wait for Postgres, run `prisma migrate deploy` directly (~90 MiB).\\nserve:     validate, wait for Postgres, exec litellm with the Prisma CLI hidden so the\\n           proxy skips its own startup migration pass (~500 MiB of Node on top of Python).\\n\"\"\"\\n\\nimport importlib.util\\nimport os\\nimport re\\nimport signal\\nimport subprocess\\nimport sys\\nimport tempfile\\nimport time\\n\\nimport yaml\\n\\nMIB = 2**20\\n# Measured on the pinned image: serve peaks ~800 MiB at readiness, ~1.3 GiB with LiteLLM\\'\"'\"'s own\\n# startup migration pass. Below MIN_MEMORY nothing works; below COMFORTABLE_MEMORY it runs but\\n# has little headroom under load.\\nMIN_MEMORY = 900 * MIB\\nCOMFORTABLE_MEMORY = 1536 * MIB\\n\\n\\nclass StartupError(Exception):\\n    \"\"\"Safe-to-log startup failure; never include user values or driver messages.\"\"\"\\n\\n\\ndef validate_environment(environ):\\n    for name in (\"LITELLM_MASTER_KEY\", \"LITELLM_SALT_KEY\"):\\n        if not environ.get(name, \"\").strip():\\n            raise StartupError(f\"{name} must be nonblank\")\\n    port = environ.get(\"PORT\", \"4000\")\\n    if not re.fullmatch(r\"[0-9]{1,5}\", port) or not 1 <= int(port) <= 65535:\\n        raise StartupError(\"PORT must be an integer from 1 to 65535\")\\n    host = environ.get(\"HOST\", \"0.0.0.0\")\\n    if host != \"0.0.0.0\":\\n        raise StartupError(\"HOST must be 0.0.0.0 for the template public binding\")\\n    return host, str(int(port))\\n\\n\\ndef validate_config(raw):\\n    if not raw or not raw.strip():\\n        return None\\n    try:\\n        config = yaml.safe_load(raw)\\n    except yaml.YAMLError as exc:\\n        mark = getattr(exc, \"problem_mark\", None)\\n        location = f\" at line {mark.line + 1}, column {mark.column + 1}\" if mark else \"\"\\n        raise StartupError(f\"LITELLM_CONFIG_YAML is invalid YAML{location}\") from None\\n    if not isinstance(config, dict) or not config:\\n        raise StartupError(\"LITELLM_CONFIG_YAML root must be a nonempty mapping\")\\n    for key in (\"general_settings\", \"litellm_settings\", \"router_settings\"):\\n        if key in config and not isinstance(config[key], dict):\\n            raise StartupError(f\"LITELLM_CONFIG_YAML {key} must be a mapping\")\\n    if \"model_list\" in config and not isinstance(config[\"model_list\"], list):\\n        raise StartupError(\"LITELLM_CONFIG_YAML model_list must be a list\")\\n    return config\\n\\n\\ndef resolve_database_url(config, environ):\\n    \"\"\"Match LiteLLM\\'\"'\"'s YAML-over-environment database selection before probing.\"\"\"\\n    configured = (config or {}).get(\"general_settings\", {}).get(\"database_url\")\\n    value = configured if configured is not None else environ.get(\"DATABASE_URL\")\\n    if isinstance(value, str) and value.startswith(\"os.environ/\"):\\n        value = environ.get(value.removeprefix(\"os.environ/\"))\\n    if not isinstance(value, str) or not value.strip():\\n        raise StartupError(\"Database URL must be nonblank; check DATABASE_URL and general_settings.database_url\")\\n    return value\\n\\n\\ndef memory_limit(path=\"/sys/fs/cgroup/memory.max\"):\\n    \"\"\"Container memory limit in bytes, or None when unlimited/unknown.\"\"\"\\n    try:\\n        with open(path) as f:\\n            value = f.read().strip()\\n    except OSError:\\n        return None\\n    return int(value) if value.isdigit() else None\\n\\n\\ndef check_memory(limit):\\n    if limit is None:\\n        return\\n    if limit < MIN_MEMORY:\\n        raise StartupError(\\n            f\"this container allows {limit // MIB} MiB of memory; LiteLLM needs at least 1 GB to start \"\\n            \"(Railway Trial) and 2 GB or more for real use (Hobby plan or higher). Upgrade the plan or \"\\n            \"raise the service memory limit, then redeploy\"\\n        )\\n    if limit < COMFORTABLE_MEMORY:\\n        print(\\n            f\"LiteLLM startup: {limit // MIB} MiB memory limit is enough to start but leaves little headroom; \"\\n            \"expect restarts under load. 2 GB or more is recommended\",\\n            file=sys.stderr, flush=True,\\n        )\\n\\n\\ndef prisma_cli():\\n    cli = os.environ.get(\"PRISMA_CLI_PATH\")\\n    if not cli:\\n        raise StartupError(\"The pinned image must provide PRISMA_CLI_PATH\")\\n    return cli\\n\\n\\ndef database_probe(dsn, timeout):\\n    \"\"\"Use the image\\'\"'\"'s offline Prisma toolchain, not an extra Python DB driver.\"\"\"\\n    command = [prisma_cli(), \"db\", \"execute\", \"--url\", dsn, \"--stdin\"]\\n    with subprocess.Popen(\\n        command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,\\n        text=True, start_new_session=True,\\n    ) as process:\\n        try:\\n            _, error = process.communicate(\"SELECT 1;\", timeout=timeout)\\n        except subprocess.TimeoutExpired:\\n            # Retire the native schema-engine child as well as the CLI parent.\\n            os.killpg(process.pid, signal.SIGKILL)\\n            process.communicate()\\n            return False\\n        if process.returncode == 0:\\n            return True\\n    # Driver output can contain credentials and SQL. Classify it, never print it.\\n    code = re.search(r\"\\\\bP[0-9]{4}\\\\b\", error)\\n    if code and code.group() in {\"P1001\", \"P1002\", \"P1008\", \"P1017\", \"P2024\", \"P2037\"}:\\n        return False\\n    if \"database system is starting up\" in error.lower():\\n        return False\\n    raise StartupError(\"Database connection rejected; check DATABASE_URL and credentials\")\\n\\n\\ndef wait_for_database(dsn):\\n    deadline = time.monotonic() + 90\\n    waiting = False\\n    while (remaining := deadline - time.monotonic()) > 0:\\n        if database_probe(dsn, min(5, remaining)):\\n            return\\n        if not waiting:\\n            print(\"LiteLLM startup: waiting up to 90 seconds for database readiness\", file=sys.stderr, flush=True)\\n            waiting = True\\n        remaining = deadline - time.monotonic()\\n        if remaining > 0:\\n            time.sleep(min(2, remaining))\\n    raise StartupError(\"Database readiness timed out after 90 seconds\")\\n\\n\\ndef migrations_dir():\\n    \"\"\"Directory holding LiteLLM\\'\"'\"'s schema.prisma and migrations, found without importing it.\"\"\"\\n    spec = importlib.util.find_spec(\"litellm_proxy_extras\")\\n    if spec is None or not spec.submodule_search_locations:\\n        raise StartupError(\"The pinned image must provide litellm_proxy_extras\")\\n    return list(spec.submodule_search_locations)[0]\\n\\n\\ndef migrate(dsn):\\n    \"\"\"`prisma migrate deploy` exactly as LiteLLM\\'\"'\"'s v2 resolver runs it, minus a 1.3 GiB Python import.\"\"\"\\n    env = os.environ | {\"DATABASE_URL\": dsn, \"NPM_CONFIG_PREFER_OFFLINE\": \"true\", \"PRISMA_HIDE_UPDATE_MESSAGE\": \"1\"}\\n    result = subprocess.run([prisma_cli(), \"migrate\", \"deploy\"], cwd=migrations_dir(), env=env)\\n    if result.returncode:\\n        raise StartupError(\"database migration failed; see the Prisma output above\")\\n\\n\\ndef serve_path(environ):\\n    \"\"\"PATH with every venv tool except `prisma`: LiteLLM then skips its startup migration pass.\"\"\"\\n    venv_bin = os.path.dirname(sys.executable)\\n    shim = tempfile.mkdtemp(prefix=\"litellm-bin-\")\\n    for name in os.listdir(venv_bin):\\n        if name != \"prisma\":\\n            os.symlink(os.path.join(venv_bin, name), os.path.join(shim, name))\\n    rest = [p for p in environ.get(\"PATH\", \"\").split(os.pathsep) if p and p != venv_bin]\\n    return os.pathsep.join([shim, *rest])\\n\\n\\ndef fail(message):\\n    \"\"\"Print the reason, then linger briefly: Railway drops the logs of containers that exit within ~1 s.\"\"\"\\n    print(message, file=sys.stderr, flush=True)\\n    time.sleep(3)\\n    return 1\\n\\n\\ndef main():\\n    path = None\\n    try:\\n        if len(sys.argv) != 2 or sys.argv[1] not in (\"predeploy\", \"serve\"):\\n            raise StartupError(\"Expected mode: predeploy or serve\")\\n        host, port = validate_environment(os.environ)\\n        raw = os.environ.get(\"LITELLM_CONFIG_YAML\")\\n        parsed = validate_config(raw)\\n        check_memory(memory_limit())\\n        dsn = resolve_database_url(parsed, os.environ)\\n        wait_for_database(dsn)\\n        if sys.argv[1] == \"predeploy\":\\n            migrate(dsn)\\n            return 0\\n        command = [os.path.join(os.path.dirname(sys.executable), \"litellm\"), \"--host\", host, \"--port\", port]\\n        if parsed is not None:\\n            # NamedTemporaryFile creates mode 0600. Preserve the original YAML,\\n            # including environment references, rather than merging or dumping it.\\n            with tempfile.NamedTemporaryFile(mode=\"w\", encoding=\"utf-8\", suffix=\".yaml\", delete=False) as config:\\n                path = config.name\\n                config.write(raw)\\n            command.extend([\"--config\", path])\\n        os.execve(command[0], command, os.environ | {\"PATH\": serve_path(os.environ)})\\n    except StartupError as exc:\\n        return fail(f\"LiteLLM startup: {exc}\")\\n    except OSError:\\n        return fail(\"LiteLLM startup: unable to write config or execute litellm\")\\n    finally:\\n        # Successful exec leaves the private file available for LiteLLM to read.\\n        if path is not None:\\n            os.unlink(path)\\n\\n\\nif __name__ == \"__main__\":\\n    sys.exit(main())\\n'\"'\"')' predeploy"
                    ]
                  },
                  "source": {
                    "image": "ghcr.io/berriai/litellm:v1.100.1@sha256:a3715fa7ad8387941ab697259bd2881d68931657247a41984f90fae6d11c62bf"
                  },
                  "variables": {
                    "HOST": {
                      "isOptional": false,
                      "description": "Public listen address. Keep 0.0.0.0; the startup helper rejects a binding Railway cannot reach.",
                      "defaultValue": "0.0.0.0"
                    },
                    "PORT": {
                      "isOptional": false,
                      "description": "Port used by both LiteLLM and Railway readiness. Default 4000; the startup helper validates and passes this value to the server.",
                      "defaultValue": "4000"
                    },
                    "REDIS_URL": {
                      "description": "Enables response caching and coordinates rate limits across replicas.",
                      "defaultValue": "${{Redis.REDIS_URL}}"
                    },
                    "OR_APP_NAME": {
                      "isOptional": true,
                      "description": "Optional - Sent to OpenRouter as X-Title for attribution.",
                      "defaultValue": ""
                    },
                    "OR_SITE_URL": {
                      "isOptional": true,
                      "description": "Optional - Sent to OpenRouter as HTTP-Referer for attribution.",
                      "defaultValue": "https://${{RAILWAY_PUBLIC_DOMAIN}}"
                    },
                    "UI_PASSWORD": {
                      "isOptional": true,
                      "description": "Optional - Leave blank to log in with LITELLM_MASTER_KEY.",
                      "defaultValue": ""
                    },
                    "UI_USERNAME": {
                      "isOptional": true,
                      "description": "Optional - Admin UI username.",
                      "defaultValue": "admin"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "Stores models, virtual keys, budgets and spend.",
                      "defaultValue": "${{Postgres.DATABASE_URL}}"
                    },
                    "GEMINI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional",
                      "defaultValue": ""
                    },
                    "OPENAI_API_KEY": {
                      "isOptional": true,
                      "description": "Optional - Models are normally added from the Admin UI instead.",
                      "defaultValue": ""
                    },
                    "LITELLM_SALT_KEY": {
                      "isOptional": false,
                      "description": "Encrypts provider credentials at rest. Generated once and NEVER changed - rotating it silently makes every stored provider key unreadable.",
                      "defaultValue": "{{LITELLM_SALT_KEY}}"
                    },
                    "ANTHROPIC_API_KEY": {
                      "isOptional": true,
                      "description": "Optional",
                      "defaultValue": ""
                    },
                    "STORE_MODEL_IN_DB": {
                      "isOptional": false,
                      "description": "Lets you add models from the Admin UI. Without it, Add Model returns HTTP 500.",
                      "defaultValue": "True"
                    },
                    "LITELLM_MASTER_KEY": {
                      "isOptional": false,
                      "description": "Your API key AND the Admin UI password. Generated for you - copy it from this service's Variables tab after deploying.",
                      "defaultValue": "{{LITELLM_MASTER_KEY}}"
                    },
                    "OPENROUTER_API_KEY": {
                      "isOptional": true,
                      "description": "Optional - One key unlocks ~100 models via the openrouter/* wildcard.",
                      "defaultValue": ""
                    },
                    "LITELLM_CONFIG_YAML": {
                      "isOptional": true,
                      "description": "Optional - Advanced config.yaml, e.g. Prometheus. Whitespace-only is ignored; invalid YAML fails pre-deploy before migrations. Max 32768 characters.",
                      "defaultValue": ""
                    },
                    "LITELLM_LOCAL_MODEL_COST_MAP": {
                      "description": "Uses the bundled pricing table instead of fetching it at boot. Halves startup time.",
                      "defaultValue": "True"
                    },
                    "ENFORCE_PRISMA_MIGRATION_CHECK": {
                      "description": "Fails the deploy loudly on a bad migration instead of serving a half-migrated database.",
                      "defaultValue": "true"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "description": "Allows 120 seconds for the previous gateway deployment to drain. Long requests may exceed this; dependency redeploys still interrupt service.",
                      "defaultValue": "120"
                    }
                  },
                  "networking": {
                    "serviceDomains": {
                      "<hasDomain>": {}
                    }
                  }
                },
                "6d63359f-deb4-46e7-a1c8-08de18538182": {
                  "icon": "https://devicons.railway.app/i/postgresql.svg",
                  "name": "Postgres",
                  "deploy": {
                    "startCommand": null,
                    "healthcheckPath": null
                  },
                  "source": {
                    "image": "ghcr.io/railwayapp-templates/postgres-ssl:18@sha256:469c779c7c57ec6bad4670a0a3cb5a830aa6e0ce4f3707137608de5223a5041c"
                  },
                  "variables": {
                    "PGDATA": {
                      "isOptional": false,
                      "description": "Postgres data directory, inside the mounted volume.",
                      "defaultValue": "/var/lib/postgresql/data/pgdata"
                    },
                    "PGHOST": {
                      "isOptional": false,
                      "description": "Private-network hostname of the Postgres service.",
                      "defaultValue": "${{RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "PGPORT": {
                      "isOptional": false,
                      "description": "Port Postgres listens on.",
                      "defaultValue": "5432"
                    },
                    "PGUSER": {
                      "isOptional": false,
                      "description": "Postgres superuser name.",
                      "defaultValue": "${{POSTGRES_USER}}"
                    },
                    "PGDATABASE": {
                      "isOptional": false,
                      "description": "Default database name used by psql and the Postgres client tools.",
                      "defaultValue": "${{POSTGRES_DB}}"
                    },
                    "PGPASSWORD": {
                      "isOptional": false,
                      "description": "Password for the Postgres superuser.",
                      "defaultValue": "${{POSTGRES_PASSWORD}}"
                    },
                    "POSTGRES_DB": {
                      "isOptional": false,
                      "description": "Database created on first boot.",
                      "defaultValue": "railway"
                    },
                    "DATABASE_URL": {
                      "isOptional": false,
                      "description": "Private-network connection string. LiteLLM stores models, virtual keys, budgets and spend history here.",
                      "defaultValue": "postgresql://${{POSTGRES_USER}}:${{POSTGRES_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:5432/${{POSTGRES_DB}}"
                    },
                    "POSTGRES_USER": {
                      "isOptional": false,
                      "description": "Superuser created on first boot.",
                      "defaultValue": "postgres"
                    },
                    "SSL_CERT_DAYS": {
                      "isOptional": false,
                      "description": "Validity window, in days, for the self-signed TLS certificate this image generates.",
                      "defaultValue": "820"
                    },
                    "POSTGRES_PASSWORD": {
                      "isOptional": false,
                      "description": "Superuser password, generated once at deploy time.",
                      "defaultValue": "{{POSTGRES_PASSWORD}}"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "description": "Seconds Railway keeps the old container alive after the new one is live, so in-flight requests can finish.",
                      "defaultValue": "60"
                    }
                  },
                  "volumeMounts": {
                    "6d63359f-deb4-46e7-a1c8-08de18538182": {
                      "mountPath": "/var/lib/postgresql/data"
                    }
                  },
                  "haTemplateCode": "postgres-ha",
                  "haConversionConfig": {
                    "edge": {
                      "label": "Reverse Proxy",
                      "options": [
                        2,
                        3,
                        4,
                        5
                      ],
                      "nodeLabel": "HAProxy",
                      "description": "Routes traffic to the primary",
                      "defaultValue": 3
                    },
                    "replica": {
                      "label": "Replicas",
                      "options": [
                        2,
                        3,
                        4,
                        5,
                        6,
                        7
                      ],
                      "nodeLabel": "Postgres",
                      "description": "Streaming replicas for automatic failover",
                      "defaultValue": 2
                    },
                    "internal": {
                      "label": "Coordinator Nodes",
                      "options": [
                        3,
                        5,
                        7,
                        9
                      ],
                      "nodeLabel": "etcd",
                      "defaultValue": 3
                    },
                    "description": "This will convert your Postgres database to an HA cluster with Patroni failover and etcd consensus."
                  }
                },
                "d71b525d-b229-4736-8464-c4cecb79eeb6": {
                  "icon": "https://devicons.railway.app/i/redis.svg",
                  "name": "Redis",
                  "deploy": {
                    "startCommand": "/bin/sh -c 'exec docker-entrypoint.sh redis-server --requirepass \"${REDIS_PASSWORD:?REDIS_PASSWORD must be nonblank}\" --appendonly no --save \"\"'",
                    "healthcheckPath": null
                  },
                  "source": {
                    "image": "redis:8.2@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d"
                  },
                  "variables": {
                    "REDISHOST": {
                      "isOptional": false,
                      "description": "Private-network hostname of the Redis service.",
                      "defaultValue": "${{RAILWAY_PRIVATE_DOMAIN}}"
                    },
                    "REDISPORT": {
                      "isOptional": false,
                      "description": "Port Redis listens on.",
                      "defaultValue": "6379"
                    },
                    "REDISUSER": {
                      "isOptional": false,
                      "description": "Redis username for this image's default user.",
                      "defaultValue": "default"
                    },
                    "REDIS_URL": {
                      "isOptional": false,
                      "description": "Private-network connection string. LiteLLM uses Redis for response caching and cross-replica rate limiting.",
                      "defaultValue": "redis://default:${{REDIS_PASSWORD}}@${{RAILWAY_PRIVATE_DOMAIN}}:6379"
                    },
                    "REDIS_PASSWORD": {
                      "isOptional": false,
                      "description": "Redis password, generated once at deploy time.",
                      "defaultValue": "{{REDIS_PASSWORD}}"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "description": "Seconds Railway keeps the old container alive after the new one is live, so in-flight requests can finish.",
                      "defaultValue": "60"
                    }
                  },
                  "volumeMounts": {
                    "d71b525d-b229-4736-8464-c4cecb79eeb6": {
                      "mountPath": "/data"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "LiteLLM",
      "method": "GET",
      "path": "/health/readiness",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 3,
    "needs_volume": true
  },
  "generated_at": "2026-09-21T10:14:42.516Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-21T09:52:50.308Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_8de14ca6a8194e4fb968",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "healthcheck",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 108,
    "typical_build_seconds": 0,
    "typical_start_seconds": 31,
    "slowest_service": "LiteLLM"
  }
}
