Skip to main content

modelduel

Dos modelos, una tarea, los mismos tests. modelduel enfrenta a dos modelos de IA (o a una liga de hasta seis) con un problema de programación, ejecuta tus tests sobre el código de cada uno y te da un informe con quién acierta, cuánto tarda y cuánto cuesta.

PyPI CI Licencia MIT Python 3.12

Web: https://bertmarti.github.io/modelduel/ · Ver un duelo en directo (reproducción pregrabada, sin claves) · Informe de demostración: https://bertmarti.github.io/modelduel/demo/ · Clasificación pública: https://bertmarti.github.io/modelduel/leaderboard/

Documentación: Guía de uso (instalación, duelos reales, crear tus tareas, liga, reintentos, reanudación y leer el informe) · Registro de cambios · Cómo contribuir

  #   Contendiente     Tareas   Tests   Tiempo   Tokens (ent/sal)        Coste
  ----------------------------------------------------------------------------
  1   A replay:alfa       2/3   31/32   10,1 s          1.401/703   0,0017 USD
  2   C replay:gamma      2/3   30/32    3,7 s          1.314/426   0,0003 USD
  3   B replay:beta       2/3   30/32   28,1 s        1.373/2.132   0,0248 USD
  (precios ficticios de demostración)

Por qué

Los rankings de modelos miden tareas que no son las tuyas. La forma honesta de elegir es hacer tu propia comparativa: el mismo enunciado, los mismos tests y los números a la vista. modelduel convierte ese ejercicio en una orden.

Novedades de v0.7.0

  • Señal de vida: si una llamada a un proveedor real tarda más de unos 10 s (por ejemplo, tras «reintento 2/3» con un 503), modelduel escribe por stderr cada 10 s ·· esperando a gemini:gemini-3.5-flash (tarea slugify)… 30 s. Si parece colgado, es que espera respuesta; Ctrl+C guarda lo hecho y --resume lo retoma.
  • El límite por defecto de las peticiones HTTP baja de 180 s a 90 s (MODELDUEL_HTTP_TIMEOUT lo cambia).

Antes (v0.6.0)

  • Informe en Markdown: modelduel report results.json --format md --out DIR (y run ... --format html,md) genera informe.md, listo para pegar en un PR o un issue de GitHub: marcador, veredicto con su criterio, tabla por tarea y por contendiente, coste y avisos. Todo lo que viene de results.json se trata como dato no fiable y se escapa (barras de tabla, backticks, HTML, enlaces, menciones y referencias).
  • Imagen social (og:image, twitter:card) en la web.

v0.5.0

  • Duelo en directo en la web: un duelo pregrabado que se reproduce en el navegador (código escribiéndose, tests cayendo, marcador), con pausa y sin servidor ni claves.
  • Informes y portada más legibles: enlaces con foco visible, «▲ mejor» / «▼ peor» con glifo y texto, y una navegación más corta.

Antes (v0.2.0)

  • Liga de 2 a 6 contendientes con --model repetible: clasificación, comparativa, matriz por tarea y paleta de seis colores con contraste AA (siempre con su letra A-F).
  • Reintentos ante HTTP 429/5xx y cortes de conexión, con espera exponencial y Retry-After (--retries).
  • Guardado incremental y --resume: un corte no pierde lo ya hecho y el duelo se continúa con la misma orden.
  • Publicación en PyPI (pip install "modelduel[pytest]") y modelduel demo: prueba sin claves ni coste y demo --copy para empezar desde una plantilla.

Detalle en el registro de cambios.

Instalación

Necesitas Python 3.12 o superior. modelduel solo usa la biblioteca estándar; pytest hace falta para ejecutar los tests de las tareas.

pip install "modelduel[pytest]"     # o: pip install modelduel pytest
modelduel demo                       # prueba sin claves ni coste, con los ejemplos incluidos

El paquete está en PyPI. Para probar la última versión sin publicar, instálalo desde GitHub: pip install git+https://github.com/BertMarti/modelduel pytest.

modelduel demo enfrenta a tres modelos ficticios (respuestas grabadas) y escribe el informe en modelduel-demo/; modelduel demo --copy MIS-EJEMPLOS copia las tareas y respuestas de ejemplo para que las uses como plantilla.

Para desarrollar:

git clone https://github.com/BertMarti/modelduel
cd modelduel
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
ruff check . && ruff format --check . && pytest --cov

Uso rápido

# Demo sin claves ni coste, con respuestas grabadas: una liga de tres contendientes
modelduel run examples/tasks --model replay:alfa --model replay:beta --model replay:gamma --out runs/demo

# Dos modelos reales, tres ejecuciones por tarea
export GEMINI_API_KEY=...        # PowerShell: $env:GEMINI_API_KEY = "..."
export OPENAI_API_KEY=...
modelduel run examples/tasks --a gemini:<modelo> --b openai:<modelo> --runs 3 --prices precios.json --out runs/duelo

# Regenerar el HTML a partir de los resultados
modelduel report runs/duelo/results.json --out runs/duelo

# Informe en Markdown para pegar en un PR o un issue (informe.md)
modelduel report runs/duelo/results.json --format md --out runs/duelo

# Ver las tareas de una carpeta
modelduel list-tasks examples/tasks
Opción Qué hace
--a, --b Contendientes A y B en formato proveedor:modelo (un duelo de dos, como en v0.1.0).
--model, -m Contendiente proveedor:modelo; repítelo para una liga de 2 a 6 (se pueden mezclar con --a/--b: van primero A y B, después cada --model). Los nombres repetidos se rechazan; para repetir un modelo usa --runs.
--runs N Ejecuciones por tarea (1 por defecto). Una sola ejecución es una señal débil.
--timeout S Límite en segundos para los tests de cada respuesta (20 por defecto).
--retries N Reintentos ante HTTP 429/500/502/503/504 y cortes de conexión, con espera exponencial y respetando Retry-After (3 por defecto; 0 los desactiva).
--resume Continúa el duelo de --out saltando los intentos ya terminados (ver «Cortes y reanudación»).
--format F Formatos del informe: html (por defecto), md o html,md. results.json se guarda siempre. También lo acepta report.
--prices f.json Tabla de precios adicional (ver «Coste»).
--replays DIR Carpeta de respuestas grabadas para replay (por defecto, replays/ junto a la carpeta de tareas).
--out DIR Carpeta donde se escriben results.json y el informe (index.html, informe.md o los dos, según --format). Se comprueba antes de llamar a las APIs.

Códigos de salida: 0 duelo completado (aunque los modelos fallen tests), 1 no se pudieron escribir los resultados, 2 error de uso o de configuración (argumentos, tareas, proveedores, precios, results.json) y 130 interrumpido con Ctrl+C.

Liga. Con tres o más contendientes el informe cambia de forma: una clasificación (tareas resueltas, luego tests superados y luego coste, menos es mejor; los empates comparten posición), una comparativa con una barra fina por contendiente y una matriz por tarea. Cada contendiente tiene una letra (A a F) y un color con contraste AA sobre el fondo oscuro (lima, rosa, cian, ámbar, violeta y coral), pero la letra siempre acompaña al color. Con dos contendientes sigue el informe enfrentado de siempre. El coste solo desempata si todos tienen precio y en la misma moneda.

Cortes y reanudación. results.json se reescribe de forma atómica (archivo temporal y reemplazo) tras cada intento, así que un corte —Ctrl+C, un apagón, una API caída— no pierde lo ya hecho, que además queda reflejado en un informe parcial marcado como «Duelo incompleto». Para continuar, repite la misma orden añadiendo --resume: se saltan los intentos terminados, se repiten los que acabaron en «error del proveedor» y se avisa si algo no coincide (tareas modificadas, otro límite de tiempo, otro número de ejecuciones). Si los contendientes son otros, la orden se detiene con un error. Sin --resume, modelduel se niega a sobrescribir un duelo incompleto.

El informe index.html es un único archivo autocontenido: CSS en línea, gráficas SVG, sin JavaScript y con hoja de impresión clara.

Formato de una tarea

Una tarea es una carpeta:

examples/tasks/slugify/
├── task.md        # enunciado para el modelo: pide una función concreta con nombre y firma
├── test_task.py   # tests pytest que importan de `solution`
└── meta.toml      # opcional: title, difficulty, order
# test_task.py
from solution import slugify


def test_acentos():
    assert slugify("Canción de Añoranza") == "cancion-de-anoranza"
# meta.toml
title = "Convertir un texto en slug"
difficulty = "fácil"
order = 1

modelduel añade al enunciado la instrucción de responder con un único bloque de código Python, extrae el primer bloque python (o el único bloque de la respuesta), lo guarda como solution.py junto a una copia de test_task.py en un directorio temporal y ejecuta python -m pytest con límite de tiempo. Los resultados se leen del XML JUnit de pytest. Se distinguen estos casos: tests ejecutados, sin bloque de código, error al importar, tiempo agotado, error al ejecutar (p. ej. una tarea en la que pytest no encuentra tests) y error del proveedor. La salida de pytest se recorta conservando el principio y el final, donde está el resumen.

Los archivos de la tarea se leen en UTF-8 (se acepta el BOM de los editores de Windows) y un test_task.py con errores de sintaxis se rechaza antes de llamar a los modelos.

Las tres tareas de ejemplo son originales y de dificultad creciente: slugify, merge_intervals y parse_duration.

Proveedores y variables de entorno

Especificación Qué usa Variables
replay:<nombre> Respuestas grabadas en examples/replays/<nombre>/<tarea>.md, con tokens y latencia en un front-matter. Sin red. —
gemini:<modelo> API REST generateContent de Google Generative Language. GEMINI_API_KEY (obligatoria), GEMINI_BASE_URL (opcional)
openai:<modelo> Cualquier API compatible con Chat Completions de OpenAI: OpenAI, OpenRouter, Ollama… OPENAI_API_KEY, OPENAI_BASE_URL (por defecto https://api.openai.com/v1)
omniroute:<modelo> OmniRoute, router local compatible con OpenAI (omniroute serve). OMNIROUTE_BASE_URL (por defecto http://localhost:20128/v1), OMNIROUTE_API_KEY (opcional)
  • OpenRouter: OPENAI_BASE_URL=https://openrouter.ai/api/v1 y tu clave de OpenRouter en OPENAI_API_KEY.
  • Ollama: OPENAI_BASE_URL=http://localhost:11434/v1. En servidores locales la clave no es obligatoria.
  • MODELDUEL_HTTP_TIMEOUT cambia el límite de las peticiones HTTP (90 s por defecto; antes, 180 s). Si una llamada tarda más de unos 10 s, modelduel avisa cada 10 s por stderr («esperando a …»).

Las claves se leen solo de variables de entorno: nunca de archivos del repositorio, nunca en la URL (Gemini recibe la clave en una cabecera) y nunca en los mensajes de error ni en el informe. Tampoco llegan al subproceso que ejecuta el código de los modelos.

Formato de una respuesta grabada (examples/replays/alfa/slugify.md):

---
input_tokens: 412
output_tokens: 188
latency_s: 2.84
---
```python
def slugify(text: str, separator: str = "-") -> str:
    ...
```

Coste

coste = entrada / 1e6 × tarifa_entrada + salida / 1e6 × tarifa_salida

Las tarifas son por millón de tokens. Como los precios reales cambian a menudo, modelduel no trae precios reales: los pones tú con --prices:

{
  "_nota": "Precios por millón de tokens. Compruébalos en la web del proveedor.",
  "openai:mi-modelo": { "input": 0.15, "output": 0.60, "currency": "USD" },
  "gemini:otro-modelo": { "input": 0.10, "output": 0.40, "currency": "EUR" }
}

Se busca primero por proveedor:modelo y después solo por modelo. Si un modelo no tiene precio, o el proveedor no devuelve tokens, el coste es «sin datos»: nunca se inventa. Los precios integrados de replay:alfa y replay:beta son ficticios y el informe lo indica.

Clasificación pública

modelduel leaderboard results/ --out site/leaderboard agrega los results.json de una carpeta (uno por duelo o liga) y genera una página estática, sin JavaScript, con la clasificación por modelo y el histórico de duelos con enlace a cada informe. El CI la publica en la web a partir de results/, que trae tres resultados de ejemplo con respuestas grabadas; la guía explica cómo añadir resultados reales.

Más documentación

  • docs/USO.md: guía para personas usuarias, paso a paso y con solución de problemas.
  • CHANGELOG.md: qué trae cada versión.
  • CONTRIBUTING.md: entorno, comandos, ramas, commits, cómo añadir un proveedor, una tarea o un color a la liga, y cómo publicar una versión.

Estructura

src/modelduel/
├── cli.py              # run, report, leaderboard, list-tasks, demo
├── tasks.py            # carga de tareas y recuento de tests
├── extract.py          # extracción del bloque de código
├── runner.py           # prompt + ejecución de pytest en temporal con límite
├── duel.py             # orquestación del duelo
├── resume.py           # reanudación de un duelo interrumpido
├── demo.py             # localiza los ejemplos incluidos (modelduel demo)
├── results.py          # results.json y resumen del marcador
├── pricing.py          # tarifas y fórmula de coste
├── providers/          # replay, gemini, openai_compat (openai y omniroute)
├── leaderboard.py      # clasificación pública: agregado de results/*.json y página estática
└── report/             # informe HTML (duelo de dos y liga de 3 a 6, string.Template + SVG) y Markdown (`markdown.py`)
docs/USO.md             # guía de uso para personas usuarias
CHANGELOG.md            # registro de cambios (Keep a Changelog)
examples/tasks/         # tareas originales de ejemplo
examples/replays/       # respuestas grabadas de alfa, beta y gamma
site/                   # web del proyecto (la demo se genera en el CI)
tests/                  # pytest, sin llamadas de red

Stack

  • Python 3.12, solo biblioteca estándar en ejecución (argparse, urllib, string.Template, xml.etree, tomllib).
  • pytest para los tests y para ejecutar las tareas; ruff para lint y formato.
  • GitHub Actions: CI en Ubuntu y Windows, y despliegue en GitHub Pages de la web y del informe de demostración.

Cómo se ha construido

Este proyecto lo ha desarrollado un equipo de agentes de IA (Claude Code), cada uno en su rama y con pull requests, supervisado por Alberto Martínez, que revisa y fusiona todo:

  • v0.1.0: lead, builder y qa con Claude Opus; la documentación (docs), con Claude Sonnet.
  • v0.2.0: todo con Claude Sonnet, guiado por issues del hito y un pull request por issue.
  • OpenCode estaba previsto para la documentación, pero no pudo ejecutarse en modo autónomo, así que la escribe Claude Code. Las decisiones y el estado del proyecto están en MEMORY.md y las reglas del equipo en AGENTS.md.

Licencia

MIT.

Metadata

Release files for modelduel 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for modelduel 0.7.0
File Size Uploaded
modelduel-0.7.0.tar.gz 132.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for modelduel 0.7.0
File Interpreter ABI Platform
modelduel-0.7.0-py3-none-any.whl Python 3 none any Details

Total release size: 213.9 kB

Release files / modelduel-0.7.0.tar.gz

Download URL modelduel-0.7.0.tar.gz
Size 132.6 kB
Tags Source
SHA-256 checksum
How to use checksums
72737c9a1a45b842a5e7c13221f7db8c682e5d36ce6ead1100c22c28dbb44e39
BLAKE2b-256 checksum
How to use checksums
6ba97153fdf0a743ce42ef4ebe8cca5c7440cd82f6e5caa2c719922160de335e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / modelduel-0.7.0-py3-none-any.whl

Download URL modelduel-0.7.0-py3-none-any.whl
Size 81.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
462cec17671219ba51e80f6f0c17524203f4e8c1301d8d0ebf1a0076ec0a3f6d
BLAKE2b-256 checksum
How to use checksums
80ea8be5a6179b8958ec9e9be65b5d8bd86e293574f77d0c62ce4457b02c689d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.0 This release

2 release files

0.6.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page