fix(flywheel): o juiz e o distiller usam o modelo da instalação, não um id fixo

Os dois pontos do flywheel pediam `claude-haiku-4-5` por nome fixo no emissor.
Esse id só existe no vocabulário da Anthropic, e `providers.ts` entrega o id
verbatim ao provider da org: numa instalação apontada para a OpenRouter o
provedor devolvia 400 `claude-haiku-4-5 is not a valid model ID` e a rodada
agendada morria a cada disparo, sem nenhuma proposta chegando à tela. Medido em
produção em 2026-09-05, nas rodadas de 05:37 e 12:12.

Sem `model` no call site, cada ponto resolve pela cadeia normal do resolvedor —
escolha do painel de provedores, senão o padrão da organização —, que é a mesma
que faz todos os outros pontos de IA funcionarem naquela instalação.

O veredito passa a gravar o provider e o modelo que a chamada realmente usou.
Antes gravava o literal 'anthropic' e o id fixo, o que era falso em qualquer
instalação que não fosse Anthropic direto.

O teste novo é a guarda da regressão, provada nos dois sentidos: com o `model:`
fixo de volta no emissor ele reprova.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
maugarciasa
2026-09-06 16:01:12 -03:00
committed by Rafael Melgaço
co-authored by Claude Opus 5
parent 4bc0fda442
commit 38611d4e8f
3 changed files with 58 additions and 9 deletions
@@ -0,0 +1,13 @@
---
impacto: nada_mudou
secao: corrigido
titulo: A avaliação automática do atendimento volta a rodar em quem não usa Anthropic
---
A rodada que revisa os atendimentos e sugere melhorias pedia um modelo pelo nome
fixo `claude-haiku-4-5`. Esse nome só existe no vocabulário da Anthropic, então
em instalação apontada para outro provedor (OpenRouter, por exemplo) o provedor
recusava a chamada e a rodada morria a cada disparo, sem sugestão nenhuma
chegando à tela de Propostas. Agora os dois pontos do flywheel usam o modelo
escolhido no painel de provedores e, na falta dele, o padrão da organização — o
mesmo caminho de todos os outros pontos de IA.
+13 -9
View File
@@ -10,11 +10,16 @@ import { runModelCall, type LlmEdgeConfig } from '../edge/llm/run-model-call';
import type { Logger } from '../obs/logger';
import { aggregateFollowupOutcomes, type FlowOutcomeStat } from '../../followup/outcome-stats';
const JUDGE_MODEL = 'claude-haiku-4-5';
// O distiller PRECISA de modelo próprio: o flywheel roda org-wide sem turno/agent
// para herdar, então sem isto ele cai no settings.llm.default_model — não setado
// em self-host configurado pela tela — e a rodada falha "modelo LLM não definido".
const DISTILLER_MODEL = 'claude-haiku-4-5';
// Os dois pontos do flywheel NÃO fixam modelo aqui. Fixavam `claude-haiku-4-5`,
// e um id de modelo só é válido no vocabulário do provedor que a instalação usa:
// numa VPS com a org apontada para OpenRouter, esse id literal voltava 400
// `claude-haiku-4-5 is not a valid model ID` e a rodada agendada morria a cada
// disparo (medido em produção em 2026-09-05). Sem `model`, cada ponto resolve
// pela cadeia normal — escolha do painel de provedores, senão o padrão da
// organização —, que é a mesma que faz todos os outros pontos funcionarem na
// instalação. Onde não houver nenhum dos dois, o erro passa a ser o acionável
// "modelo LLM não definido — configure o ponto no painel de provedores", em vez
// de um 400 do provedor.
const DIMENSION = 'memory_hygiene';
const DATASET = 'live';
@@ -152,7 +157,6 @@ export async function runFlywheelOnce(
leadId: turn.contact_id,
jobId: turn.job_id,
purpose: 'flywheel_judge',
model: JUDGE_MODEL,
messages: [{ role: 'user', content: judgePrompt(material, optionOrder) }],
},
{ log },
@@ -163,7 +167,7 @@ export async function runFlywheelOnce(
const { rowCount } = await pool.query(
`insert into flywheel_judge_verdicts
(organization_id, dataset, trace_id, dimension, verdict, option_order, judge_family, model, provenance, run_id)
values ($1,$2,$3,$4,$5,$6,'anthropic',$7,$8,$9)
values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
on conflict (dataset, trace_id, dimension) do nothing`,
[
turn.organization_id,
@@ -172,7 +176,8 @@ export async function runFlywheelOnce(
DIMENSION,
verdictValue,
optionOrder,
JUDGE_MODEL,
judgedCall.provider,
judgedCall.model,
JSON.stringify({ source: 'live_turn', job_id: turn.job_id, contact_id: turn.contact_id }),
runId,
],
@@ -190,7 +195,6 @@ export async function runFlywheelOnce(
leadId: turn.contact_id,
jobId: turn.job_id,
purpose: 'flywheel_distiller',
model: DISTILLER_MODEL,
messages: [{ role: 'user', content: distillerPrompt(verdict.missing_facts ?? []) }],
},
{ log },
@@ -0,0 +1,32 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
/**
* Os dois pontos do flywheel (`flywheel_judge` e `flywheel_distiller`) não podem
* fixar id de modelo no código.
*
* Fixavam `claude-haiku-4-5`, e id de modelo só vale no vocabulário do provedor
* que a instalação usa: numa VPS com a org apontada para OpenRouter o provedor
* devolvia 400 `claude-haiku-4-5 is not a valid model ID` e a rodada agendada
* morria a cada disparo — medido em produção em 2026-09-05. Sem `model`, o ponto
* resolve pela cadeia normal (painel de provedores, senão o padrão da org), que é
* a que faz todos os outros pontos funcionarem naquela mesma instalação.
*/
describe("flywheel vivo", () => {
const fonte = readFileSync(
resolve(process.cwd(), "lib/agent-engine/flywheel/live.ts"),
"utf-8",
);
it("não passa modelo fixo para runModelCall", () => {
const dentroDeChamada = fonte.match(/^\s*model:\s*.+$/gm) ?? [];
expect(dentroDeChamada).toEqual([]);
});
it("grava no veredito o provedor e o modelo que a chamada realmente usou", () => {
expect(fonte).toContain("judgedCall.provider");
expect(fonte).toContain("judgedCall.model");
});
});