LLM
Config de proveedor
El proveedor lo selecciona y configura el runtime, no el programa — el .syn nunca nombra un host ni una clave, así que no puede redirigir la llamada ni filtrar la clave. Cada knob resuelve environ del proceso > .env > default.
.env (gitignoreado)§
SYNSEMA_LLM_PROVIDER=deepseek
DEEPSEEK_API_KEY=sk-...
La clave llega al runtime sin entrar al environ del proceso — ningún proceso hijo ni programa puede leerla (el programa necesitaría require secret, y aun así la ve redactada). No hace falta export/source.
Knobs§
Env var / entrada .env | Propósito | Default |
|---|---|---|
ANTHROPIC_API_KEY / OPENAI_API_KEY / MINIMAX_API_KEY / DEEPSEEK_API_KEY | API key; su presencia auto-selecciona el proveedor | — (offline si falta) |
SYNSEMA_LLM_PROVIDER | Forzar anthropic / openai / minimax / deepseek | auto |
SYNSEMA_LLM_MODEL | Id de modelo | claude-sonnet-4-6 / gpt-4o / MiniMax-M3 / deepseek-chat |
SYNSEMA_LLM_MAX_TOKENS | Tope de salida | 4096 |
SYNSEMA_LLM_BASE_URL | Base del endpoint (apuntar a un server local/compatible) | oficial |
SYNSEMA_LLM_TIMEOUT | Timeout HTTP en segundos de los providers de red. Con el transporte streaming (default) mide silencio entre bytes — cada chunk lo renueva — así que una generación de minutos fluye y un host muerto sigue fallando rápido. Inválido/≤0 → default | 60 |
SYNSEMA_LLM_HTTP_STREAM | Transporte SSE interno de los providers de red (las ops del lenguaje siguen devolviendo texto completo; llm_stream emite chunks reales). 0/false → camino no-stream clásico, escape para proxies raros | 1 (on) |
SYNSEMA_LLM_BUDGET_PER_IDENTITY | id=N,id2=M (engine v0.6.20+, serve): techos de tokens por identidad, para la identidad que devuelve la task de auth; un contador del proceso por identidad; al llegar, las ops de esa identidad degradan a [llm budget exceeded for identity …], las demás siguen | — |
SYNSEMA_LLM_BUDGET | Presupuesto de tokens LLM por proceso (input + output, todas las ops). Al tope, toda op degrada al marker [llm budget exceeded: used N of M tokens] — sin error, sin llamada de red, un aviso por stderr. El consumo se lee con llm_usage() (ver Primitivas LLM). Inválido/0 → sin presupuesto, con aviso | — (ilimitado) |
También podés forzar el proveedor por corrida: synsema run app.syn --provider anthropic (flag > env > .env).
El proveedor es lo que nombra un techo con --labels (motor v0.6.46+). Con las etiquetas prendidas, un dato privado llega a un modelo sólo por un ítem del techo llm=<principal>@<host>, y el host es el del proveedor configurado: api.anthropic.com, api.openai.com, api.deepseek.com, api.minimax.io, local para el proveedor embebido (y para uno sin configurar), o el host de SYNSEMA_LLM_BASE_URL sin el puerto (llm=customers@localhost para la configuración de Ollama de abajo). Cambiar estas perillas cambia qué ítem tiene que escribir el host — ver Primitivas LLM § Datos privados a un modelo.
El timeout ya no limita las generaciones largas. Los providers de red piden la respuesta a la API en modo streaming y la rearman internamente, así que el read-timeout detecta *conexiones muertas* (60s de silencio real) en vez de limitar la duración total de la generación. Verificado en vivo: una generación de 177s / 68 KB (MiniMax-M3, modelo razonador) completa donde la ventana fija de 60s la mataba. Con SYNSEMA_LLM_HTTP_STREAM=0 el timeout limita la llamada completa, como antes. Local / on-prem (100% privado)§
Cualquier server compatible con OpenAI (Ollama, LM Studio, vLLM):
SYNSEMA_LLM_PROVIDER=openai
SYNSEMA_LLM_BASE_URL=http://localhost:11434/v1 # Ollama
SYNSEMA_LLM_MODEL=llama3.1
OPENAI_API_KEY=ollama # cualquier valor no vacío
Proveedor local embebido (local) — un modelo dentro del proceso, cero red§
Los binarios oficiales ya lo traen: el runtime corre un GGUF cuantizado dentro del proceso de Synsema, sin server, sin API key y sin socket — el único proveedor que funciona bajo un deny net total. Siempre explícito, jamás auto-seleccionado.
Desde v0.6.27, SYNSEMA_LLM_MODEL acepta tres formas, y ninguna descarga nada:
SYNSEMA_LLM_PROVIDER=local
SYNSEMA_LLM_MODEL=/models/qwen2.5-3b-instruct-q4_k_m.gguf # 1. una ruta a un .gguf
SYNSEMA_LLM_MODEL=qwen3:0.6b # 2. un modelo:tag que ya está en el cache de Ollama
SYNSEMA_LLM_MODEL=org/repo # 3. un repo que ya está en el cache de Hugging Face
Si ya usás Ollama, listo — nada que bajar, nada que convertir. Ollama guarda blobs GGUF planos en un store direccionado por contenido, así que Synsema lee el archivo directo y el sha256 de los pesos le sale gratis. ollama no necesita estar corriendo. Un nombre que no está lista los tres lugares donde buscó, con rutas reales.
| Knob | Propósito | Default |
|---|---|---|
SYNSEMA_LLM_MODEL | Ruta, modelo:tag o org/repo (obligatorio para local) | — |
SYNSEMA_INFER_BACKEND | rust = el motor propio; cualquier otra cosa = candle | candle |
SYNSEMA_INFER_ARCHDEF | Directorio de archivos <arch>.archdef — arquitecturas sin recompilar (Inferencia local) | — |
SYNSEMA_LLM_CTX | Ventana de contexto (capada al límite del propio GGUF) | 4096 |
SYNSEMA_LLM_THREADS | Threads de CPU para la inferencia | todos los cores |
SYNSEMA_LLM_TEMPERATURE | 0 = greedy/determinista; >0 = muestreo (seed fija) | 0 |
SYNSEMA_LLM_MAX_CONCURRENT | Instancias máximas del modelo; 1 serializa llamadas concurrentes bajo serve | 1 |
SYNSEMA_LLM_STREAM_BUFFER | Chunks en vuelo entre la generación y la emisión de llm_stream | 32 |
SYNSEMA_LLM_MAX_TOKENS aplica como siempre.
Qué arquitecturas corren§
La arquitectura es el string general.architecture del header del GGUF, no la marca del modelo. Cuáles corren depende del motor — la diferencia, y cómo sumar una vos mismo, están en Inferencia local y arquitecturas:
| Motor | Arquitecturas |
|---|---|
candle (default) | llama, qwen2, qwen3 — compiladas en el binario |
SYNSEMA_INFER_BACKEND=rust | llama, qwen2, qwen3, gemma3 — cada una es un archivo que podés leer, reemplazar o sumar |
Cualquier otra falla con un [local error: …] claro que nombra el motor que sí puede correrla. Muchas familias populares se convierten a GGUF declarando uno de esos nombres, así que la cobertura real es más amplia de lo que sugiere la lista. Familias verificadas en vivo (sonda = cargar + pregunta + respuesta coherente + corte limpio):
| Modelo (GGUF sondeado) | declara | Verificado |
|---|---|---|
| Qwen2.5 Instruct 0.5B/3B | qwen2 | ✅ |
| Qwen3 0.6B | qwen3 | ✅ modelo razonador — emite <think>…</think> crudo; presupuestá SYNSEMA_LLM_MAX_TOKENS para eso |
| Qwen3 4B Instruct-2507 | qwen3 | ✅ (GGUF de 2.5 GB, corrió en 8 GB de RAM). La 4B Thinking-2507 también corre pero piensa miles de tokens por respuesta — inviable en CPU; preferí la variante Instruct |
| Gemma 3 270M | gemma3 | ✅ sólo con el motor rust — token a token lo mismo que genera Ollama para el mismo prompt |
| Mistral 7B Instruct v0.3 | llama | ✅ su template [INST] se detecta solo |
| Llama 3.2 1B Instruct | llama | ✅ template llama3 detectado solo |
| SmolLM2 135M Instruct | llama | ✅ chatml |
| DeepSeek-R1-Distill-Qwen 1.5B | qwen2 | ✅ corre en modo plain; filtrá los tags <think> en tu código |
| TinyLlama 1.1B Chat | llama | ⚠️ carga y corre, pero su template zephyr no se reconoce → cae a plain y se comporta como modelo base |
Una arch soportada carga y corre; la usabilidad como chat además necesita un template reconocido (chatml / llama3 / [INST] / gemma; si no, fallback plain). Los GGUF de phi y glm se siguen rechazando.
Si corriste un GGUF de llama, Mistral o Gemma antes de v0.6.27, sus respuestas estaban mal. Esos archivos guardan rangos de token, no log-probabilidades, y el tokenizer los segmentaba como si lo fueran: The capital of France is entraba al modelo como once fragmentos en vez de cinco palabras. Nada fallaba — el modelo simplemente respondía mal. Arreglado en v0.6.27. La familia BPE (qwen, llama 3) nunca estuvo afectada. Qué quedó configurado de verdad — synsema llm status§
Imprime los modelos que ya están en disco y las arquitecturas del motor que de verdad va a correr, cada una con su origen y su sha. --json trae lo mismo bajo la clave inference, con el sha completo, así un script puede comparar dos corridas en vez de leer prosa:
Modelos locales ya descargados (2):
gemma3:270m ollama:gemma3:270m sha256:735af2139dc6
qwen3:0.6b ollama:qwen3:0.6b sha256:7f4030143c1c
Usá SYNSEMA_LLM_MODEL=<nombre> — no se descarga nada.
Arquitecturas que corre el backend `rust` (4):
gemma3 (20 pasos por capa, en el binario, sha b935fcfab2d6)
llama (16 pasos por capa, en el binario, sha a580fa45fdb6)
qwen2 (19 pasos por capa, en el binario, sha fbba11641175)
qwen3 (18 pasos por capa, ./archdefs/qwen3.archdef, sha 5b8d9db76a68)
Definiciones del operador: ./archdefs
En un binario sin la feature, SYNSEMA_LLM_PROVIDER=local avisa por stderr y queda offline — jamás degrada en silencio a otro proveedor.
Límites honestos, y compilar para velocidad§
Medido (Qwen2.5 Q4_K_M, sin recompilar): generación ~13 tok/s (0.5B) / ~5 tok/s (3B); prefill ~12 tok/s — pensado para prompts cortos (cientos de tokens, no miles). La carga del modelo se paga una vez por proceso (~6s / ~19s): bajo serve, la primera request carga y el resto reusa. RAM con candle: ~1GB (0.5B) / ~2.4GB (3B); con el motor rust, aproximadamente lo que pesa el .gguf (el archivo se mapea y los pesos grandes se quedan cuantizados).
Recompilar para velocidad sólo vale la pena con candle. candle elige sus kernels cuantizados AVX2 en tiempo de compilación (#[cfg(target_feature = "avx2")]), y el target x86-64 default de Rust no habilita AVX2 — así que un build simple corre el camino escalar. Ganancia medida: prefill ~3,1× en un 0.5B (~11 → ~35 tok/s). El motor rust no lo necesita: despacha según la CPU que encuentra (AVX / AVX2+FMA / AVX-512 / NEON), así que el binario que bajaste ya usa las instrucciones de tu máquina.
# sólo si te quedás en candle:
RUSTFLAGS="-C target-cpu=native" cargo install --path crates/synsema-cli --features llm-local --force
native ata el binario a la CPU de esa máquina — ideal para tu propio VPS; para un binario que distribuís, usá -C target-cpu=x86-64-v3 (AVX2+FMA, CPUs x86 desde ~2015).
---
El egress al host configurado es parte de require llm — no un grant net aparte (el proveedor embebido no necesita egress en absoluto). Offline (sin clave), las ops devuelven placeholders; ramificá con llm_available(). Para saltarte las ops integradas y pegarle a la API vos mismo, mirá API del proveedor directa.