# Unified AI Proxy Un solo server Bun/Hono con **3 backends gratis** (sin API keys, sin pagos) detrás de un gateway **OpenAI-compatible** + un hub web con chat y generación de imágenes. > **No se requiere ninguna API key.** Todos los endpoints de chat son OpenAI-compatible > (`/v1/chat/completions`, `/v1/models`). Si definiste `API_KEY`, los endpoints piden > `Authorization: Bearer `; sin la variable, todo está abierto. --- ## 🔗 URLs reales del server (todas, con tu IP de ejemplo) Con tu server en `http://13.140.128.216:25630`: | URL | Qué es | Para qué | |---|---|---| | `http://13.140.128.216:25630/` | **Hub GUI** (chat + 🖼️ imágenes) | La interfaz principal. Elige modelo en el dropdown. | | `http://13.140.128.216:25630/docs` | **Estos docs** (texto plano) | Leer esta guía desde el server | | `http://13.140.128.216:25630/healthz` | Salud + **versión del deploy** | Si `version` ≠ la esperada, los archivos del server no llegaron | | `http://13.140.128.216:25630/v1/chat/completions` | Gateway OpenAI (POST) | Para apps externas (SillyTavern, scripts, etc.) | | `http://13.140.128.216:25630/v1/models` | Lista de modelos (GET) | Descubrimiento para clientes OpenAI | | `http://13.140.128.216:25630/perchance/` | **GUI dedicada de perchance** (pestañas 🖼️ Imagen / 💬 Texto con todos los controles) | La experiencia completa de perchance | | `http://13.140.128.216:25630/zai/gui` | **GUI dedicada de z.ai** | Chat solo con z.ai | | `http://13.140.128.216:25630/cn/gui` | **GUI dedicada de chatglm.cn** | Chat solo con CN | | `http://13.140.128.216:25630/perchance/mint?target=image` | Forzar mint de clave (POST) | Debug | | `http://13.140.128.216:25630/zai/admin/recycle` | Rotar identidad guest de z.ai (POST) | Debug | | `http://13.140.128.216:25630/cn/admin/recycle` | Rotar identidad guest de CN (POST) | Debug | **Nota:** `/perchance` (sin slash final) también sirve la GUI dedicada. Las GUIs dedicadas de z.ai y CN son OPCIONALES — el hub en `/` ya puede chatear con todos los modelos. --- ## Modelos (gateway `POST /v1/chat/completions`) | model | backend | notas | |---|---|---| | `cn-glm-5.3` | chatglm.cn | GLM-5.3 completo, guest curl, sin captcha | | `cn-glm-5.3-flash` | chatglm.cn | versión flash | | `cn-glm-5.3-thinking` | chatglm.cn | deep thinking (reasoning_content separado) | | `cn-glm-5.3-thinking-search` | chatglm.cn | thinking + búsqueda web | | `zai-glm-5.3-flash` | chat.z.ai | con pool de cvps precocinados (rápido) | | `zai-glm-5.3-flash-thinking` | chat.z.ai | con thinking | | `zai-glm-5.3-flash-search` | chat.z.ai | con búsqueda web | | `perchance` | perchance.org | texto (stream OpenAI-style desde v4) | | `perchance-img` (solo GUI) | perchance.org | imágenes SDXL con estilo/negative/seed/guidance | Cualquier otro nombre de modelo cae al backend `cn` por defecto (prefijo `zai-`/`cn-` enruta). ### Ejemplo ```bash curl http://TU-SERVER:25630/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"cn-glm-5.3","stream":true,"messages":[{"role":"user","content":"hola"}]}' ``` Parámetros extra aceptados: `reasoning_effort` (low|high|max), `web_search: true`, `advanced_search: true`, `stream` (default true). ## 📏 Contexto largo (1M) — cómo funciona de verdad **El proxy NO trunca NADA.** Todo el historial que envía el cliente llega completo al backend upstream (`zai-client.ts` y `cn-client.ts` reenvían el array `messages` íntegro). Si tu cliente manda 500k caracteres, esos 500k salen hacia el backend tal cual. Lo que hay que entender: el "1M de contexto" es la capacidad del MODELO GLM-5.3 vía la API oficial (de pago). Aquí usamos los backends web (chatglm.cn guest y chat.z.ai), que son gratis pero aplican sus propios límites de entrada por debajo del 1M — si mandas algo que su tier web no traga, el backend lo corta o lo degrada POR SU CUENTA, y eso no es controlable desde fuera sin la API oficial. En la práctica: - Contextos de decenas de miles de tokens (uso normal de agente/chat): pasan y funcionan sin problema. - Contextos de cientos de miles: el request sale completo del proxy, pero el backend web puede rechazarlo o ignorar parte. Ahí verás respuestas que "no recuerdan el inicio" — es el backend, no el proxy. - Si algún día necesitas el 1M real garantizado, la única vía es la API oficial de GLM con API key (de pago). ## 🔧 Tool calling (function calling) — para enchufar agentes **Soportado desde v5.1 en los modelos de chat** (cn, zai), estilo OpenAI. Los backends web no soportan tools nativamente, así que el gateway las implementa por prompt: inyecta el protocolo, parsea la respuesta y emite formato OpenAI estándar. ```jsonc // POST /v1/chat/completions { "model": "cn-glm-5.3", "messages": [{ "role": "user", "content": "¿Qué tiempo hace en Madrid?" }], "tools": [{ "type": "function", "function": { "name": "get_weather", "description": "Obtiene el clima actual de una ciudad", "parameters": { "type": "object", "properties": { "city": { "type": "string" } }, "required": ["city"] } } }] } // → respuesta con "finish_reason":"tool_calls" y message.tool_calls[0].function.arguments // → devuelves {"role":"tool","tool_call_id":"...","content":"18°C, soleado"} y continúa ``` Detalles: `tool_choice:"none"` desactiva; los mensajes `role:"tool"` y `assistant.tool_calls` del historial se convierten automáticamente; con `stream:true` la respuesta se emite al completarse (el parseo requiere el texto completo). ### Enchufar MiMoCode (u otro agente) al proxy En el config del cliente/provider OpenAI-compatible: - **Base URL**: `http://13.140.128.216:25630/v1` - **API key**: cualquiera (no se valida salvo que definas `API_KEY` en el server) - **Model**: `cn-glm-5.3` (el más capaz), `cn-glm-5.3-thinking` (razona más), `zai-glm-5.3-flash` (rápido) ## Endpoints | Ruta | Qué es | |---|---| | `/` | Hub GUI: chat de todos los modelos + 🖼️ imágenes detalladas | | `/v1/chat/completions` | Gateway OpenAI (todos los modelos de la tabla) | | `/v1/models` | Catálogo | | `/healthz` | Salud de los 3 backends | | `/perchance/*` | Backend perchance completo (imágenes + texto, GUI dedicada en `/perchance/`) | | `/zai/*`, `/cn/*` | Backends individuales (OpenAI-compatible cada uno) | | `/perchance/mint?target=image\|text` | Forzar mint de clave | | `/zai/admin/recycle`, `/cn/admin/recycle` | Rotar identidad guest | ## GUI del hub (v4) - **Chat**: elige cualquier modelo en el dropdown. Campo "instrucciones" = system prompt (visible con perchance texto; también se envía a cn/zai como system). - **🖼️ perchance (imagen)**: prompt + negative prompt + estilo (lista real de `/perchance/styles`) + forma (vertical/cuadrada/horizontal) + seed + guidance scale. La imagen se muestra inline y se sirve por `/perchance/images/:id`. - El footer muestra la **versión del deploy** (`unified v4`): si no dice v4, el `gui.html` nuevo no llegó al server. --- ## Deploy (FeatherPanel, yolks bun_latest) 1. Subir TODOS los archivos de la carpeta (o al menos: `index.js`, `index.ts`, `package.json`, `gui.html`, `glmcn/`, `zai/`, `perchance/`). `bun install` corre solo al arrancar (playwright-core se auto-instala si falta). 2. Restart. El primer arranque descarga CloakBrowser (~206MB, solo una vez; queda en `/home/container/.cloakbrowser` y sobrevive a restarts). 3. Verificación visual en ~2 min: - Footer del hub dice **`unified v4`**. - Header muestra `perchance:✓ zai:✓ cn:✓` (desde `/healthz`). ### Checklist de verificación por el log del panel (ahora con timestamps `[HH:MM:SS.mmm]`) | Línea esperada | Significado | |---|---| | `[unified] escuchando en http://0.0.0.0:25630` | server arriba | | `[bootstrap] smoke OK with LD_LIBRARY_PATH` | libs Linux listas para CloakBrowser | | `[keysvc] mint OK: (valid: true)` | clave de IMAGEN perchance lista | | `[textkeysvc] mint OK: (valid: true)` | clave de TEXTO perchance lista | | `[mint] key OK en 12.3s (target=image)` | duración real del mint | | `[zai] warmup OK — browser caliente` | pool de z.ai se está llenando (~150s tras boot) | | `[zai-minter] prefill OK en 18.1s (pool=2/3)` | cvps precocinados disponibles | | `[zai-minter] pool hit — cvp precocinado` | request de z.ai servido sin mint (~3-5s total) | | `[cn] guest OK user=...` | sesión guest de chatglm.cn lista | ### Tiempos esperados (container FeatherPanel) - **cn-glm-5.3**: primer token ~2-5s. - **zai-glm-5.3-flash**: con pool hit ~3-6s. Tras >90s sin uso, el primer request paga un mint inline (~15-25s en el container) y el pool se rellena solo en background. - **perchance imagen**: ~6-20s por imagen (no cuenta mint si la clave está vigente). - **perchance texto**: ~3-10s. - Los mints de perchance ocurren solos cuando la clave vence (watchdog cada 60s) o cuando una generación es rechazada; no hace falta hacer nada. --- ## Arquitectura (lo mínimo para no perderse) ``` index.js → timestamps globales + importa index.ts index.ts → Hono root: monta /perchance /zai /cn, gateway /v1/*, hub GUI, warmup de z.ai (150s), auto-instalador de deps, blindaje de crashes glmcn/ → chatglm.cn: guest curl sin captcha (sesión 20h, reciclaje automático) zai/ → chat.z.ai: CloakBrowser + truco del abort (cvp de un solo uso), POOL de cvps precocinados (máx 3, vencen a los 90s), recycle en fallos perchance/→ imágenes (claves userKey via Turnstile en browser stealth, watchdogs) + texto (mismo mecanismo, origen text-generation) ``` Detalles finos que valen la pena saber: - **Pool de cvps (z.ai)**: el "truco del abort" mintea un cvp SIN consumirlo → se precocinan hasta 3 en background y cada request consume uno al instante. Tras >90s de idle el pool vence (los cvp caducan); el siguiente request mintea inline y el pool se re-llena solo. Un fallo de prefill NUNCA recicla la sesión (solo deja el pool vacío). - **Purga del perfil de mint (perchance)**: en cada mint se limpian SOLO los marcadores de lock del embed (`recentlyVerified-*`, `anotherEmbedIsVerifying*`, etc.). La clave vigente NO se borra: si sigue válida, el mint la confirma en ~4-7s sin resolver Turnstile. Nunca pre-definir `window.turnstile` (el api.js de Cloudflare tiene guard anti-doble-carga y dejaría el widget muerto). - **Idioma CN**: regla inline en el último mensaje del usuario (gibberish → inglés; nunca chino salvo input chino). - **Batching de stream**: todos los backends agrupan deltas ~120ms/48 chars para evitar el "1 carácter por línea". - **perchance texto**: la ruta OpenAI emite SSE estilo OpenAI (antes emitía `{"text"}` y el hub lo mostraba vacío). Soporta `startWith` y system messages. ## Variables de entorno (todas opcionales) | Var | Default | Qué hace | |---|---|---| | `PORT` | `SERVER_PORT` o 25630 | puerto | | `API_KEY` | (vacío = abierto) | exige `Authorization: Bearer` | | `ZAI_WARMUP` | `1` | precocina cvps al arrancar (`0` lo apaga) | | `ZAI_WARMUP_DELAY_MS` | `10000` | ms tras bootstrap para el primer mint de z.ai (antes 150s — v5.2 lo adelanta al mínimo; `0` = inmediato) | | `ZAI_FAST_MINT` | obsoleto | (el pool sustituyó al fast-path) | | `KEY_MINT_TIMEOUT_MS` / `TEXT_MINT_TIMEOUT_MS` | 150000 | budget del mint de perchance | | `KEY_WATCHDOG_INTERVAL_MS` | 60000 | chequeo de validez de claves | | `AUTO_KEY_REFRESH` | `1` | `0` desactiva los watchdogs de perchance | | `MAX_CONCURRENCY` / `MAX_QUEUE_LENGTH` | 1 / 10 | cola de generaciones perchance | | `PROXY_URL` | (vacío = directo) | proxy de salida para TODO el tráfico: los curl de perchance/z.ai/cn y el Chrome de z.ai. `http://user:pass@host:puerto` o `socks5://…` | | `ZAI_DEADLINE_MS` | `90000` | presupuesto máximo de un request a z.ai en el gateway; al agotarse, failover a cn | | `CN_DEADLINE_MS` | `180000` | idem para chatglm.cn | | `PERCHANCE_DEADLINE_MS` | `150000` | idem para perchance | | `BACKEND_FAIL_THRESHOLD` | `2` | fallos seguidos antes de abrir el breaker de un backend | | `BACKEND_OPEN_MS` | `120000` | ms con el breaker abierto antes de dejar pasar un request de prueba | | `ZAI_AUTH_BLOCK_MS` | `600000` | minutos que z.ai queda marcado como "muro de /auth" tras detectarlo | | `ZAI_PREFILL_TAB` | `0` | `1` = 2ª pestaña de Chrome para prefill en paralelo. **Apagada por defecto**: en hosts free el container mata el renderer ("Page crashed") | ## Failover y salud real (v5.9) Los 3 backends no son equivalentes en fiabilidad, así que el gateway no confía en ninguno a ciegas: - **Circuit breaker por backend.** Tras `BACKEND_FAIL_THRESHOLD` fallos seguidos, el backend deja de recibir tráfico durante `BACKEND_OPEN_MS`; luego pasa un request de prueba (half-open) y se recupera solo si va bien. - **Failover `zai → cn`.** Un `zai-*` con z.ai muerto se responde por `cn-glm-5.3` (incluso conservando los sufijos `-search`/`-thinking`). perchance no tiene reserva: es un generador de imágenes. Cuando hay failover, la respuesta lleva la cabecera `x-unified-failover: zai -> cn`. - **Deadline por request.** Ningún request se cuelga indefinidamente: cada intento tiene presupuesto y se responde con `502` si tampoco hay reserva. - **`/healthz` dice la verdad.** Antes devolvía `up: true` fijo. Ahora cada backend trae `status` (`up`/`flaky`/`down`), `fails`, `lastError` y `openMsLeft`, más `failover`, `deadlines` y si el proxy está activo. `GET /v1/models` incluye el `status` de cada modelo. El hub pinta `✓` / `~` / `✗` y el error en el tooltip. ### El muro de `/auth` (la causa de "zai no sirve") Si chat.z.ai redirige a `/auth`, el shell Svelte sigue pintando el textarea pero **no hay sesión guest**: el Enter no dispara nada. Antes eso costaba 30s de timeout, un `recycle()` (borrar perfil → relanzar Chrome → `goto` de 2 min → `Page crashed`) y el keep-warm lo repetía cada 70s — 22 minutos de CPU sin un solo captcha. Ahora el minter comprueba la URL tras la navegación y **falla en <1s** sin reciclar nada (reciclar no arregla un muro de auth: no es una sesión quemada, es que no hay sesión). Durante `ZAI_AUTH_BLOCK_MS` todos los caminos fallan al instante y el gateway responde por `cn`. Es casi siempre la **IP del host**: la vía es `PROXY_URL` (o cambiar de IP) y luego `POST /zai/admin/recycle` para reintentar. ## Troubleshooting | Síntoma | Mirar | Fix | |---|---|---| | Hub no muestra 🖼️ ni la versión nueva | footer | re-subir `gui.html` | | `/healthz` no dice la versión esperada | campo `version` | los archivos .ts no llegaron — re-subir TODO | | `/healthz` muestra `"FALTA"` en `files` | campo `files` | ese archivo no llegó — súbelo (compara `size` con tu copia local) | | `[cn] last_error ... 10062` en el log | — | **chatglm.cn en hora pico pone a los guests en cola** ("高峰期排队中"). Es del servicio gratuito, no del proxy: reintenta en 1-2 min o usa zai/perchance. El hub muestra el aviso en español. | | z.ai tarda 20-60s el primer mensaje tras un rato sin usar | `[zai-minter]` | normal: los cvps del pool caducan a los 90s; el mint corre una vez y el pool se re-llena solo. El hub muestra "preparando captcha…" mientras tanto. Mensajes en ráfaga salen al instante (pool hit). | | `perchance:✗` en el header | `[keysvc]`/`[mint]` en el log | si `mint failed ... backing off`, es rate-limit de Turnstile: esperar el backoff (se recupera solo) | | "(sin respuesta)" | log con timestamps | con v5 los errores se muestran como texto en el chat; si ves el mensaje vacío igualmente, pega el tramo del log | | Imagen da `invalid_key` | `[keysvc] minting...` | el mint está corriendo; reintentar en 1-2 min | | chrome muere `libnspr4.so` | `[bootstrap]` | ya resuelto con el rootfs + `LD_LIBRARY_PATH` heredado | | `zai:✗` y `lastError` dice "redirige a /auth" | `[zai-minter] MURO DE AUTH` | tu IP no saca sesión guest en chat.z.ai. Los requests `zai-*` ya se sirven por `cn` (failover). Para arreglar z.ai de verdad: pon `PROXY_URL` y luego `POST /zai/admin/recycle` | | `goto: Page crashed` / `Target crashed` | `[zai-minter] renderer muerto` | Chrome se queda sin RAM. Se queda a 1 pestaña y sin GPU por defecto; si persiste, sube la RAM del container | | z.ai se cae cada 2 min con `timeout` | `[gateway] zai falló` | el deadline cortó el request, pero el trabajo huérfano sigue en el browser (el deadline no cancela el curl a medio hacer). Con tráfico sostenible, sube `ZAI_DEADLINE_MS` | ### Tiempos de mint reales (medidos) | Dónde | z.ai (mint captcha) | perchance (confirmar clave) | perchance (Turnstile completo) | |---|---|---|---| | Tu PC local | 6-9s | 4-7s | ~11s | | Container FeatherPanel | **38-55s** | ~12s | ~23s | La diferencia es la **cuota de CPU del container**, no el hardware. Por eso todo lo pesado corre en background (watchdogs de perchance, pool de z.ai) y tu request solo espera un mint tras un rato de inactividad. ## Verificación rápida post-deploy (2 minutos) 1. `/healthz` → `version` es la esperada y NINGÚN archivo en `files` dice `"FALTA"`. 2. Hub footer = la versión nueva. 3. Chat `cn-glm-5.3` → responde en tu idioma. 4. Chat `zai-glm-5.3-flash` ×2 seguidos → el 2º y siguientes salen en ~3-6s (log: `pool hit`). 5. 🖼️ imagen con estilo → aparece inline. 6. Chat `perchance` (texto) → responde con stream normal.