Saltar al contenido
openplate

La aplicación

Configuración

La clave de la base de datos de alimentos, la Content-Security-Policy, la analítica, las instancias gestionadas y los endpoints de IA personalizados y provistos por la instancia

Esta página es una traducción automática de la documentación en inglés.

openplate arranca sin nada configurado. No hay URL de base de datos, ni clave de sesión, ni clave de cifrado, porque el servidor no guarda cuentas ni almacena nada. Cada variable es un ajuste opcional.

La mayoría de las variables se leen en un solo lugar, app/config/index.ts, y se exponen como un objeto CONFIG tipado:

typescript
import { CONFIG } from '#config';

const port = CONFIG.server.port;
const appUrl = CONFIG.app.url;

.env.example contiene la lista completa con notas explicativas. Cópialo en .env para modificar alguna.

Variables de entorno

environment-variables.md lista cada variable que lee la aplicación, con su valor predeterminado. También lista las variables para el servidor central y el servicio de inferencia. Las secciones de abajo explican las funciones más grandes en detalle.

Una variable no tiene sección propia. DEFAULT_UI_LANGUAGE define el idioma que ve un visitante antes de elegir uno: en, el predeterminado, de, fr, it, es o tr. La elección de la persona siempre tiene prioridad. Este ajuste no traduce ningún nombre de comida, ninguna respuesta de IA ni nada que una persona haya escrito. Cualquier otro valor detiene el arranque.

Las claves de API de los proveedores nunca se leen del entorno. La clave de un usuario se introduce en el navegador, se guarda en el dispositivo y se envía directamente del navegador al proveedor; el servidor no guarda ninguna copia. MISTRAL_API_KEY y OPENROUTER_API_KEY en .env.example solo existen para que quien desarrolle pueda apuntar los scripts de verificación a un proveedor activo. Definirlos en una instancia desplegada no tiene efecto.

La clave de la base de datos de alimentos

FOOD_DB_API_URL y FOOD_DB_API_KEY son dos decisiones distintas y conviene no confundirlas.

La URL decide si esta instancia consulta alimentos o no. Si la dejas vacía, ningún nombre de alimento saldrá jamás de tu máquina.

La clave decide cuánto puedes consultar. Hay tres niveles:

nivelqué hacesqué obtienes
anónimonadauna cuota diaria pequeña, compartida por dirección de red
gratuitoindicar una dirección de correo en lowcarbcheck.org/developersuna cuota mensual generosa
asociadosolicitarsin límite mensual

LowCarbCheck cuenta en créditos, y una búsqueda de alimentos cuesta uno. En el momento de escribir esto, el nivel anónimo dispone de 1.000 créditos al día y la clave gratuita de 100.000 al mes; lowcarbcheck.org/developers incluye las cifras actuales. LowCarbCheck ve la dirección del servidor de tu app, no las direcciones de sus usuarios. En el nivel anónimo, todas las personas de tu instancia comparten una sola cuota diaria.

Una instancia sin clave sigue funcionando. Corresponde al nivel anónimo, y suele bastar para una sola persona que esté probando openplate. Un hogar, o cualquier entorno donde se escaneen varios platos al día, necesitará la clave gratuita.

FOOD_DB_DAILY_CALL_LIMIT limita cuántas llamadas a LowCarbCheck hace este servidor en un día UTC. El valor por defecto, 3200, mantiene el consumo mensual dentro de las 100 000 de la clave gratuita. Si alguien busca un nombre consultado en los últimos cinco minutos, se responde desde la memoria sin coste alguno. Al superar el límite, las búsquedas de alimentos se pausan hasta la medianoche UTC, la app muestra un aviso y los escaneos se completan usando solo los datos de la IA. Auméntalo si tu clave permite más. El contador reside en memoria, por lo que se reinicia tras cada reinicio del servicio.

En una instancia administrada, consultar un alimento también requiere una cuenta con la sesión iniciada. La aplicación envía la sesión de la cuenta con cada consulta, y el servidor de la aplicación le pregunta al servidor central en CORE_URL si la sesión sigue activa antes de que nada llegue a LowCarbCheck. Por tanto, el servidor de la aplicación también debe llegar a esa dirección. Si no puede, rechaza las consultas hasta que pueda, y los análisis se siguen completando con las propias cifras de la IA. Una instancia abierta responde a cada consulta, como antes.

La consulta falla en modo abierto en cualquier caso: si la base de datos de alimentos no está disponible, rechaza la conexión o agota la cuota, el escaneo se completa y sigue mostrando valores. Dichos valores serán entonces la propia estimación de la IA en lugar de un dato de la base de datos, y la app lo indicará en pantalla en vez de dejar que la diferencia pase desapercibida.

Propuestas a la base de datos de alimentos

Con FOOD_DB_BACKFILL=true y una clave, openplate envía a LowCarbCheck, a través de este servidor, cada alimento que una persona guarda a partir de una foto o de una comida que haya escrito:

  • Un alimento que la persona vinculó a una fila de LowCarbCheck envía los nombres del alimento en todos los idiomas de la aplicación, de modo que la fila recibe los títulos que le faltan.
  • Un alimento sin coincidencias envía sus nombres en todos los idiomas de la aplicación y sus macros por 100 g, para que LowCarbCheck pueda añadirlo. Requiere un nombre en inglés y los cuatro valores de carbohidratos, grasas, proteínas y energía. Si no los tiene, el alimento no se envía.
  • Nunca se envía un nombre que la persona haya escrito o modificado directamente.

Una propuesta incluye los nombres, los macros y si el alimento proviene de una foto o de una comida escrita. No incluye ninguna cuenta, ninguna entrada del diario, ninguna foto ni la dirección de la persona. LowCarbCheck solo ve tu servidor y tu clave. LowCarbCheck evalúa cada propuesta con un modelo cuando llega y publica lo que pasa el filtro. Un alimento publicado de este modo vuelve de la base de datos de alimentos marcado como una estimación, y openplate lo muestra y almacena como tal, nunca como una fuente verificada.

Cada persona puede desactivarlo en su dispositivo en Ajustes → IA. Permanece desactivado en todas las instancias hasta que quien opera el sistema configure FOOD_DB_BACKFILL=true.

Suscripción al boletín

openplate no incluye lista de correo. Si defines a la vez NEWSLETTER_SUBSCRIBE_URL y NEWSLETTER_TURNSTILE_SITE_KEY, la página de inicio añade un formulario de registro. El navegador lo envía al servidor de openplate, que reenvía {email, locale, consent, source, turnstileToken} a tu URL, con un máximo de cinco peticiones por minuto desde una misma dirección. El navegador nunca llega a conocer esa URL, por lo que puede residir en una red privada. Si defines uno sin el otro, el arranque se detiene. Si dejas ambos sin definir, que es el valor predeterminado, no habrá formulario, ni script adicional, ni cambios en la CSP.

La comprobación de nuevas versiones

El servidor descarga https://openplate.de/latest.json cada seis horas, y una vez unos 90 segundos después de arrancar. Este pequeño archivo indica la versión más reciente. El servidor compara esa versión con la que tiene en ejecución y muestra el resultado en Ajustes > Acerca de. El botón «Comprobar ahora» también lanza una comprobación, con un límite de una petición real por minuto para toda la instancia.

La petición la hace el servidor, no el navegador. Si añadieras openplate.de a la connect-src de producción, ampliarías la lista de permisos que impide que un script inyectado robe una clave BYOK. La petición es un simple GET sin cuerpo, cadena de consulta, token ni identificador de instancia. Como cualquier petición HTTPS, envía la dirección IP del servidor. También incluye un User-Agent con el formato openplate/<version> (<platform>; <arch>), como por ejemplo openplate/1.2.3 (linux; arm64). No contiene nada más. El proyecto contabiliza las direcciones únicas que hacen consultas cada día y solo almacena los totales diarios. Los registros del proxy conservan las direcciones IP hasta 15 días, exactamente igual que las visitas a un sitio web habitual. Consulta ADR-0021.

bash
UPDATE_CHECK=off

Este ajuste desactiva las comprobaciones por completo. El servidor no inicia ningún temporizador, no envía peticiones, excluye la instancia de los recuentos del proyecto e indica en la página «Acerca de» que las comprobaciones están desactivadas.

La comprobación solo informa. openplate es un único contenedor sin estado y no puede reemplazar su propia imagen, por lo que actualizar sigue siendo el mismo proceso de siempre:

bash
docker compose -f compose.yml pull && docker compose -f compose.yml up -d

El único botón de la aplicación que cambia algo es "Recargar para actualizar", que aparece cuando el servidor ya sirve una compilación más reciente que la que ejecuta la página abierta. Eso recarga el navegador con los recursos que tiene el servidor y no modifica nada en el host.

Trasladar usuarios a otra instancia

Cuando cierras una instancia y sus usuarios se mudan a otra, mantén el contenedor antiguo en ejecución con un único ajuste:

bash
MOVED_TO_URL=https://app.openplate.example

A partir de ese momento, cada ruta de la instancia antigua sirve una única página de aviso. En ella se indica dónde está openplate ahora, se ofrece un botón hacia la página de inicio de sesión de allí y se pide a quienes añadieron la app a su pantalla de inicio que eliminen ese icono y añadan la nueva dirección. La página elige su idioma en este orden: la preferencia guardada del usuario, los idiomas solicitados por el navegador y, después, DEFAULT_UI_LANGUAGE.

La página informa a los usuarios de que su cuenta y su diario se han trasladado con ellos. Activa este modo solo cuando eso sea cierto: la nueva instancia usa el mismo servidor central, o migraste las cuentas allí. Un diario guardado únicamente en un navegador se queda en ese navegador bajo la dirección antigua; la nueva dirección no puede leerlo.

Una redirección HTTP no puede resolver esta migración. Un teléfono con la app instalada ejecuta un service worker que guarda en caché las páginas de la aplicación. Los navegadores no siguen redirecciones cuando comprueban las actualizaciones de ese worker, por lo que una app instalada seguiría abriendo su copia guardada. En este modo, /sw.js sirve un worker pequeño que elimina toda la caché bajo la dirección antigua, anula su propio registro y recarga la página. La siguiente solicitud carga entonces el aviso directamente desde el servidor. El worker no modifica los datos del diario guardados en el navegador.

La API devuelve 410 Gone con la nueva dirección en el cuerpo de la respuesta. /healthcheck responde como antes y el manifiesto de la aplicación web no cambia, por lo que un icono de la pantalla de inicio sigue abriendo la dirección antigua y cargando la página. Mantén este modo activo hasta que cese el tráfico hacia la dirección antigua. El valor debe ser una dirección https:// en un host distinto de APP_URL, sin nombre de usuario ni contraseña; cualquier otra cosa detiene el arranque.

La Content-Security-Policy

Tanto la llamada de visión BYOK como la clave residen por completo en el navegador, por lo que la compilación de producción incluye una Content-Security-Policy estricta. Su connect-src permite:

  • 'self'
  • los orígenes de los proveedores integrados (OpenRouter, Mistral, Anthropic), derivados automáticamente del registro de proveedores: consulta ADR-0007
  • localhost y 127.0.0.1 en cualquier puerto. [::1] no está en la lista, porque un origen de CSP no puede designar una dirección IPv6; apunta el cliente a localhost en su lugar.
  • tus CORE_URL y DEFAULT_INFERENCE_BASE_URL, si están definidos
  • cualquier elemento de CSP_CONNECT_EXTRA

Esa lista de permisos es lo que impide que un script inyectado filtre una clave que reside en la página. Amplíala con precaución.

En una instancia administrada, el proxy de IA es el servidor central con el que el cliente ya se comunica, por lo que su origen es CORE_URL, que ya está en la lista de arriba. No hay un segundo punto de conexión remoto que permitir ni nada adicional que añadir a CSP_CONNECT_EXTRA para ello.

Analítica

openplate puede registrar cómo se utiliza. No registra nada hasta que lo configuras, y nunca registra lo que comes.

Define MATOMO_URL y MATOMO_SITE_ID para apuntar una instancia a una instalación de Matomo que gestiones tú. Si dejas ambos sin definir, que es el valor predeterminado, la instancia no carga ningún script de analítica, no envía peticiones y sirve la misma cabecera Content-Security-Policy que servía antes de que existiera la analítica. Si defines uno sin el otro, el arranque falla a propósito: quien opera la instancia y cree tener analítica sin tenerla está en peor situación que si ve un error.

El rastreador se ejecuta con las cookies desactivadas. No almacena nada en el dispositivo, por lo que no es necesario crear un banner de consentimiento.

La comprobación de versiones va por separado. No utiliza ningún rastreador ni Matomo. UPDATE_CHECK=off detiene la comprobación y el recuento diario de peticiones del proyecto. Consulta La comprobación de nuevas versiones.

Qué determina un nivel

MATOMO_EVENT_LEVEL determina cuánto tiene permitido informar la instancia. Solo se aplica cuando la analítica ya está activada.

NivelQué cuenta
pageviewsSolo visitas de página. Nunca se activa ningún evento de funcionalidad.
productVisitas de página más uso del software. La opción predeterminada.
researchTodo, incluidos ayuno, peso, datos compartidos con personal clínico y participación en estudios.

Si no se define, significa product. Un valor no reconocido detiene el arranque. Un nivel definido en una instancia sin Matomo configurado también detiene el arranque, por el mismo motivo que lo hace un par a medio configurar.

Qué cuenta product

36 eventos, todos ellos sobre el software y no sobre la persona.

ÁreaEventos
Onboardingcompletado, paso completado (enfoque, peso, cuerpo), paso omitido
Escaneocompletado correctamente, fallido (con una categoría de fallo fija), no encontró nada, modo elegido, iniciado desde una foto compartida
Diarioregistrado (con la vía de entrada: búsqueda, manual, escaneo de plato, escaneo de etiqueta, chip, copiar día, registrar de nuevo, comida guardada), entrada editada, entrada eliminada, entrada restaurada, comida guardada
Alimentos personalizadoseditado, eliminado
Proveedor de IAconectado (manual, OAuth, ajuste predeterminado de la instancia), fallo al comprobar la clave, desconectado
Preferenciascambiada (tema o idioma)
Copia de seguridadexportada, importada, CSV exportado, caché de fotos vaciada
Cuentacreada, eliminada, contraseña cambiada, restablecimiento de contraseña solicitado, restablecimiento de contraseña completado, configuración completada
Invitacionesenlace pegado, unión completada
Instalación de la appaviso de instalación mostrado, instalada, visita de página sin conexión
Página de aterrizajesuscripción al boletín, clic en llamada a la acción

Qué añade research

12 eventos más. Cada uno revela datos sobre la salud de una persona o sobre su participación en un estudio, por lo que están desactivados a menos que los solicites.

ÁreaEventos
Ayunoayuno iniciado (ahora o programado), ayuno finalizado
Objetivos y pesoobjetivos guardados (metas o métricas corporales), peso registrado
Compartición con personal clínicoacceso concedido, revocado, clave rotada, identidad creada, diario compartido abierto
Estudios de investigacióninscrito, retirado, contribución enviada

No contienen valores. Un evento de ayuno no indica su duración, y un evento de peso no indica un peso. Pero los eventos llevan marca temporal, como todo evento analítico, de modo que un inicio y un fin permiten calcular una duración por resta, y un evento de compartición revela que la persona tiene un profesional clínico asignado. La participación en estudios constituye una categoría especial de datos según el Art. 9 del RGPD.

Ese es el único motivo por el que existe este nivel. Si un investigador dirige un estudio en su propia instancia, necesita estas cifras y puede recopilarlas legalmente de participantes que hayan dado su consentimiento. Una instancia de uso general no debería recopilarlas y, por defecto, no lo hace.

Si activas research, indícalo en tu propia política de privacidad. La política de openplate describe las instancias alojadas de openplate, no la tuya.

Lo que nunca se contabiliza, en ningún nivel

  • Nada procedente de un diario. Ningún nombre de alimento, peso, objetivo, foto, hora de comida ni id de estudio.
  • Ninguna cifra medida en una persona. Los eventos llevan una etiqueta fija o nada en absoluto.
  • Ningún identificador. Ningún id de cuenta, dirección de correo electrónico ni id de dispositivo.
  • Cadenas de consulta y fragmentos de URL, que se descartan por completo antes de notificar una visualización de página. openplate ubica allí tokens de un solo uso.
  • Identificadores en una ruta. /diary/entry/<id> y /shared/<account id> se reemplazan por un marcador de posición antes de notificar la visualización de página.

Las reglas se aplican mediante tipos y no mediante revisión. Cada función de eventos en app/lib/matomo-events.ts no recibe nada o recibe un único valor de una lista fija, por lo que no es posible pasar un nombre de alimento sin provocar un error de compilación. tests/unit/no-telemetry-wiring.test.ts detiene la compilación si cualquier otro archivo intenta acceder directamente al rastreador, o si se escribe en el código fuente un host o id de sitio de Matomo, que es lo que haría que una instancia autohospedada enviara datos a la cuenta de otra persona.

Consulta ADR-0010 para ver la decisión y sus motivos.

Instancias gestionadas

Una instancia que define INSTANCE_MODE=managed es una instancia gestionada: un administrador invita a personas por correo electrónico y cada cuenta incluye una cuota diaria de IA, de modo que iniciar sesión da acceso a la vez al diario y a la IA en un solo paso. Las instancias alojadas en beta.openplate.de y app.openplate.de usan este modo. Viene desactivado de forma predeterminada: si la persona que se autoaloja la app no define nada, obtiene la versión abierta.

INSTANCE_MODE=managed requiere CORE_URL. La cuenta es lo que une el diario y la cuota; declarar managed sin un servidor central detiene el arranque en lugar de dejar cosas a medio activar.

Un administrador invita a usuarios desde la propia aplicación, en /admin, o con la API de administración de openplate-core y ADMIN_TOKEN. La primera cuenta de todas, antes de que exista ningún administrador, sale de esa API. self-hosting.md contiene el comando. Con el correo configurado en el servidor central, la invitación se envía por correo. Si no hay correo configurado, la respuesta incluye el enlace y tú lo pasas. Las contraseñas olvidadas se restablecen con un enlace. Ese enlace se envía por correo o, si no hay correo, lo crea un administrador (mira self-hosting.md). El servidor guarda en custodia un código de recuperación que descifra la clave de datos tras el restablecimiento (mira sync.md).

Una instancia gestionada con IA también necesita tres valores en el servidor central: UPSTREAM_BASE_URL y UPSTREAM_API_KEY (el proveedor y su clave) y un modelo. El modelo es AI_ADVERTISED_MODEL, el que usa cada escaneo. O bien define AI_TIERS_FILE=bundled, y el modelo provendrá del archivo de niveles del núcleo, ai-tiers.json, que reúne el modelo, su enrutamiento y su precio en un único lugar revisado (también sirve una ruta a un archivo que montes; consulta el proxy de IA). Se requiere un modelo para los escaneos. Sin uno, el servidor central notifica que no hay modelo, y la aplicación se niega a escanear en lugar de elegir un modelo a tu costa. Nombra el modelo como lo hace tu proveedor, por ejemplo vendor/model-name con UPSTREAM_BASE_URL=https://openrouter.ai/api/v1, o openplate-plate-1 delante de openplate-inference. Con un archivo de niveles, AI_ADVERTISED_MODEL solo queda como una anulación de emergencia del modelo del nivel por defecto, y el núcleo registra una advertencia en cada arranque. Cada cuenta necesita entonces una cuota diaria, que empieza en 0. Asígnala con "dailyAiLimit" cuando crees la invitación, o defínela más tarde en /admin.

Qué cambia cuando se define:

  • /welcome ofrece exactamente dos acciones: Iniciar sesión, y Tengo un enlace de invitación (que recibe un enlace pegado y lo transfiere a /join). No existe un botón "Empezar".
  • /onboarding redirige a /welcome si el dispositivo no tiene ni un diario local ni una cuenta. La vía anónima solo local está cerrada, no simplemente oculta: en una instancia administrada no lleva a ninguna parte, porque no hay IA sin una cuenta y ningún diario sobrevive al dispositivo sin ella. Un dispositivo que ya contiene un diario nunca se expulsa.
  • /join ejecuta un único proceso: la propia solicitud de registro canjea la invitación en una sola transacción, y a partir de ese momento la cuenta contiene tanto el diario como la cuota. La opción «Omitir, ya tengo una cuenta» desaparece, porque en una instancia así la persona a la que se le ofrece no tiene ninguna de las dos cosas.
  • Ajustes → Cuenta, con la sesión cerrada, ofrece iniciar sesión e indica que las cuentas aquí provienen de un enlace de invitación. No hay ningún botón de «crear una cuenta».

Todo lo anterior se mantiene sin cambios en una instancia abierta (INSTANCE_MODE no definido o open), y una prueba fija ambas variantes una al lado de la otra.

Invitaciones de miembros

En una instancia administrada, un administrador no es la única persona que puede invitar. Tres variables de openplate-core deciden si un miembro ordinario puede invitar a alguien, y en qué condiciones. Se configuran en el servidor central, no en la aplicación. compose.core.yml y compose.full.yml pasan las tres desde .env al servidor central.

VariablePor defectoDescripción
MEMBER_INVITE_DAILY_AI_LIMITsin definir (invitaciones desactivadas)Cuántas solicitudes de IA por día UTC recibe la cuenta invitada. Configura esta y MEMBER_INVITE_ALLOWANCE_DAYS, o ninguna.
MEMBER_INVITE_ALLOWANCE_DAYSsin definir (invitaciones desactivadas)Cuántos días después del registro dura esa cuota. Configura esta y MEMBER_INVITE_DAILY_AI_LIMIT, o ninguna.
MEMBER_INVITE_LIFETIME_CAP5Cuántas invitaciones en total puede enviar un miembro en toda la historia. Un entero igual o mayor que 0. Requiere que las dos anteriores estén configuradas.

Las dos primeras van juntas o no se pone ninguna. Si defines solo una de las dos, el arranque se detiene e indica la que falta. Si no defines ninguna, que es el comportamiento por defecto, los miembros no pueden invitar a nadie y POST /v1/auth/invites responde 404 a todos. En ese caso generas cada invitación tú mismo.

Lo que otorga una invitación es el periodo de prueba. La persona invitada recibe su propia cuenta y su propio diario, además de MEMBER_INVITE_DAILY_AI_LIMIT solicitudes de IA al día durante MEMBER_INVITE_ALLOWANCE_DAYS días tras registrarse. Cuando la ventana concluye, el proxy de IA responde 403. Su diario sigue funcionando. La sincronización nunca se restringe por una cuota. Quien invita no decide nada de esto. Solo envía una dirección y nada más.

El límite cuenta los envíos, no los registros con éxito. Retirar una invitación no la devuelve. El cómputo es por cuenta, no por instancia. MEMBER_INVITE_LIFETIME_CAP=0 deja la ruta montada y deja a cada miembro sin saldo disponible. Eso es distinto a quitar la pareja de variables y retirar la ruta. Interpreta este límite junto con AI_INSTANCE_DAILY_LIMIT. La cuota diaria anterior se multiplica por cada miembro de la instancia y por este límite antes de reflejarse en la factura de tu proveedor.

Los administradores están exentos. El límite no se aplica a ellos, ni tampoco la regla de que una dirección que ya consumió una invitación de miembro no recibe una segunda. Invitan desde /admin tantas veces como quieran.

Un miembro ve en la app cuántas invitaciones le quedan. Se encuentra en Ajustes, Cuenta, bajo Invitar a alguien. Esa sección solo aparece en una instancia donde la función está activada.

Endpoints de IA personalizados

openplate-inference es un endpoint para foto del plato autohospedado y compatible con OpenAI que ejecutas en tu propio hardware con modelos de pesos abiertos, de modo que nadie necesita una clave de IA en la nube. Este, Ollama, vLLM, LM Studio o cualquier otra opción que use el protocolo chat-completions de OpenAI se conectan de la misma forma: en Ajustes → IA, añade un proveedor openai-compatible e introduce tu URL base.

  • Un endpoint local en la misma máquina (http://localhost:11434/v1 y similares) no necesita configuración: la excepción para loopback ya lo cubre.
  • Un endpoint remoto (otra máquina en tu LAN o un servidor de inferencia que hospedes tú) queda bloqueado por la CSP por defecto. Añade su origen y reinicia la app:
    bash
    echo "CSP_CONNECT_EXTRA=https://ai.example.com" >> .env
    docker compose -f compose.yml up -d
  • api.openai.com nunca es accesible desde un navegador: la API de OpenAI bloquea las solicitudes de origen cruzado. Enruta los modelos de OpenAI a través de OpenRouter en su lugar, con independencia de CSP_CONNECT_EXTRA.

IA provista por la instancia

En vez de pedir a cada visitante que aporte una clave, una instancia puede ofrecer su propio endpoint.

Antes de configurar DEFAULT_INFERENCE_API_KEY, ten en cuenta que es público: está incrustado en el HTML de la página y cualquiera que pueda abrir la app puede leerlo con «ver código fuente». La regla completa es el segundo punto a continuación. Léela primero.

Configura DEFAULT_INFERENCE_BASE_URL (más DEFAULT_INFERENCE_MODEL, y DEFAULT_INFERENCE_API_KEY si el endpoint lo necesita) y la página de ajustes de IA y la pantalla de escaneo mostrarán una opción de un toque para conectar «este openplate proporciona su propia IA». Si no lo defines, usar tu propia clave será la única vía; no se renderiza nada adicional ni se envía nada extra al navegador.

Tres reglas:

  • Debe ser una dirección a la que un NAVEGADOR pueda acceder. La foto va del dispositivo al endpoint, nunca pasa por el servidor de openplate, por lo que un nombre de host de compose como http://openplate-inference:8080/v1 no funciona. Publica el endpoint o colócalo detrás de tu proxy inverso. Su origen se añade al CSP automáticamente.
  • DEFAULT_INFERENCE_MODEL debe indicar un modelo que tu endpoint realmente sirva. El valor por defecto, openplate-plate-1, es el id que sirve openplate-inference, y se envía tal cual. Si apuntas la URL base a Ollama, vLLM o LM Studio, debes asignarle el nombre que sirve ese runtime, o todas las peticiones fallarán.
  • DEFAULT_INFERENCE_API_KEY es público. No se guarda en el servidor: está incrustado en el HTML de la página y cualquiera que pueda abrir la app puede leerlo con «ver código fuente». Esto no supone un problema para un endpoint al que solo tu hogar o tu tailnet puedan acceder. no es adecuado para una clave de un proveedor cloud de pago por uso, y no es adecuado en una instancia expuesta a internet sin una VPN, tailnet o proxy de autenticación delante. Si tu endpoint no necesita clave, déjalo sin definir.

Un valor con formato incorrecto en DEFAULT_INFERENCE_BASE_URL detiene el arranque de forma deliberada, para que un error tipográfico no parezca que «el botón simplemente no apareció».

Este ajuste predeterminado de la instancia (connectedVia: 'preset') lo define el propio operador de la instancia para cada visitante. connectedVia: 'invite' es un valor relacionado, ahora obsoleto: marcaba una fila de ajustes de IA creada por el antiguo flujo de invitación de openplate-gateway, retirado en M192. Una instancia administrada ya no escribe esa fila en absoluto: la propia cuenta incluye la cuota, y se accede al proxy de IA a través de CORE_URL, no mediante una entrada de ajustes independiente.

Conectar con OpenRouter

Ajustes → IA → Conectar con OpenRouter es un flujo OAuth (PKCE) de un solo clic que funciona solo en el navegador, sin claves que copiar y pegar. Lleva esta pestaña a la pantalla de consentimiento de OpenRouter y de vuelta; apruébalo y la clave emitida se guardará directamente en el almacenamiento local de este navegador. El servidor de openplate nunca interviene en ese proceso: nunca ve, almacena ni actúa como proxy de la clave.

  • Establece un límite de gasto mientras estés allí. La pantalla de consentimiento de OpenRouter ofrece un límite de crédito autogestionado (opcional, con intervalo de reinicio) junto al selector de cuenta. Limita el gasto máximo que la clave conectada puede realizar, con total independencia de openplate.
  • El modelo predeterminado es google/gemini-3.5-flash-lite, un modelo de pago, aproximadamente 0,001 $ por escaneo. Se eligió frente a cualquier modelo :free porque los endpoints :free de OpenRouter solo están disponibles tras activar las opciones de cuenta «puede entrenar con datos de peticiones» / «puede publicar prompts», y algunos modelos de visión gratuitos conservan los prompts durante semanas. Aún puedes elegir tú mismo un modelo :free en la lista de modelos, mediante una opción explícita e informada.
  • La introducción manual de claves funciona con todos los proveedores, OpenRouter incluido, mediante el panel «pegar una clave de API manualmente».
  • La clave conectada aparece en tu Ajustes de claves de OpenRouter con la etiqueta «Una app». Desconectar en openplate solo elimina la clave de ese dispositivo: no la revoca en OpenRouter. Revócala tú mismo desde esa página.
  • Misma garantía que cualquier otra clave: guardada únicamente en el almacenamiento local del dispositivo, excluida de la copia de seguridad/exportación JSON, nunca enviada al servidor de openplate.

Funciona desde cualquier origen seguro. La URL de devolución de llamada de OAuth se deriva en el momento de la petición a partir de window.location.origin. Nunca se codifica fija ni se registra previamente en OpenRouter. El botón funciona sin cambios en http://localhost:3000 o en tu propio dominio https://. En una dirección de LAN ordinaria con http:// falla, porque aplica una función hash a su código de un solo uso mediante la Web Crypto API, que los navegadores deshabilitan en ese contexto. En su lugar, pega una clave a mano o consulta self-hosting.md.

Un detalle si te alojas tú mismo detrás de un proxy inverso que registre las URL de petición: la URL de callback (incluido su parámetro de un solo uso state) aparecerá en tus registros de acceso como cualquier otra URL. No es un secreto (conocerla no da acceso a la clave de nadie), pero límpiala si la retención de registros te preocupa.

Edita esta página en GitHub