modelduel 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. modelduel-0.6.0/.gitignore +20 -0
  2. modelduel-0.6.0/CONTRIBUTING.md +106 -0
  3. modelduel-0.6.0/LICENSE +21 -0
  4. modelduel-0.6.0/PKG-INFO +277 -0
  5. modelduel-0.6.0/README.md +245 -0
  6. modelduel-0.6.0/docs/USO.md +808 -0
  7. modelduel-0.6.0/docs/specs/v0.4.md +168 -0
  8. modelduel-0.6.0/docs/specs/v0.5.md +66 -0
  9. modelduel-0.6.0/docs/specs/v0.6.md +131 -0
  10. modelduel-0.6.0/examples/replays/alfa/merge_intervals.md +21 -0
  11. modelduel-0.6.0/examples/replays/alfa/parse_duration.md +28 -0
  12. modelduel-0.6.0/examples/replays/alfa/slugify.md +17 -0
  13. modelduel-0.6.0/examples/replays/beta/merge_intervals.md +37 -0
  14. modelduel-0.6.0/examples/replays/beta/parse_duration.md +43 -0
  15. modelduel-0.6.0/examples/replays/beta/slugify.md +33 -0
  16. modelduel-0.6.0/examples/replays/gamma/merge_intervals.md +18 -0
  17. modelduel-0.6.0/examples/replays/gamma/parse_duration.md +21 -0
  18. modelduel-0.6.0/examples/replays/gamma/slugify.md +17 -0
  19. modelduel-0.6.0/examples/tasks/merge_intervals/meta.toml +3 -0
  20. modelduel-0.6.0/examples/tasks/merge_intervals/task.md +30 -0
  21. modelduel-0.6.0/examples/tasks/merge_intervals/test_task.py +41 -0
  22. modelduel-0.6.0/examples/tasks/parse_duration/meta.toml +3 -0
  23. modelduel-0.6.0/examples/tasks/parse_duration/task.md +27 -0
  24. modelduel-0.6.0/examples/tasks/parse_duration/test_task.py +42 -0
  25. modelduel-0.6.0/examples/tasks/slugify/meta.toml +3 -0
  26. modelduel-0.6.0/examples/tasks/slugify/task.md +24 -0
  27. modelduel-0.6.0/examples/tasks/slugify/test_task.py +38 -0
  28. modelduel-0.6.0/pyproject.toml +89 -0
  29. modelduel-0.6.0/src/modelduel/__init__.py +3 -0
  30. modelduel-0.6.0/src/modelduel/__main__.py +3 -0
  31. modelduel-0.6.0/src/modelduel/cli.py +535 -0
  32. modelduel-0.6.0/src/modelduel/demo.py +26 -0
  33. modelduel-0.6.0/src/modelduel/duel.py +169 -0
  34. modelduel-0.6.0/src/modelduel/extract.py +51 -0
  35. modelduel-0.6.0/src/modelduel/leaderboard.py +238 -0
  36. modelduel-0.6.0/src/modelduel/pricing.py +95 -0
  37. modelduel-0.6.0/src/modelduel/providers/__init__.py +42 -0
  38. modelduel-0.6.0/src/modelduel/providers/base.py +280 -0
  39. modelduel-0.6.0/src/modelduel/providers/gemini.py +91 -0
  40. modelduel-0.6.0/src/modelduel/providers/openai_compat.py +127 -0
  41. modelduel-0.6.0/src/modelduel/providers/replay.py +76 -0
  42. modelduel-0.6.0/src/modelduel/report/__init__.py +37 -0
  43. modelduel-0.6.0/src/modelduel/report/html.py +499 -0
  44. modelduel-0.6.0/src/modelduel/report/leaderboard.html +105 -0
  45. modelduel-0.6.0/src/modelduel/report/league.html +64 -0
  46. modelduel-0.6.0/src/modelduel/report/league.py +293 -0
  47. modelduel-0.6.0/src/modelduel/report/markdown.py +198 -0
  48. modelduel-0.6.0/src/modelduel/report/style.css +223 -0
  49. modelduel-0.6.0/src/modelduel/report/template.html +74 -0
  50. modelduel-0.6.0/src/modelduel/results.py +173 -0
  51. modelduel-0.6.0/src/modelduel/resume.py +89 -0
  52. modelduel-0.6.0/src/modelduel/runner.py +372 -0
  53. modelduel-0.6.0/src/modelduel/tasks.py +157 -0
  54. modelduel-0.6.0/tests/__init__.py +0 -0
  55. modelduel-0.6.0/tests/conftest.py +64 -0
  56. modelduel-0.6.0/tests/js/demo.test.cjs +168 -0
  57. modelduel-0.6.0/tests/js/ui.test.cjs +178 -0
  58. modelduel-0.6.0/tests/test_cli.py +309 -0
  59. modelduel-0.6.0/tests/test_duel.py +72 -0
  60. modelduel-0.6.0/tests/test_extract.py +68 -0
  61. modelduel-0.6.0/tests/test_leaderboard.py +408 -0
  62. modelduel-0.6.0/tests/test_league.py +411 -0
  63. modelduel-0.6.0/tests/test_markdown.py +285 -0
  64. modelduel-0.6.0/tests/test_omniroute.py +121 -0
  65. modelduel-0.6.0/tests/test_package.py +169 -0
  66. modelduel-0.6.0/tests/test_pricing.py +70 -0
  67. modelduel-0.6.0/tests/test_providers.py +292 -0
  68. modelduel-0.6.0/tests/test_report.py +261 -0
  69. modelduel-0.6.0/tests/test_resume.py +515 -0
  70. modelduel-0.6.0/tests/test_retries.py +328 -0
  71. modelduel-0.6.0/tests/test_runner.py +234 -0
  72. modelduel-0.6.0/tests/test_site.py +254 -0
  73. modelduel-0.6.0/tests/test_tasks.py +105 -0
  74. modelduel-0.6.0/tests/test_verdict.py +117 -0
@@ -0,0 +1,20 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .env
10
+ runs/
11
+
12
+ # Demo y clasificación generadas en el CI
13
+ site/demo/
14
+ site/leaderboard/
15
+ .coverage
16
+ .coverage.*
17
+ htmlcov/
18
+
19
+ # Grafo de conocimiento local (graphify)
20
+ graphify-out/
@@ -0,0 +1,106 @@
1
+ # Contribuir a modelduel
2
+
3
+ Gracias por querer ayudar. Esta guía es breve; las reglas completas del equipo están en [`AGENTS.md`](AGENTS.md) y el estado del proyecto en [`MEMORY.md`](MEMORY.md). Si solo quieres **usar** modelduel, lee [`docs/USO.md`](docs/USO.md).
4
+
5
+ ## Entorno
6
+
7
+ Necesitas Python 3.12 o superior.
8
+
9
+ ```bash
10
+ git clone https://github.com/BertMarti/modelduel
11
+ cd modelduel
12
+ python -m venv .venv
13
+ source .venv/bin/activate # Windows (PowerShell): .venv\Scripts\Activate.ps1
14
+ pip install -e ".[dev]"
15
+ ```
16
+
17
+ El extra `dev` instala pytest, pytest-cov y ruff. En ejecución el programa solo usa la biblioteca estándar: **no añadas dependencias** sin justificarlo en «Decisiones» de `MEMORY.md`.
18
+
19
+ ## Comandos
20
+
21
+ Antes de abrir un pull request tienen que pasar los mismos pasos que el CI (Ubuntu y Windows):
22
+
23
+ ```bash
24
+ ruff check . # lint
25
+ ruff format --check . # formato (usa «ruff format .» para aplicarlo)
26
+ pytest --cov # tests con cobertura; el mínimo es el 90 %
27
+ modelduel run examples/tasks --model replay:alfa --model replay:beta --model replay:gamma --out runs/demo # demo
28
+ ```
29
+
30
+ - Los tests **no pueden usar la red** ni claves reales: las APIs se simulan.
31
+ - La demo debe terminar con código 0 y generar `runs/demo/index.html` y `runs/demo/results.json`. El CI comprueba además que `--a`/`--b` sigue dando el «Informe de duelo» (la carpeta `runs/` no se sube al repositorio).
32
+ - El informe HTML y la página de clasificación no pueden contener JavaScript. El CI genera `site/leaderboard` con `modelduel leaderboard results --out site/leaderboard`.
33
+ - `results/` son datos versionados (un `results.json` por duelo o liga): añade ahí los reales con el procedimiento de [`docs/USO.md`](docs/USO.md#clasificación-pública).
34
+
35
+ ## Ramas y pull requests
36
+
37
+ - **Nunca hagas commit directo a `main`.** Trabaja en una rama (`agent/<rol>/<n>-<slug>` para los agentes, con `<n>` el número del issue, por ejemplo `agent/docs/9-documentacion-v0.2`; `feat/…` o `fix/…` para personas) y abre un pull request.
38
+ - Cambios pequeños y con sentido propio. Describe qué cambia y por qué, y cómo lo has comprobado.
39
+ - Al terminar, actualiza `MEMORY.md` (estado, decisiones, siguiente paso y una línea en «Registro de sesiones»).
40
+ - La documentación y la interfaz, en español; los nombres del código, en inglés.
41
+ - No subas claves, tokens, `.env` ni datos personales: el repositorio es público.
42
+
43
+ ## Commits
44
+
45
+ [Commits convencionales](https://www.conventionalcommits.org/es/) en español: `feat:`, `fix:`, `test:`, `docs:`, `ci:`, `chore:`, `refactor:`.
46
+
47
+ ```text
48
+ fix: distinguir el timeout de la petición del de los tests
49
+ ```
50
+
51
+ ## Añadir un proveedor
52
+
53
+ Un proveedor es una clase con esta interfaz (`Provider`, en `src/modelduel/providers/base.py`):
54
+
55
+ ```python
56
+ class MiProveedor:
57
+ spec: str # "miproveedor:<modelo>", tal como lo escribe la persona usuaria
58
+
59
+ def complete(self, prompt: str, *, task_id: str | None = None) -> Response: ...
60
+ ```
61
+
62
+ `Response` lleva `text`, `input_tokens`, `output_tokens` (pueden ser `None` si el proveedor no los da) y `latency_s`. `task_id` solo lo usa `replay`; el resto lo ignora.
63
+
64
+ 1. Crea `src/modelduel/providers/<nombre>.py` (usa como modelo `gemini.py` o `openai_compat.py`). Para HTTP usa `post_json` de `base.py`, que ya convierte los fallos de red y HTTP en `ProviderError` con mensajes en español, y pásale las claves en `secrets` para que se redacten. Lee las claves **solo de variables de entorno** (`require_env`), nunca las pongas en la URL ni en los mensajes de error.
65
+ 2. Registra el proveedor en `src/modelduel/providers/__init__.py`: añade su nombre a `PROVIDERS` y una rama en `get_provider`.
66
+ 3. Si tiene precios propios o variables nuevas, documéntalo en el README (tabla «Proveedores y variables de entorno») y en `docs/USO.md`.
67
+ 4. Escribe los tests en `tests/test_providers.py` **sin red**: simula `urllib.request.urlopen` con el ayudante `_fake_urlopen` del propio archivo y prueba la respuesta buena, la falta de clave, las respuestas vacías o raras y los errores HTTP (401, 429, 5xx).
68
+
69
+ Toda excepción de un proveedor debe ser `ProviderError`: el duelo la registra como «error del proveedor» y sigue con el resto de intentos.
70
+
71
+ ## Añadir una tarea de ejemplo
72
+
73
+ Una tarea es una carpeta en `examples/tasks/<id>/` con `task.md`, `test_task.py` y `meta.toml` (formato en el README y en [`docs/USO.md`](docs/USO.md#crear-tu-propia-tarea-paso-a-paso)). Las tareas de ejemplo son **originales**: no copies enunciados ni tests de otras fuentes.
74
+
75
+ 1. Crea los tres archivos. Incluye casos límite y da un `order` y una `difficulty` coherentes con las demás.
76
+ 2. Graba una respuesta por contendiente en `examples/replays/alfa/<id>.md` y `examples/replays/beta/<id>.md` (con el front-matter de tokens y latencia). Son ficticias y se escriben a mano.
77
+ 3. Actualiza lo que dependa del conjunto de tareas: `tests/test_tasks.py` (orden esperado) y `tests/test_cli.py` (resultados de la demo), además de las cifras del README y de la web (`site/index.html`) si cambian los marcadores.
78
+ 4. Comprueba `modelduel list-tasks examples/tasks`, ejecuta la demo y revisa el informe.
79
+
80
+ ## Añadir un color a la paleta de la liga
81
+
82
+ La liga admite hasta seis contendientes (`A` a `F`), cada uno con un acento. Para subir el máximo y añadir un séptimo (`G`), hay que tocar estos sitios:
83
+
84
+ 1. **Letra:** añade `"g"` a `ALL_SIDES` en `src/modelduel/results.py` (`MAX_CONTENDERS` se calcula solo).
85
+ 2. **Color:** en `src/modelduel/report/style.css` añade `--g` en `:root`, las reglas `.g { color: var(--g); }` y `.fill-g { fill: var(--g); }`, y el valor para la hoja de impresión en el `:root` de `@media print`.
86
+ 3. **Elige un color que pase el contraste AA** (mínimo 4,5:1): en pantalla, sobre el fondo `#0f0f11` y sobre el panel `#151518`; en impresión, sobre blanco y sobre el panel claro `#f6f6f6`. Además, el valor «perdedor» atenuado (`opacity: .8`) sigue teniendo que llegar a 4,5:1, y el color tiene que distinguirse de los demás (con la letra siempre al lado: el color nunca es la única pista).
87
+ 4. **Tests:** `tests/test_league.py` calcula el contraste de toda la paleta (`test_paleta_ampliada_cumple_contraste_aa` y `test_contraste_aa_de_toda_la_paleta_sobre_todos_fondos`) y exige colores distintos: si tu color no cumple, fallan. Varios tests fijan «6» a mano (`_html(make_task, 6)`, «máximo 6», `len(...) == 6`): actualízalos a 7. Ejecuta `pytest tests/test_league.py tests/test_report.py`.
88
+ 5. **Documentación:** la paleta aparece en `AGENTS.md` (sección «Diseño»), `docs/USO.md` y el README (límite de seis), y en la web (`site/index.html`).
89
+
90
+ ## Publicar una versión en PyPI (mantenedor)
91
+
92
+ La publicación es automática y **no usa tokens**: `release.yml` usa *Trusted Publishing* (OIDC) de PyPI, a través del *environment* de GitHub `pypi`.
93
+
94
+ **Una sola vez (configuración previa):**
95
+
96
+ 1. En PyPI: *Your projects* > *Publishing* > *Add a new pending publisher*, con proyecto `modelduel`, propietario `BertMarti`, repositorio `modelduel`, workflow `release.yml` y environment `pypi`.
97
+ 2. En GitHub: *Settings* > *Environments* > crea el environment `pypi` (puedes añadirle revisores obligatorios para aprobar cada publicación).
98
+
99
+ **En cada versión:**
100
+
101
+ 1. Sube la versión en `src/modelduel/__init__.py` (única fuente: `pyproject.toml` la lee de ahí), pasa la sección «Sin publicar» de `CHANGELOG.md` a la nueva versión con su fecha (y actualiza los enlaces de comparación del final), actualiza `MEMORY.md` y fusiona el PR en `main`.
102
+ 2. Crea una *Release* en GitHub sobre un commit de `main`, con una etiqueta `vX.Y.Z` igual a la versión (por ejemplo, `v0.2.0`).
103
+ 3. El workflow construye el sdist y la wheel, comprueba que el commit está en `main` y que la etiqueta coincide con la versión, ejecuta `twine check --strict`, prueba la wheel en un entorno limpio y, si todo va bien, publica en PyPI a través del *environment* `pypi`. Sigue el resultado en la pestaña *Actions*.
104
+ 4. Comprueba la publicación desde un entorno limpio: `pip install "modelduel[pytest]"` y `modelduel demo`.
105
+
106
+ Para comprobar el paquete en local sin publicar: `pip install build twine`, `python -m build` y `twine check dist/*`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alberto Martínez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,277 @@
1
+ Metadata-Version: 2.5
2
+ Name: modelduel
3
+ Version: 0.6.0
4
+ Summary: Dos modelos, una tarea, los mismos tests: compara modelos de IA programando.
5
+ Project-URL: Homepage, https://bertmarti.github.io/modelduel/
6
+ Project-URL: Documentation, https://github.com/BertMarti/modelduel/blob/main/docs/USO.md
7
+ Project-URL: Source, https://github.com/BertMarti/modelduel
8
+ Project-URL: Issues, https://github.com/BertMarti/modelduel/issues
9
+ Author: Alberto Martínez
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,benchmark,cli,coding,evaluation,llm,pytest
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Natural Language :: Spanish
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
+ Classifier: Topic :: Software Development :: Quality Assurance
23
+ Classifier: Topic :: Software Development :: Testing
24
+ Requires-Python: >=3.12
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Provides-Extra: pytest
30
+ Requires-Dist: pytest>=8; extra == 'pytest'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # modelduel
34
+
35
+ **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.
36
+
37
+ [![CI](https://github.com/BertMarti/modelduel/actions/workflows/ci.yml/badge.svg)](https://github.com/BertMarti/modelduel/actions/workflows/ci.yml)
38
+ [![Licencia MIT](https://img.shields.io/badge/licencia-MIT-b5e853.svg)](https://github.com/BertMarti/modelduel/blob/main/LICENSE)
39
+ ![Python 3.12](https://img.shields.io/badge/python-3.12-ff7eb6.svg)
40
+
41
+ **Web:** <https://bertmarti.github.io/modelduel/> · **[Ver un duelo en directo](https://bertmarti.github.io/modelduel/#demo)** (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/>
42
+
43
+ **Documentación:** [Guía de uso](https://github.com/BertMarti/modelduel/blob/main/docs/USO.md) (instalación, duelos reales, crear tus tareas, liga, reintentos, reanudación y leer el informe) · [Registro de cambios](https://github.com/BertMarti/modelduel/blob/main/CHANGELOG.md) · [Cómo contribuir](https://github.com/BertMarti/modelduel/blob/main/CONTRIBUTING.md)
44
+
45
+ ```text
46
+ # Contendiente Tareas Tests Tiempo Tokens (ent/sal) Coste
47
+ ----------------------------------------------------------------------------
48
+ 1 A replay:alfa 2/3 31/32 10,1 s 1.401/703 0,0017 USD
49
+ 2 C replay:gamma 2/3 30/32 3,7 s 1.314/426 0,0003 USD
50
+ 3 B replay:beta 2/3 30/32 28,1 s 1.373/2.132 0,0248 USD
51
+ (precios ficticios de demostración)
52
+ ```
53
+
54
+ > [!WARNING]
55
+ > **El código generado por los modelos se ejecuta en tu máquina.** modelduel lo ejecuta siempre en un directorio temporal, en un subproceso con límite de tiempo y sin tus variables de entorno secretas, y al terminar mata todos los procesos que haya lanzado (Job Object en Windows, grupo de procesos en Linux/macOS), pero eso **no es un aislamiento real**: el código puede leer y escribir en el resto del disco, y un proceso que se desligue a propósito (`setsid`, `CREATE_BREAKAWAY_FROM_JOB`) puede sobrevivir. Úsalo solo con tareas de confianza e, idealmente, dentro de un contenedor o una máquina virtual.
56
+
57
+ ## Por qué
58
+
59
+ 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.
60
+
61
+ ## Novedades de v0.6.0
62
+
63
+ - **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).
64
+ - Imagen social (`og:image`, `twitter:card`) en la web.
65
+
66
+ ### Antes (v0.5.0)
67
+
68
+ - **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.
69
+ - Informes y portada más legibles: enlaces con foco visible, «▲ mejor» / «▼ peor» con glifo y texto, y una navegación más corta.
70
+
71
+ ### Antes (v0.2.0)
72
+
73
+ - **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`).
74
+ - **Reintentos** ante HTTP 429/5xx y cortes de conexión, con espera exponencial y `Retry-After` (`--retries`).
75
+ - **Guardado incremental y `--resume`**: un corte no pierde lo ya hecho y el duelo se continúa con la misma orden.
76
+ - **Publicación en PyPI** (`pip install modelduel`) y **`modelduel demo`**: prueba sin claves ni coste y `demo --copy` para empezar desde una plantilla.
77
+
78
+ Detalle en el [registro de cambios](https://github.com/BertMarti/modelduel/blob/main/CHANGELOG.md).
79
+
80
+ ## Instalación
81
+
82
+ Necesitas Python 3.12 o superior. modelduel solo usa la biblioteca estándar; pytest hace falta para ejecutar los tests de las tareas.
83
+
84
+ ```bash
85
+ pip install "modelduel[pytest]" # o: pip install modelduel pytest
86
+ modelduel demo # prueba sin claves ni coste, con los ejemplos incluidos
87
+ ```
88
+
89
+ > [!NOTE]
90
+ > El paquete se publica en PyPI al crearse la *release* `v0.2.0`. Hasta entonces, instálalo desde GitHub: `pip install git+https://github.com/BertMarti/modelduel pytest`.
91
+
92
+ `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.
93
+
94
+ Para desarrollar:
95
+
96
+ ```bash
97
+ git clone https://github.com/BertMarti/modelduel
98
+ cd modelduel
99
+ python -m venv .venv
100
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
101
+ pip install -e ".[dev]"
102
+ ruff check . && ruff format --check . && pytest --cov
103
+ ```
104
+
105
+ ## Uso rápido
106
+
107
+ ```bash
108
+ # Demo sin claves ni coste, con respuestas grabadas: una liga de tres contendientes
109
+ modelduel run examples/tasks --model replay:alfa --model replay:beta --model replay:gamma --out runs/demo
110
+
111
+ # Dos modelos reales, tres ejecuciones por tarea
112
+ export GEMINI_API_KEY=... # PowerShell: $env:GEMINI_API_KEY = "..."
113
+ export OPENAI_API_KEY=...
114
+ modelduel run examples/tasks --a gemini:<modelo> --b openai:<modelo> --runs 3 --prices precios.json --out runs/duelo
115
+
116
+ # Regenerar el HTML a partir de los resultados
117
+ modelduel report runs/duelo/results.json --out runs/duelo
118
+
119
+ # Informe en Markdown para pegar en un PR o un issue (informe.md)
120
+ modelduel report runs/duelo/results.json --format md --out runs/duelo
121
+
122
+ # Ver las tareas de una carpeta
123
+ modelduel list-tasks examples/tasks
124
+ ```
125
+
126
+ | Opción | Qué hace |
127
+ |---|---|
128
+ | `--a`, `--b` | Contendientes A y B en formato `proveedor:modelo` (un duelo de dos, como en v0.1.0). |
129
+ | `--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`. |
130
+ | `--runs N` | Ejecuciones por tarea (1 por defecto). Una sola ejecución es una señal débil. |
131
+ | `--timeout S` | Límite en segundos para los tests de cada respuesta (20 por defecto). |
132
+ | `--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). |
133
+ | `--resume` | Continúa el duelo de `--out` saltando los intentos ya terminados (ver «Cortes y reanudación»). |
134
+ | `--format F` | Formatos del informe: `html` (por defecto), `md` o `html,md`. `results.json` se guarda siempre. También lo acepta `report`. |
135
+ | `--prices f.json` | Tabla de precios adicional (ver «Coste»). |
136
+ | `--replays DIR` | Carpeta de respuestas grabadas para `replay` (por defecto, `replays/` junto a la carpeta de tareas). |
137
+ | `--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. |
138
+
139
+ 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.
140
+
141
+ **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.
142
+
143
+ **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.
144
+
145
+ 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.
146
+
147
+ ## Formato de una tarea
148
+
149
+ Una tarea es una carpeta:
150
+
151
+ ```text
152
+ examples/tasks/slugify/
153
+ ├── task.md # enunciado para el modelo: pide una función concreta con nombre y firma
154
+ ├── test_task.py # tests pytest que importan de `solution`
155
+ └── meta.toml # opcional: title, difficulty, order
156
+ ```
157
+
158
+ ```python
159
+ # test_task.py
160
+ from solution import slugify
161
+
162
+
163
+ def test_acentos():
164
+ assert slugify("Canción de Añoranza") == "cancion-de-anoranza"
165
+ ```
166
+
167
+ ```toml
168
+ # meta.toml
169
+ title = "Convertir un texto en slug"
170
+ difficulty = "fácil"
171
+ order = 1
172
+ ```
173
+
174
+ 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.
175
+
176
+ 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.
177
+
178
+ Las tres tareas de ejemplo son originales y de dificultad creciente: `slugify`, `merge_intervals` y `parse_duration`.
179
+
180
+ ## Proveedores y variables de entorno
181
+
182
+ | Especificación | Qué usa | Variables |
183
+ |---|---|---|
184
+ | `replay:<nombre>` | Respuestas grabadas en `examples/replays/<nombre>/<tarea>.md`, con tokens y latencia en un front-matter. Sin red. | — |
185
+ | `gemini:<modelo>` | API REST `generateContent` de Google Generative Language. | `GEMINI_API_KEY` (obligatoria), `GEMINI_BASE_URL` (opcional) |
186
+ | `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`) |
187
+ | `omniroute:<modelo>` | OmniRoute, router local compatible con OpenAI (`omniroute serve`). | `OMNIROUTE_BASE_URL` (por defecto `http://localhost:20128/v1`), `OMNIROUTE_API_KEY` (opcional) |
188
+
189
+ - **OpenRouter:** `OPENAI_BASE_URL=https://openrouter.ai/api/v1` y tu clave de OpenRouter en `OPENAI_API_KEY`.
190
+ - **Ollama:** `OPENAI_BASE_URL=http://localhost:11434/v1`. En servidores locales la clave no es obligatoria.
191
+ - `MODELDUEL_HTTP_TIMEOUT` cambia el límite de las peticiones HTTP (180 s por defecto).
192
+
193
+ 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.
194
+
195
+ Formato de una respuesta grabada (`examples/replays/alfa/slugify.md`):
196
+
197
+ ````markdown
198
+ ---
199
+ input_tokens: 412
200
+ output_tokens: 188
201
+ latency_s: 2.84
202
+ ---
203
+ ```python
204
+ def slugify(text: str, separator: str = "-") -> str:
205
+ ...
206
+ ```
207
+ ````
208
+
209
+ ## Coste
210
+
211
+ ```text
212
+ coste = entrada / 1e6 × tarifa_entrada + salida / 1e6 × tarifa_salida
213
+ ```
214
+
215
+ 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`:
216
+
217
+ ```json
218
+ {
219
+ "_nota": "Precios por millón de tokens. Compruébalos en la web del proveedor.",
220
+ "openai:mi-modelo": { "input": 0.15, "output": 0.60, "currency": "USD" },
221
+ "gemini:otro-modelo": { "input": 0.10, "output": 0.40, "currency": "EUR" }
222
+ }
223
+ ```
224
+
225
+ 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.
226
+
227
+ ## Clasificación pública
228
+
229
+ `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/`](https://github.com/BertMarti/modelduel/tree/main/results), que trae tres resultados de ejemplo con respuestas grabadas; la guía explica [cómo añadir resultados reales](https://github.com/BertMarti/modelduel/blob/main/docs/USO.md#clasificación-pública).
230
+
231
+ ## Más documentación
232
+
233
+ - [`docs/USO.md`](https://github.com/BertMarti/modelduel/blob/main/docs/USO.md): guía para personas usuarias, paso a paso y con solución de problemas.
234
+ - [`CHANGELOG.md`](https://github.com/BertMarti/modelduel/blob/main/CHANGELOG.md): qué trae cada versión.
235
+ - [`CONTRIBUTING.md`](https://github.com/BertMarti/modelduel/blob/main/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.
236
+
237
+ ## Estructura
238
+
239
+ ```text
240
+ src/modelduel/
241
+ ├── cli.py # run, report, leaderboard, list-tasks, demo
242
+ ├── tasks.py # carga de tareas y recuento de tests
243
+ ├── extract.py # extracción del bloque de código
244
+ ├── runner.py # prompt + ejecución de pytest en temporal con límite
245
+ ├── duel.py # orquestación del duelo
246
+ ├── resume.py # reanudación de un duelo interrumpido
247
+ ├── demo.py # localiza los ejemplos incluidos (modelduel demo)
248
+ ├── results.py # results.json y resumen del marcador
249
+ ├── pricing.py # tarifas y fórmula de coste
250
+ ├── providers/ # replay, gemini, openai_compat (openai y omniroute)
251
+ ├── leaderboard.py # clasificación pública: agregado de results/*.json y página estática
252
+ └── report/ # informe HTML (duelo de dos y liga de 3 a 6, string.Template + SVG) y Markdown (`markdown.py`)
253
+ docs/USO.md # guía de uso para personas usuarias
254
+ CHANGELOG.md # registro de cambios (Keep a Changelog)
255
+ examples/tasks/ # tareas originales de ejemplo
256
+ examples/replays/ # respuestas grabadas de alfa, beta y gamma
257
+ site/ # web del proyecto (la demo se genera en el CI)
258
+ tests/ # pytest, sin llamadas de red
259
+ ```
260
+
261
+ ## Stack
262
+
263
+ - Python 3.12, solo biblioteca estándar en ejecución (`argparse`, `urllib`, `string.Template`, `xml.etree`, `tomllib`).
264
+ - pytest para los tests y para ejecutar las tareas; ruff para lint y formato.
265
+ - GitHub Actions: CI en Ubuntu y Windows, y despliegue en GitHub Pages de la web y del informe de demostración.
266
+
267
+ ## Cómo se ha construido
268
+
269
+ 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:
270
+
271
+ - **v0.1.0:** lead, builder y qa con Claude Opus; la documentación (docs), con Claude Sonnet.
272
+ - **v0.2.0:** todo con Claude Sonnet, guiado por issues del hito y un pull request por issue.
273
+ - **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`](https://github.com/BertMarti/modelduel/blob/main/MEMORY.md) y las reglas del equipo en [`AGENTS.md`](https://github.com/BertMarti/modelduel/blob/main/AGENTS.md).
274
+
275
+ ## Licencia
276
+
277
+ [MIT](https://github.com/BertMarti/modelduel/blob/main/LICENSE).