diff --git a/src/components/docs/DocsTopBar.astro b/src/components/docs/DocsTopBar.astro
index 4368237..032c3e6 100644
--- a/src/components/docs/DocsTopBar.astro
+++ b/src/components/docs/DocsTopBar.astro
@@ -32,7 +32,7 @@ const t = {
reference: 'API Reference',
platform: 'Platform ↗',
site: 'nan.builders ↗',
- home: 'NaN, back to the docs',
+ home: 'NaN, go to nan.builders',
menu: 'Toggle menu',
language: 'Language',
},
@@ -42,7 +42,7 @@ const t = {
reference: 'Referencia API',
platform: 'Plataforma ↗',
site: 'nan.builders ↗',
- home: 'NaN, volver a los docs',
+ home: 'NaN, ir a nan.builders',
menu: 'Abrir o cerrar el menú',
language: 'Idioma',
},
@@ -57,7 +57,12 @@ const t = {
-
+ {/*
+ The wordmark goes to the site, not to the docs index. It is the brand
+ mark: clicking a logo is how you get out to the home page, and "Guides"
+ right next to it already covers going to the docs index.
+ */}
+ {t.docs}
diff --git a/src/components/docs/RateLimits.astro b/src/components/docs/RateLimits.astro
index f14963e..b0e9fa2 100644
--- a/src/components/docs/RateLimits.astro
+++ b/src/components/docs/RateLimits.astro
@@ -5,24 +5,32 @@ import {
getRateLimitsConfig,
windowedModelBody,
windowedModelHeadline,
+ rateLimitsLabels,
} from '../../lib/rateLimits';
const { perKey, tokensPerMinuteByModel, requestsPerMinuteByModel, windowedModels } =
getRateLimitsConfig(env);
+
+// This card is embedded from both the English and the Spanish guides, and MDX
+// content cannot pass props down from the layout, so the locale is read off the
+// route the page was rendered for.
+const lang = Astro.url.pathname.startsWith('/es/') ? 'es' : 'en';
+
+const T = rateLimitsLabels(lang);
---
{
diff --git a/src/content.config.ts b/src/content.config.ts
index 4eef4c1..2b09dd6 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -2,24 +2,41 @@ import { defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
import { z } from 'astro/zod';
+const docsSchema = z.object({
+ title: z.string(),
+ description: z.string(),
+ order: z.number().int().min(0),
+ /*
+ * The heading the page appears under in the docs navigation.
+ *
+ * helmcode's nav writes the groups by hand in the layout; here they live in
+ * the data so adding a guide stays a matter of creating a file rather than
+ * also editing the layout, which is how these things drift apart. The order
+ * between groups comes from the lowest `order` in each, so there is no
+ * second list to maintain either.
+ */
+ group: z.string().default('Guides'),
+ locale: z.string().default('es'),
+});
+
const docs = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs' }),
- schema: z.object({
- title: z.string(),
- description: z.string(),
- order: z.number().int().min(0),
- /*
- * The heading the page appears under in the docs navigation.
- *
- * helmcode's nav writes the groups by hand in the layout; here they live in
- * the data so adding a guide stays a matter of creating a file rather than
- * also editing the layout, which is how these things drift apart. The order
- * between groups comes from the lowest `order` in each, so there is no
- * second list to maintain either.
- */
- group: z.string().default('Guides'),
- locale: z.string().default('es'),
- }),
+ schema: docsSchema,
+});
+
+/*
+ * The Spanish guides live in their own directory rather than under a locale
+ * subfolder of `docs`.
+ *
+ * A `docs/en/…` + `docs/es/…` layout would turn every entry id into `en/intro`
+ * and the like, and SAFE_SLUG in src/lib/docsApi.ts rejects slashes: the
+ * manifest route throws on the first one, so /api/docs/manifest.json would
+ * answer 500 and the Discord bot would lose everything. Keeping English where
+ * it is leaves those slugs untouched.
+ */
+const docsEs = defineCollection({
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/docs-es' }),
+ schema: docsSchema,
});
-export const collections = { docs };
+export const collections = { docs, docsEs };
diff --git a/src/content/docs-es/agents.md b/src/content/docs-es/agents.md
new file mode 100644
index 0000000..4c301c4
--- /dev/null
+++ b/src/content/docs-es/agents.md
@@ -0,0 +1,116 @@
+---
+title: Agentes
+description: "Despliega agentes de IA en una microVM aislada con QEMU: Hermes, terminal web, subida de ficheros y observabilidad."
+order: 5
+group: Guías
+---
+
+# Agentes.
+
+NaN Cloud te permite desplegar agentes de IA en tu propia **microVM**: una máquina virtual ligera con QEMU y KVM, con su propio kernel, su propio sistema de ficheros y acceso root completo. Aislada del host y del resto de miembros. El primer tipo de agente disponible es **Hermes**.
+
+> **¿Usas un agente que alojas tú?**
+> Si ejecutas tu propio agente compatible con MCP en otro sitio, puedes enchufarle nuestras herramientas (como la búsqueda web) directamente, con la misma API key, a través de nuestro [servidor MCP](/es/docs/api#tag/mcp) remoto.
+
+## Arquitectura
+
+Cada agente corre dentro de su propia microVM de QEMU. En vez de compartir el kernel del host (como haría un contenedor normal), arranca con su propio kernel de Linux. La VM monta un disco ext4 de 20 GiB sobre un volumen persistente en modo bloque. Todo lo que hagas dentro (`apt install`, `pip install`, cambios en `/etc`, ficheros que subas) vive en ese disco y sobrevive a los reinicios.
+
+El apagado es *limpio*: cuando reinicias o borras el agente, el sistema fuerza un `sync` y espera a que el journal de ext4 termine de volcarse antes de matar la VM. Sin corrupción.
+
+## Hermes
+
+Hermes es un agente de IA conversacional que se conecta a Telegram. Puedes chatear con él, pedirle que gestione notas, que ejecute comandos en su entorno, que genere webs y bastante más.
+
+### 1. Crea un bot de Telegram
+
+Necesitas un bot de Telegram. Abre Telegram, busca [@BotFather](https://core.telegram.org/bots/tutorial#obtain-your-bot-token) y sigue las instrucciones para crear uno nuevo. Copia el token que te dé.
+
+### 2. Crea el agente
+
+Entra en [cloud.nan.builders/agents/new](https://cloud.nan.builders/agents/new) y rellena: nombre, tipo (Hermes), el token de Telegram, el modelo y, opcionalmente, un *soul* (system prompt) que defina la personalidad de tu agente.
+
+
+
+### 3. Espera a que esté Running
+
+Después de crear el agente, espera unos 30 segundos a que arranque la microVM, se formatee el disco por primera vez (`mkfs.ext4`) y se siembre el sistema de ficheros. El estado pasa a `Running` y Hermes a `Ready`.
+
+### 4. Habla con tu agente
+
+Busca tu bot en Telegram y mándale un mensaje. Hermes responderá con el modelo que hayas configurado.
+
+
+
+> **Tu agente está listo.**
+> Con estos 4 pasos ya tienes Hermes funcionando. Lo que viene a continuación son funciones adicionales del panel del agente: terminal web, subida de ficheros, observabilidad, exposición HTTP, la UI de Hermes y la gestión de variables de entorno.
+
+## Console: terminal web
+
+La pestaña **Console** abre una terminal interactiva (`bash --login`) dentro de tu microVM, sin necesidad de configurar SSH. El stream va por WebSocket con xterm.js: se redimensiona sola al ajustar el panel, tiene una pastilla de estado arriba a la derecha y un botón de reconexión por si se cae la sesión.
+
+Casos de uso típicos:
+
+- Instalar paquetes: `apt update && apt install -y nginx`
+- Revisar los logs internos del agente
+- Mover a su sitio los ficheros que hayas subido
+- Usar `htop`, `df -h`, `journalctl`, etc.
+
+> **Límites operativos**
+> 1 sesión simultánea por agente · 10 min de timeout por inactividad · 30 min de duración máxima por sesión.
+
+## Files: subida de ficheros
+
+La pestaña **Files** permite subir ficheros a la microVM arrastrándolos o desde el selector. Admite varios a la vez, con cola secuencial y barra de progreso en vivo con MiB/s. Los ficheros aterrizan en `/persist/uploads/` y desde ahí puedes moverlos con la Console.
+
+- Tamaño máximo: **200 MiB** por fichero.
+- Transporte: WebSocket con trozos de 256 KiB y backpressure de extremo a extremo.
+- El nombre del fichero se sanea en servidor (sin path traversal).
+- Listado en vivo de los ficheros subidos (se refresca cada 5s).
+
+## Observabilidad
+
+La pestaña **Observability** agrupa tres sub-pestañas:
+
+- **Logs**: stream en vivo del stdout y stderr del agente por WebSocket. Búfer de las últimas 500 líneas en el cliente.
+- **Events**: eventos del Pod de Kubernetes (BackOff, Scheduled, Pulled, Killing...) con tipo, motivo, mensaje, antigüedad y recuento. Se refresca solo cada 15s.
+- **Metrics**: consumo real de CPU, RAM y disco frente a los límites configurados. CPU y RAM vía Prometheus (kubelet-cadvisor), disco con `df` dentro de la microVM (el sistema de ficheros es de modo bloque y kubelet no lo ve). Se refresca cada 10s.
+
+## Web: exposición pública
+
+La pestaña **Web** tiene dos sub-pestañas para exponer servicios HTTP del agente:
+
+### HTTP
+
+Cualquier servicio que tu agente sirva por HTTP (nginx, una API, un sitio estático) lo puedes exponer públicamente. Por ejemplo, pídele a Hermes que instale nginx con una página HTML propia:
+
+
+
+En la pestaña **Web → HTTP**, pulsa **Enable HTTP**. Por defecto se expone el puerto `80`; si tu servicio escucha en otro, indícalo en **Container Port**. La plataforma genera una URL pública en `*.apps.nan.builders`.
+
+
+
+### La UI de Hermes
+
+Hermes incluye una UI web ligera ([nesquena/hermes-webui](https://github.com/nesquena/hermes-webui)) que corre siempre dentro del agente. Desde **Web → Hermes UI** puedes activar el acceso externo: la plataforma genera una URL del tipo `webui--.apps.nan.builders`, protegida por una contraseña por agente que se muestra en el panel.
+
+## Variables de entorno
+
+La pestaña **Env** te permite añadir, editar y borrar variables de entorno del agente sin tocar el Deployment. Útil para inyectar API keys de terceros, configurar el comportamiento de Hermes, etc.
+
+Hay dos variables **protegidas** (solo se pueden editar, no borrar): `OPENAI_API_KEY` (tu key del clúster, que gestiona la plataforma) y `TELEGRAM_BOT_TOKEN`. El resto las puedes crear, editar o borrar libremente.
+
+## Recursos y límites
+
+Cada microVM se aprovisiona con:
+
+| Recurso | Request | Límite |
+|---|---|---|
+| CPU | 200m | 1 vCPU |
+| RAM | 512 Mi | 2 GiB |
+| Disco | (sin request) | 20 GiB (PVC en modo bloque) |
+
+La CPU y la RAM son los límites máximos de la microVM; el consumo real suele quedar muy por debajo. El disco es persistente: todo lo que instales o modifiques (paquetes, ficheros, configuraciones) se conserva entre reinicios. Si el disco se llena (por encima del 90%), libéralo desde la Console (`du -sh /persist/*`).
+
+> **Límite actual**
+> Ahora mismo cada miembro puede desplegar **1 agente en microVM**. Este límite se ampliará en versiones futuras.
diff --git a/src/content/docs-es/apps.md b/src/content/docs-es/apps.md
new file mode 100644
index 0000000..138e56c
--- /dev/null
+++ b/src/content/docs-es/apps.md
@@ -0,0 +1,73 @@
+---
+title: Apps
+description: Despliega tus apps desde GitHub a NaN Cloud en minutos.
+order: 6
+group: Guías
+---
+
+# Apps.
+
+NaN Cloud te permite **desplegar tus propias apps desde un repositorio de GitHub**: construimos tu imagen, la publicamos en tu entorno aislado y la servimos tras un dominio público con HTTPS. Todo en un clic.
+
+> **Antes de empezar**
+> Las apps viven dentro de un **Space**: tu propio entorno, con su cuota de recursos. Si tienes una suscripción de inferencia activa, tu membresía incluye **un Space Basic gratis**. Si no, puedes comprar uno en [cloud.nan.builders/spaces](https://cloud.nan.builders/spaces).
+
+## Tiers disponibles
+
+Cada Space pertenece a un tier. El tier define la cuota total de CPU, RAM y almacenamiento que comparten todas las apps que despliegues dentro. Puedes **subir o bajar de tier** cuando quieras desde el panel del Space (bajar solo se permite si tu consumo actual cabe en el tier nuevo).
+
+| Tier | CPU | RAM | Disco | Pods | Precio |
+|---|---|---|---|---|---|
+| Basic | 2 vCPU | 4 GiB | 20 GiB | 5 | Gratis con inferencia · 6 € al mes |
+| Medium | 4 vCPU | 8 GiB | 40 GiB | 10 | 12 € al mes |
+| Large | 4 vCPU | 16 GiB | 80 GiB | 20 | 24 € al mes |
+
+La CPU y la RAM son los **límites agregados del Space** (la suma de todas tus apps). Por defecto, cada app que creas arranca con un límite holgado de `500m` de CPU y `500 MiB` de RAM, suficiente para una API o un worker típicos; puedes subir el límite por app desde la sección *Opciones avanzadas* del formulario hasta agotar el tier. El disco se comparte mediante PVCs (5/10/20 según el tier) y solo lo consumen las apps que marques como *persistentes*.
+
+## 1. Crea un Space
+
+Entra en [cloud.nan.builders/spaces](https://cloud.nan.builders/spaces). Si eres miembro de inferencia verás un panel que te ofrece un Space Basic gratis: elige un *slug* (de 1 a 20 caracteres, en minúscula y sin espacios) y pulsa **Claim free Basic**. Ese slug se usará para construir los dominios públicos de tus apps, así que elígelo con cabeza.
+
+
+
+El Space se activa al instante.
+
+## 2. Crea una App dentro del Space
+
+Abre el Space que acabas de crear. Verás el resumen de consumo de recursos, el botón **Change plan** por si quieres subir de tier en algún momento, y la sección **Apps in this Space**. Pulsa **New App** para empezar el formulario.
+
+
+
+## 3. Conecta GitHub y configura la build
+
+Conecta tu cuenta de GitHub autorizando la GitHub App de NaN Cloud en el repositorio que quieras desplegar (la primera vez te lleva al flujo de instalación oficial en github.com). Una vez conectado, elige el repo de la lista, la rama, y dale un nombre a tu App.
+
+> **Requisito obligatorio: Dockerfile**
+> Tu repositorio **tiene que contener un `Dockerfile`** en la raíz (o en la ruta que configures). Sin Dockerfile no podemos construir tu imagen y la app no se desplegará. A cambio, tienes control total sobre el runtime, las dependencias y los procesos que arrancan dentro de tu app.
+
+Si tu app es un servicio HTTP (una web, una API, un panel de administración, etc.), marca **Expose over HTTP** e indica el **puerto** en el que escucha internamente. Por ejemplo, si arrancas con `node server.js` escuchando en `:8080`, pon `8080` aquí. Del resto nos encargamos nosotros: publicarla en un dominio público con HTTPS.
+
+Si tu app es un proceso que no necesita ser accesible desde fuera (un worker, un cron, un consumidor de cola, etc.), desmarca *Expose over HTTP*: la app arrancará en modo worker, sin URL pública.
+
+
+
+El bloque **Environment variables** (opcional) te permite añadir variables tanto de runtime como de build. Y en **Advanced options** puedes ajustar réplicas, CPU y memoria, y añadir almacenamiento persistente si tu app necesita guardar estado.
+
+Pulsa **Deploy**. En la pantalla de detalle de la App verás la build en tiempo real. Al terminar, si todo ha ido bien, el estado cambiará a `Running`.
+
+## 4. Abre tu App
+
+Cuando el estado sea `Running`, pulsa el botón **Open** de arriba a la derecha. Abre la URL pública de tu app en una pestaña nueva.
+
+
+
+Desde esa misma pantalla tienes acceso a los logs en vivo de tu app, a los eventos, a las métricas (CPU, memoria, disco), a la gestión de variables de entorno y a un panel de ajustes para cambiar sobre la marcha la rama, el Dockerfile, el puerto y los recursos.
+
+## 5. Tu app, en producción
+
+Ya está. Tu repositorio de GitHub está sirviendo tráfico real desde un dominio público con HTTPS, sobre nuestra infraestructura. Cada `git push` a la rama configurada (con el auto-deploy activado) dispara una build nueva automáticamente.
+
+
+
+> **Tu App está en marcha.**
+> Con estos 5 pasos ya tienes tu app desplegada. Si necesitas escalar (más recursos, más réplicas, almacenamiento persistente, o más Spaces para separar entornos de dev, staging y producción), puedes hacerlo cuando quieras desde el panel. Apps y Spaces están en **Beta**: si encuentras algún problema, repórtalo en `#support` de Discord.
diff --git a/src/content/docs-es/examples.md b/src/content/docs-es/examples.md
new file mode 100644
index 0000000..935a4e7
--- /dev/null
+++ b/src/content/docs-es/examples.md
@@ -0,0 +1,643 @@
+---
+title: Ejemplos
+description: Fragmentos de código para conectarte a la API de NaN con Python, Node.js, curl y más.
+order: 4
+group: Guías
+---
+
+# Fragmentos de código.
+
+Ejemplos para conectarte a la API desde distintos lenguajes y herramientas. Usa `https://api.nan.builders/v1` como base URL y tu API key personal.
+
+## model: qwen3.6
+
+generación de texto y chat
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "model": "qwen3.6",
+ "messages": [{"role": "user", "content": "Hello, how are you?"}],
+ "max_tokens": 500
+ }'
+```
+
+### python (openai)
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="sk-your-key-here",
+ base_url="https://api.nan.builders/v1"
+)
+
+response = client.chat.completions.create(
+ model="qwen3.6",
+ messages=[{"role": "user", "content": "Write a hello world in Rust"}],
+ max_tokens=500,
+ stream=True
+)
+
+for chunk in response:
+ content = chunk.choices[0].delta.content
+ if content:
+ print(content, end="", flush=True)
+```
+
+Instalación: `pip install openai`
+
+### node.js (openai)
+
+```javascript
+import OpenAI from "openai";
+
+const client = new OpenAI({
+ apiKey: "sk-your-key-here",
+ baseURL: "https://api.nan.builders/v1",
+});
+
+const stream = await client.chat.completions.create({
+ model: "qwen3.6",
+ messages: [{ role: "user", content: "Write a hello world in Zig" }],
+ max_tokens: 500,
+ stream: true,
+});
+
+for await (const chunk of stream) {
+ const content = chunk.choices[0]?.delta?.content;
+ if (content) process.stdout.write(content);
+}
+```
+
+Instalación: `npm install openai`
+
+### opencode.json (config)
+
+```json
+{
+ "$schema": "https://opencode.ai/config.json",
+ "provider": {
+ "nan": {
+ "npm": "@ai-sdk/openai-compatible",
+ "name": "NaN",
+ "options": {
+ "baseURL": "https://api.nan.builders/v1",
+ "apiKey": "sk-your-key-here"
+ },
+ "models": {
+ "qwen3.6": {
+ "name": "Qwen 3.6",
+ "contextWindow": 262144,
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ }
+ },
+ "gemma4": {
+ "name": "Gemma 4",
+ "contextWindow": 262144,
+ "modalities": {
+ "input": ["text", "image"],
+ "output": ["text"]
+ }
+ },
+ "deepseek-v4-flash": {
+ "name": "DeepSeek V4 Flash",
+ "contextWindow": 500000,
+ "modalities": {
+ "input": ["text"],
+ "output": ["text"]
+ }
+ },
+ "mimo-v2.5": {
+ "name": "Xiaomi MiMo V2.5",
+ "contextWindow": 500000,
+ "modalities": {
+ "input": ["text", "image", "audio"],
+ "output": ["text"]
+ }
+ }
+ }
+ }
+ },
+ "compaction": {
+ "auto": true,
+ "prune": true,
+ "reserved": 50000
+ }
+}
+```
+
+Esta es la configuración para conectar IDEs (Cursor, OpenCode) con los 4 modelos LLM disponibles: `qwen3.6`, `gemma4`, `deepseek-v4-flash` y `mimo-v2.5`.
+
+### .pi/agent/models.json (config)
+
+```json
+{
+ "providers": {
+ "nan": {
+ "baseUrl": "https://api.nan.builders/v1",
+ "api": "openai-completions",
+ "apiKey": "",
+ "compat": {
+ "supportsDeveloperRole": true
+ },
+ "models": [
+ {
+ "id": "qwen3.6",
+ "name": "Qwen 3.6",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 262144,
+ "maxTokens": 16384
+ },
+ {
+ "id": "gemma4",
+ "name": "Gemma 4",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 262144,
+ "maxTokens": 16384
+ }
+ ]
+ }
+ }
+}
+```
+
+Configuración para `~/.pi/agent/models.json`
+
+### .pi/agent/settings.json (config)
+
+```json
+{
+ "defaultProvider": "nan",
+ "defaultModel": "qwen3.6"
+}
+```
+
+Configuración para `~/.pi/agent/settings.json`. Sin `defaultProvider` ni `defaultModel`, Pi usa su proveedor por defecto y devuelve un error de autenticación (401).
+
+### openclaw.json (config)
+
+```json
+{
+ "models": {
+ "providers": {
+ "nan": {
+ "baseUrl": "https://api.nan.builders/v1",
+ "apiKey": "sk-...",
+ "api": "openai-completions",
+ "models": [
+ {
+ "id": "qwen3.6",
+ "name": "Qwen 3.6",
+ "reasoning": true,
+ "input": ["text", "image"],
+ "contextWindow": 262144,
+ "maxTokens": 65536
+ }
+ ]
+ }
+ }
+ },
+ "agents": {
+ "defaults": {
+ "model": { "primary": "nan/qwen3.6" },
+ "models": {
+ "nan/qwen3.6": {
+ "params": {
+ "maxTokens": 16000
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+Configuración para `~/.openclaw/openclaw.json`
+
+`maxTokens: 65536` es el máximo que admite el modelo. `params.maxTokens: 16000` es lo que se manda en cada petición. 16K es un buen equilibrio para la mayoría de tareas. Si necesitas respuestas más largas, súbelo, pero ten en cuenta que el razonamiento también consume de ese presupuesto.
+
+
settings.json (Zed)
+
+```json
+{
+ "language_models": {
+ "openai": {
+ "api_url": "https://api.nan.builders/v1",
+ "available_models": [
+ {
+ "name": "qwen3.6",
+ "display_name": "NaN",
+ "max_tokens": 262144
+ }
+ ]
+ }
+ },
+ "edit_predictions": {
+ "open_ai_compatible_api": {
+ "api_url": "https://api.nan.builders/v1",
+ "model": "qwen3.6"
+ }
+ }
+}
+```
+
+Configuración para `~/.config/zed/settings.json`. Incluye las predicciones en línea.
+
+## model: qwen3-embedding
+
+embeddings vectoriales
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/embeddings \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "model": "qwen3-embedding",
+ "input": ["Hello world", "Hola mundo"],
+ "encoding_format": "float"
+ }'
+# → 4096-dimensional vectors per input
+```
+
+### python
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="sk-your-key-here",
+ base_url="https://api.nan.builders/v1"
+)
+
+response = client.embeddings.create(
+ model="qwen3-embedding",
+ input=["Kubernetes pod scheduling", "Pod scheduling in Kubernetes"],
+ encoding_format="float"
+)
+
+embeddings = [d.embedding for d in response.data]
+print(len(embeddings[0])) // 4096
+```
+
+### node.js
+
+```javascript
+import OpenAI from "openai";
+
+const client = new OpenAI({
+ apiKey: "sk-your-key-here",
+ baseURL: "https://api.nan.builders/v1",
+});
+
+const response = await client.embeddings.create({
+ model: "qwen3-embedding",
+ input: ["Hello world", "Hola mundo"],
+ encoding_format: "float",
+});
+
+const embeddings = response.data.map((d) => d.embedding);
+console.log(embeddings[0].length); // 4096
+```
+
+## model: rerank
+
+reordenado semántico, completa el stack de RAG
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/rerank \
+ -H "Authorization: Bearer $NAN_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "model": "rerank",
+ "query": "What is the capital of France?",
+ "documents": [
+ "Paris is the capital of France and home to the Eiffel Tower.",
+ "Berlin is the capital of Germany.",
+ "Madrid is the capital of Spain."
+ ]
+ }'
+# → results[] ordered by relevance_score desc, with original index
+```
+
+### python
+
+```python
+import os
+from openai import OpenAI
+
+client = OpenAI(
+ api_key=os.environ["NAN_API_KEY"],
+ base_url="https://api.nan.builders/v1"
+)
+
+# The /rerank endpoint is not part of the standard OpenAI client,
+# but we can invoke it with client.post().
+response = client.post(
+ path="/rerank",
+ cast_to=object,
+ body={
+ "model": "rerank",
+ "query": "What is the capital of France?",
+ "documents": [
+ "Paris is the capital of France and home to the Eiffel Tower.",
+ "Berlin is the capital of Germany.",
+ "Madrid is the capital of Spain.",
+ ],
+ },
+)
+
+for r in response["results"]:
+ print(f"{r['index']}: {r['relevance_score']:.3f}")
+```
+
+También funciona con `requests` a pelo o con cualquier cliente HTTP: el endpoint es compatible con OpenAI tanto en la autenticación como en el formato del cuerpo.
+
+## model: kokoro
+
+texto a voz
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/audio/speech \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "model": "kokoro",
+ "input": "Welcome to NaN builders.",
+ "voice": "af_heart"
+ }' \
+ -o speech.mp3
+
+# English female voice (af_heart), Spanish (ef_dora), etc.
+# See all voices: https://github.com/hexgrad/Kokoro-82M
+```
+
+### python
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="sk-your-key-here",
+ base_url="https://api.nan.builders/v1"
+)
+
+response = client.audio.speech.create(
+ model="kokoro",
+ voice="af_heart",
+ input="Hello, welcome to NaN builders.",
+ speed=1.0,
+ response_format="mp3"
+)
+
+response.stream_to_file("output.mp3")
+
+# Spanish voice
+response = client.audio.speech.create(
+ model="kokoro",
+ voice="ef_dora",
+ input="Hola, bienvenido a NaN builders.",
+ response_format="mp3"
+)
+```
+
+### node.js
+
+```javascript
+import OpenAI from "openai";
+import fs from "fs";
+
+const client = new OpenAI({
+ apiKey: "sk-your-key-here",
+ baseURL: "https://api.nan.builders/v1",
+});
+
+const response = await client.audio.speech.create({
+ model: "kokoro",
+ voice: "af_heart",
+ input: "Hello, welcome to NaN builders.",
+ speed: 1.0,
+ response_format: "mp3",
+});
+
+const buffer = Buffer.from(await response.arrayBuffer());
+fs.writeFileSync("output.mp3", buffer);
+```
+
+## model: whisper
+
+voz a texto
+
+### curl
+
+```bash
+# Transcribe audio file
+curl https://api.nan.builders/v1/audio/transcriptions \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -F "model=whisper" \
+ -F "file=@recording.mp3" \
+ -F "language=en"
+
+# → {"text":"Transcribed text","language":"en","duration":5.2}
+
+# Translate to English
+curl https://api.nan.builders/v1/audio/translations \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -F "model=whisper" \
+ -F "file=@recording.mp3"
+```
+
+### python
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="sk-your-key-here",
+ base_url="https://api.nan.builders/v1"
+)
+
+# Transcribe English audio
+with open("recording.mp3", "rb") as f:
+ result = client.audio.transcriptions.create(
+ model="whisper",
+ file=f,
+ language="en",
+ response_format="verbose_json"
+ )
+
+print(result.text) # Transcribed text
+print(result.language) # "en"
+print(result.duration) # 5.2 (seconds)
+
+# Translate to English
+with open("recording.mp3", "rb") as f:
+ translation = client.audio.translations.create(
+ model="whisper",
+ file=f
+ )
+print(translation.text) # English translation
+```
+
+### node.js
+
+```javascript
+import OpenAI from "openai";
+import fs from "fs";
+import FormData from "form-data";
+
+const client = new OpenAI({
+ apiKey: "sk-your-key-here",
+ baseURL: "https://api.nan.builders/v1",
+});
+
+// Transcribe audio
+const file = fs.createReadStream("recording.mp3");
+const form = FormData();
+form.append("file", file);
+
+const result = await client.audio.transcriptions.create({
+ model: "whisper",
+ file,
+ language: "en",
+ response_format: "verbose_json",
+});
+
+console.log(result.text); // Transcribed text
+console.log(result.language); // "en"
+console.log(result.duration); // 5.2
+```
+
+## model: mimo-v2.5
+
+omnimodal: chat, visión y audio
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "model": "mimo-v2.5",
+ "messages": [{"role": "user", "content": "Hello, how are you?"}],
+ "max_tokens": 500
+ }'
+```
+
+Con el razonamiento activado se recomienda `max_tokens ≥ 300`, para dejarle sitio.
+
+### visión (curl)
+
+```bash
+curl https://api.nan.builders/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "model": "mimo-v2.5",
+ "messages": [{
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "What's in this image?"},
+ {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
+ ]
+ }],
+ "max_tokens": 500
+ }'
+```
+
+### python (openai)
+
+```python
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="sk-your-key-here",
+ base_url="https://api.nan.builders/v1"
+)
+
+response = client.chat.completions.create(
+ model="mimo-v2.5",
+ messages=[{
+ "role": "user",
+ "content": [
+ {"type": "text", "text": "Describe this image."},
+ {"type": "image_url", "image_url": {"url": "https://example.com/photo.jpg"}}
+ ]
+ }],
+ max_tokens=500
+)
+
+print(response.choices[0].message.content)
+```
+
+## tool: web search
+
+búsqueda web autenticada para agentes: `POST /v1/search`
+
+### curl
+
+```bash
+curl https://api.nan.builders/v1/search \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer sk-your-key-here" \
+ -d '{
+ "query": "latest go release",
+ "count": 5,
+ "freshness": "pw"
+ }'
+# → {"results":[{"title":...,"url":...,"snippet":...,"source":...}],"cached":false}
+```
+
+### python
+
+```python
+import os
+from openai import OpenAI
+
+client = OpenAI(
+ api_key=os.environ["NAN_API_KEY"],
+ base_url="https://api.nan.builders/v1"
+)
+
+# /search is not part of the standard OpenAI client, but we can invoke it with client.post().
+response = client.post(
+ path="/search",
+ cast_to=object,
+ body={
+ "query": "latest go release",
+ "count": 5,
+ "freshness": "pw",
+ },
+)
+
+for r in response["results"]:
+ print(r["title"], "-", r["url"])
+```
+
+También funciona con `requests` a pelo o con cualquier cliente HTTP: manda el cuerpo JSON con tu key en el Bearer.
+
+## Integración con IDEs
+
+- **Cursor**: Settings → OpenAI API → Base URL: `https://api.nan.builders/v1`, API Key: tu key
+- **Zed**: Settings → `settings.json` → mira la [configuración completa de arriba](#qwen36-zed)
+- **Cline / Continue / Aider**: define las variables de entorno:
+
+```bash
+export OPENAI_BASE_URL="https://api.nan.builders/v1"
+export OPENAI_API_KEY="sk-your-key-here"
+```
diff --git a/src/content/docs-es/getting-started.md b/src/content/docs-es/getting-started.md
new file mode 100644
index 0000000..4408838
--- /dev/null
+++ b/src/content/docs-es/getting-started.md
@@ -0,0 +1,39 @@
+---
+title: Primeros pasos
+description: Configura tu IDE o herramienta favorita para conectarte a los modelos de NaN.
+order: 1
+group: Primeros pasos
+---
+
+# Primeros pasos.
+
+El acceso es vía LiteLLM con una API compatible con OpenAI. Funciona con cualquier herramienta que acepte una `base URL` y una `API key`: Cursor, Cline, Continue, Aider, Open Code, Open WebUI o cualquier SDK compatible con OpenAI.
+
+## Consigue tu API Key
+
+Tienes que ser miembro de la comunidad de NaN. Si ya estás suscrito, genera tu API Key desde los ajustes de usuario, en el apartado "API Keys" de la [plataforma](https://cloud.nan.builders/). La key es personal e intransferible.
+
+> **Nota**
+> El soporte es solo para incidencias técnicas.
+
+## Configura tu herramienta
+
+| Campo | Valor |
+|---|---|
+| base URL | `https://api.nan.builders/v1` |
+| API Key | `sk-your-key-here` |
+| Modelo | `qwen3.6` |
+
+Ejemplo de configuración compatible con OpenAI:
+
+```json
+provider: {
+ openai: {
+ npm: "@ai-sdk/openai",
+ name: "NaN",
+ apiKey: "sk-your-key-here",
+ baseURL: "https://api.nan.builders/v1",
+ model: "qwen3.6"
+ }
+}
+```
diff --git a/src/content/docs-es/intro.md b/src/content/docs-es/intro.md
new file mode 100644
index 0000000..5594671
--- /dev/null
+++ b/src/content/docs-es/intro.md
@@ -0,0 +1,27 @@
+---
+title: Introducción
+description: Conecta tus herramientas favoritas (OpenCode, Cursor, Cline, etc.) a nuestro clúster de inferencia compartido.
+order: 0
+group: Primeros pasos
+---
+
+# Bienvenido a NaN.
+
+Esta documentación explica cómo conectar tus herramientas a nuestras GPUs. El clúster ejecuta modelos abiertos con una API compatible con OpenAI. Si algo acepta una `base URL` y una `API key`, funciona con NaN.
+
+> **Para conseguir tu API Key**
+> Tienes que ser miembro de la comunidad de NaN. Puedes generar tu API Key desde los ajustes de usuario, en el apartado "API Keys" de la [plataforma](https://cloud.nan.builders/). La key es personal e intransferible.
+
+## Rate limits
+
+| Métrica | Valor |
+|---|---|
+| Peticiones por minuto | 60 rpm |
+| Máximo en paralelo | 5 concurrentes |
+
+## Por dónde seguir
+
+- [Primeros pasos](/es/docs/getting-started): endpoint, autenticación y configuración paso a paso.
+- [Modelos](/es/docs/models): capacidades y límites de los modelos.
+- [Ejemplos](/es/docs/examples): fragmentos en Python, Node.js y curl.
+- Soporte: reporta incidencias en `#support` de Discord.
diff --git a/src/content/docs-es/models.mdx b/src/content/docs-es/models.mdx
new file mode 100644
index 0000000..529f7ef
--- /dev/null
+++ b/src/content/docs-es/models.mdx
@@ -0,0 +1,258 @@
+---
+title: Modelos
+description: Especificaciones técnicas, capacidades y parámetros de los modelos del clúster compartido.
+order: 3
+group: Referencia
+---
+
+import ModelCard from '../../components/docs/ModelCard.astro';
+import LimitationsCard from '../../components/docs/LimitationsCard.astro';
+import RateLimits from '../../components/docs/RateLimits.astro';
+
+# Modelos del clúster.
+
+Modelos de la comunidad. A todos se accede con la misma API compatible con
+OpenAI y la misma `base URL`.
+
+
+
+max_tokens ≥ 300)',
+ 'Visión (entrada de imagen)',
+ 'Audio (entrada de audio)',
+ 'Contexto de 1M tokens',
+ 'Generación en streaming (SSE)',
+ ]}
+/>
+
+
+
+
+
+
+
+
+
+
+
+af_heart · inglés (femenina)',
+ 'ef_dora · español (femenina)',
+ 'em_alex · español (masculina)',
+ '67 packs de voces en total (ver la lista completa)',
+ ]}
+/>
+
+
+
+524 (timeout) antes de que termine la transcripción. Usa formatos comprimidos como OGG/Opus y parte los ficheros largos en tramos de 2 minutos o menos para evitarlo.',
+ },
+ {
+ title: 'Formatos recomendados',
+ body: 'OGG/Opus y MP3: ficheros más pequeños con la misma calidad de transcripción. Un audio de 60 minutos en OGG/Opus a 48 kbps ocupa unos 20 MB frente a los ~550 MB del WAV.',
+ },
+ ]}
+/>
+
+/v1/images/generations)',
+ 'Imagen a imagen con hasta 4 referencias (/v1/images/edits)',
+ 'Salida como URL temporal (R2, ~60 min) o base64',
+ 'Reproducibilidad con seed y control de guidance',
+ ]}
+/>
+
+
diff --git a/src/layouts/Docs.astro b/src/layouts/Docs.astro
index ab9ea68..73576df 100644
--- a/src/layouts/Docs.astro
+++ b/src/layouts/Docs.astro
@@ -23,11 +23,40 @@ import {
interface Props {
title: string;
description?: string;
+ lang?: 'en' | 'es';
}
-// The documentation exists in English only (the MDX collection is not
-// translated), so the page language, the default copy and the social card are.
-const { title, description = 'NaN docs. Connect to our open models through an OpenAI-compatible API.' } = Astro.props;
+// The guides exist in both languages, each in its own collection, so the page
+// language, the default copy and the social card follow the `lang` prop.
+const { title, lang = 'en' } = Astro.props;
+
+const description =
+ Astro.props.description ??
+ (lang === 'es'
+ ? 'Documentación de NaN. Conecta con nuestros modelos abiertos a través de una API compatible con OpenAI.'
+ : 'NaN docs. Connect to our open models through an OpenAI-compatible API.');
+
+const pfx = lang === 'es' ? '/es' : '';
+const T = {
+ en: {
+ onThisPage: 'On this page',
+ search: 'Search docs…',
+ skip: 'Skip to content',
+ prev: '← Previous',
+ next: 'Next →',
+ docs: 'Docs',
+ close: 'Close menu',
+ },
+ es: {
+ onThisPage: 'En esta página',
+ search: 'Buscar en la documentación…',
+ skip: 'Saltar al contenido',
+ prev: '← Anterior',
+ next: 'Siguiente →',
+ docs: 'Docs',
+ close: 'Cerrar menú',
+ },
+}[lang];
const siteUrl = 'https://nan.builders';
@@ -37,7 +66,7 @@ const canonical = `${siteUrl}${Astro.url.pathname.replace(/\/+$/, '') || '/'}`;
const ogImage = `${siteUrl}/og/og-en.png`;
const socialTitle = `${title} · NaN Docs`;
-const entries = await getCollection('docs');
+const entries = await getCollection(lang === 'es' ? 'docsEs' : 'docs');
/*
* The API reference does not come from the collection: Scalar serves /docs/api
@@ -47,24 +76,27 @@ const entries = await getCollection('docs');
*/
const navItems: DocsNavItem[] = [
...entries.map((entry) => ({
- slug: entry.id === 'intro' ? '/docs' : `/docs/${entry.id}`,
+ slug: entry.id === 'intro' ? `${pfx}/docs` : `${pfx}/docs/${entry.id}`,
label: entry.data.title,
order: entry.data.order,
group: entry.data.group,
description: entry.data.description,
})),
{
- slug: `/docs/${API_DOC_SLUG}`,
+ slug: `${pfx}/docs/${API_DOC_SLUG}`,
label: API_DOC_META.title,
order: API_DOC_META.order,
- group: API_DOC_META.group,
+ group: API_DOC_META.group[lang],
description: API_DOC_META.description,
},
].sort((a, b) => a.order - b.order);
const navGroups = groupDocsNav(navItems);
-const here = Astro.url.pathname.replace(/\/+$/, '') || '/docs';
+const enPath = Astro.url.pathname.replace(/^\/es(?=\/|$)/, '') || '/docs';
+const esPath = `/es${enPath}`;
+
+const here = Astro.url.pathname.replace(/\/+$/, '') || `${pfx}/docs`;
const isActive = (slug: string) => isActiveDocPath(here, slug);
/*
@@ -72,11 +104,13 @@ const isActive = (slug: string) => isActiveDocPath(here, slug);
* its structure dropped them; they are restored because under /docs they are
* the only clue to where you are when you arrive from a search engine.
*/
-const breadcrumb = Astro.url.pathname
+const breadcrumb = enPath
.split('/')
.filter(Boolean)
.map((part, i, parts) => {
- const path = '/' + parts.slice(0, i + 1).join('/');
+ // Built from the locale-stripped path: counting `/es` as a segment made the
+ // Spanish pages show an extra crumb for the docs index.
+ const path = `${pfx}/` + parts.slice(0, i + 1).join('/');
const item = navItems.find((n) => n.slug === path);
return {
label: item ? item.label : part.replace(/-/g, ' '),
@@ -103,7 +137,7 @@ const collectionIndex = await Promise.all(
const { headings } = await render(entry);
return {
title: entry.data.title,
- href: entry.id === 'intro' ? '/docs' : `/docs/${entry.id}`,
+ href: entry.id === 'intro' ? `${pfx}/docs` : `${pfx}/docs/${entry.id}`,
description: entry.data.description,
headings: headings
.filter((h) => h.depth >= 2 && h.depth <= 3)
@@ -116,7 +150,7 @@ const searchData = [
...collectionIndex,
{
title: API_DOC_META.title,
- href: `/docs/${API_DOC_SLUG}`,
+ href: `${pfx}/docs/${API_DOC_SLUG}`,
description: API_DOC_META.description,
headings: apiSearchHeadings(spec),
},
@@ -124,7 +158,7 @@ const searchData = [
---
-
+
@@ -134,6 +168,10 @@ const searchData = [
+ {/* Every guide exists at both paths, so each declares the other. */}
+
+
+
@@ -143,7 +181,7 @@ const searchData = [
-
+
@@ -161,9 +199,9 @@ const searchData = [
{socialTitle}
- Skip to content
+ {T.skip}
-
+
@@ -176,7 +214,7 @@ const searchData = [
going back to the hamburger, none of which are obvious affordances on
a small screen.
*/}
-