Compare commits
13
Commits
f6ac45d73f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1c5b4865a | ||
|
|
2b3e0e605a | ||
|
|
4a2a00966d | ||
|
|
162e03dad2 | ||
|
|
cc06215319 | ||
|
|
c9a2e73c61 | ||
|
|
9bd3d1052d | ||
|
|
b77d275fe0 | ||
|
|
a7a686499e | ||
|
|
afd786394a | ||
|
|
19fac12ff6 | ||
|
|
f99baf0d92 | ||
|
|
bdf42e74a3 |
@@ -0,0 +1,2 @@
|
|||||||
|
# Backups locales: pueden contener claves y secretos
|
||||||
|
ubuntu-config-*.tar.gz
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
# OpenClaw con servidor local
|
||||||
|
|
||||||
|
Guía para ejecutar OpenClaw usando el modelo local administrado por Ollama y
|
||||||
|
expuesto mediante LiteLLM.
|
||||||
|
|
||||||
|
> El video de referencia es [Aprende OpenClaw Ahora! curso completo desde cero
|
||||||
|
> para programadores](https://www.youtube.com/watch?v=4UtyJt2rMfo). Esta guía
|
||||||
|
> adapta el flujo de configuración a este servidor local y no depende de un
|
||||||
|
> proveedor cloud.
|
||||||
|
|
||||||
|
## Arquitectura
|
||||||
|
|
||||||
|
```text
|
||||||
|
OpenClaw Gateway
|
||||||
|
|
|
||||||
|
| OpenAI-compatible: http://127.0.0.1:8000/v1
|
||||||
|
v
|
||||||
|
LiteLLM Proxy
|
||||||
|
|
|
||||||
|
| Ollama native API: http://127.0.0.1:11434
|
||||||
|
v
|
||||||
|
Ollama -> qwen2.5-coder:14b -> NVIDIA GPU
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando OpenClaw y el servidor LLM están en la misma máquina, usa `127.0.0.1`.
|
||||||
|
Si OpenClaw corre en otro equipo, reemplaza esa dirección por la IP privada del
|
||||||
|
servidor, por ejemplo `http://192.168.1.50:8000/v1`.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
En el servidor LLM:
|
||||||
|
|
||||||
|
- Ubuntu 22.04 o 24.04.
|
||||||
|
- Node.js 24.16 o superior para OpenClaw.
|
||||||
|
- GPU NVIDIA con controladores funcionales.
|
||||||
|
- 16 GB de RAM como mínimo y espacio para el modelo.
|
||||||
|
- `sudo`, `curl`, `git`, Python 3 y conexión a Internet.
|
||||||
|
|
||||||
|
En el cliente remoto solo necesitas Node.js compatible con OpenClaw y acceso de
|
||||||
|
red al puerto `8000` del servidor.
|
||||||
|
|
||||||
|
## 1. Preparar el servidor LLM
|
||||||
|
|
||||||
|
Clona este repositorio y ejecuta el instalador principal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
|
||||||
|
cd llm-server-setup
|
||||||
|
chmod +x setup_llm_server.sh
|
||||||
|
sudo ./setup_llm_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El script instala y configura:
|
||||||
|
|
||||||
|
- controladores NVIDIA y un límite de potencia de 300 W;
|
||||||
|
- 8 GB de swap;
|
||||||
|
- Ollama como servicio systemd en `0.0.0.0:11434`;
|
||||||
|
- el modelo `qwen2.5-coder:14b`;
|
||||||
|
- LiteLLM en `/opt/litellm-env`;
|
||||||
|
- LiteLLM como `litellm.service` en `0.0.0.0:8000`;
|
||||||
|
- reglas UFW para SSH y LiteLLM.
|
||||||
|
|
||||||
|
Si se instalaron nuevos controladores, reinicia antes de continuar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Verificar Ollama y LiteLLM
|
||||||
|
|
||||||
|
Ejecuta estas comprobaciones en el servidor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl is-active ollama
|
||||||
|
systemctl is-active litellm
|
||||||
|
nvidia-smi
|
||||||
|
curl http://127.0.0.1:11434/api/tags
|
||||||
|
curl http://127.0.0.1:8000/health/liveliness
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirma que el modelo existe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama list
|
||||||
|
```
|
||||||
|
|
||||||
|
La configuración generada por `setup_llm_server.sh` está en:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/litellm_config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
El servicio LiteLLM debe ejecutarse con esa configuración y escuchar en el
|
||||||
|
puerto `8000`. Para revisar errores:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo journalctl -u ollama -n 100 --no-pager
|
||||||
|
sudo journalctl -u litellm -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Probar el endpoint OpenAI-compatible
|
||||||
|
|
||||||
|
Antes de instalar OpenClaw, prueba LiteLLM directamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8000/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
Después envía una consulta usando una clave local de prueba. El proxy de este
|
||||||
|
repositorio no configura autenticación, pero OpenClaw espera un valor de API
|
||||||
|
key para el proveedor LiteLLM; `local` sirve como valor de configuración local:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:8000/v1/chat/completions \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'Authorization: Bearer local' \
|
||||||
|
-d '{
|
||||||
|
"model": "qwen2.5-coder:14b",
|
||||||
|
"messages": [{"role": "user", "content": "Responde exactamente: OK"}],
|
||||||
|
"temperature": 0
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
La respuesta debe contener `OK` y no un error de conexión o de modelo.
|
||||||
|
|
||||||
|
## 4. Instalar OpenClaw
|
||||||
|
|
||||||
|
En el servidor o en el equipo donde se ejecutará el Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash install_openclaw.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El script instala OpenClaw sin iniciar el asistente. También puedes ejecutar el
|
||||||
|
instalador oficial directamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://openclaw.ai/install.sh | bash -s -- --no-onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Comprueba Node.js y OpenClaw:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node --version
|
||||||
|
openclaw --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Configurar OpenClaw con LiteLLM local
|
||||||
|
|
||||||
|
La configuración de OpenClaw se guarda en:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.openclaw/openclaw.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Crea o edita ese archivo como JSON5:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
litellm: {
|
||||||
|
baseUrl: "http://127.0.0.1:8000/v1",
|
||||||
|
apiKey: "local",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "qwen2.5-coder:14b",
|
||||||
|
name: "Qwen 2.5 Coder 14B local",
|
||||||
|
reasoning: false,
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 32768,
|
||||||
|
maxTokens: 32768
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: {
|
||||||
|
primary: "litellm/qwen2.5-coder:14b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Puntos importantes:
|
||||||
|
|
||||||
|
- En `baseUrl` incluye `/v1` porque OpenClaw usa la API compatible con OpenAI de
|
||||||
|
LiteLLM.
|
||||||
|
- No uses `http://127.0.0.1:11434/v1` directamente con el proveedor LiteLLM.
|
||||||
|
- Para Ollama nativo, OpenClaw usa otra configuración y no debe mezclarse con
|
||||||
|
esta ruta.
|
||||||
|
- `apiKey: "local"` coincide con el valor usado en las pruebas. No es una
|
||||||
|
credencial real porque el servicio actual no habilita autenticación.
|
||||||
|
|
||||||
|
Valida la configuración:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw doctor
|
||||||
|
openclaw models list --provider litellm --refresh --json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Configurar con el asistente
|
||||||
|
|
||||||
|
Como alternativa a editar JSON5, ejecuta el onboarding indicando LiteLLM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw onboard --auth-choice litellm-api-key
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando solicite la URL del proxy, usa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Y como API key local:
|
||||||
|
|
||||||
|
```text
|
||||||
|
local
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el asistente permite modo no interactivo en tu versión instalada:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LITELLM_API_KEY=local
|
||||||
|
openclaw onboard --non-interactive --accept-risk --skip-health \
|
||||||
|
--auth-choice litellm-api-key \
|
||||||
|
--litellm-api-key "$LITELLM_API_KEY" \
|
||||||
|
--custom-base-url "http://127.0.0.1:8000/v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
Después revisa el modelo primario con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw config get agents.defaults.model.primary
|
||||||
|
```
|
||||||
|
|
||||||
|
Debe devolver `litellm/qwen2.5-coder:14b`.
|
||||||
|
|
||||||
|
## 7. Instalar y verificar el Gateway
|
||||||
|
|
||||||
|
Instala el servicio de usuario de OpenClaw:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw gateway install
|
||||||
|
openclaw gateway status
|
||||||
|
```
|
||||||
|
|
||||||
|
El Gateway normalmente escucha en el puerto `18789`. Abre el dashboard:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
También puedes verificarlo desde el navegador en:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Prueba un mensaje desde el dashboard. En otra terminal puedes observar los
|
||||||
|
logs del Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw logs --follow
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Acceder desde Windows con PuTTY o PowerShell
|
||||||
|
|
||||||
|
En este servidor, la IP de la máquina Ubuntu es:
|
||||||
|
|
||||||
|
```text
|
||||||
|
192.168.0.225
|
||||||
|
```
|
||||||
|
|
||||||
|
El Gateway está enlazado intencionalmente a `127.0.0.1:18789`, por lo que no
|
||||||
|
debes abrir `192.168.0.225:18789` directamente. Usa un túnel SSH desde el PC
|
||||||
|
Windows.
|
||||||
|
|
||||||
|
### PowerShell
|
||||||
|
|
||||||
|
Ejecuta este comando en PowerShell del PC cliente:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
|
||||||
|
```
|
||||||
|
|
||||||
|
Introduce la contraseña de `ctello` y deja esa ventana abierta. El parámetro
|
||||||
|
`-L` significa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Puerto local del PC:18789 -> servidor:127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
En otra ventana de PowerShell, comprueba el túnel:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection 127.0.0.1 -Port 18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Debe mostrar:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TcpTestSucceeded : True
|
||||||
|
```
|
||||||
|
|
||||||
|
Después abre el dashboard en el navegador del PC:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el puerto local `18789` ya está ocupado, usa otro puerto solo en el PC:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -N -L 18790:127.0.0.1:18789 ctello@192.168.0.225
|
||||||
|
```
|
||||||
|
|
||||||
|
En ese caso abre:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:18790
|
||||||
|
```
|
||||||
|
|
||||||
|
El destino remoto sigue siendo siempre `127.0.0.1:18789`.
|
||||||
|
|
||||||
|
### PuTTY
|
||||||
|
|
||||||
|
Configura la sesión con:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Host Name: 192.168.0.225
|
||||||
|
Port: 22
|
||||||
|
Connection type: SSH
|
||||||
|
```
|
||||||
|
|
||||||
|
Después ve a `Connection > SSH > Tunnels` y añade:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Source port: 18789
|
||||||
|
Destination: 127.0.0.1:18789
|
||||||
|
Type: Local
|
||||||
|
```
|
||||||
|
|
||||||
|
Pulsa `Add`, conecta la sesión y abre en el navegador:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Mantén abierta la sesión SSH mientras uses el dashboard.
|
||||||
|
|
||||||
|
## 9. Usar OpenClaw desde otro equipo
|
||||||
|
|
||||||
|
Si el Gateway corre en otro equipo distinto al servidor LLM, cambia únicamente
|
||||||
|
`baseUrl` en `~/.openclaw/openclaw.json`:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
litellm: {
|
||||||
|
baseUrl: "http://192.168.1.50:8000/v1",
|
||||||
|
apiKey: "local",
|
||||||
|
api: "openai-completions",
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
id: "qwen2.5-coder:14b",
|
||||||
|
name: "Qwen 2.5 Coder 14B local",
|
||||||
|
input: ["text"],
|
||||||
|
contextWindow: 32768,
|
||||||
|
maxTokens: 32768
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "litellm/qwen2.5-coder:14b" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
En el servidor, permite el acceso solo desde la red privada. Por ejemplo,
|
||||||
|
reemplaza la regla abierta actual de UFW por una regla limitada a tu subred:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw delete allow 8000/tcp
|
||||||
|
sudo ufw allow from 192.168.1.0/24 to any port 8000 proto tcp
|
||||||
|
sudo ufw status verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
No expongas el puerto `8000` directamente a Internet: el proxy actual no tiene
|
||||||
|
una API key real ni TLS.
|
||||||
|
|
||||||
|
## Solución de problemas
|
||||||
|
|
||||||
|
### Gateway activo, pero el navegador muestra `ERR_CONNECTION_REFUSED`
|
||||||
|
|
||||||
|
Comprueba el estado en Ubuntu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw gateway status
|
||||||
|
ss -ltnp | grep 18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Debe aparecer:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Runtime: running
|
||||||
|
Connectivity probe: ok
|
||||||
|
Listening: 127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el Gateway no está activo, configura el modo local y arráncalo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw config set gateway.mode local
|
||||||
|
openclaw gateway install
|
||||||
|
openclaw gateway status
|
||||||
|
```
|
||||||
|
|
||||||
|
Si el servicio de usuario necesita una sesión persistente, habilita el
|
||||||
|
`linger` para `ctello` desde una cuenta con `sudo`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo loginctl enable-linger ctello
|
||||||
|
```
|
||||||
|
|
||||||
|
Después vuelve a iniciar sesión SSH como `ctello` y ejecuta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now openclaw-gateway.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Si `systemctl --user` muestra que faltan `DBUS_SESSION_BUS_ADDRESS` o
|
||||||
|
`XDG_RUNTIME_DIR`, vuelve a conectarte por SSH después de habilitar `linger`.
|
||||||
|
No ejecutes un segundo `openclaw gateway run` si el servicio ya está activo.
|
||||||
|
|
||||||
|
Para revisar errores:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl --user -u openclaw-gateway.service -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
### El túnel de PowerShell no conecta
|
||||||
|
|
||||||
|
En el PC cliente verifica primero que SSH funciona:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection 192.168.0.225 -Port 22
|
||||||
|
```
|
||||||
|
|
||||||
|
Si SSH responde pero el puerto del túnel no, comprueba en Ubuntu que el
|
||||||
|
Gateway esté escuchando en `127.0.0.1:18789`. No abras el puerto `18789` en
|
||||||
|
UFW ni cambies el Gateway a `0.0.0.0` solo para evitar el túnel.
|
||||||
|
|
||||||
|
### `Connection refused` en el puerto 8000
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl restart ollama litellm
|
||||||
|
sudo systemctl status ollama litellm
|
||||||
|
sudo journalctl -u litellm -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
### LiteLLM no encuentra el modelo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ollama list
|
||||||
|
ollama pull qwen2.5-coder:14b
|
||||||
|
curl http://127.0.0.1:8000/v1/models
|
||||||
|
```
|
||||||
|
|
||||||
|
El nombre debe coincidir exactamente en los tres lugares:
|
||||||
|
|
||||||
|
```text
|
||||||
|
qwen2.5-coder:14b
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenClaw muestra respuestas de herramientas como texto
|
||||||
|
|
||||||
|
Comprueba que OpenClaw usa LiteLLM con `/v1` y no la URL equivocada de Ollama:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw config get models.providers.litellm.baseUrl
|
||||||
|
```
|
||||||
|
|
||||||
|
Debe devolver:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### El modelo se queda sin memoria
|
||||||
|
|
||||||
|
Reduce `contextWindow` y `maxTokens` en la configuración de OpenClaw y revisa
|
||||||
|
la VRAM disponible:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nvidia-smi
|
||||||
|
```
|
||||||
|
|
||||||
|
### La configuración no es válida
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw doctor
|
||||||
|
openclaw doctor --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenClaw valida estrictamente `openclaw.json`; un campo desconocido o un tipo
|
||||||
|
incorrecto puede impedir que el Gateway arranque.
|
||||||
|
|
||||||
|
## Referencias
|
||||||
|
|
||||||
|
- [Documentación oficial de OpenClaw](https://docs.openclaw.ai/)
|
||||||
|
- [Proveedor LiteLLM en OpenClaw](https://docs.openclaw.ai/providers/litellm)
|
||||||
|
- [Proveedor Ollama en OpenClaw](https://docs.openclaw.ai/providers/ollama)
|
||||||
|
- [Configuración del Gateway](https://docs.openclaw.ai/gateway/configuration)
|
||||||
@@ -1,5 +1,231 @@
|
|||||||
nano setup_llm_server.sh
|
# LLM Server
|
||||||
|
|
||||||
|
Despliegue automatizado de un servidor LLM local con Ollama, LiteLLM y
|
||||||
|
OpenClaw sobre Ubuntu 22.04/24.04.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Ubuntu 22.04 o 24.04
|
||||||
|
- CPU de 4 núcleos o superior
|
||||||
|
- 16 GB de RAM como mínimo
|
||||||
|
- GPU NVIDIA compatible con sus controladores Linux
|
||||||
|
- Acceso `sudo` y conexión a Internet
|
||||||
|
|
||||||
|
La configuración está orientada a una NVIDIA RTX 3090 de 24 GB, pero el
|
||||||
|
script puede utilizarse con otras GPU compatibles ajustando el modelo si es
|
||||||
|
necesario.
|
||||||
|
|
||||||
|
## Instalación
|
||||||
|
|
||||||
|
Clona el repositorio en el servidor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://gitea.oemspot.com.ar/carlostellocba/llm-server-setup.git
|
||||||
|
cd llm-server-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
Ejecuta el instalador principal como `root` o mediante `sudo`:
|
||||||
|
|
||||||
|
```bash
|
||||||
chmod +x setup_llm_server.sh
|
chmod +x setup_llm_server.sh
|
||||||
sudo ./setup_llm_server.sh
|
sudo ./setup_llm_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El instalador:
|
||||||
|
|
||||||
|
- actualiza los paquetes del sistema e instala dependencias;
|
||||||
|
- configura un archivo de swap de 8 GB;
|
||||||
|
- instala los controladores NVIDIA si no están disponibles;
|
||||||
|
- instala y configura Ollama en `0.0.0.0:11434`;
|
||||||
|
- crea un entorno virtual de LiteLLM en `/opt/litellm-env`;
|
||||||
|
- descarga `qwen2.5-coder:14b`;
|
||||||
|
- crea el servicio `litellm.service` en el puerto `8000`;
|
||||||
|
- permite SSH y LiteLLM mediante UFW.
|
||||||
|
|
||||||
|
El proceso puede tardar varios minutos y requiere espacio suficiente para el
|
||||||
|
modelo. Si se instalan controladores NVIDIA nuevos, reinicia el servidor:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verificación
|
||||||
|
|
||||||
|
Después de la instalación, comprueba los servicios:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status ollama
|
||||||
|
systemctl status litellm
|
||||||
|
nvidia-smi
|
||||||
|
curl http://127.0.0.1:11434/api/tags
|
||||||
|
curl http://127.0.0.1:8000/health/liveliness
|
||||||
|
```
|
||||||
|
|
||||||
|
La configuración de LiteLLM se genera en el directorio personal del usuario
|
||||||
|
que ejecuta `sudo` y queda guardada como `~/litellm_config.yaml`.
|
||||||
|
|
||||||
|
## Instalación de OpenClaw
|
||||||
|
|
||||||
|
Para la configuración completa de OpenClaw con el servidor local, consulta
|
||||||
|
[OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
|
||||||
|
|
||||||
|
El script `install_openclaw.sh` utiliza el instalador oficial de OpenClaw y
|
||||||
|
está pensado para Linux y WSL2. Para instalarlo sin abrir el asistente inicial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash install_openclaw.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Para instalarlo y ejecutar el asistente inmediatamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash install_openclaw.sh --onboard
|
||||||
|
```
|
||||||
|
|
||||||
|
Si la instalación se hizo sin asistente, puedes iniciarlo después y configurar
|
||||||
|
el servicio de Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw onboard --install-daemon
|
||||||
|
```
|
||||||
|
|
||||||
|
Comprueba la instalación con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openclaw --version
|
||||||
|
openclaw doctor
|
||||||
|
openclaw gateway status
|
||||||
|
```
|
||||||
|
|
||||||
|
Si OpenClaw devuelve respuestas vacías o de un token, ejecuta el reparador
|
||||||
|
local como el usuario que ejecuta OpenClaw (`ctello`, no `root`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x fix_openclaw_local.sh
|
||||||
|
bash ./fix_openclaw_local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El script respalda `openclaw.json`, fuerza el Gateway local, desactiva el
|
||||||
|
razonamiento predeterminado y la memoria semántica sin embeddings, inicializa
|
||||||
|
el workspace Git y reinicia el Gateway. No borra sesiones ni secretos.
|
||||||
|
|
||||||
|
Después crea una sesión nueva en el dashboard y prueba:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Responde exactamente: SESION NUEVA OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Para acceder al dashboard desde Windows mediante PowerShell, usa un túnel SSH
|
||||||
|
hacia el servidor Ubuntu:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh -N -L 18789:127.0.0.1:18789 ctello@192.168.0.225
|
||||||
|
```
|
||||||
|
|
||||||
|
Mantén la sesión abierta y entra desde el navegador en:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:18789
|
||||||
|
```
|
||||||
|
|
||||||
|
La guía completa de acceso desde PuTTY, PowerShell y solución de problemas está
|
||||||
|
en [OPENCLAW_LOCAL_SERVER.md](OPENCLAW_LOCAL_SERVER.md).
|
||||||
|
|
||||||
|
## Respaldo y recuperación
|
||||||
|
|
||||||
|
Para ejecutar un diagnóstico independiente y de solo lectura del servidor usa
|
||||||
|
`ubuntu_server_diagnostic.sh`. Comprueba Ubuntu, hardware, servicios, puertos,
|
||||||
|
Ollama, LiteLLM, OpenClaw y UFW:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x ubuntu_server_diagnostic.sh
|
||||||
|
sudo ./ubuntu_server_diagnostic.sh --report /tmp/ubuntu-server-test.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Puedes cambiar los endpoints o el modelo esperado mediante variables de entorno:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
OLLAMA_URL=http://127.0.0.1:11434 \
|
||||||
|
LITELLM_URL=http://127.0.0.1:8000 \
|
||||||
|
MODEL_NAME=qwen2.5-coder:14b \
|
||||||
|
sudo -E ./ubuntu_server_diagnostic.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Para guardar la configuración del servidor antes de realizar cambios, ejecuta
|
||||||
|
el script como `root`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x backup_ubuntu_config.sh restore_ubuntu_config.sh
|
||||||
|
sudo ./backup_ubuntu_config.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
El respaldo se guarda por defecto en `/var/backups` e incluye inventario de
|
||||||
|
hardware, paquetes, servicios, red, UFW, systemd, NVIDIA, Ollama, LiteLLM,
|
||||||
|
OpenClaw y configuraciones de usuario. No incluye modelos Ollama, el entorno
|
||||||
|
virtual de LiteLLM, bases de datos ni logs completos.
|
||||||
|
|
||||||
|
Las claves SSH, certificados privados y credenciales de OpenClaw quedan fuera
|
||||||
|
por defecto. Para incluirlos explícitamente, protege el archivo resultante:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./backup_ubuntu_config.sh --include-secrets
|
||||||
|
sudo chmod 600 /var/backups/ubuntu-config-*.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
En una instalación Ubuntu nueva, instala primero el sistema base y crea los
|
||||||
|
usuarios necesarios. Después copia el archivo de respaldo y usa el restaurador
|
||||||
|
en modo vista previa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Para aplicar la configuración:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Puedes reinstalar los paquetes APT y arrancar los servicios habilitados de la
|
||||||
|
máquina original de forma explícita:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ./restore_ubuntu_config.sh /ruta/al/respaldo.tar.gz \
|
||||||
|
--yes --install-packages --start-services
|
||||||
|
```
|
||||||
|
|
||||||
|
Revisa siempre los servicios y el firewall después de restaurar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --failed
|
||||||
|
systemctl status ollama litellm
|
||||||
|
sudo ufw status verbose
|
||||||
|
nvidia-smi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool calling y rutas de Windows
|
||||||
|
|
||||||
|
Cuando un modelo envía rutas de Windows dentro de argumentos JSON, las barras
|
||||||
|
invertidas deben estar escapadas. Por ejemplo:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"path": "C:\\Users\\name\\file.txt"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Si las barras no están escapadas, el parser JSON puede fallar antes de que la
|
||||||
|
herramienta llegue a ejecutarse.
|
||||||
|
|
||||||
|
## Archivos principales
|
||||||
|
|
||||||
|
- `setup_llm_server.sh`: instala y configura Ollama, LiteLLM, NVIDIA y UFW.
|
||||||
|
- `install_openclaw.sh`: instala OpenClaw mediante el instalador oficial.
|
||||||
|
- `litellm_config.yaml`: configuración local opcional para LiteLLM.
|
||||||
|
- `tool_call_demo.py`: ejemplo local de manejo de llamadas a herramientas.
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
El instalador expone Ollama en todas las interfaces y abre el puerto `8000`
|
||||||
|
en UFW. En un servidor conectado a Internet, limita esos puertos a la red
|
||||||
|
privada o protégelos detrás de un proxy con autenticación y HTTPS.
|
||||||
Executable
+210
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
readonly SCRIPT_NAME="$(basename "$0")"
|
||||||
|
readonly DEFAULT_OUTPUT_DIR="/var/backups"
|
||||||
|
|
||||||
|
include_secrets=false
|
||||||
|
output_dir="$DEFAULT_OUTPUT_DIR"
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso: sudo $SCRIPT_NAME [opciones]
|
||||||
|
|
||||||
|
Crea un respaldo comprimido de la configuración de Ubuntu para replicarla en
|
||||||
|
otra instalación. No incluye modelos Ollama, entornos virtuales ni bases de datos.
|
||||||
|
|
||||||
|
Opciones:
|
||||||
|
--output-dir DIR Directorio donde guardar el archivo .tar.gz.
|
||||||
|
--include-secrets Incluye claves SSH, certificados privados y secretos
|
||||||
|
de OpenClaw. El archivo resultante debe protegerse.
|
||||||
|
-h, --help Muestra esta ayuda.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--output-dir)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Falta DIR para --output-dir" >&2; exit 2; }
|
||||||
|
output_dir="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--include-secrets)
|
||||||
|
include_secrets=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Opción desconocida: $1" >&2
|
||||||
|
show_help >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo "Ejecuta este script con sudo o como root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
|
||||||
|
command -v gzip >/dev/null || { echo "gzip es obligatorio." >&2; exit 1; }
|
||||||
|
|
||||||
|
hostname_value="$(hostname -s 2>/dev/null || echo unknown)"
|
||||||
|
timestamp="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
backup_name="ubuntu-config-${hostname_value}-${timestamp}"
|
||||||
|
work_dir="$(mktemp -d)"
|
||||||
|
archive_path="${output_dir}/${backup_name}.tar.gz"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
mkdir -p "$work_dir/metadata" "$work_dir/files" "$work_dir/packages" "$work_dir/services"
|
||||||
|
mkdir -p "$output_dir"
|
||||||
|
umask 077
|
||||||
|
|
||||||
|
copy_path() {
|
||||||
|
local source="$1"
|
||||||
|
local destination="$work_dir/files$source"
|
||||||
|
|
||||||
|
if [[ -e "$source" || -L "$source" ]]; then
|
||||||
|
mkdir -p "$(dirname "$destination")"
|
||||||
|
cp -a "$source" "$destination"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
capture() {
|
||||||
|
local name="$1"
|
||||||
|
shift
|
||||||
|
"$@" > "$work_dir/metadata/$name.txt" 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_shell() {
|
||||||
|
local name="$1"
|
||||||
|
local command_text="$2"
|
||||||
|
bash -c "$command_text" > "$work_dir/metadata/$name.txt" 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
printf 'Creando respaldo en %s\n' "$archive_path"
|
||||||
|
|
||||||
|
printf '%s\n' "hostname=$hostname_value" "created_at=$(date --iso-8601=seconds)" \
|
||||||
|
"include_secrets=$include_secrets" > "$work_dir/metadata/backup-info.txt"
|
||||||
|
|
||||||
|
capture_shell os-release 'cat /etc/os-release'
|
||||||
|
capture_shell kernel 'uname -a'
|
||||||
|
capture_shell hardware 'lscpu; echo; free -h; echo; lsblk -f'
|
||||||
|
capture_shell disks 'df -hT; echo; findmnt'
|
||||||
|
capture_shell network 'ip -brief address; echo; ip route; echo; resolvectl status 2>/dev/null || true'
|
||||||
|
capture_shell users 'getent passwd | awk -F: '\''$3 >= 1000 || $1 == "root" {print $1 ":" $3 ":" $4 ":" $6}'\'''
|
||||||
|
capture_shell mounts 'mount'
|
||||||
|
# Registrar nombres, no valores que puedan contener credenciales.
|
||||||
|
capture_shell environment 'printenv | cut -d= -f1 | sort -u'
|
||||||
|
capture_shell nvidia 'command -v nvidia-smi && nvidia-smi -q || true'
|
||||||
|
capture_shell ollama 'command -v ollama && ollama list || true'
|
||||||
|
capture_shell versions 'command -v node && node --version || true; command -v npm && npm --version || true; command -v python3 && python3 --version || true; command -v ollama && ollama --version || true; command -v openclaw && openclaw --version || true'
|
||||||
|
capture_shell ufw 'command -v ufw && ufw status verbose || true'
|
||||||
|
capture_shell iptables 'command -v iptables-save && iptables-save || true'
|
||||||
|
capture_shell sysctl 'sysctl -a 2>/dev/null'
|
||||||
|
capture_shell timers 'systemctl list-timers --all --no-pager'
|
||||||
|
capture_shell enabled-services 'systemctl list-unit-files --state=enabled --no-legend --no-pager'
|
||||||
|
capture_shell failed-services 'systemctl --failed --no-pager'
|
||||||
|
|
||||||
|
if command -v dpkg-query >/dev/null; then
|
||||||
|
dpkg-query -W -f='${binary:Package}\t${Version}\n' > "$work_dir/packages/dpkg-status.tsv" || true
|
||||||
|
fi
|
||||||
|
if command -v apt-mark >/dev/null; then
|
||||||
|
apt-mark showmanual | sort > "$work_dir/packages/apt-manual.txt" || true
|
||||||
|
fi
|
||||||
|
if command -v snap >/dev/null; then
|
||||||
|
snap list > "$work_dir/packages/snap-list.txt" 2>&1 || true
|
||||||
|
fi
|
||||||
|
if command -v pip3 >/dev/null; then
|
||||||
|
pip3 freeze > "$work_dir/packages/pip3-freeze.txt" 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl list-unit-files --type=service --no-legend --no-pager \
|
||||||
|
> "$work_dir/services/all-service-units.txt" 2>&1 || true
|
||||||
|
systemctl list-unit-files --state=enabled --type=service --no-legend --no-pager \
|
||||||
|
| awk '{print $1}' | sort -u > "$work_dir/services/enabled-service-names.txt" || true
|
||||||
|
|
||||||
|
# Configuración del sistema y de los servicios usados por este proyecto.
|
||||||
|
for path in \
|
||||||
|
/etc/apt \
|
||||||
|
/etc/default \
|
||||||
|
/etc/environment \
|
||||||
|
/etc/fstab \
|
||||||
|
/etc/hostname \
|
||||||
|
/etc/hosts \
|
||||||
|
/etc/issue \
|
||||||
|
/etc/netplan \
|
||||||
|
/etc/NetworkManager \
|
||||||
|
/etc/systemd/system \
|
||||||
|
/etc/sysctl.d \
|
||||||
|
/etc/modprobe.d \
|
||||||
|
/etc/ufw \
|
||||||
|
/etc/ollama \
|
||||||
|
/etc/nvidia \
|
||||||
|
/etc/profile.d
|
||||||
|
do
|
||||||
|
copy_path "$path"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Configuraciones de usuario que no suelen contener credenciales.
|
||||||
|
while IFS=: read -r username _ uid _ _ home _; do
|
||||||
|
[[ -d "$home" ]] || continue
|
||||||
|
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
|
||||||
|
for relative_path in .bashrc .profile .config/systemd/user litellm_config.yaml; do
|
||||||
|
copy_path "$home/$relative_path"
|
||||||
|
done
|
||||||
|
printf '%s\t%s\t%s\t%s\n' "$username" "$uid" "$(id -g "$username" 2>/dev/null || echo 0)" "$home" \
|
||||||
|
>> "$work_dir/metadata/users.tsv"
|
||||||
|
done < <(getent passwd)
|
||||||
|
|
||||||
|
if [[ "$include_secrets" == true ]]; then
|
||||||
|
printf 'Incluyendo archivos sensibles: SSH, certificados y configuración privada de OpenClaw.\n'
|
||||||
|
for path in /etc/ssh /etc/ssl/private /etc/letsencrypt; do
|
||||||
|
copy_path "$path"
|
||||||
|
done
|
||||||
|
while IFS=: read -r username _ uid _ _ home _; do
|
||||||
|
[[ -d "$home" ]] || continue
|
||||||
|
[[ "$uid" -ge 1000 || "$username" == root ]] || continue
|
||||||
|
for relative_path in .ssh .openclaw .npmrc; do
|
||||||
|
copy_path "$home/$relative_path"
|
||||||
|
done
|
||||||
|
done < <(getent passwd)
|
||||||
|
else
|
||||||
|
cat > "$work_dir/metadata/excluded-secrets.txt" <<EOF
|
||||||
|
No se incluyeron secretos. Para incluirlos explícitamente, usa:
|
||||||
|
sudo $SCRIPT_NAME --include-secrets
|
||||||
|
Excluidos por defecto: /etc/ssh, /etc/ssl/private, /etc/letsencrypt, ~/.ssh,
|
||||||
|
~/.openclaw y ~/.npmrc.
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$work_dir/RESTORE.txt" <<EOF
|
||||||
|
Este archivo fue generado por $SCRIPT_NAME.
|
||||||
|
|
||||||
|
1. Instala Ubuntu con la misma arquitectura y crea el usuario de servicio.
|
||||||
|
2. Copia este archivo a la nueva máquina.
|
||||||
|
3. Ejecuta restore_ubuntu_config.sh con este archivo y revisa los cambios.
|
||||||
|
4. Reinstala los modelos Ollama y valida los servicios antes de exponerlos.
|
||||||
|
|
||||||
|
El respaldo no contiene modelos Ollama, /opt/litellm-env, bases de datos ni
|
||||||
|
logs completos. Si no usaste --include-secrets, debes restaurar las credenciales
|
||||||
|
manualmente.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf 'Archivos incluidos:\n'
|
||||||
|
find "$work_dir" -type f -printf '%P\n' | sort > "$work_dir/metadata/file-list.txt"
|
||||||
|
cat "$work_dir/metadata/file-list.txt"
|
||||||
|
|
||||||
|
tar -C "$work_dir" -czf "$archive_path" .
|
||||||
|
chmod 600 "$archive_path"
|
||||||
|
printf '\nRespaldo creado: %s\n' "$archive_path"
|
||||||
|
printf 'Protección: permisos 600\n'
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
readonly SCRIPT_NAME="$(basename "$0")"
|
||||||
|
readonly OPENCLAW_HOME="${OPENCLAW_HOME:-$HOME/.openclaw}"
|
||||||
|
readonly CONFIG_FILE="${OPENCLAW_CONFIG_PATH:-$OPENCLAW_HOME/openclaw.json}"
|
||||||
|
readonly WORKSPACE_DIR="$OPENCLAW_HOME/workspace"
|
||||||
|
readonly BACKUP_DIR="$OPENCLAW_HOME/config-backups"
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso: $SCRIPT_NAME
|
||||||
|
|
||||||
|
Corrige la configuración local de OpenClaw para este servidor:
|
||||||
|
- usa Gateway local;
|
||||||
|
- desactiva thinking por defecto;
|
||||||
|
- desactiva memoria semántica sin proveedor de embeddings;
|
||||||
|
- inicializa el workspace Git requerido por OpenClaw;
|
||||||
|
- reinicia el Gateway de usuario;
|
||||||
|
- verifica LiteLLM y el modelo local.
|
||||||
|
|
||||||
|
El script no borra sesiones, no elimina secretos y no publica archivos.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v openclaw >/dev/null 2>&1; then
|
||||||
|
echo "Error: openclaw no está disponible en PATH." >&2
|
||||||
|
echo "Ejecuta: source ~/.bashrc" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
||||||
|
echo "Error: no existe la configuración: $CONFIG_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
backup_file="$BACKUP_DIR/openclaw.json.$(date +%Y%m%d-%H%M%S).bak"
|
||||||
|
cp -p "$CONFIG_FILE" "$backup_file"
|
||||||
|
chmod 600 "$backup_file"
|
||||||
|
echo "Respaldo de configuración: $backup_file"
|
||||||
|
|
||||||
|
echo "Aplicando configuración local..."
|
||||||
|
openclaw config set gateway.mode local
|
||||||
|
openclaw config set agents.defaults.thinkingDefault off
|
||||||
|
openclaw config set memory.search.enabled false
|
||||||
|
|
||||||
|
if [[ ! -d "$WORKSPACE_DIR/.git" ]]; then
|
||||||
|
mkdir -p "$WORKSPACE_DIR"
|
||||||
|
git -C "$WORKSPACE_DIR" init
|
||||||
|
git -C "$WORKSPACE_DIR" config user.name "OpenClaw"
|
||||||
|
git -C "$WORKSPACE_DIR" config user.email "openclaw@localhost"
|
||||||
|
touch "$WORKSPACE_DIR/.gitkeep"
|
||||||
|
git -C "$WORKSPACE_DIR" add .gitkeep
|
||||||
|
git -C "$WORKSPACE_DIR" commit -m "Initialize OpenClaw workspace" >/dev/null
|
||||||
|
echo "Workspace Git inicializado: $WORKSPACE_DIR"
|
||||||
|
else
|
||||||
|
echo "Workspace Git ya estaba inicializado: $WORKSPACE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
if systemctl --user daemon-reload 2>/dev/null; then
|
||||||
|
systemctl --user enable openclaw-gateway.service >/dev/null 2>&1 || true
|
||||||
|
systemctl --user restart openclaw-gateway.service
|
||||||
|
else
|
||||||
|
echo "Aviso: no se pudo conectar al bus systemd de usuario." >&2
|
||||||
|
echo "Cierra esta sesión SSH y vuelve a entrar como $(id -un)." >&2
|
||||||
|
echo "Después ejecuta: systemctl --user restart openclaw-gateway.service" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Verificando configuración..."
|
||||||
|
openclaw config get gateway.mode
|
||||||
|
openclaw config get agents.defaults.thinkingDefault
|
||||||
|
openclaw config get agents.defaults.model.primary
|
||||||
|
openclaw config get models.providers.litellm.baseUrl
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl --fail --silent --show-error --max-time 10 \
|
||||||
|
http://127.0.0.1:8000/v1/models | grep -q 'qwen2.5-coder:14b'; then
|
||||||
|
echo "OK: LiteLLM publica qwen2.5-coder:14b"
|
||||||
|
else
|
||||||
|
echo "ERROR: LiteLLM no publica qwen2.5-coder:14b" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
|
systemctl --user is-active --quiet openclaw-gateway.service && \
|
||||||
|
echo "OK: Gateway activo" || \
|
||||||
|
echo "Aviso: Gateway no está activo; revisa openclaw gateway status" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Configuración aplicada.
|
||||||
|
|
||||||
|
IMPORTANTE: la sesión antigua puede seguir teniendo un historial demasiado
|
||||||
|
largo. En la web crea una sesión nueva y prueba:
|
||||||
|
|
||||||
|
Responde exactamente: SESION NUEVA OK
|
||||||
|
|
||||||
|
No continúes usando la sesión que mostraba estimatedInput=49724 y output=1.
|
||||||
|
|
||||||
|
Comprobación manual:
|
||||||
|
openclaw gateway status
|
||||||
|
openclaw doctor
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
readonly INSTALLER_URL="https://openclaw.ai/install.sh"
|
||||||
|
readonly SCRIPT_NAME="$(basename "$0")"
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso: $SCRIPT_NAME [--onboard]
|
||||||
|
|
||||||
|
Instala OpenClaw usando el instalador oficial.
|
||||||
|
|
||||||
|
Opciones:
|
||||||
|
--onboard Ejecuta el asistente inicial después de instalar.
|
||||||
|
-h, --help Muestra esta ayuda.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
onboard=false
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--onboard)
|
||||||
|
onboard=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Error: opción desconocida: %s\n\n' "$1" >&2
|
||||||
|
show_help >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$(uname -s)" != "Linux" ]]; then
|
||||||
|
printf 'Error: este script está pensado para Linux/WSL2.\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v curl >/dev/null 2>&1; then
|
||||||
|
printf 'Error: curl es obligatorio. Instálalo con: sudo apt-get update && sudo apt-get install -y curl\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
installer_file="$(mktemp)"
|
||||||
|
cleanup() {
|
||||||
|
rm -f "$installer_file"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
printf 'Descargando el instalador oficial de OpenClaw...\n'
|
||||||
|
curl --fail --silent --show-error --location \
|
||||||
|
--proto '=https' --tlsv1.2 \
|
||||||
|
"$INSTALLER_URL" -o "$installer_file"
|
||||||
|
|
||||||
|
if [[ "$onboard" == true ]]; then
|
||||||
|
bash "$installer_file"
|
||||||
|
else
|
||||||
|
bash "$installer_file" --no-onboard
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\nOpenClaw se ha instalado. Comprueba la instalación con:\n'
|
||||||
|
printf ' openclaw --version\n'
|
||||||
|
printf ' openclaw doctor\n'
|
||||||
|
|
||||||
|
if [[ "$onboard" == false ]]; then
|
||||||
|
printf '\nPara configurar OpenClaw más adelante, ejecuta:\n'
|
||||||
|
printf ' openclaw onboard --install-daemon\n'
|
||||||
|
fi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
model_list:
|
||||||
|
- model_name: qwen2.5-coder:14b
|
||||||
|
litellm_params:
|
||||||
|
model: ollama_chat/qwen2.5-coder:14b
|
||||||
|
api_base: http://127.0.0.1:11434
|
||||||
|
max_tokens: 4096
|
||||||
|
|
||||||
|
litellm_settings:
|
||||||
|
drop_params: true
|
||||||
|
json_to_tool_call: true
|
||||||
Executable
+164
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
readonly SCRIPT_NAME="$(basename "$0")"
|
||||||
|
|
||||||
|
archive_path=""
|
||||||
|
confirmed=false
|
||||||
|
install_packages=false
|
||||||
|
start_services=false
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso: sudo $SCRIPT_NAME ARCHIVO.tar.gz [opciones]
|
||||||
|
|
||||||
|
Restaura configuraciones de un respaldo creado por backup_ubuntu_config.sh.
|
||||||
|
Por seguridad, sin --yes solo muestra lo que se restauraría.
|
||||||
|
|
||||||
|
Opciones:
|
||||||
|
--yes Ejecuta la restauración.
|
||||||
|
--install-packages Instala los paquetes marcados como manuales en el respaldo.
|
||||||
|
--start-services Habilita y arranca los servicios que estaban habilitados.
|
||||||
|
-h, --help Muestra esta ayuda.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--yes)
|
||||||
|
confirmed=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--install-packages)
|
||||||
|
install_packages=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--start-services)
|
||||||
|
start_services=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo "Opción desconocida: $1" >&2
|
||||||
|
show_help >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [[ -n "$archive_path" ]]; then
|
||||||
|
echo "Solo se admite un archivo de respaldo." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
archive_path="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$archive_path" ]]; then
|
||||||
|
echo "Debes indicar un archivo .tar.gz." >&2
|
||||||
|
show_help >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${EUID}" -ne 0 ]]; then
|
||||||
|
echo "Ejecuta este script con sudo o como root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -f "$archive_path" ]] || { echo "No existe: $archive_path" >&2; exit 1; }
|
||||||
|
command -v tar >/dev/null || { echo "tar es obligatorio." >&2; exit 1; }
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$work_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
tar -xzf "$archive_path" -C "$work_dir" --no-same-owner
|
||||||
|
|
||||||
|
if [[ ! -d "$work_dir/files" || ! -f "$work_dir/metadata/backup-info.txt" ]]; then
|
||||||
|
echo "El archivo no parece un respaldo válido de backup_ubuntu_config.sh." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'Respaldo: %s\n' "$archive_path"
|
||||||
|
cat "$work_dir/metadata/backup-info.txt"
|
||||||
|
printf '\nArchivos de configuración que se restaurarían:\n'
|
||||||
|
find "$work_dir/files" -mindepth 1 -maxdepth 8 -printf '%P\n' | sort | sed -n '1,200p'
|
||||||
|
|
||||||
|
if [[ "$confirmed" != true ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Vista previa solamente. Para aplicar estos cambios:
|
||||||
|
sudo $SCRIPT_NAME "$archive_path" --yes
|
||||||
|
|
||||||
|
Opciones adicionales:
|
||||||
|
--install-packages Reinstala paquetes APT manuales.
|
||||||
|
--start-services Habilita y arranca los servicios respaldados.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$install_packages" == true && -s "$work_dir/packages/apt-manual.txt" ]]; then
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
xargs -r apt-get install -y --no-install-recommends < "$work_dir/packages/apt-manual.txt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$work_dir/files/etc" ]]; then
|
||||||
|
cp -a "$work_dir/files/etc/." /etc/
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$work_dir/files/home" ]]; then
|
||||||
|
cp -a "$work_dir/files/home/." /home/
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$work_dir/files/root" ]]; then
|
||||||
|
cp -a "$work_dir/files/root/." /root/
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "$work_dir/metadata/users.tsv" ]]; then
|
||||||
|
while IFS=$'\t' read -r username _ _ home; do
|
||||||
|
[[ -n "$username" && -d "$home" ]] || continue
|
||||||
|
if id "$username" >/dev/null 2>&1; then
|
||||||
|
chown -R "$username:$(id -gn "$username")" "$home" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
echo "Aviso: no existe el usuario $username; revisa $home manualmente." >&2
|
||||||
|
fi
|
||||||
|
done < "$work_dir/metadata/users.tsv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
if [[ "$start_services" == true && -s "$work_dir/services/enabled-service-names.txt" ]]; then
|
||||||
|
while IFS= read -r service; do
|
||||||
|
[[ -n "$service" ]] || continue
|
||||||
|
systemctl enable "$service" 2>/dev/null || echo "Aviso: no se pudo habilitar $service" >&2
|
||||||
|
done < "$work_dir/services/enabled-service-names.txt"
|
||||||
|
|
||||||
|
systemctl restart ollama 2>/dev/null || true
|
||||||
|
systemctl restart litellm 2>/dev/null || true
|
||||||
|
systemctl restart openclaw-gateway 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ufw >/dev/null 2>&1; then
|
||||||
|
ufw --force reload 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
|
||||||
|
Restauración completada.
|
||||||
|
|
||||||
|
Revisa antes de exponer el servidor:
|
||||||
|
systemctl --failed
|
||||||
|
systemctl status ollama litellm
|
||||||
|
ufw status verbose
|
||||||
|
nvidia-smi
|
||||||
|
|
||||||
|
Los modelos Ollama y el entorno virtual de LiteLLM no forman parte del respaldo.
|
||||||
|
Debes reinstalarlos o descargarlos por separado.
|
||||||
|
EOF
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SYSTEM_PROMPT = (
|
||||||
|
"You are a helpful assistant. "
|
||||||
|
"When outputting Windows file paths in JSON arguments, you must strictly escape all backslashes "
|
||||||
|
"(for example: C:\\\\Users\\\\name\\\\file.txt). "
|
||||||
|
"If you need to read a file, emit a tool call with the path field."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path: str) -> str:
|
||||||
|
"""Read a file from disk and return its contents."""
|
||||||
|
file_path = Path(path)
|
||||||
|
try:
|
||||||
|
return file_path.read_text(encoding="utf-8")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return f"ERROR: File not found: {path}"
|
||||||
|
except Exception as exc: # pragma: no cover - demo only
|
||||||
|
return f"ERROR: {type(exc).__name__}: {exc}"
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_tool_arguments(raw_arguments: str):
|
||||||
|
"""Repair malformed JSON emitted by the model when Windows paths are not escaped."""
|
||||||
|
try:
|
||||||
|
return json.loads(raw_arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
repaired = raw_arguments.replace("\\", "\\\\")
|
||||||
|
return json.loads(repaired)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_tool_call(response: dict) -> dict:
|
||||||
|
"""Detect a tool call in the LLM response and execute it locally."""
|
||||||
|
tool_calls = response.get("tool_calls") or response.get("function_call")
|
||||||
|
|
||||||
|
if tool_calls is None:
|
||||||
|
return {"status": "final_response", "content": response}
|
||||||
|
|
||||||
|
if isinstance(tool_calls, dict):
|
||||||
|
tool_calls = [tool_calls]
|
||||||
|
|
||||||
|
for tool_call in tool_calls:
|
||||||
|
function_data = tool_call.get("function", tool_call)
|
||||||
|
name = function_data.get("name")
|
||||||
|
arguments = function_data.get("arguments", {})
|
||||||
|
|
||||||
|
if isinstance(arguments, str):
|
||||||
|
try:
|
||||||
|
arguments = json.loads(arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
try:
|
||||||
|
arguments = sanitize_tool_arguments(arguments)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {
|
||||||
|
"status": "invalid_arguments",
|
||||||
|
"raw_arguments": arguments,
|
||||||
|
"message": "The model emitted malformed JSON. Ensure backslashes are escaped.",
|
||||||
|
}
|
||||||
|
|
||||||
|
if name == "read_file":
|
||||||
|
file_path = arguments.get("path")
|
||||||
|
content = read_file(file_path)
|
||||||
|
return {
|
||||||
|
"status": "tool_result",
|
||||||
|
"tool_call_id": tool_call.get("id"),
|
||||||
|
"content": content,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"status": "unsupported_tool_call", "raw": response}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# A valid OpenAI-style tool call with a Windows path escaped correctly.
|
||||||
|
valid_response = {
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_read_001",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "read_file",
|
||||||
|
"arguments": '{"path": "c:\\Workspace\\llm-server-setup\\README.md"}',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# This reproduces the common bug: malformed Windows path in JSON.
|
||||||
|
broken_response = {
|
||||||
|
"tool_calls": [
|
||||||
|
{
|
||||||
|
"id": "call_read_002",
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "read_file",
|
||||||
|
"arguments": '{"path": "c:\Workspace\llm-server-setup\README.md"}',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
result = handle_tool_call(valid_response)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
print("\n--- malformed-path fallback sample ---")
|
||||||
|
fallback = handle_tool_call(broken_response)
|
||||||
|
print(json.dumps(fallback, ensure_ascii=False, indent=2))
|
||||||
Executable
+310
@@ -0,0 +1,310 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
readonly SCRIPT_NAME="$(basename "$0")"
|
||||||
|
readonly DEFAULT_OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434}"
|
||||||
|
readonly DEFAULT_LITELLM_URL="${LITELLM_URL:-http://127.0.0.1:8000}"
|
||||||
|
readonly MODEL_NAME="${MODEL_NAME:-qwen2.5-coder:14b}"
|
||||||
|
readonly OPENCLAW_CONFIG="${OPENCLAW_CONFIG:-${HOME}/.openclaw/openclaw.json}"
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
warnings=0
|
||||||
|
report_file=""
|
||||||
|
|
||||||
|
show_help() {
|
||||||
|
cat <<EOF
|
||||||
|
Uso: $SCRIPT_NAME [opciones]
|
||||||
|
|
||||||
|
Ejecuta un diagnóstico de solo lectura del servidor Ubuntu y de los servicios
|
||||||
|
Ollama, LiteLLM y OpenClaw. No modifica archivos ni reinicia servicios.
|
||||||
|
|
||||||
|
Opciones:
|
||||||
|
--report FILE Guarda la salida en FILE además de mostrarla en pantalla.
|
||||||
|
-h, --help Muestra esta ayuda.
|
||||||
|
|
||||||
|
Variables opcionales:
|
||||||
|
OLLAMA_URL URL de Ollama (por defecto: $DEFAULT_OLLAMA_URL)
|
||||||
|
LITELLM_URL URL de LiteLLM (por defecto: $DEFAULT_LITELLM_URL)
|
||||||
|
MODEL_NAME Modelo esperado (por defecto: $MODEL_NAME)
|
||||||
|
OPENCLAW_CONFIG Ruta de configuración de OpenClaw.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--report)
|
||||||
|
[[ $# -ge 2 ]] || { echo "Falta FILE para --report" >&2; exit 2; }
|
||||||
|
report_file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Opción desconocida: $1" >&2
|
||||||
|
show_help >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -n "$report_file" ]]; then
|
||||||
|
mkdir -p "$(dirname "$report_file")"
|
||||||
|
: > "$report_file" || { echo "No se puede escribir $report_file" >&2; exit 1; }
|
||||||
|
exec > >(tee -a "$report_file") 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf '[ OK ] %s\n' "$1"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '[FAIL] %s\n' "$1"
|
||||||
|
failed=$((failed + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
printf '[WARN] %s\n' "$1"
|
||||||
|
warnings=$((warnings + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf '\n=== %s ===\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
has_command() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_command() {
|
||||||
|
local command_name="$1"
|
||||||
|
local description="$2"
|
||||||
|
|
||||||
|
if has_command "$command_name"; then
|
||||||
|
pass "$description: $command_name"
|
||||||
|
else
|
||||||
|
fail "$description: falta $command_name"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_service() {
|
||||||
|
local service="$1"
|
||||||
|
|
||||||
|
if ! has_command systemctl; then
|
||||||
|
warn "No se puede consultar $service: falta systemctl"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "$service"; then
|
||||||
|
pass "Servicio activo: $service"
|
||||||
|
elif systemctl is-enabled --quiet "$service" 2>/dev/null; then
|
||||||
|
warn "Servicio habilitado pero no activo: $service"
|
||||||
|
else
|
||||||
|
fail "Servicio no activo: $service"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_file() {
|
||||||
|
local file_path="$1"
|
||||||
|
local description="$2"
|
||||||
|
|
||||||
|
if [[ -e "$file_path" || -L "$file_path" ]]; then
|
||||||
|
pass "$description: $file_path"
|
||||||
|
else
|
||||||
|
warn "$description no encontrado: $file_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
http_check() {
|
||||||
|
local url="$1"
|
||||||
|
local description="$2"
|
||||||
|
local status
|
||||||
|
|
||||||
|
if ! has_command curl; then
|
||||||
|
fail "$description: falta curl"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
status="$(curl --silent --show-error --max-time 10 --output /dev/null --write-out '%{http_code}' "$url" 2>/dev/null)"
|
||||||
|
if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then
|
||||||
|
pass "$description: HTTP $status ($url)"
|
||||||
|
else
|
||||||
|
fail "$description: HTTP ${status:-sin respuesta} ($url)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
section "Sistema"
|
||||||
|
printf 'Fecha: %s\n' "$(date --iso-8601=seconds 2>/dev/null || date)"
|
||||||
|
printf 'Host: %s\n' "$(hostname 2>/dev/null || echo desconocido)"
|
||||||
|
printf 'Usuario: %s\n' "$(id -un 2>/dev/null || echo desconocido)"
|
||||||
|
|
||||||
|
if [[ -r /etc/os-release ]]; then
|
||||||
|
. /etc/os-release
|
||||||
|
printf 'Sistema: %s %s\n' "${NAME:-desconocido}" "${VERSION_ID:-desconocida}"
|
||||||
|
if [[ "${ID:-}" == ubuntu ]]; then
|
||||||
|
pass "Sistema operativo Ubuntu"
|
||||||
|
else
|
||||||
|
warn "El sistema operativo detectado no es Ubuntu: ${ID:-desconocido}"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "No se puede leer /etc/os-release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_command curl "Herramienta HTTP"
|
||||||
|
check_command systemctl "Gestor de servicios"
|
||||||
|
check_command ss "Inspección de puertos"
|
||||||
|
|
||||||
|
if has_command free; then
|
||||||
|
free -h
|
||||||
|
fi
|
||||||
|
if has_command df; then
|
||||||
|
df -hT / 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
if has_command swapon; then
|
||||||
|
if swapon --show --noheadings | grep -q .; then
|
||||||
|
pass "Swap activa"
|
||||||
|
else
|
||||||
|
warn "No hay swap activa"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Hardware"
|
||||||
|
if has_command nvidia-smi; then
|
||||||
|
if nvidia-smi --query-gpu=name,driver_version,memory.total,memory.used --format=csv,noheader 2>/dev/null; then
|
||||||
|
pass "NVIDIA disponible"
|
||||||
|
else
|
||||||
|
fail "nvidia-smi no puede consultar la GPU"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "nvidia-smi no está instalado o la GPU NVIDIA no está disponible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -r /proc/sys/vm/swappiness ]]; then
|
||||||
|
printf 'vm.swappiness: %s\n' "$(cat /proc/sys/vm/swappiness)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Archivos de configuración"
|
||||||
|
check_file /etc/systemd/system/ollama.service.d/override.conf "Override de Ollama"
|
||||||
|
check_file /etc/systemd/system/litellm.service "Servicio de LiteLLM"
|
||||||
|
check_file /etc/systemd/system/nvidia-power-limit.service "Servicio de límite NVIDIA"
|
||||||
|
check_file /etc/ufw "Configuración UFW"
|
||||||
|
check_file /etc/netplan "Configuración Netplan"
|
||||||
|
check_file "$OPENCLAW_CONFIG" "Configuración OpenClaw"
|
||||||
|
|
||||||
|
if [[ -f "$OPENCLAW_CONFIG" ]]; then
|
||||||
|
if grep -Eq '"(apiKey|token|password|secret)"[[:space:]]*:' "$OPENCLAW_CONFIG"; then
|
||||||
|
warn "La configuración OpenClaw contiene campos sensibles; no se mostrarán"
|
||||||
|
fi
|
||||||
|
if grep -q 'litellm/qwen2.5-coder:14b' "$OPENCLAW_CONFIG"; then
|
||||||
|
pass "OpenClaw apunta al modelo LiteLLM esperado"
|
||||||
|
else
|
||||||
|
warn "No se encontró el modelo LiteLLM esperado en OpenClaw"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for config_path in \
|
||||||
|
/etc/litellm_config.yaml \
|
||||||
|
/root/litellm_config.yaml \
|
||||||
|
"$HOME/litellm_config.yaml"
|
||||||
|
do
|
||||||
|
if [[ -f "$config_path" ]]; then
|
||||||
|
printf 'LiteLLM config detectada: %s\n' "$config_path"
|
||||||
|
if grep -q "$MODEL_NAME" "$config_path"; then
|
||||||
|
pass "LiteLLM contiene el modelo $MODEL_NAME"
|
||||||
|
else
|
||||||
|
warn "LiteLLM no contiene el modelo esperado: $MODEL_NAME"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Servicios"
|
||||||
|
check_service ollama
|
||||||
|
check_service litellm
|
||||||
|
if has_command openclaw; then
|
||||||
|
printf 'El Gateway de OpenClaw se validará con su propio comando de estado.\n'
|
||||||
|
else
|
||||||
|
warn "OpenClaw no está instalado en PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Puertos"
|
||||||
|
if has_command ss; then
|
||||||
|
printf 'Puertos escuchando relevantes:\n'
|
||||||
|
ss -ltnp 2>/dev/null | awk 'NR == 1 || /:8000|:11434|:18789/' || true
|
||||||
|
if ss -ltn 2>/dev/null | grep -Eq ':11434[[:space:]]'; then
|
||||||
|
pass "Ollama escucha en el puerto 11434"
|
||||||
|
else
|
||||||
|
fail "No se detecta Ollama escuchando en el puerto 11434"
|
||||||
|
fi
|
||||||
|
if ss -ltn 2>/dev/null | grep -Eq ':8000[[:space:]]'; then
|
||||||
|
pass "LiteLLM escucha en el puerto 8000"
|
||||||
|
else
|
||||||
|
fail "No se detecta LiteLLM escuchando en el puerto 8000"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Ollama"
|
||||||
|
http_check "$DEFAULT_OLLAMA_URL/api/tags" "API de Ollama"
|
||||||
|
if has_command ollama; then
|
||||||
|
if ollama list 2>/dev/null | awk 'NR > 1 {print $1}' | grep -Fxq "$MODEL_NAME"; then
|
||||||
|
pass "Modelo Ollama instalado: $MODEL_NAME"
|
||||||
|
else
|
||||||
|
fail "Modelo Ollama no encontrado: $MODEL_NAME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Comando ollama no disponible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "LiteLLM"
|
||||||
|
http_check "$DEFAULT_LITELLM_URL/health/liveliness" "Salud de LiteLLM"
|
||||||
|
if has_command curl; then
|
||||||
|
models_response="$(curl --silent --show-error --max-time 10 "$DEFAULT_LITELLM_URL/v1/models" 2>/dev/null || true)"
|
||||||
|
if [[ "$models_response" == *"$MODEL_NAME"* ]]; then
|
||||||
|
pass "LiteLLM publica el modelo $MODEL_NAME"
|
||||||
|
else
|
||||||
|
fail "LiteLLM no publica el modelo $MODEL_NAME en /v1/models"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "OpenClaw"
|
||||||
|
if has_command openclaw; then
|
||||||
|
openclaw --version 2>/dev/null || true
|
||||||
|
if openclaw doctor 2>&1; then
|
||||||
|
pass "openclaw doctor terminó correctamente"
|
||||||
|
else
|
||||||
|
fail "openclaw doctor reportó errores"
|
||||||
|
fi
|
||||||
|
if openclaw gateway status 2>&1; then
|
||||||
|
pass "Estado del Gateway consultado correctamente"
|
||||||
|
else
|
||||||
|
warn "No se pudo consultar correctamente el Gateway"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "No se puede probar OpenClaw porque no está instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Firewall"
|
||||||
|
if has_command ufw; then
|
||||||
|
ufw status verbose 2>/dev/null || true
|
||||||
|
if ufw status 2>/dev/null | grep -qi active; then
|
||||||
|
pass "UFW está activo"
|
||||||
|
else
|
||||||
|
warn "UFW no está activo"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "UFW no está instalado"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Resumen"
|
||||||
|
printf 'Correctos: %d | Fallos: %d | Advertencias: %d\n' "$passed" "$failed" "$warnings"
|
||||||
|
if [[ -n "$report_file" ]]; then
|
||||||
|
printf 'Reporte: %s\n' "$report_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$failed" -gt 0 ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user