Synsemadocsv0.6.xENES

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 .envPropósitoDefault
ANTHROPIC_API_KEY / OPENAI_API_KEY / MINIMAX_API_KEY / DEEPSEEK_API_KEYAPI key; su presencia auto-selecciona el proveedor— (offline si falta)
SYNSEMA_LLM_PROVIDERForzar anthropic / openai / minimax / deepseekauto
SYNSEMA_LLM_MODELId de modeloclaude-sonnet-4-6 / gpt-4o / MiniMax-M3 / deepseek-chat
SYNSEMA_LLM_MAX_TOKENSTope de salida4096
SYNSEMA_LLM_BASE_URLBase del endpoint (apuntar a un server local/compatible)oficial
SYNSEMA_LLM_TIMEOUTTimeout 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 → default60
SYNSEMA_LLM_HTTP_STREAMTransporte 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 raros1 (on)
SYNSEMA_LLM_BUDGET_PER_IDENTITYid=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_BUDGETPresupuesto 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.

KnobPropósitoDefault
SYNSEMA_LLM_MODELRuta, modelo:tag o org/repo (obligatorio para local)—
SYNSEMA_INFER_BACKENDrust = el motor propio; cualquier otra cosa = candlecandle
SYNSEMA_INFER_ARCHDEFDirectorio de archivos <arch>.archdef — arquitecturas sin recompilar (Inferencia local)—
SYNSEMA_LLM_CTXVentana de contexto (capada al límite del propio GGUF)4096
SYNSEMA_LLM_THREADSThreads de CPU para la inferenciatodos los cores
SYNSEMA_LLM_TEMPERATURE0 = greedy/determinista; >0 = muestreo (seed fija)0
SYNSEMA_LLM_MAX_CONCURRENTInstancias máximas del modelo; 1 serializa llamadas concurrentes bajo serve1
SYNSEMA_LLM_STREAM_BUFFERChunks en vuelo entre la generación y la emisión de llm_stream32

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:

MotorArquitecturas
candle (default)llama, qwen2, qwen3 — compiladas en el binario
SYNSEMA_INFER_BACKEND=rustllama, 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)declaraVerificado
Qwen2.5 Instruct 0.5B/3Bqwen2✅
Qwen3 0.6Bqwen3✅ modelo razonador — emite <think>…</think> crudo; presupuestá SYNSEMA_LLM_MAX_TOKENS para eso
Qwen3 4B Instruct-2507qwen3✅ (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 270Mgemma3✅ sólo con el motor rust — token a token lo mismo que genera Ollama para el mismo prompt
Mistral 7B Instruct v0.3llama✅ su template [INST] se detecta solo
Llama 3.2 1B Instructllama✅ template llama3 detectado solo
SmolLM2 135M Instructllama✅ chatml
DeepSeek-R1-Distill-Qwen 1.5Bqwen2✅ corre en modo plain; filtrá los tags <think> en tu código
TinyLlama 1.1B Chatllama⚠️ 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.