Ctrl K
ComunidadGuías…

src/domain/hosting-platform-profiles.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { LOCAL_MODEL_RUNTIME_IMAGE } from "./local-n8n-model.js";
import { createSearchImageFiles, modelEnvironment, searchEnvironment } from "./hosting-deployment-common.js";

const RENDER_REGIONS = ["oregon", "ohio", "frankfurt", "singapore", "virginia"];
const RENDER_PLANS = ["2c-4g", "2c-8g", "2c-16g", "4c-16g", "4c-32g", "8c-32g"];
const option = value => ({ value, label: value });
const renderRegion = { name: "region", label: "Existing n8n region", type: "select", required: true, options: RENDER_REGIONS.map(option), description: "Choose the actual Render region with private-network access to the n8n backend." };
const flyRegion = { name: "region", label: "Existing n8n Fly region", type: "text", required: true, pattern: "^[a-z]{3}$", description: "Three-letter Fly region code where the n8n backend can reach this private app." };
const searchRequirements = ["n8n's execution backend must reach the private search service on port 8080; approve outbound HTTPS/DNS from SearXNG to search engines.", "Generate a unique, cryptographically random secret (at least 32 characters) in the provider secret store as SEARXNG_SECRET; never place it in this bundle or source control."];
const searchLimitations = ["NOT-RUN on a provider: JSON search, backend routing, egress/rate limits, memory and account policy require an operator-owned runtime check.", "Search cache is intentionally disposable; configuration is baked into the image. This is not a public search proxy."];
const modelLimitations = ["NOT-RUN on a provider: model download, real inference, capacity, private DNS and account policy require an operator-owned runtime check.", "The Ollama image is pinned; manual pulling of a mutable model tag is not Relmio's managed model-digest attestation. A successful pull or TCP health is not inference-ready evidence."];

function checkModelCapacity(model, memoryGB, diskGB) {
  if (model.memoryBytes + 1024 ** 3 > memoryGB * 1024 ** 3 ||
      (diskGB !== undefined && model.expectedDownloadBytes * 2 + 5 * 1024 ** 3 > diskGB * 1024 ** 3)) {
    throw new RangeError("The selected compute or persistent cache cannot cover this model's planning allowance.");
  }
}

function modelPullSteps(model, baseUrl) {
  return [
    `After deploying the private service, from an operator-controlled n8n backend on its permitted private network use an HTTP Request: POST ${baseUrl.replace(/\/v1$/u, "")}/api/pull, JSON body ${JSON.stringify({ model: model.id, stream: false })}, with a download-appropriate timeout. Require terminal status success; do not publish the unauthenticated model-management port to make the pull work.`,
    `Then execute a fresh, bounded n8n Chat Completions workflow using ${baseUrl}, model ${model.id}, Responses OFF and reasoning_effort none when applicable. Require a real answer; a successful pull or model list is insufficient.`,
  ];
}

function yamlEnvironment(env) {
  return Object.entries(env).map(([key, value]) => `      - key: ${key}\n        value: '${value}'`).join("\n");
}

function renderModel({ resourceName, model, inputs }) {
  const memoryGB = Number(inputs.plan.split("-")[1].replace("g", ""));
  if (inputs.diskGB % 5 !== 0) throw new TypeError("Render disk size must be a multiple of 5 GB.");
  checkModelCapacity(model, memoryGB, inputs.diskGB);
  const env = { ...modelEnvironment(model), PORT: "11434" };
  const baseUrl = "the actual Render Connect Internal address on port 11434/v1";
  return {
    files: [{ name: "render.yaml", mediaType: "text/yaml", content: `services:
  - type: pserv
    runtime: image
    name: ${resourceName}
    region: ${inputs.region}
    plan: ${inputs.plan}
    numInstances: 1
    image:
      url: ${LOCAL_MODEL_RUNTIME_IMAGE}
    disk:
      name: ${resourceName}-data
      mountPath: /var/data
      sizeGB: ${inputs.diskGB}
    envVars:
${yamlEnvironment(env)}
` }],
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: [
      "Review service name against your account before creating a NEW Render Blueprint for only this private service. Do not sync this file into a Blueprint managing existing n8n, databases or other services: names alone cannot prove non-adoption; do not remove existing resources.",
      `Deploy this single private service in ${inputs.region} on ${inputs.plan}, with its own ${inputs.diskGB} GB runtime disk. Leave the image's default ollama serve startup intact; disk is mounted only while service runs. Verify that the private service has no public onrender.com route.`,
      "In Render Connect copy the actual Internal address, append :11434/v1 for the n8n credential, and use its same host on port 11434 for model acquisition; do not use a guessed hostname. Check the n8n worker is in the same permitted private network/region.",
      ...modelPullSteps(model, baseUrl),
    ],
    connection: { discovery: "Copy Render Connect Internal address and append :11434/v1", protocol: "OpenAI-compatible Chat Completions", authentication: "private-network-only; no public raw model API" },
    requirements: ["Render paid private service and persistent disk; confirm current price/capacity before creation.", "Render and the n8n execution backend must share an allowed private network; model download needs approved outbound access."],
    limitations: [...modelLimitations, "Render disks are per instance and unavailable during build/pre-deploy; disk attachment disables zero-downtime deploys."],
  };
}

function renderSearch({ resourceName, inputs }) {
  const env = { ...searchEnvironment(), PORT: "8080" };
  const files = createSearchImageFiles();
  files.push({ name: "render.yaml", mediaType: "text/yaml", content: `services:
  - type: pserv
    runtime: docker
    name: ${resourceName}
    region: ${inputs.region}
    plan: ${inputs.plan}
    numInstances: 1
    envVars:
${yamlEnvironment(env)}
      - key: SEARXNG_SECRET
        sync: false
` });
  return {
    files,
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: ["In a fresh Git repository containing ONLY these generated search files, review the unique name and create a new Render Blueprint private service. Do not synchronize a Blueprint that owns existing n8n/resources; the source repository supplies the Dockerfile build context.", "At Blueprint creation enter SEARXNG_SECRET through Render's secret prompt; use a newly generated 32+ character random value, never a tracked env file. Keep the service private and the n8n backend in the same permitted Render region/network.", "From the n8n execution backend copy the actual Render Connect Internal address, request GET /search?q=n8n&format=json at port 8080 and require a JSON object with results array; upstream blocking is distinct from transport failure. Add its private base URL as N8N_INSTANCE_AI_SEARXNG_URL only if this is your selected Assistant search provider; Brave or UI credentials may take precedence."],
    connection: { discovery: "Copy Render Connect Internal address and append :8080", testPath: "/search?q=n8n&format=json" },
    requirements: searchRequirements,
    limitations: searchLimitations,
  };
}

function railwayModel({ resourceName, model, inputs }) {
  checkModelCapacity(model, inputs.memoryGB, inputs.volumeGB);
  const env = modelEnvironment(model, "[::]:11434");
  const baseUrl = `http://${resourceName}.railway.internal:11434/v1`;
  return {
    files: [],
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: [
      `In the existing Railway project and exact environment reachable by n8n, create a NEW private service named ${resourceName} from the Docker image ${LOCAL_MODEL_RUNTIME_IMAGE}. Do not select existing n8n, add a public domain/TCP proxy, use deprecated railway.json, or apply a whole-project IaC plan. Preserve the image's default serve command.`,
      `Create a new Railway volume for only this service, mount it at /var/data, choose ${inputs.volumeGB} GB in Railway's volume size control (${inputs.volumeGB * 1024} MB), and attach it to the service. The volume is available at runtime, not build/pre-deploy time.`,
      `Set the listed environment variables in this new service and review its memory limit: reserve at least ${inputs.memoryGB} GB for this model plus n8n elsewhere. Keep one replica and confirm no public domain/TCP proxy exists. Railway private DNS is environment-scoped; legacy environments may resolve IPv6 only.`,
      ...modelPullSteps(model, baseUrl),
    ],
    connection: { baseUrl, protocol: "OpenAI-compatible Chat Completions", authentication: "same Railway environment private network only" },
    requirements: ["An existing Railway project/environment with n8n backend network access and a separately paid persistent volume.", "Confirm service resource allocation, volume capacity, current billing and outbound registry access."],
    limitations: [...modelLimitations, "Railway services are nonprivileged; this is not a DinD Assistant runner."],
  };
}

function railwaySearch({ resourceName, inputs }) {
  const env = searchEnvironment("::");
  return {
    files: createSearchImageFiles(),
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: [
      `Put only the generated Dockerfile, settings.yml, start.sh and .dockerignore in a new Git repository. In n8n's existing Railway project/environment, add a NEW service ${resourceName} from that repository with Dockerfile builder and one replica. Do not adopt n8n or apply whole-project IaC.`,
      "Set the listed runtime variables and create SEARXNG_SECRET as an owner-generated random 32+ character value in Railway service Variables; do not put it in the repo or a generated download. Do not create a public domain or TCP proxy. Search cache is ephemeral; no volume is required.",
      `From the n8n backend in this exact environment GET http://${resourceName}.railway.internal:8080/search?q=n8n&format=json; require JSON results array. If using Assistant, use this private base URL for N8N_INSTANCE_AI_SEARXNG_URL and confirm search precedence.`,
    ],
    connection: { baseUrl: `http://${resourceName}.railway.internal:8080`, testPath: "/search?q=n8n&format=json" },
    requirements: searchRequirements,
    limitations: [...searchLimitations, "Legacy Railway environments may have IPv6-only private DNS; runtime listens on IPv6."],
  };
}

function flyModel({ resourceName, model, inputs }) {
  const minMB = (inputs.cpuKind === "shared" ? 256 : 2048) * inputs.cpus;
  const maxMB = (inputs.cpuKind === "shared" ? 2048 : 8192) * inputs.cpus;
  if ((inputs.cpuKind === "shared" && inputs.cpus > 8) || inputs.memoryMB < minMB || inputs.memoryMB > maxMB) {
    throw new RangeError("Fly CPU and memory selection is outside reviewed Machine sizes.");
  }
  checkModelCapacity(model, inputs.memoryMB / 1024, inputs.volumeGB);
  const env = modelEnvironment(model, "[::]:11434");
  const baseUrl = `http://${resourceName}.internal:11434/v1`;
  return {
    files: [{ name: "fly.toml", mediaType: "text/plain", content: `app = "${resourceName}"
primary_region = "${inputs.region}"

[build]
  image = "${LOCAL_MODEL_RUNTIME_IMAGE}"

[env]
${Object.entries(env).map(([key, value]) => `  ${key} = "${value}"`).join("\n")}

[[mounts]]
  source = "${resourceName.replace(/-/gu, "_")}_data"
  destination = "/var/data"
  initial_size = "${inputs.volumeGB}gb"

[[vm]]
  cpu_kind = "${inputs.cpuKind}"
  cpus = ${inputs.cpus}
  memory = "${inputs.memoryMB}mb"
` }],
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: [
      `In the existing permitted Fly organization create only a NEW app ${resourceName} in ${inputs.region}, not the existing n8n app. Review its name for collisions and deploy this fly.toml from an isolated directory. Do not allocate a public IPv4/IPv6/Flycast address; this file intentionally has no services/http_service block.`,
      `Provision/attach exactly one ${inputs.volumeGB} GB volume to one Machine in ${inputs.region} (the mount initial_size applies on first deploy). Explicitly set/verify exactly ONE running Machine and one attached volume after deployment; fly deploy alone does not prove singleton placement. Leave the machine running: direct 6PN traffic does not use Fly Proxy autostart.`,
      "Confirm the actual n8n execution backend joins the same permitted Fly organization 6PN and can reach the running private Machine; never publish the raw model API as a connectivity workaround.",
      ...modelPullSteps(model, baseUrl),
    ],
    connection: { baseUrl, protocol: "OpenAI-compatible Chat Completions", authentication: "same-organization Fly 6PN only" },
    requirements: ["Fly organization, region, persistent per-Machine volume and private 6PN from the n8n backend.", "Review paid resources and memory/CPU capacity before deployment."],
    limitations: [...modelLimitations, "A Fly volume is local to one Machine/region with no automatic replication, and stopped Machines disappear from .internal DNS."],
  };
}

function flySearch({ resourceName, inputs }) {
  const env = searchEnvironment("::");
  const files = createSearchImageFiles();
  files.push({ name: "fly.toml", mediaType: "text/plain", content: `app = "${resourceName}"
primary_region = "${inputs.region}"

[build]
  dockerfile = "Dockerfile"

[env]
${Object.entries(env).map(([key, value]) => `  ${key} = "${value}"`).join("\n")}

[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory = "${inputs.memoryMB}mb"
` });
  return {
    files,
    settings: Object.entries(env).map(([name, value]) => ({ name, value })),
    steps: [`In the existing Fly organization create a NEW app ${resourceName}, review name collision, deploy only these files from an isolated directory and verify one running Machine in ${inputs.region}. No services/http_service, public IPv4/IPv6 or Flycast address: direct .internal 6PN reachability requires an already permitted n8n backend.`, "Create SEARXNG_SECRET as a 32+ character cryptographically random Fly app secret through the operator's secure secret workflow, not as a fly.toml environment value or repository file. The startup wrapper refuses a missing/short secret.", `From the n8n execution backend GET http://${resourceName}.internal:8080/search?q=n8n&format=json; require a JSON results array. Configure N8N_INSTANCE_AI_SEARXNG_URL to the private base URL only if Assistant uses SearXNG and check provider precedence.`],
    connection: { baseUrl: `http://${resourceName}.internal:8080`, testPath: "/search?q=n8n&format=json" },
    requirements: searchRequirements,
    limitations: [...searchLimitations, "A stopped Fly Machine is absent from .internal DNS; no Fly Proxy autostart is configured."],
  };
}

function digitaloceanSearch({ resourceName, inputs }) {
  const env = searchEnvironment();
  return {
    files: createSearchImageFiles(),
    settings: [...Object.entries(env).map(([name, value]) => ({ name, value })), { name: "instance_size_slug", value: inputs.instanceSize }, { name: "internal_ports", value: "8080" }],
    steps: [
      `Put only these four generated files in a new Git repository. In the EXISTING DigitalOcean App Platform app (same app as its n8n execution component), add a NEW Service component ${resourceName} sourced from this repo and Dockerfile; do not replace the existing app spec or existing n8n component. Verify the component name is unused before creation.`,
      `In the app spec editor for ONLY the new service, choose instance_size_slug ${inputs.instanceSize}, instance_count 1, internal_ports [8080]; remove its http_port and any ingress rules matching this service. Preserve all existing app ingress routes, n8n, database and other components. Save only after reviewing the complete proposed diff.`,
      "Set the listed Granian runtime variables and an independently generated 32+ character SEARXNG_SECRET as an encrypted RUN_TIME SECRET through App Platform settings, not a tracked app spec/env file. This service must have outbound DNS/HTTPS but no public route.",
      `From the n8n execution component in this same app GET http://${resourceName}:8080/search?q=n8n&format=json and require a JSON results array. Only then optionally use its base URL for N8N_INSTANCE_AI_SEARXNG_URL; check Brave/UI precedence.`,
    ],
    connection: { baseUrl: `http://${resourceName}:8080`, testPath: "/search?q=n8n&format=json" },
    requirements: ["Existing DigitalOcean App Platform app with an n8n execution component and access to add one internal service; confirm billing before operator action.", ...searchRequirements],
    limitations: [...searchLimitations, "App Platform has no persistent volumes; cache is disposable, local filesystem is capped and replaced on deployments. This is not a persistent Ollama model lane."],
  };
}

const daytonaPlatforms = ["render", "railway", "fly", "digitalocean-app", "ecs-ec2", "ecs-fargate", "eks", "eks-fargate", "gke", "gke-autopilot", "aks", "cloud-run", "azure-container-apps", "azure-container-instances", "lightsail-containers"];
function daytonaAssistant({ providerId }) {
  return {
    files: [],
    settings: [
      { name: "N8N_INSTANCE_AI_SANDBOX_ENABLED", value: "true" },
      { name: "N8N_INSTANCE_AI_SANDBOX_PROVIDER", value: "daytona" },
      { name: "N8N_INSTANCE_AI_SANDBOX_IMAGE", value: "daytonaio/sandbox:0.5.3-slim" },
      { name: "DAYTONA_API_URL", value: "https://app.daytona.io/api" },
    ],
    steps: [
      `For self-hosted n8n on ${providerId}, first check the instance's Assistant/instance-ai entitlement and operator-owned Daytona account/capacity. Daytona replaces only sandbox execution; choose and configure a real model provider independently. This is NOT a Relmio-managed runner.`,
      "In the EXISTING n8n environment configuration, preserve every N8N_ENABLED_MODULES entry and include instance-ai. An operator, not this plan, reviews and applies all n8n configuration changes and handles any necessary restart in their maintenance window; do not let this guide mutate or restart n8n.",
      "Add the listed nonsecret settings to the n8n runtime. Put the actual DAYTONA_API_KEY from the operator's own Daytona account into its existing secret manager as a runtime secret; never write its value into this plan, a downloaded file or a shared app spec.",
      "Select the exact supported N8N_INSTANCE_AI_MODEL and supply its independent N8N_INSTANCE_AI_MODEL_API_KEY as an operator-owned runtime secret when that provider requires a key. A custom endpoint may additionally need N8N_INSTANCE_AI_MODEL_URL; it must be reachable from the n8n backend.",
      "If search is desired, configure one reachable N8N_INSTANCE_AI_SEARXNG_URL or an operator-owned Brave key, then check n8n search-provider precedence. Generate and execute one harmless workflow that actually performs a Daytona sandbox operation; a login or health check alone is not proof.",
    ],
    connection: { provider: "daytona", apiUrl: "https://app.daytona.io/api", integration: "operator-configured n8n self-hosted Assistant sandbox" },
    requirements: ["Self-hosted n8n with instance-ai available, operator-owned Daytona account/capacity and approved outbound HTTPS.", "Operator-owned Assistant model and secret management; no credentials are collected by the generator."],
    limitations: ["NOT-RUN: Daytona account capacity, sandbox action and model/search compatibility need a real operator-owned execution.", "The n8n-documented Daytona image tag is date-qualified guidance, not a Relmio-attested immutable image digest; review current vendor guidance before applying.", "This handoff does not install a local privileged runner, alter existing n8n, or provide Ollama, SearXNG or credential bridges."],
  };
}

const modelFields = [renderRegion, { name: "plan", label: "New service plan", type: "select", required: true, options: RENDER_PLANS.map(option), description: "Current Render compute slugs; review live price and model memory headroom." }, { name: "diskGB", label: "Persistent disk GB", type: "number", required: true, min: 5, max: 500, defaultValue: 20, description: "Use 5 GB multiples; model cache and download need capacity." }];
const railwayModelFields = [{ name: "volumeGB", label: "New persistent volume GB", type: "number", required: true, min: 10, max: 500, defaultValue: 20 }, { name: "memoryGB", label: "Reviewed service memory GB", type: "number", required: true, min: 4, max: 128, defaultValue: 8 }];
const flyModelFields = [flyRegion, { name: "cpuKind", label: "Machine CPU type", type: "select", required: true, options: ["shared", "performance"].map(option) }, { name: "cpus", label: "Machine CPUs", type: "select", required: true, options: [1, 2, 4, 8, 16].map(value => ({ value, label: `${value} CPU${value === 1 ? "" : "s"}` })), defaultValue: 2, description: "Shared supports at most 8 CPUs; 16 requires performance. Memory limits depend on CPU kind and count." }, { name: "memoryMB", label: "Machine memory MB", type: "select", required: true, options: [4096, 8192, 16384, 32768].map(value => ({ value, label: `${value} MB` })) }, { name: "volumeGB", label: "New persistent volume GB", type: "number", required: true, min: 10, max: 500, defaultValue: 20 }];
const flySearchFields = [flyRegion, { name: "memoryMB", label: "Machine memory MB", type: "select", required: true, options: [1024, 2048].map(value => ({ value, label: `${value} MB` })) }];

export const PLATFORM_HOSTING_PROFILES = [
  { providerId: "render", component: "model", fields: modelFields, render: renderModel },
  { providerId: "render", component: "searxng", fields: [renderRegion, { name: "plan", label: "New private service plan", type: "select", required: true, options: ["1c-2g", "2c-4g", "2c-8g"].map(option) }], render: renderSearch },
  { providerId: "railway", component: "model", fields: railwayModelFields, render: railwayModel },
  { providerId: "railway", component: "searxng", fields: [], render: railwaySearch },
  { providerId: "fly", component: "model", fields: flyModelFields, render: flyModel },
  { providerId: "fly", component: "searxng", fields: flySearchFields, render: flySearch },
  { providerId: "digitalocean-app", component: "searxng", fields: [{ name: "instanceSize", label: "New service size", type: "select", required: true, options: ["apps-s-1vcpu-2gb", "apps-s-2vcpu-4gb"].map(option) }], render: digitaloceanSearch },
  ...daytonaPlatforms.map(providerId => ({ providerId, component: "assistant", fields: [], render: daytonaAssistant })),
];