TypeSafe sacó Jev y mi feed entero perdió la puta cabeza. Lo llaman un modelo System One: le pasas un estado hecho un desastre más unas cuantas preguntas tipadas, y te devuelve una opción, una puntuación o un sí/no con una probabilidad de verdad pegada. Sin redacciones, sin “¡Claro! Aquí tienes el JSON que pediste:”, sin volver a parsear prosa para sacar la única palabra que querías. Hay que reconocerlo, es la puta idea correcta.
Luego lees la letra pequeña. API alojada. Acceso anticipado. Lista de espera. Sin pesos. Y lo que querrías que juzgara es, por definición, lo delicado, “este agente está a punto de borrar la tabla de clientes”, así que tiene que salir de tu red y esperar en la cola de otro antes de que te llegue tu sí o tu no. Ni de coña.
Así que hice lo mismo que ya había hecho con talkies para la voz, flickies para el vídeo y predictalot para las predicciones. Repasé los modelos abiertos que hacen este trabajo, me quedé con los mejores, Laya y Von, más tarde añadí CLM como tercero, y los clavé a todos en una sola imagen de Docker detrás de una sola API. Eso es decidealot, el Jev offline: la misma forma de petición y respuesta de System One, MCP en el mismo puerto, tu hardware, sin factura de la nube, sin lista de espera, y con los modelos locales lo que estás consultando no sale nunca de tu máquina. Dale una clave de TypeSafe y también te sirve el Jev alojado de verdad, el único camino por el que tu estado sí sale. Luego lo metí a empujones en aigate justo al lado de sus hermanos, así que si ya tienes ese stack montado, decidealot te queda a una variable de entorno de distancia.
Pedirle una Decisión a un Generador de Texto Es una Puta Gilipollez
Jev reventó porque lo que hay ahora mismo es tonto de cojones:
- LLMs como clasificadores. Un modelo hecho para generar texto, generando texto, que luego tú parseas para sacar la única palabra que querías. Suplicas “SOLO JSON válido, sin explicaciones” y te llega envuelto en bloques de markdown con una notita muy servicial sobre su razonamiento, más esa vez de cada cincuenta en la que se inventa una cuarta categoría que tú nunca pusiste. “Responde solo con JSON” no es un contrato, es una plegaria, y los modos de salida estructurada solo trasladan el parseo al sampler de otro. Sigues pagando decodificación token a token para producir una etiqueta.
- Confianza autodeclarada. Pregúntale a un chatbot lo seguro que está y te suelta “85%”, un número que se ha sacado del culo sin pestañear porque en ese hueco quedaba bonito un número. Ese número no ha pasado ni de lejos por un softmax. No puedes ponerle un umbral, no puedes calibrarlo y no puedes meterlo en un log de auditoría y defenderlo después.
- Latencia tirada a la basura. Cada “¡Por supuesto! Según el contexto proporcionado” es tiempo de reloj metido entre tu evento y tu decisión.
- Los modelos de pesos abiertos. Los dos mejores que hacen este trabajo en local son Laya, de NandhaKishorM, y Von, de wfzyx, con los pesos bajo Apache 2.0. Genial. Cada uno trae su propio servidor con sus propias manías sobre cómo tiene que ser una petición, su propia instalación y su propio runtime de Torch, y ninguno de los dos acepta tal cual la petición oficial de TypeSafe. ¿Quieres los dos? Pues te comes dos servidores, dos instalaciones y dos runtimes de Torch, y el pegamento lo escribes tú.
Yo quería un solo servicio que arranco una vez y al que apunto todo. El contrato de la versión alojada, para que el código escrito contra él no tenga que aprenderse una segunda API. Todos los modelos detrás, cada uno con su interruptor. MCP para los agentes. Y nada de tener un modelo cargado y un runtime de Torch entero chupando RAM mientras nadie le pregunta una mierda.
Tres Tipos de Pregunta, Una Sola Petición
El contrato es pequeño. Mandas un model, un state y un mapa de questions con nombre. El estado es lo que quieras que se juzgue: un string, un objeto JSON o un array. Los nombres de las preguntas los pones tú y vuelven como claves dentro de answers. Cada pregunta es de uno de estos tres tipos:
choiceelige una etiqueta de entre las claves decriteria. Esas claves son las únicas respuestas posibles. El modelo no puede inventarse un cuarto cajón, porque no está escribiendo ni una puta palabra. Recibes lachoice, unaconfidencey una probabilidad para cada etiqueta.scorerecibe un array ordenado de criterios en el que la posición es la puntuación, empezando por 0. Devuelve una puntuación esperada, así que 1.9 en una rúbrica de tres niveles es una respuesta de verdad que significa “blocking, con una pizca de soon”, más las probabilidades de cada nivel y unalegendque traduce las posiciones de vuelta a tus propias palabras.noules sí o no. Un solo campo,noul, la probabilidad de que la afirmación sea cierta. No hay campo de confianza aparte, porque ese número ya es la confianza.
Mete los tres en una sola petición y se responden todos contra el mismo estado:
curl --fail http://127.0.0.1:8080/v1/systemone \
--header 'Content-Type: application/json' \
--data '{
"model": "laya",
"state": "You billed me twice for March. Refund the duplicate today or I am cancelling.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which team should handle this?",
"criteria": {
"billing": "Invoices, payments, refunds.",
"technical": "Bugs, outages, errors.",
"other": "Everything else."
}
},
"urgency": {
"type": "score",
"criteria": ["not urgent", "soon", "blocking"]
},
"churn_risk": {
"type": "noul",
"instructions": "Does the customer threaten to leave?"
}
}
}'Y no es un ejemplo inventado. Esto es lo que Laya devolvió de verdad, corriendo en la imagen CUDA detrás de mi propio aigate:
{
"model": "laya",
"answers": {
"department": {
"type": "choice",
"choice": "billing",
"confidence": 0.8055,
"probabilities": { "billing": 0.9543, "technical": 0.0317, "other": 0.014 }
},
"urgency": {
"type": "score",
"score": 1.8986,
"confidence": 0.7053,
"legend": { "0": "not urgent", "1": "soon", "2": "blocking" },
"probabilities": { "0": 0.0222, "1": 0.0571, "2": 0.9208 }
},
"churn_risk": {
"type": "noul",
"noul": 0.2673
}
},
"usage": { "input_tokens": 156, "output_tokens": 0 }
}Mira output_tokens. Cero. Laya no escribió ni un token, así que no hay nada que pueda volver como “¡Claro! Aquí tienes”. La misma petición a Von reporta tres, que sigue estando a años luz de un párrafo. En los dos casos la forma de la respuesta la fija el schema, no si hoy al modelo le salió de los cojones hacerle caso a tus instrucciones.
Ahora mira churn_risk. El cliente escribió literalmente “or I am cancelling” y Laya puso la amenaza en 0.27. Le mandé la misma petición a Von como segunda opinión y Von dijo 0.23. Billing y blocking, los dos clavados. La amenaza de darse de baja, los dos pasaron olímpicamente. Eso no es decidealot destrozando nada, es lo que respondieron los modelos, y es la razón entera de que exista el párrafo siguiente. Prueba tus preguntas con tus propios casos antes de colgarles un umbral. Reformular la pregunta sale barato. Enterarte en producción, no.
decidealot te da los números y ahí se queda. No actúa. El umbral es cosa de tu código: permitir cuando allow pase de 0.95, mandar todo lo demás a la cola de un humano, guardar la respuesta entera junto al registro de la acción. La misma postura que predictalot, solo que una capa más allá. Entran números, y la decisión de qué hacer con ellos sigue siendo tuya.
Tres Modelos Locales Más Jev Alojado, Cada Uno Con Su Interruptor
Cada petición nombra un modelo. No hay valor por defecto, así que tus decisiones no cambian de cerebro en silencio porque algún gilipollas tocó una variable de entorno. GET /v1/models devuelve el catálogo de lo que tenga activado este despliegue. Con los tres providers locales activados, son los catorce de abajo, más los nombres que te devuelva TypeSafe si además activas el Jev alojado:
laya,laya-auto,laya-latest: Laya con enrutado automático de checkpoint. Mira el sistema de escritura y el idioma del estado y elige por su cuenta el checkpoint inglés o el multilingüe.laya-english: el checkpoint inglés, para inglés en alfabeto latino.laya-multilingual: el checkpoint multilingüe, para todo lo demás, incluido el texto corto en alfabeto latino que no sea claramente inglés.laya-typed-decisions: el checkpoint ajustado para llamadas estructuradas repetidas dentro de un flujo de trabajo, como políticas, enrutado, triaje y aprobaciones. Pruébalo con tus propios casos antes de fiarte de él.von,von-latest,von-1.1,von-1.1.0: Von, un modelo independiente, solo en inglés, para decisiones cortas y bien planteadas. Útil por sí solo, y útil como segunda opinión antes de estandarizar un flujo de trabajo sobre Laya.clm,clm-latest,clm-0.1,clm-0.1-8b: CLM v0.1, montado de una forma que no se parece en nada a los otros dos. Justo aquí abajo te lo cuento.
Laya es una familia de modelos con tres checkpoints. Von es otro modelo distinto, hecho por otra gente. CLM, que añadí más tarde, es otro bicho muy diferente: una projection head de 75 MB de Contrastive-LM que por sí sola no sabe leer ni una palabra. CLM manda tu estado y tus criterios, renderizados como texto, a un único endpoint /v1/embeddings compatible con OpenAI, que tiene que devolverle vectores Qwen3-8B del último token de exactamente 4096 floats de ancho, tira cualquier vector que no tenga justo ese ancho, normaliza el resto antes de que la head llegue a correr y, a partir de esos vectores, puntúa cada respuesta frente al estado. Los textos de respuesta que ya pasó por embeddings salen de una caché acotada con TTL en vez de volver a llamar al endpoint, pero tu estado en bruto nunca se cachea y se vuelve a mandar a embeddings cada vez. Los tres aceptan la misma petición y devuelven los mismos tipos de respuesta, que es justo la gracia de meterlos detrás de un solo contrato: cambias el string de model y comparas.
Cada uno de los tres locales tiene su propio interruptor: DECIDEALOT_LAYA_ENABLED, DECIDEALOT_VON_ENABLED y DECIDEALOT_CLM_ENABLED. Corre los tres, dos cualesquiera o uno solo, pero al menos un provider tiene que estar encendido, porque una máquina con cero providers se niega a arrancar en vez de quedarse ahí sin contestar nada. Laya y Von vienen activados por defecto. CLM viene en auto, que lo enciende en cuanto pones DECIDEALOT_CLM_EMBEDDINGS_URL. Ponlo en true y sin URL el arranque revienta, ponlo en false y CLM se queda apagado aunque haya URL. Lo que hay que pensarse es la URL: tu estado viaja a ese endpoint, así que CLM es exactamente igual de privado que lo que haya detrás. Apunta CLM a algo que corras tú, o como mínimo a algo de lo que te fíes. Un provider apagado no se descarga, no arranca, no aparece ni en GET /v1/models ni en la lista de modelos de MCP, y sus nombres se rechazan. Una máquina solo con CLM se baja el único checkpoint de CLM y nada más. Y sí, ahora hay un cuarto interruptor, para el Jev alojado al que este post le estaba haciendo una peineta desde la primera línea. DECIDEALOT_JEV_ENABLED también viene en auto, que lo enciende en cuanto pones DECIDEALOT_TYPESAFE_API_KEY. Sus nombres de modelo salen directamente del catálogo de TypeSafe, guardado en caché 60 segundos y pedido de nuevo con la primera petición que llegue después, sin ningún alias local jev, y cada decisión de Jev le manda a TypeSafe todo tu estado y todas tus preguntas. Es justo lo contrario de offline, así que si no es lo que quieres, deja la clave sin poner y Jev no existe. Con la clave puesta puedes incluso apagar los tres locales y correr una máquina solo con Jev que no descarga nada.
Descargar un Modelo Es Matar el Proceso
Esta es la parte que de verdad me importa. En la imagen hay dos virtualenvs de Python. /opt/app-venv es el gateway: FastAPI, httpx, el SDK de MCP, pydantic, uvicorn. Ahí no hay Torch, y el gateway nunca lo importa. /opt/torch-venv es la parte de los modelos, compartida por Laya, Von y CLM: los paquetes de los modelos entran desde un único fichero bloqueado por hash, encima de mis imágenes Torchbase, que existen para que las capas gordas de Python, Torch y CUDA se construyan una vez y se reutilicen, en vez de reconstruirse cada vez que toco una línea del código de la app. Eso sí, compartir un venv no es compartir un proceso. El proceso que responde a tus peticiones HTTP nunca tiene un modelo dentro.
Cada modelo corre como proceso hijo del gateway, arrancado por un supervisor desde un comando fijo sin shell, escuchando en un puerto fijo de loopback. Por defecto solo hay uno residente a la vez, y DECIDEALOT_MAX_RESIDENT_LOCAL_PROVIDERS sube eso a dos o tres si tienes RAM o VRAM para ello. Con el valor por defecto, pide Von con Laya en memoria y el supervisor espera a que termine cada petición de Laya que esté en curso, mata a Laya, arranca Von y consulta el /health de Von cada cuarto de segundo hasta que contesta. Con más huecos solo mata algo cuando están todos ocupados, y entonces le toca al inactivo que lleva más tiempo sin usarse. El Jev alojado no toca nada de esto, es solo una llamada HTTPS. Moverse entre los selectores de Laya no sale del proceso de Laya, porque son checkpoints del mismo modelo. Recortado al mínimo, el candado queda así:
async with self._provider_switch_condition:
spec = self._require_spec(provider_name)
while (
provider_name not in self._processes
and len(self._processes) >= self._settings.max_resident_local_providers
):
idle_candidates = [
name for name in self._processes if self._active_requests[name] == 0
]
if idle_candidates:
oldest = min(idle_candidates, key=lambda name: self._last_used_at.get(name, 0))
await self._stop_provider_locked(oldest, reason="model_switch")
continue
await self._provider_switch_condition.wait()
await self._start_provider_locked(spec)
self._active_requests[provider_name] += 1
try:
yield
finally:
async with self._provider_switch_condition:
self._active_requests[provider_name] -= 1
self._last_used_at[provider_name] = asyncio.get_running_loop().time()
self._provider_switch_condition.notify_all()¿Por qué un proceso entero en lugar de del model y rezarle al recolector de basura? Porque eso no devuelve la memoria. El caching allocator de PyTorch se aferra a lo que ya pilló, y el contexto de CUDA sigue ahí mientras viva el proceso. Matar el proceso es la única descarga que descarga de verdad, así que eso es exactamente lo que hace la descarga: terminate, diez segundos de cortesía, y luego kill si el proceso se pone gilipollas. Los pesos, las reservas de memoria de Torch, los hilos de trabajo y el contexto de CUDA se van todos con él. La siguiente petición lo vuelve a arrancar.
Ese arranque no sale gratis, y no voy a hacer como que sí. En mi máquina con GPU, a través de aigate, la primera petición a Laya después de un arranque en frío tardó unos 61 segundos, porque el proceso tenía que levantarse y cargar el modelo. La siguiente petición idéntica volvió en 55 milisegundos de punta a punta, red incluida, con la misma respuesta byte a byte. Cambiar a Von tardó unos dos minutos, porque primero había que tumbar Laya y luego levantar Von. Von en caliente respondió en unos 115 milisegundos. Así que el timeout de inactividad es un trade-off de verdad: ponlo lo bastante largo para que tu tráfico no se pase la vida pagando el arranque en frío, y lo bastante corto para que la GPU no se quede de niñera de un modelo que no usa nadie.
Hay dos cosas que disparan la descarga. Un reaper de inactividad descarga el proveedor que lleva sin usarse DECIDEALOT_PROVIDER_IDLE_UNLOAD_SECONDS, 600 por defecto, y lo comprueba cada décima parte de esa ventana, con el intervalo acotado entre 10 milisegundos y 30 segundos. Ponlo a 0 y lo único que se apaga es el temporizador. Y POST /v1/models/unload lo hace bajo demanda. Esa ruta es todo o nada: si algún proveedor local está en mitad de una petición te comes un 409 PROVIDER_BUSY y no se libera nada. Jamás le arranca un modelo de las manos a quien lo está usando.
Bájalo Una Vez y Adiós al Hub
Montas un directorio del host en /models, y decidealot gestiona /models/laya, /models/von y, con CLM activado, /models/clm ahí dentro. En el primer arranque el supervisor ejecuta un paso prepare para cada modelo activado dentro del venv de Torch, que se baja el snapshot de Hugging Face, convaiinnovations/laya y wfzyx/von, cada uno fijado a una revisión exacta de commit. Luego comprueba que cada fichero que necesita el runtime está de verdad ahí: cuatro ficheros en cada uno de los tres directorios de checkpoint de Laya, seis para Von. CLM se baja un único fichero de Contrastive-LM/CLM-v0.1-8B, también en una revisión fijada, y no lo usa a menos que su SHA-256 coincida con el fijado. Nada de eso importa Torch. /health responde 503 hasta que todos los bundles activados están listos. Con Laya y Von de serie son unos 5.3 GB y unos cuantos minutos la primera vez, y CLM suma sus 75 MB encima. Después los ficheros ya están ahí y no se baja nada.
Y antes de cargar un modelo, decidealot pone HF_HUB_OFFLINE=1 y TRANSFORMERS_OFFLINE=1. Con el bundle ya verificado, ningún modelo puede darse un paseo de vuelta al Hub a mitad de ejecución y traerse alguna mierda que tú no fijaste.
Cómo Hacer que Dos Servidores Upstream Hablen un Solo Contrato
El schema oficial dice que instructions es opcional y puede ser un valor JSON anidado, y lo mismo vale para los valores de los criterios. Laya quiere un campo instructions en todas y cada una de las preguntas. Von quiere que sea un string. Mándale a cualquiera de los dos una petición que la API oficial acepta sin rechistar y te la rechaza por una diferencia de forma que nadie pidió.
Así que decidealot valida primero tu petición contra los modelos de datos oficiales de la petición, y luego construye a partir de ella los cuerpos nativos de Laya y de Von: un instructions que falta se convierte en un string vacío, y los valores anidados se renderizan como texto JSON compacto. Tus etiquetas de choice, tu orden de score y tu state pasan sin que nadie los toque. A la vuelta, la respuesta del proveedor se valida contra el schema de respuesta oficial. La basura que solo mete el proveedor, como el bloque de enrutado de Laya, se tira, y model se rellena con el nombre público de lo que haya respondido de verdad. Si un proveedor devuelve algo que no encaja en el schema, te llega un 503 limpio en lugar de basura disfrazada de decisión. Si un proveedor rechaza una petición con un mensaje pelado, ese mensaje se reescribe como el sobre de validación oficial {"detail": [...]}, así que tu manejo de errores solo ve una forma, siempre la misma.
Y luego está el servidor de Von, que encuentra su backend a través de un singleton de todo el proceso cuyo constructor público no tiene manera de enterarse de dónde vive el checkpoint. Así que decidealot construye el motor por su cuenta y lo encaja a presión en el hueco del que lee el servidor:
engine = engine_type(
backend_name=os.environ.get(_von_backend_env, _default_von_backend),
device=os.environ.get(_von_device_env),
)
engine.backend = option_marker_backend_type(checkpoint_dir=str(model_dir), device=engine.device)
# Von's server resolves its backend from this singleton, whose public constructor
# has no checkpoint-directory argument.
with engine_type._lock:
engine_type._instance = engineSí, eso es meterle mano a un singleton privado. Es feo de cojones, y es exactamente tan feo como hace falta para apuntar Von a un directorio que controlas tú.
MCP en el Mismo Puerto
El mismo container sirve MCP Streamable HTTP en /mcp, con cuatro herramientas: system_one, system_one_batch, list_models y unload_models. Pasan por el mismo servicio de decisión que las rutas REST, o sea, la misma validación, el mismo supervisor, el mismo límite de cuerpo y el mismo bearer token. system_one recibe exactamente el model, el state, las questions y el config opcional que mandarías por POST, y devuelve el mismo resultado estructurado, así que un agente puede leer las probabilidades antes de decidir qué hace después. system_one_batch es la cara MCP de POST /v1/systemone/batch: una lista de peticiones independientes, cada una con su modelo, su estado y sus preguntas, con las respuestas en el orden en que las mandaste y cada resultado diciendo qué modelo respondió de verdad. Las peticiones al mismo modelo van una detrás de otra, DECIDEALOT_MAX_BATCH_CONCURRENCY limita cuántas llamadas a providers se solapan, y un solo elemento que falle tumba el batch entero, sin resultados parciales. Un fallo de validación vuelve como isError: true con el cuerpo detail de TypeSafe en el texto, así que el agente ve en qué la ha cagado en lugar de un “error” pelado.
Ninguna herramienta acepta una URL, una ruta del sistema de ficheros, un ejecutable, un directorio de modelo ni una opción de runtime arbitraria. El único mando que tiene una petición es config, y de momento solo CLM acepta algo ahí: una temperature validada. Todos los demás contestan con un 422 en cuanto le metes algo. El router de modelos asocia cada alias local a un endpoint fijo de loopback y cada nombre de Jev a la única API HTTPS fija de TypeSafe, y quien llama nunca llega a elegir un destino de red. Lo máximo que puede hacer aquí un agente es elegir un nombre del catálogo. La URL de embeddings de CLM y la clave de TypeSafe las fija una vez quien despliega la máquina, nunca una petición.
Ponerlo detrás de un reverse proxy o de un túnel no significa apagar la protección contra DNS rebinding. Metes en la lista blanca el Host público exacto en DECIDEALOT_MCP_ALLOWED_HOSTS y, para clientes de navegador, el origin exacto en DECIDEALOT_MCP_ALLOWED_ORIGINS. Las comprobaciones van en un orden fijo: un bearer que falta o está mal se lleva primero un 401, luego un host desconocido se lleva un 421, y luego un origin de navegador desconocido se lleva un 403. No metas comodines en esas listas en una máquina expuesta a internet. Pon DECIDEALOT_API_KEY a un secreto de verdad antes de que el container se acerque siquiera a una dirección pública.
La Mierda Aburrida que Te Deja Tenerlo Corriendo Sin Miedo
- Un container que no puede hacer gran cosa. La ejecución documentada lleva sistema de ficheros raíz de solo lectura,
--cap-drop ALL,no-new-privileges, tmpfsnoexecpara/tmpy/var/run, un límite de pids, un tope de memoria y un puerto publicado solo en loopback. - Tu UID, no el de la imagen. El container corre con el
--userque le pases, así que el directorio de modelos que acabas de crear conmkdirtiene permisos de escritura sin el bailecito delchown. La imagen solo cae al usuario sin root1000:1000cuando no dices nada. - Un solo agujero de exec para CUDA, y ni uno más. Triton compila pequeños helpers de CUDA en tiempo de ejecución y tiene que cargarlos desde algún sitio, así que la ejecución con CUDA añade un único tmpfs
execen/var/cache. Todo lo demás sigue en solo lectura ynoexec. - Auth bearer que no filtra tiempos. Opcional, apagada salvo que definas
DECIDEALOT_API_KEY, y el token se compara conhmac.compare_digestsobre bytes codificados./healthse queda abierto para tu liveness probe. - Un tope para el cuerpo. 1 MiB por defecto vía
DECIDEALOT_MAX_REQUEST_BYTES, y cualquier cosa que declare un tamaño mayor se lleva un413antes de que un modelo llegue a verla. - IDs de petición que puedes buscar con grep. Manda un UUID o un ULID en
X-Request-Idy el ID acompaña a la petición por todos los logs. Manda basura o nada y decidealot genera un UUID. En los dos casos el ID vuelve en la respuesta. - Una barrera de supply chain para la que los modelos eran demasiado jóvenes. Las dependencias del gateway están detrás de una barrera de antigüedad
exclude-newerde uv, y el stack de los modelos se instala en el venv de Torchbase con--require-hashesdesde un fichero bloqueado por hash. Los dos paquetes de modelos son más jóvenes de lo que permite la barrera. La release fijada de Laya ni siquiera está en PyPI, así que se instala desde el tarball del commit upstream con el SHA-256 en el lock. Cada paquete lleva una excepción por escrito y aprobada por el dueño. La barrera hizo su trabajo y yo firmé la autorización, como un padre antes de una excursión. - Tests con listón mínimo. Cobertura de ramas con un mínimo duro del 90%, más ejecuciones HTTP reales contra los pesos de CPU y de CUDA ya bajados.
Dos imágenes, cada una encima de la imagen de Torchbase que le toca. La de CPU ronda los 300 MB comprimida en Docker Hub y es la opción por defecto sensata. La de CUDA ronda los 5.4 GB, está construida sobre CUDA 12.6, solo amd64, y necesita el NVIDIA Container Toolkit y --gpus all. Tira de ella solo cuando la velocidad y la memoria del modelo justifiquen de verdad montar la GPU.
Arráncalo
model_directory="$HOME/.local/share/decidealot/models"
mkdir --parents "$model_directory"
docker run --detach --name decidealot --init --restart unless-stopped \
--user "$(id -u):$(id -g)" \
--read-only --cap-drop ALL --security-opt no-new-privileges:true \
--pids-limit 512 --memory 8g --cpus 4 \
--tmpfs /tmp:rw,noexec,nosuid,size=128m \
--tmpfs /var/run:rw,noexec,nosuid,size=8m \
--mount type=bind,source="$model_directory",target=/models \
--publish 127.0.0.1:8080:8080 \
psyb0t/decidealot:latest
curl --fail http://127.0.0.1:8080/healthEspera a que /health se ponga en verde y luego manda la petición de arriba. Para la GPU, añade --gpus all, el tmpfs de /var/cache, y usa psyb0t/decidealot:latest-cuda. El documento de despliegue del repo tiene la receta completa de CUDA.
Si ya tienes aigate, sáltate toda esa mierda. Pon DECIDEALOT=1 para el servicio de CPU en /decidealot/ o DECIDEALOT_CUDA=1 para el de GPU en /decidealot-cuda/, con MCP debajo de cada uno, y las herramientas se suman además al /mcp/ agregado de aigate, junto a todo lo demás con lo que ya hablan tus agentes. Los dos servicios pueden correr a la vez y compartir un solo directorio de modelos, así que los 5.3 GB caen en disco una sola vez. Ahí CLM viene encendido por defecto: make run-bg levanta también la ruta propia de llama.cpp en CUDA de aigate, local-llamacpp-cuda-qwen3-8b, que sirve embeddings de Qwen3 8B con exactamente las 4096 dimensiones que quiere CLM, por la red interna. Así que ni siquiera el estado que manda CLM sale de tu stack. Esa ruta necesita una GPU NVIDIA incluso al lado del servicio de CPU, así que en una máquina sin ella pon DECIDEALOT_CLM_ENABLED=false. El Jev alojado solo aparece si metes DECIDEALOT_TYPESAFE_API_KEY en el .env de aigate, y ese estado sí que sale. Una pega: aigate fija decidealot en v0.6.0, así que el ajuste temperature de CLM que llegó en v0.7.0 todavía no está ahí.
aigate además te gestiona la memoria sin que tengas que estar encima. Antes de que cualquier otro modelo local que aigate enruta por LiteLLM (Ollama, sd.cpp, talkies, vLLM, llama.cpp) corra en el mismo hardware, el gestor de recursos de aigate le dice a decidealot que descargue. Las decisiones de Laya y Von cogen el mismo lock de hardware que todo lo demás, entren por REST, por MCP o en un batch, y en CUDA aigate descarga su propio encoder de Qwen antes de que se cargue Laya o Von. CLM le deja el lock a su llamada anidada de embeddings. POST /v1/unload/cuda o /v1/unload/cpu descarga decidealot junto con el resto del stack. Una decisión que está en curso responde 409 y conserva su modelo hasta que salte el temporizador de inactividad, así que a nadie le arrancan nada de las manos a mitad de llamada. El agujero que queda: una petición mandada directamente a /decidealot/ solo echa a ese encoder, no a todos los demás modelos que estén okupando la GPU, así que un modelo residente que no pinta nada aquí todavía te puede comer la VRAM.
Instalarlo en Tu Agente
Un agente que está a punto de actuar según una probabilidad debería al menos saber qué significa esa probabilidad. La skill le explica cómo desplegar el container, elegir Laya, Von, CLM o el Jev alojado, mandar una decisión tipada, leer las probabilidades sin tomarlas por un permiso y usar MCP directamente. Todo lo que hay bajo .agents/ está catalogado en un solo marketplace, así que son dos comandos:
claude plugin marketplace add psyb0t/agents
claude plugin install decidealot@psyb0tCodex usa el mismo marketplace con otro verbo, codex plugin add decidealot@psyb0t, porque codex plugin install no existe. OpenClaw recibe la skill y, opcionalmente, un puente stdio para clientes que solo saben hablar con un servidor MCP stdio local. El puente reenvía todo al container que ya tienes corriendo:
openclaw skills install @psyb0t/decidealot
openclaw plugins install clawhub:@psyb0t/decidealotDecide la Máquina. Actúas Tú.
Jev tenía la idea buena, pero en casa ajena. decidealot es la misma idea en la tuya: un conjunto cerrado de respuestas, una probabilidad de verdad en cada una y cero posibilidades de que te devuelva una redacción. No sabe cuál debería ser tu umbral, y tampoco hace como que lo sabe. Tú eliges el corte según lo que te cuesta equivocarte, y en eso el modelo no tiene ni voz ni voto.
Píllalo en github.com/psyb0t/decidealot o bájate psyb0t/decidealot de Docker Hub. El código es WTFPL, así que haz con él lo que te salga de los cojones. Laya, Von, CLM, PyTorch, Transformers y los pesos que te bajas mantienen cada uno su propia licencia, así que léetelas antes de atornillar un modelo en algo que vendas. Y ahora deja de pedirle a un chatbot un sí o un no.