Files
OmniRoute/scripts/check/check-provider-consistency.ts
Tushar Agarwal a298dc6b73 feat(check): make serviceKinds required and add the reverse-walk provider consistency gate (#11392)
serviceKinds now drops .optional() in providerSchema.ts, and check-provider-consistency gains the reverse walk: a canonical provider whose serviceKinds include "llm" must have a REGISTRY entry unless it is in the new KNOWN_CATALOG_ONLY allowlist (providers routed through a connection baseUrl or a specialised executor). That turns "catalog entry outlived its registry entry" — the half-finished provider:remove — into a checkable invariant instead of something a reviewer has to notice.

Reconciled on merge, and worth reading before comparing diffs. The branch's 18 files had landed at the repository ROOT: git diff --name-status showed A gateways.ts, A providerSchema.ts, A check-provider-consistency.test.ts, A backfill-servicekinds.mjs with no directory component. The real provider files, schema, gate and test were never touched, so the +5093/-0 diff was root files AGENTS.md forbids (a test outside tests/, a script outside scripts/) and a no-op for the feature. The content was also 227 commits stale — the root gateways.ts was missing oneminai, among 267 divergent lines.

So each file's actual delta was reapplied onto the current tip rather than copied: the schema one-liner; the gate's KNOWN_CATALOG_ONLY, findCatalogOnlyLlmProviders(), the main() check and the summary line (the branch's copy also repeated the file header and imports at the end — 12 lines of residue from the same accident, dropped); the test's import block and five reverse-walk cases; and backfill-servicekinds.mjs placed at scripts/ad-hoc/, the path its own docstring names, then run against the current catalog: 315 insertions, 352/352 entries declaring serviceKinds, idempotent on a second run.

Two entries the mechanical pass could not get right, both surfaced by doing it against the live tree:

- github in oauth.ts is a single-line object, so the script's id:-per-line regex skipped it — the one failure it reported. Declared ["llm"] by hand, which is what the script's own rule computes.
- magnific came out as ["llm"] but is an image provider (icon: "image", registered in imageRegistry.ts). It is freepik renamed by migration 160, and freepik is in the script's NO_LLM set, so the rename left that set no longer matching. Your reverse walk caught it on its first run — a fair demonstration of why the gate is worth having. Corrected to [], with magnific added to NO_LLM and a note so a re-run cannot reintroduce it.

Verified: check:provider-consistency OK (268 REGISTRY entries, 352 canonical providers, 0 registry-only exceptions, 32 catalog-only), typecheck:core clean, 137/137 across the provider/schema/serviceKinds suites, check-file-size and check:cycles green.

Thanks @Tushar49 — the design is sound and the backfill script did the heavy lifting; only its placement and freshness needed fixing.
2026-09-02 00:32:53 -03:00

133 lines
7.2 KiB
JavaScript

#!/usr/bin/env node
// scripts/check/check-provider-consistency.ts
// Gate anti-alucinação nº1: toda entrada em REGISTRY (open-sse/config/providerRegistry.ts)
// deve corresponder a um provider canônico em src/shared/constants/providers.ts.
// Pega entradas de registry inventadas/meia-registradas (provider com baseUrl+models
// mas ausente da lista canônica → não selecionável pela máquina normal de providers).
// Catraca: exceções pré-existentes ficam em KNOWN_REGISTRY_ONLY; só NOVOS órfãos falham.
// Stale-enforcement (6A.3): entrada em KNOWN_REGISTRY_ONLY que não suprime nenhum órfão
// real → gate falha com instrução de remoção (evita furo de regressão silencioso).
//
// Reverse walk (#10513): providers.ts → REGISTRY. Um provider canônico cujo
// serviceKinds inclui "llm" DEVE ter entrada no REGISTRY — a não ser que esteja em
// KNOWN_CATALOG_ONLY (providers que roteiam via baseUrl de conexão / executor
// especializado sem entrada de registry). Isso torna provider:remove --dry-run
// verificável: um provider removido do REGISTRY mas esquecido em providers.ts
// aparece como órfão reverso e o gate falha.
import { pathToFileURL } from "node:url";
import { AI_PROVIDERS, getProviderById } from "@/shared/constants/providers.ts";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry.ts";
import { assertNoStale } from "./lib/allowlist.mjs";
// Entradas registry-only conhecidas (meia-registro pré-existente). Cada uma com
// justificativa. Remover daqui ao registrar o provider em providers.ts.
export const KNOWN_REGISTRY_ONLY: Record<string, string> = {};
/**
* Providers canônicos com serviceKinds llm que LEGITIMAMENTE não têm entrada no
* REGISTRY. Cada um roteia via baseUrl de conexão (providerSpecificData.baseUrl) ou
* executor especializado, então a ausência de registro não é órfão.
*/
export const KNOWN_CATALOG_ONLY: Record<string, string> = {
"amazon-q": "OAuth/IDE provider roteado via KiroExecutor sem entrada de registry.",
zed: "OAuth/IDE provider (Zed) roteado via executor especializado; sem entrada de registry.",
piapi: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
getgoapi: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
laozhang: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
thebai: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
fenayai: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
empower: "Gateway OpenAI-compatible roteado via baseUrl de conexão.",
"arcee-ai": "API-key provider roteado via baseUrl de conexão.",
"azure-openai": "Azure OpenAI roteado via AzureOpenAIExecutor + baseUrl de conexão.",
"azure-ai": "Azure AI Foundry roteado via AzureAiExecutor + baseUrl de conexão.",
watsonx: "Enterprise provider roteado via baseUrl de conexão.",
oci: "OCI Generative AI roteado via baseUrl de conexão.",
sap: "SAP AI Core roteado via baseUrl de conexão.",
datarobot: "Enterprise provider roteado via baseUrl de conexão.",
clarifai: "Clarifai PAT roteado via baseUrl de conexão.",
"360ai": "Regional provider roteado via baseUrl de conexão.",
gitlab: "GitLab (non-Duo) roteado via executor especializado + baseUrl de conexão.",
"poe-web": "Web/cookie provider roteado via executor especializado (PoeWebExecutor).",
"venice-web": "Web/cookie provider roteado via executor especializado (VeniceWeb).",
"v0-vercel-web": "Web/cookie provider roteado via executor especializado (V0VercelWeb).",
"gemini-business": "Enterprise Gemini roteado via executor especializado + baseUrl de conexão.",
"ollama-local": "Local provider (Ollama) roteado via baseUrl de conexão; sem registry.",
"lm-studio": "Local provider (LM Studio) roteado via baseUrl de conexão.",
vllm: "Local provider (vLLM) roteado via baseUrl de conexão.",
lemonade: "Local provider roteado via baseUrl de conexão.",
llamafile: "Local provider roteado via baseUrl de conexão.",
"llama-cpp": "Local provider roteado via baseUrl de conexão.",
triton: "Local provider (Triton) roteado via baseUrl de conexão.",
"docker-model-runner": "Local provider roteado via baseUrl de conexão.",
xinference: "Local provider (XInference) roteado via baseUrl de conexão.",
oobabooga: "Local provider (Oobabooga) roteado via baseUrl de conexão.",
};
/** Ids do REGISTRY que não são providers canônicos e não estão na allowlist. */
export function findOrphanRegistryIds(
registryIds: string[],
isKnownProvider: (id: string) => boolean,
allowlist: Record<string, string>
): string[] {
return registryIds.filter((id) => !isKnownProvider(id) && !(id in allowlist));
}
/**
* Providers canônicos com serviceKinds llm sem entrada no REGISTRY e fora da
* allowlist — metade de um provider:remove (registro apagado, catálogo esquecido).
*/
export function findCatalogOnlyLlmProviders(
canonicalProviders: Record<string, { serviceKinds?: string[] }>,
registryIds: string[],
allowlist: Record<string, string>
): string[] {
const registry = new Set(registryIds);
return Object.entries(canonicalProviders)
.filter(([id, p]) => {
if (registry.has(id) || id in allowlist) return false;
return Array.isArray(p.serviceKinds) && p.serviceKinds.includes("llm");
})
.map(([id]) => id);
}
function main(): void {
const canonical = new Set(Object.keys(AI_PROVIDERS));
const isKnown = (id: string) => canonical.has(id) || Boolean(getProviderById(id));
// Live orphans BEFORE allowlist filtering (needed for stale-enforcement).
const liveOrphans = Object.keys(REGISTRY).filter((id) => !isKnown(id));
assertNoStale(Object.keys(KNOWN_REGISTRY_ONLY), liveOrphans, "provider-consistency");
const orphans = liveOrphans.filter((id) => !(id in KNOWN_REGISTRY_ONLY));
if (orphans.length) {
console.error(
`[provider-consistency] ${orphans.length} entrada(s) no REGISTRY sem provider canônico em providers.ts:\n` +
orphans.map((id) => `${id}`).join("\n") +
`\n → registre o provider em src/shared/constants/providers.ts ou adicione a KNOWN_REGISTRY_ONLY (scripts/check/check-provider-consistency.ts) com justificativa.`
);
process.exitCode = 1;
}
// Reverse walk: llm-kind canonical provider sem REGISTRY = órfão reverso.
const catalogOnlyLlm = findCatalogOnlyLlmProviders(
AI_PROVIDERS as Record<string, { serviceKinds?: string[] }>,
Object.keys(REGISTRY),
KNOWN_CATALOG_ONLY
);
if (catalogOnlyLlm.length) {
console.error(
`[provider-consistency] ${catalogOnlyLlm.length} provider(s) canônico(s) llm sem entrada no REGISTRY:\n` +
catalogOnlyLlm.map((id) => `${id}`).join("\n") +
`\n → registre o provider em open-sse/config/providers/registry/<id>/ ou adicione a KNOWN_CATALOG_ONLY (scripts/check/check-provider-consistency.ts) com justificativa — órfão reverso de um provider:remove incompleto?`
);
process.exitCode = 1;
}
if (!process.exitCode) {
console.log(
`[provider-consistency] OK — ${Object.keys(REGISTRY).length} entradas REGISTRY, ${canonical.size} providers canônicos, ${Object.keys(KNOWN_REGISTRY_ONLY).length} exceção(ões) registry-only, ${Object.keys(KNOWN_CATALOG_ONLY).length} catalog-only`
);
}
}
if (import.meta.url === pathToFileURL(process.argv[1] || "").href) main();