npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

prompt-builder-cli

v0.1.1

Published

Benchmark LLMs and evolve system prompts from the terminal. Built for coding agents: self-documenting, budget-aware, model/think-level aware.

Readme

Prompt Builder

Arena de benchmark paralelo de LLMs sobre a OpenRouter. Em três modos: comparar vários modelos no mesmo desafio, testar vários prompts em um modelo, ou treinar um prompt que evolui sozinho. Você dá um tema, o sistema gera cenários com um modelo, faz os participantes responderem ao mesmo tempo, um modelo juiz ranqueia às cegas e a interface mostra placar, heatmap, custo e o texto sendo gerado token a token — tudo ao vivo (SSE).

Em uma frase: "dado um tema, descubra qual modelo (ou qual prompt) responde melhor — e quais respostas são boas o bastante para usar no trabalho de verdade — com evidência, ranking e custo."

CLI para agentes de programação (prompt-builder)

Publicado no npm. Feito para ser dirigido por Claude Code, Codex, opencode, Cursor, Gemini CLI — sem prompt interativo, --json em tudo, e auto-documentação versionada dentro do próprio pacote.

# a ferramenta ensina o agente a usá-la (docs embarcadas, casadas com a versão)
npx prompt-builder-cli docs quickstart

# descobre o modelo do ambiente e QUAIS níveis de raciocínio ele aceita
npx prompt-builder-cli models show anthropic/claude-opus-5 --json

# valida e estima o custo SEM gastar nada
npx prompt-builder-cli train --config arena.json --budget 3 --dry-run

# treina com teto de gasto, emitindo um evento JSON por linha
npx prompt-builder-cli train --config arena.json --budget 3 --output-format ndjson

# instala a skill no repositório (.claude/skills, .agents/skills)
npx prompt-builder-cli init --agent all

Também expõe um servidor MCP no mesmo binário:

claude mcp add --transport stdio arena -- npx -y prompt-builder-cli mcp

Três coisas que o CLI garante e a UI não garantia:

  • Custo real. O gasto sai de usage.cost (o valor cobrado), quebrado por papel — juiz, gabarito, duelos e reescritor incluídos. Antes só as respostas dos competidores eram contadas, subcontando o total por um múltiplo.
  • Orçamento que não corrompe o resultado. Ao estourar o teto, a run para numa fronteira de fase e entrega o parcial honesto (exit 7), em vez de virar uma run "concluída" com vereditos inventados por falta de dinheiro.
  • Think levels do catálogo. models show diz exatamente quais degraus aquele modelo aceita e o que vai no fio para cada nível pedido — é o que permite a um agente treinar contra o próprio modelo sem tomar HTTP 400.

Documentação completa: npx prompt-builder-cli docs --list.


A documentação das telas está em TELAS.md. Convenções para agentes de código (Claude Code, Codex, Cursor…) estão em AGENTS.md e na biblioteca de skills em .agents/skills/ — veja Sistema de Knowledge Skills.


Sumário


Como funciona (visão geral)

O backend orquestra um pipeline em etapas. Você define N etapas; cada etapa é um mini-benchmark independente e auto-contido:

flowchart LR
  T([Tema + config]) --> DG[1 · Datagen<br/>gera o cenário]
  DG --> C{2 · Participantes<br/>respondem em paralelo}
  C --> J[3 · Juiz<br/>veredito vs gabarito<br/>+ duelos Copeland]
  J --> S[(Placar + Heatmap)]
  S -->|próxima etapa| DG
  S --> R([Run finalizada])
  1. Datagen — um modelo recebe o tema (e um scenarioBrief opcional) e produz os cenários em lotes paralelos: uma pergunta de usuário (question), um contexto de produto (productContext, que vira o system prompt: políticas, FAQs, dados, restrições) e um teto de tokens sugerido (maxTokens). Cada etapa varia o tipo de tarefa (extração, raciocínio, comparação, recusa…). Um pacote de cenários importado vira seed e mescla com os gerados.
  2. Participantes — respondem ao mesmo cenário em paralelo (com limite de concorrência), em streaming. A UI mostra o texto crescendo, a velocidade (chars/s), latência, tokens e custo.
  3. Julgamento — por default (fora do compare clássico) é por referência: um gabarito temp-0 é gerado por cenário, o juiz classifica cada resposta isoladamente contra ele (resolve / parcial / não, com explicação de 1 frase) e os melhores disputam duelos Copeland (cada par nas duas ordens; empate em desacordo). Sem gabarito (ou no compare clássico), cai no juiz listwise clássico: ordena as respostas às cegas e dá o veredito de aceitabilidade ("dá para usar em produção sem causar erro/dano?").
  4. Todas as etapas rodam em paralelo (cenários pré-gerados juntos; execução concorrente limitada por um semáforo global adaptativo). O placar é aditivo, então a ordem de término não importa; ao final a run é finished e fica no histórico (com export JSON/CSV).

Tudo é transmitido ao navegador em tempo real via Server-Sent Events (SSE): durante a run a tela mostra um visualizador de processo (etapas em paralelo + previews ao vivo) e revela o placar / heatmap só quando tudo termina. Detalhes do motor em FUNCIONAMENTO.md.


Os três modos

O assistente de Nova Run tem 5 passos (Objetivo → Tema → Participantes → Avaliação → Revisar) e atende três objetivos. O que muda é quem é o "participante" (Contestant):

| Modo | O que compara | Participante | Endpoint | Requisito | |---|---|---|---|---| | Comparar modelos (compare) | Vários modelos, mesmo desafio | cada modelo (id === modelId) | POST /runs | ≥ 2 competitorModelIds | | Testar prompts (variation) | Um modelo, vários system prompts | mesmo modelId, systemPrompt distinto | POST /runs | 1 contestantModelId + ≥ 2 variações | | Treinar prompt (training) | Um prompt que evolui por iteração | idem variation, encadeado | POST /sessions | + iterations (2–10) |

  • Variação gera as versões do prompt de dois jeitos: otimização ligada → um modelo optimizer reescreve o basePrompt aplicando técnicas selecionadas (techniqueIds, biblioteca curada em src/techniques.ts); desligada → você escreve as variações manualmente (manualVariants). Um basePrompt opcional roda como controle.
  • Treino repete a variação por N iterações (src/trainer.ts): a melhor versão de cada rodada é a semente da próxima — mas só é promovida se superar o campeão por minGain (default 1 p.p.); sem margem, a sessão converge e para. Os cenários são congelados após a iteração 0 (pinnedStages, com split de holdout) para comparação justa; o feedback vem de lições determinísticas das falhas do campeão (sem LLM extra). Ao final, uma run de holdout e uma significância bootstrap validam o campeão. Acompanhe em TrainingView.
  • Nos modos de um modelo, o juiz nunca é o modelo sob teste (anti-viés de auto-preferência), e há a opção "juiz em 2 ordens" (judgePasses: 2) contra viés de posição.

RunConfig é uma união discriminada por mode (src/types.ts), validada por Zod em src/routes.ts.


Os papéis dos modelos

Toda run tem modelos de apoio (gerador + juiz) além dos participantes:

| Papel | Quantos | O que faz | Configuração | |---|---|---|---| | Participante | compare: ≥2 (ou 2–12 configs); variation/training: 1 (+ variações) | Respondem ao cenário e disputam o ranking | competitorModelIds[] / competitorConfigs[] / contestantModelId | | Gerador (datagen) | exatamente 1 | Inventa os cenários (pergunta + contexto + maxTokens) | datagenModelId | | Juiz | 1 ou mais | Vereditos vs gabarito + duelos (ou ranking listwise, no fallback) | judgeModelIds[] | | Referência (gabarito) | 1 (default = 1º juiz) | Gera a resposta de referência temp-0 por cenário | referenceModelId | | Optimizer | 1 (variation/training) | Reescreve prompts aplicando técnicas | optimizerModelId (default = datagenModelId) |

Regras validadas no backend (Zod) — config inválida é recusada com 400:

  • compare: ≥ 2 competidores distintos; gerador ≠ juiz; nem gerador nem juiz são competidores.
  • variation/training: ≥ 2 variações (técnicas ou manuais, contando o basePrompt como controle); juiz ≠ modelo sob teste.

Conformidade LGPD (filtro consultivo)

No passo Tema do assistente há um bloco "Propósito / Conformidade LGPD" que filtra o catálogo de modelos conforme a área de uso e a adequação à LGPD — útil porque este repositório é do Grupo Fleury (dados de saúde = sensíveis). Você escolhe um propósito/área (Geral, Jurídico, Saúde, Financeiro, Crianças e adolescentes, Setor público — ou "Livre", que mostra tudo) e um rigor (incluir ou não modelos "permitido com ressalvas"). O filtro vale para todos os seletores (participantes, gerador, juiz) e poda automaticamente seleções que ficaram fora — inclusive os defaults de origem chinesa.

⚠️ É consultivo e não é aconselhamento jurídico: orienta e esconde modelos, mas não força o roteamento de providers no OpenRouter. O perfil escolhido é apenas gravado em RunConfig.compliance (gancho para uma futura fase de enforcement — ZDR + provider.only).

Como classifica (web/src/lgpd.ts + src/data/lgpd-compliance.json): pelo criador do modelo (prefixo do id) quando ele está nas 9 famílias do relatório; senão, por heurística de origem (China/SG → não recomendado; ocidental → permitido com ressalvas). Status ∈ permitido / permitido com ressalvas / não recomendado.

  • Base de conhecimento: src/data/lgpd-compliance.json (áreas, famílias, origem de providers/criadores, status ANPD, config ZDR recomendada).
  • Snapshot de referência dos modelos atuais por área: src/data/lgpd-allowlist.generated.json.
  • Regenerar o snapshot: node scripts/gen-lgpd-allowlist.mjs (usa os endpoints públicos /models e /endpoints/zdr — sem key).
  • Servido em GET /v1/benchmark/lgpd.

Detalhes para agentes na skill knowledge-lgpd-compliance.


Anatomia de uma etapa

sequenceDiagram
  participant O as Orquestrador
  participant D as Datagen
  participant K as Participantes
  participant J as Juiz (referência/listwise)
  participant UI as Navegador SSE

  O->>UI: stage.generating
  O->>D: gera cenário (lotes paralelos)
  D-->>O: {question, productContext, maxTokens}
  O->>UI: stage.generated (cenário completo)
  O->>J: gabarito temp 0 (referência)
  O->>UI: stage.gabarito (progresso agregado)
  par participantes em paralelo (cap = concurrency)
    O->>K: responder (streaming)
    K-->>O: deltas de texto
    O->>UI: competitor.progress (chars, ch/s, preview)
    K-->>O: resposta final (latência, tokens, custo)
    O->>UI: competitor.finished
  end
  O->>UI: stage.judging
  O->>J: vereditos vs gabarito (pointwise, cego)
  O->>J: duelos Copeland (2 ordens por par)
  O->>UI: stage.dueled / duel.progress
  J-->>O: vereditos + ordem Copeland (JudgeResult sintetizado)
  O->>UI: stage.judged (placar + custo atualizados)

Pontos-chave (src/orchestrator.ts):

  • Datagen em lotes: cenários pré-gerados em paralelo (generateStages), com dedup ROUGE-L e backfill; falha de uma etapa pula a etapa — a run nunca trava.
  • Cego (blind): antes do juiz, as respostas são embaralhadas e rotuladas A, B, C… — o juiz não sabe qual modelo é qual. A UI mostra "(era A)" depois (no fluxo listwise).
  • Referência com fallback: etapa sem gabarito (ou compare clássico) cai no juiz listwise; o resultado do julgamento por referência é sintetizado num JudgeResult para placar e UI.

Sistema de pontuação

duas leituras complementares de cada run:

1. Ranking competitivo (juiz) → placar e heatmap

A cada etapa, o juiz ordena as respostas. Pontuação estilo "corrida":

Com N respostas válidas: 1º lugar = N−1 pontos, 2º = N−2, … último = 0. Os pontos são somados em todas as etapas (src/orchestrator.tsapplyScoreboard).

O heatmap mostra a posição de cada participante em cada etapa, do verde (melhor) ao vermelho (pior); · = "não ranqueado". A classificação final ordena por: pontosposição médianº de 1ºs lugares → id.

2. Vereditos de aceitabilidade → "dá pra usar no trabalho?"

Independente do ranking, cada resposta recebe um veredito:

  • resolve — resolve a necessidade de forma correta e segura, mesmo não sendo a melhor;
  • parcial — serve em parte (falta algo ou desvia do contexto);
  • não — erro factual, viola contexto/política, ou incompleta a ponto de não servir.

"Aceitável" = veredito ≠ não. Respostas com erro/vazias são automaticamente não aceitáveis (sem gastar chamada de LLM). No julgamento por referência o veredito é pointwise contra o gabarito; no listwise, vem do próprio juiz.

É a diferença entre "quem ganhou" (ranking) e "quem serve" (aceitabilidade): um modelo pode quase nunca vencer e ainda assim ser aceitável em 100% das etapas.


Stack tecnológica

Backend

  • Node.js (ESM, "type": "module", NodeNext — imports relativos com extensão .js) + Express 4.
  • TypeScript 5 (strict) — compilado para dist/.
  • Zod 4 — validação do corpo das requisições e dos JSONs devolvidos pelas LLMs.
  • fetch nativo — chamadas à OpenRouter (sem SDK), inclusive streaming SSE.
  • EventEmitter nativo — barramento de eventos por run/sessão (src/events.ts).
  • Sem banco de dados: persistência em arquivos JSON (data/runs/*.json, data/sessions/*.json).

Frontend (web/)

  • React 18 + React Router 6 — SPA com 5 telas.
  • Vite 5 — dev server (proxy de /v1 e /health) e build.
  • TypeScript 5; EventSource (SSE) para acompanhar ao vivo.
  • Cache em IndexedDB (web/src/idb.ts, db prompt-builder) — fallback offline do histórico.
  • CSS puro (web/src/styles.css) com design tokens e tema claro/escuro, sem framework de UI.

Integração externa

  • OpenRouter — gateway único para todos os modelos. Catálogo + preços via GET /models; geração via POST /chat/completions (streaming p/ participantes, JSON-mode p/ datagen/juiz); validação de key via GET /key. /models e /endpoints/zdr são públicos.

Estrutura do projeto

prompt-builder/
├─ src/                      # Backend (TypeScript → dist/)
│  ├─ server.ts              # Express, /health, monta /v1/benchmark, serve web/dist, aborta órfãs
│  ├─ routes.ts              # Endpoints /v1/benchmark/* + validação Zod + SSE + CSV
│  ├─ orchestrator.ts        # Loop da run: datagen → participantes → juiz+avaliador → placar
│  ├─ trainer.ts             # Modo training: encadeia N iterações (sessão)
│  ├─ variator.ts            # Gera variações de prompt (técnicas / manuais)
│  ├─ datagen.ts             # Gera o cenário (question/productContext/maxTokens)
│  ├─ competitor.ts          # Roda 1 participante (streaming, retry, progresso, custo)
│  ├─ judge.ts               # Juiz listwise (fallback — ranking cego + vereditos)
│  ├─ gabarito.ts / refJudge.ts / duels.ts   # Julgamento por referência: gabarito, vereditos pointwise, duelos Copeland
│  ├─ rank.ts / holdout.ts / stats.ts        # Promoção (minGain), holdout, significância bootstrap
│  ├─ llmVariants.ts / reasoning.ts / dedup.ts / scenarioPack.ts   # compare-llms, reasoning por papel, dedup, pacote de cenários
│  ├─ openrouter.ts          # Cliente OpenRouter: models, chat, stream, custo, validateKey
│  ├─ techniques.ts          # Biblioteca curada de técnicas de prompt
│  ├─ lgpd.ts                # Serve a base de conhecimento LGPD (GET /lgpd)
│  ├─ events.ts / normalize.ts / storage.ts / types.ts
│  └─ data/                  # JSON estático VERSIONADO (lgpd-compliance, lgpd-allowlist.generated)
│
├─ web/                      # Frontend (React + Vite)
│  └─ src/
│     ├─ main.tsx            # Router, layout, navegação
│     ├─ api.ts              # Cliente HTTP/SSE + tipos + key no localStorage
│     ├─ idb.ts              # Cache IndexedDB v2 (incl. store `prompts`); theme.ts / help.ts (contexts)
│     ├─ lgpd.ts             # Classificação/filtragem de conformidade
│     ├─ styles.css          # Design tokens (claro/escuro)
│     ├─ components/         # ModelSelector, Toggle, TechniqueSelector, ManualVariantsEditor, KeySetup, HelpModal
│     └─ pages/              # NewRun (assistente 5 passos), RunsList, RunView, TrainingView, PromptsPage, Settings
│
├─ scripts/gen-lgpd-allowlist.mjs   # Regenera o snapshot LGPD (endpoints públicos)
├─ .agents/skills/          # Biblioteca de Knowledge Skills (fonte única) — ver seção abaixo
├─ .claude/skills           # symlink → ../.agents/skills (portabilidade Claude Code)
├─ AGENTS.md                # Instruções mínimas para agentes de código (CLAUDE.md é symlink)
├─ data/                    # runtime: runs/ e sessions/ (gitignored — regra /data/)
├─ .env.example             # Variáveis OPCIONAIS (o app roda sem .env)
├─ README.md  ·  TELAS.md   # Este arquivo · documentação das telas
└─ package.json  ·  tsconfig.json

Sistema de Knowledge Skills

O repositório adota um sistema de Agent Skills (formato SKILL.md) para agentes de código (Claude Code, Codex, Cursor…): o conhecimento do projeto vive em .agents/skills/ e é injetado sob demanda, em vez de o agente reler docs ou varrer o codebase a cada tarefa.

Como funciona: toda tarefa passa primeiro pela skill roteadora project-router, que seleciona e encadeia as skills relevantes antes de implementar. O progressive disclosure mantém o contexto enxuto (metadados sempre carregados; corpo no gatilho; references/ sob demanda).

.agents/skills/                  (fonte única; .claude/skills é symlink)
├─ project-router/               roteia toda tarefa para as skills certas
├─ knowledge-*/                  memória semântica: architecture, code-style, backend,
│                                frontend, openrouter, benchmark-modes, lgpd-compliance
├─ task-*/                       memória procedural (terminam com passo <evolution> + LEARNINGS.md):
│                                add-endpoint, add-wizard-step, run-and-verify
├─ meta-skill-evolution/         atualiza/cria skills a partir de aprendizados (via git diff)
├─ meta-skill-consolidate/       GC periódico: dedup, contradições, versionamento, poda
├─ catalog.md                    índice (estilo llms.txt) · skill-template.md  modelo

Memória evolutiva com salvaguardas: skills de tarefa terminam com um passo <evolution> que destila aprendizados em LEARNINGS.md. Inspirado em Voyager (persistir só após verificação) e Reflexion (feedback verbal). Gate humano inegociável: toda atualização de skill é um commit separado para revisão por git diff — pesquisa da ETH Zurich (arXiv:2602.11988) mostra que contexto auto-gerado sem curadoria piora o desempenho do agente. As skills aqui são rascunhos curados: trate-as como tal e revise antes de confiar.

Portabilidade: fonte única em .agents/skills/, frontmatter mínimo (name + description), symlinks versionados. Começo: AGENTS.md (comandos exatos + regras não-óbvias) e catalog.md.


Configuração

Não é preciso nenhum .env para rodar — todos os parâmetros têm default. A chave do OpenRouter não vai em variável de ambiente: você cola na interface (tela de Configurações / gate da Nova Run) e ela fica no localStorage do navegador, indo ao backend só no header x-openrouter-key.

Variáveis opcionais (veja .env.example):

| Variável | Default | Para quê | |---|---|---| | OPENROUTER_BASE_URL | https://openrouter.ai/api/v1 | Apontar para um proxy/gateway compatível | | OPENROUTER_APP_URL | http://localhost:3000 | Header HTTP-Referer de atribuição | | OPENROUTER_APP_TITLE | Prompt Builder | Header X-Title de atribuição | | BENCHMARK_PORT | 3001 | Porta do backend | | OPENROUTER_MAX_CONCURRENCY | 32 | Teto do limitador global adaptativo de chamadas ao OpenRouter |

Parâmetros da run (na tela de Nova Run, validados no backend):

| Campo | Faixa | Default (UI) | |---|---|---| | stages (etapas) | 1–50 | 5 | | iterations (treino) | 2–10 | 3 | | concurrency | 1–32 | 8 | | timeoutMs | 1.000–300.000 | 60.000 | | maxOutputTokens | 50–16.000 | 500 |

maxOutputTokens é um teto absoluto; o efetivo é min(maxOutputTokens, maxTokens do datagen).

A concorrência efetiva das chamadas ao OpenRouter é governada por um limitador global adaptativo (OPENROUTER_MAX_CONCURRENCY); o campo concurrency por run é legado (não limita mais o paralelismo). Ver FUNCIONAMENTO.md.


Como rodar

Um único npm install instala backend e front (postinstall cuida do web/).

Desenvolvimento

npm install
npm run dev      # backend :3001 (tsx watch) + Vite :5173 (proxy de /v1 e /health)

Abra http://localhost:5173 e cole sua chave OpenRouter na tela de setup.

Produção

npm install
npm run build    # compila backend (dist/) e front (web/dist/)
npm run start    # serve API + frontend juntos em http://localhost:3001

Em produção o Express serve web/dist e faz fallback de SPA para rotas que não comecem com /v1 ou /health.

Deploy estático (Vercel) — modo client-side

Há também um modo 100% client-side: o pipeline foi portado para web/src/engine/, então o navegador chama o OpenRouter direto (CORS liberado), orquestra os runs na própria aba e persiste no IndexedDB — sem backend stateful. Isso permite hospedar a SPA estática (ex.: Vercel) via vercel.json (build npm run web:build, output web/dist, SPA rewrite). Por que isso importa: serverless é efêmero/stateless, então um servidor de run de minutos não roda lá; no client-side a aba é o "processo vivo".

⚠️ Não publique o backend src/ na Vercel. Ele persiste runs no filesystem (data/runs/*.json via storage.ts), que no serverless é efêmero/read-only e não-compartilhado entre invocações: o deploy parece ok (serve a SPA e responde /health), mas GET /v1/benchmark/runs/:id devolve Run nao encontrada. Sintoma de deploy errado: /health responde JSON ({"status":"ok",…}) em vez do index.html da SPA — é um deploy antigo do backend preso em produção; force um novo deploy estático.

Trade-offs: a aba precisa ficar aberta durante a run, e o histórico é por navegador/dispositivo. O backend src/ continua disponível como alternativa (host de processo persistente: Railway/Render/Fly), mas não é usado pela SPA estática.

Scripts (package.json)

| Script | O que faz | |---|---| | npm run dev | Backend (watch) + Vite, em paralelo (concurrently) | | npm run build | tsc do backend + tsc -b && vite build do front | | npm run start | Roda o backend compilado (dist/server.js) | | npm run web:dev / web:build / web:install | Atalhos para web/ |

Não há test nem lint configurados — valide por type-check (npx tsc -p tsconfig.json --noEmit e cd web && npx tsc -b) + execução manual.


Fluxo de eventos (SSE)

O backend mantém um barramento de eventos por run (src/events.ts). Ao abrir GET /v1/benchmark/runs/:id/events, o cliente recebe um snapshot e depois o stream incremental.

| Evento | Quando | Carrega | |---|---|---| | snapshot | Ao conectar | Record completo | | run.started | Início | Record inicial | | stage.generating / stage.generated | Datagen | stageIndex / spec | | stage.failed | Datagen falhou (etapa pulada) | error | | competitor.started / competitor.progress / competitor.finished | Participante | modelId / chars,charsPerSec,preview / response | | stage.judging / stage.judged | Juiz | stageIndex / judge,evaluation,scoreboard,totalCostUsd | | stage.gabarito / stage.dueled / duel.progress | Julgamento por referência | progresso agregado / duels da etapa | | run.finished / run.error | Fim / erro | Record final / error |

Sessões de treino têm eventos análogos (session.started, iteration.started/finished, iteration.promoted, session.converged, session.holdout, session.finished/error) em GET /sessions/:id/events.

Runs terminais (finished/error/aborted) não abrem stream "vivo": o servidor manda o evento terminal e fecha; o cliente fecha o EventSource (sem reconexão infinita). Keepalive a cada 15 s.


Referência da API

Base: /v1/benchmark. A key vai no header x-openrouter-key (quando exigida).

| Método | Rota | Key? | Descrição | |---|---|:---:|---| | POST | /validate-key | header ou body.apiKey | Valida a key contra GET /key; devolve metadados | | GET | /models | ✅ | Lista modelos com pricing (cache de 24 h por key) | | GET | /techniques | — | Biblioteca curada de técnicas de prompt (sem o meta-prompt) | | GET | /lgpd | — | Base de conhecimento de conformidade LGPD | | POST | /runs | ✅ | Inicia run compare/variation; responde 202 { runId } | | POST | /sessions | ✅ | Inicia sessão de treino; responde 202 { sessionId } | | GET | /runs · /runs/:id | — | Histórico (resumos) · record completo | | GET | /runs/:id/events | — | Stream SSE em tempo real | | GET | /runs/:id/export.csv | — | Exporta os resultados em CSV | | GET | /sessions · /sessions/:id · /sessions/:id/events | — | Sessões de treino + stream | | GET | /health | — | Health check: { "status": "ok", "service": "prompt-builder" } |

Exemplo — iniciar uma run (compare):

curl -X POST http://localhost:3001/v1/benchmark/runs \
  -H "Content-Type: application/json" \
  -H "x-openrouter-key: sk-or-v1-..." \
  -d '{
    "mode": "compare",
    "theme": "Atendimento de clínica de exames com FAQs e políticas",
    "stages": 5,
    "competitorModelIds": ["openai/gpt-5-mini", "openai/gpt-5-nano"],
    "datagenModelId": "deepseek/deepseek-v4-pro",
    "judgeModelIds": ["moonshotai/kimi-k2.6"],
    "concurrency": 8, "timeoutMs": 60000, "maxOutputTokens": 500
  }'
# -> 202 { "runId": "..." }   (acompanhe em /runs/:id/events)

POST /runs faz um pre-flight da key (valida antes de começar) para falhar rápido com mensagem clara, em vez de quebrar lá na etapa 1.


Persistência

Runs em data/runs/<id>.json e sessões de treino em data/sessions/<id>.json (src/storage.ts). data/ é gitignored (regra /data/, ancorada para não ignorar src/data/) e criado em runtime.

  • Escrita atômica: grava em *.tmp com nome único por escrita + rename (evita corrupção e o ENOENT que ocorria quando vários participantes salvavam juntos).
  • Fila por run: gravações de uma mesma run são serializadas.
  • Órfãs viram aborted: ao subir, o servidor marca como aborted runs presas em running (markOrphansAsAborted).
  • Cache no cliente: o frontend espelha resumos/records em IndexedDB (web/src/idb.ts) — o servidor é a fonte de verdade; o cache é fallback offline.
  • Biblioteca de prompts: prompts salvos (campeões de treino/variação) vivem só no cliente, na store prompts do IndexedDB v2 (web/src/engine/promptStore.ts), com versionamento por texto — nada disso passa pelo backend.

Exportação CSV

GET /runs/:id/export.csv gera uma linha por resposta de participante, com escaping correto:

runId, sessionId, iteration, stageIndex, question, contestantId, label, technique, modelId,
status, latencyMs, tokensIn, tokensOut, costUsd, rankPosition, errorMsg, text

rankPosition é a posição (1-based) atribuída pelo juiz naquela etapa (vazio se não ranqueado).


Resiliência ("overkill")

Uma run longa não pode morrer por um soluço de rede ou de um modelo:

  • Etapa isolada: falha de datagen pula a etapa, não mata a run.
  • Datagen com 2 tentativas e timeout estendido (max(timeout, 90s)).
  • Participante com retry (retries: 1); se falhar, vira status: error (resposta vazia) e o juiz ignora respostas inválidas.
  • Juiz e avaliador em Promise.allSettled: um falhando não derruba o outro nem a run.
  • Casos-limite do juiz: 0 respostas válidas → inconclusiva; 1 resposta → auto-ranqueada.
  • Escrita atômica + fila por run; timeouts via AbortController em toda chamada à OpenRouter.
  • Mensagens de erro traduzidas (401/403 = key inválida; 402 = sem crédito; 429 = rate limit).

Segurança da API key

  • A key nunca fica em .env nem no servidor: vive no localStorage do navegador e é enviada no header x-openrouter-key das chamadas que precisam dela.
  • O backend não persiste a key — usa na requisição e descarta.
  • A validação usa o endpoint autenticado GET /key (e não /models, que é público e responderia 200 até para uma key inválida), então uma key ruim é barrada na hora.
  • O SSE de acompanhamento não exige key (a key só é necessária para iniciar a run).

Notas e limitações

  • Custo total exibido = soma das respostas dos participantes. Datagen, juiz e avaliador não entram no totalCostUsd (o foco é o custo da inferência comparada). Preços vêm do catálogo da OpenRouter.
  • Filtro LGPD é consultivo, não garante conformidade (não força roteamento) — ver Conformidade LGPD. Não é aconselhamento jurídico.
  • Sem autenticação de usuário / multiusuário: ferramenta local; o histórico é compartilhado por quem acessa o servidor.
  • Persistência em arquivo (não em banco): ótimo para uso local, não pensado para alta escala.
  • Os modelos default na Nova Run são sugestões editáveis — troque pelos que você quer comparar.

Para o funcionamento interno (pipeline, os 3 modos em detalhe e oportunidades de otimização/paralelização), veja FUNCIONAMENTO.md. Para entender cada tela, veja TELAS.md. Para trabalhar no código com um agente, comece por AGENTS.md e a biblioteca de skills.