- Python 73.4%
- Shell 26.6%
| repos-benchmark | ||
| scripts-benchmark | ||
| .gitignore | ||
| benchmark-modelos-ollama.md | ||
| benchmark-planificacion.md | ||
| benchmark-thinking-y-agentico.md | ||
| CLAUDE.md | ||
| modelos-opencode-go.md | ||
| README.md | ||
Benchmarks de LLMs
Informes y harness para comparar modelos: los que corren en local sobre Ollama y las opciones de pago. El producto son los informes; los scripts existen para poder rehacerlos.
Informes
| Informe | Contenido |
|---|---|
benchmark-modelos-ollama.md |
Evaluación de lean-ctx + comparativa de modelos en tareas de código |
benchmark-thinking-y-agentico.md |
Efecto del thinking on/off y capacidad agéntica multi-fichero |
benchmark-planificacion.md |
Capacidad de planificar un cambio antes de tocar código |
modelos-opencode-go.md |
Precios, cupos y estrategia de consumo en OpenCode Go |
Qué mide cada harness
Son dos cosas distintas y conviene no confundirlas:
- Modelo puro —
bench_lib.py(4 tareas de código, 31 casos) ybench_hard.py(6 problemas de razonamiento con respuesta numérica exacta) llaman directamente a la API nativa de Ollama (/api/chat). Es la única vía dondethink:falsese respeta; por la capa OpenAI-compatible se ignora. - Modelo + cliente agéntico — la parte agéntica de
run_bench.shpasa por un cliente, así que el resultado incluye su prompt de sistema, su bucle de tools y su herramienta de edición.
Para que esa segunda medida sea interpretable, cada escenario agéntico se corre con los dos
clientes sobre el mismo modelo local: hermes y opencode (CLIENTS en run_bench.sh).
Con el modelo fijo, una diferencia de resultado entre clientes es atribuible al cliente. Sin esa
comparación no se puede distinguir "el modelo no supo" de "el cliente lo estropeó" — un modelo
puede emitir escapes que la herramienta de edición de un cliente corrompe y la de otro no.
opencode usa los modelos locales como ollama/<modelo>. Los modelos de pago (opencode-go/*)
quedan fuera de estos harness: son otra pregunta y se miden aparte.
El bucle es por modelo y dentro por cliente, para cargar cada modelo en VRAM una sola vez.
Antes de los clientes se hace un warmup, porque si no el primero paga la carga y el segundo no:
en granite4.1:30b eso son 47 segundos de diferencia, más que la tarea entera. Cualquier
comparación de tiempos sin ese precalentamiento mide el orden del bucle, no el cliente.
Ejecutar
Requiere hermes y opencode en el PATH y un Ollama accesible (por defecto http://condor:11434).
# un modelo suelto, razonamiento con y sin thinking
python3 scripts-benchmark/bench_hard.py qwen3.6:27b true
python3 scripts-benchmark/bench_hard.py qwen3.6:27b false
# suite completa: todos los modelos, escenarios y clientes
bash scripts-benchmark/run_bench.sh
# acotada, para iterar sin gastar una hora de GPU
MODELS="gemma4:26b" SCENARIOS="regresion silencioso" bash scripts-benchmark/run_bench.sh
# pasada de referencia para escribir un informe (~3 h): 3 muestras de todo
REPS=3 AGENT_REPS=3 nohup bash scripts-benchmark/run_bench.sh > /tmp/suite.log 2>&1 &
Variables: OLLAMA_HOST, OUT, MODELS, SCENARIOS, CLIENTS, TIMEOUT, REPS
(repeticiones de las tareas de modelo puro) y AGENT_REPS (repeticiones de cada celda agéntica).
El banco corre por defecto 4 modelos: qwen3-coder:30b, gemma4:26b, qwen3.6:27b y
laguna-xs-2.1. granite4.1:30b y north-mini-code-1.0 están en EXCLUIDOS — siguen
instalados y se reactivan poniéndolos en MODELS, pero no entran en el banco: el primero por
lento (varios timeouts de 500s), el segundo por fallar dificil y contextolargo con ambos
clientes. Si instalas un modelo nuevo y no lo añades a ninguna de las dos listas, el script
avisa al arrancar.
Estados posibles: PASA, FALLA, FALLA_TESTS_MODIFICADOS, FALLA_TEST_NO_REPRODUCE,
ERROR_CLIENTE (el cliente ni arrancó: no puntúa como fallo del modelo), NO_INSTALADO, y el
sufijo +TIMEOUT cuando el run agotó el reloj (el veredicto vale, el tiempo es un tope).
opencodeno descubre los modelos de Ollama solo: hay que declararlos a mano en~/.config/opencode/opencode.json, dentro deprovider.ollama.models. Si falta uno, opencode respondeUnknownErroren un segundo y el run sale comoERROR_CLIENTE.
Cada run guarda el diff completo de los ficheros tocados, la salida de los tests y las métricas del cliente. Los resultados agénticos salen etiquetados por cliente:
dificil | hermes | gemma4:26b | r1 | PASA | 24s | turnos=11 | out_tok=1059 | in_tok=206229
dificil | opencode | gemma4:26b | r1 | PASA | 21s | turnos=10 | out_tok=519 | in_tok=88468
Los modelos no instalados se marcan NO_INSTALADO en vez de contarse como fallo, y los errores
de red aparecen como ERR/E en vez de 0/N: un problema de infraestructura nunca debe leerse
como un fallo de calidad del modelo.
El harness NO es determinista. temperature=0 y seed=42 no bastan. En 16 corridas de
bench_lib.py gemma4:26b false, sin tocar nada de la configuración: 12 dieron 26/26 y 4
dieron 24/26 (la diferencia está siempre en basic_calculator, 8/8 u 6/8).
Se descartaron por experimento tres explicaciones: modelos residentes en VRAM, num_ctx por
defecto y el orden respecto a bench_hard.py. Lo único que correlaciona es la velocidad: las
corridas de 24/26 fueron a 126-130 tok/s y las de 26/26 a ~139, lo que apunta a un estado
distinto de la GPU. La correlación no es perfecta (una corrida a 126 tok/s dio 26/26), así que
no está confirmada como causa.
Consecuencia práctica: una sola pasada no es un resultado. Diferencias de uno o dos casos entre modelos están dentro del ruido. Para comparar de verdad hay que repetir cada medición y dar el rango, no un número. Cuidado especialmente con el patrón "3 corridas iguales, luego es determinista": eso ya llevó a una conclusión equivocada.
El presupuesto de tokens y NO_CONVERGE
Con think:true el razonamiento se descuenta de num_predict, así que un modelo puede agotarlo
entero pensando y devolver cadena vacía. El presupuesto con thinking es 16384 tokens
(NUM_PREDICT_THINKING en ambos harness), y no es un número arbitrario: en la 3090 un denso de
27B va a ~40 tok/s, o sea unos 7 minutos de reloj para una sola respuesta. Ese es el techo de
lo que tiene sentido esperar de un modelo local.
Agotarlo no es un defecto del banco: es un resultado. Los harness lo detectan por
done_reason == "length" y lo marcan NO_CONVERGE(>16384tok), declarando el presupuesto usado.
Un modelo que necesita más que eso para responder no es utilizable, y el informe lo dice en vez
de esconderlo tras un "sube el presupuesto y repite".
El caso que lo ilustra es calc_parentesis en qwen3.8-27b-ud-q4kxl (tanda del 2026-08-24):
think=OFF | calc_parentesis=10/10
think=ON | calc_parentesis=0/10 | NO_CONVERGE(>16384tok): #0 #1 #2
La misma tarea que resuelve perfecta sin thinking se vuelve un cero con él, agotando el
presupuesto las tres veces. No es que le falte capacidad ni presupuesto: es que razonando no
converge, y eso es exactamente lo que hay que reportar. laguna-xs-2.1 hace lo mismo en
tareas de código, consumiendo 137 000 caracteres sin emitir una línea.
Contraejemplo que conviene tener presente: subir el techo de 8192 a 16384 sí rescató las
tareas t2 y t5 de razonamiento en ese mismo modelo, que pasaron de truncarse a 3/3. Por eso
el umbral se subió antes de fijarlo: a 8192 el banco estaba midiendo su propio límite, a 16384
mide al modelo.
La marca solo se pone cuando el modelo agotó el presupuesto sin entregar: si lo agotó pero el código pasa igual, entregó lo que hacía falta y no se etiqueta.
NUM_PREDICT sigue existiendo como variable de entorno para experimentar, pero pisa los dos
modos a la vez (ON y OFF), así que cambiarla rompe la comparabilidad de los tiempos del modo OFF
con los informes anteriores.
VRAM y contexto en condor
La GPU son 24 GB y el servidor está configurado con una ventana de 49152 tokens (48K). Ese
valor es el techo con laguna-xs-2.1 instalado, que es el modelo más grande (20,4 GB):
num_ctx |
En VRAM | Velocidad |
|---|---|---|
| 49152 (48K) | 100 % | 178 tok/s |
| 65536 (64K) | 98 % (0,4 GB en RAM) | — |
| 98304 (96K) | 90 % (2,0 GB en RAM) | 123,7 tok/s |
| 131072 (128K) | 84 % (3,4 GB en RAM) | — |
Subir la ventana cuesta un 30 % de velocidad por cada 2 GB que se salen a RAM. Para
comprobarlo: carga el modelo y mira size_vram frente a size en /api/ps (o ollama ps);
si no coinciden, hay parte en RAM.
No confundas num_ctx con num_predict. El primero es la ventana de contexto, consume VRAM
y lo fija el servidor (48K); los harness no lo tocan. El segundo es el tope de tokens a generar
en una respuesta, se controla con NUM_PREDICT y no reserva memoria por sí mismo.
Escenarios
Repos Python con bugs sembrados a propósito, documentados en
repos-benchmark/BUGS.md y nunca en el propio código: el harness copia
el escenario entero, así que anotar el bug donde está equivale a dárselo resuelto.
La línea que lo separa todo es qué se copia al directorio de trabajo:
repos-benchmark/<escenario>/ — lo ve el modelo |
repos-benchmark/ — no lo ve |
|---|---|
| el código con los bugs | BUGS.md, dónde está cada uno |
TASK.txt, el enunciado |
oculto/<esc>/test_*.py, la batería oculta |
REGLAS.md, el contrato de negocio |
verificadores/<esc>.sh, cómo se le evalúa |
test_*.py, los tests visibles |
Dos consecuencias de diseño:
- El contrato vive en
REGLAS.md, no en el docstring de la función que lo incumple. Sigue declarado —si no, el escenario mediría adivinación— pero obliga a cruzar dos sitios. - Pasar los tests visibles no basta. El verificador corre después una batería que el modelo
no ha visto, así que un parche que satisface literalmente el test que falla no cuela. Dos
bugs (el umbral de
silenciosoy la agregación decontextolargo) no tienen ningún test visible: solo constan enREGLAS.mdy en la batería oculta.
| Escenario | Qué exige | Qué distingue |
|---|---|---|
dificil/ |
Tres bugs en cascada en tres ficheros | Tres naturalezas distintas: ValueError → magnitud absurda → orden de cálculo equivocado. El tercero solo aparece con descuento, o sea a partir de tres artículos |
regresion/ |
Arreglar slugify sin tocar truncate |
truncate tiene un comportamiento contraintuitivo pero correcto, y la batería oculta lo cubre a fondo: quien reescribe el fichero entero lo rompe. El test visible usa tres espacios, así que un parche literal no generaliza |
silencioso/ |
Dos bugs, ninguno lanza excepción | El de la media está en un fichero que el test no nombra. El del umbral de aprobado no tiene test visible: solo consta en REGLAS.md, y quien arregle lo que falla y pare ahí suspende |
testprimero/ |
Escribir el test que reproduce el fallo y luego arreglarlo | El verificador corre los tests del modelo contra el código original; si también pasan ahí, no reproducen nada. Y un parche solo para 45s, el caso que cita el enunciado, no supera la batería oculta |
plan/ |
No se arregla nada: se escribe un plan. Ver abajo | Único escenario sin veredicto automático |
contextolargo/ |
Tres bugs en un proyecto de 17 ficheros y 34 tests | Dos se manifiestan en la capa alta pero viven en extremos opuestos del proyecto. El tercero es de interacción y no tiene test visible: los informes aplican el IVA al agregado en vez de sumar totales, y con una sola reserva cuadra. Métrica clave: in_tok, que delata al que se lee el repo entero |
Se eliminó el escenario facil/ (un off-by-one): los seis modelos lo pasaban, no ordenaba nada.
Los estados posibles son PASA, FALLA, FALLA_TESTS_MODIFICADOS, FALLA_TEST_NO_REPRODUCE
(solo en testprimero/) y NO_INSTALADO. Los devuelve el verificador del escenario como código
de salida: 0, 1, 3 y 2 respectivamente.
El escenario plan
Los demás escenarios miden si el modelo arregla. Este mide si sabe planificar, que es una capacidad distinta y no correlaciona: en la primera pasada, el modelo con mejor resultado agéntico (10/10) sacó 0/12 planificando, porque no llegó a leer ni un fichero del proyecto.
Se le pide un plan para añadir cancelaciones con reembolso escalonado al proyecto de
contextolargo/, con los bugs ya arreglados — planificar sobre código roto mediría otra cosa.
El proyecto se monta solo desde el original, así que no hay copias que diverjan.
bash scripts-benchmark/gen_planes.sh # genera y anonimiza los planes
Cómo se evalúa
No hay tests que pasen o fallen: hay que juzgar un texto. Para que el juicio no sea impresionista:
- La rúbrica está cerrada de antemano en
repos-benchmark/plan/RUBRICA.md: 12 criterios, la mayoría objetivos (¿mencionaesta_libre? sí/no), más tres penalizaciones. - Se puntúa a ciegas. El script deja los planes en
ciego_A.txt…, barajados por hash y sin el nombre del modelo. El mapeo va amapeo_NO_MIRAR.txt: no lo abras hasta haber puntuado. - Verifica cada afirmación contra el código. Los planes citan funciones, atributos y números de línea, y se equivocan: hay que comprobarlo, no darlo por bueno.
Los dos criterios que de verdad separan son las dependencias ocultas: al cancelar una reserva
hay que filtrarla en disponibilidad.esta_libre (o la habitación queda bloqueada para siempre) y
en informes (o las cifras quedan infladas). Un plan puede cubrir todo lo obvio y seguir siendo
peligroso si omite esos dos.
Lo único automático es la comprobación de que el modelo no editó ficheros, ya que se le pidió explícitamente que no lo hiciera. Desobedecerlo es en sí un dato de fiabilidad.