fix(db): restore node-backed synced catalogs and realign the #8944 context hints

**Production regression from #9294 (d69f521491)**

lookupModelMeta moved from getSyncedAvailableModels(providerId) to
getActiveSyncedCatalog(providerId). The new reader unions models only from rows
in `provider_connections` with isActive = 1 — but a provider NODE lives in
`provider_nodes` and NEVER has a connections row, so filtering by active
connection ids silently dropped every node's synced catalog.

The consequence was not just a missing list: lookupModelMeta reads that catalog
for RUNTIME METADATA, so for openai-compatible nodes it took out
- `supportedThinkingEfforts`, which is what splitSyncedEffortSuffix needs — so
  `<prefix>/<model>-high` stopped resolving to the base id and the effort was
  never derived (#7694), and
- `contextWindow` / `maxInputTokens`, used by the combo context-window filter.

getActiveSyncedCatalog now falls back to the provider-wide key_value set — the
exact pre-#9294 source — when no active connection carries a catalog, and marks
that fallback explicitly NON-authoritative. #9294's live-catalog gating is about
what an active connection actually serves, so a node-backed catalog informs
metadata while never being able to reject a model as unavailable. `available`
therefore stays fail-open for nodes, as it was before.

sync-reasoning-supported-efforts-7694 23/23 (was 21/2).
live-model-catalog-reconciliation-8926 11/11 and combo-provider-wildcard 23/23
confirm #9294's own coverage is untouched.

**#8944 sibling-test drift**

714a315a1a ("Treat context metadata as a routing hint") deliberately turned the
context-window check from a HARD filter into an ordering hint: a catalog-too-small
target is demoted, not removed, because a stale catalog entry must never delete
the only target that could accept the request at runtime. The PR updated one case
in this suite and left three asserting the old drop behaviour. Realigned to the
new contract — the too-small target must lose the ordering to the fitting one
while remaining present — and renamed them from "still rejects"/"still dropped"
to "is demoted"/"ordered last" so the names stop describing the removed
behaviour. 14/14.

**file-size**

tests/unit/translator-openai-to-gemini.test.ts testFrozen 1616 -> 1619: the
frozen value sat exactly at the base size, so the 3 lines the previous commit's
_toolNameMap alignment needs could not fit. Justified in the baseline.

typecheck:core exit 0.

Refs #9298
This commit is contained in:
diegosouzapw
2026-08-07 07:02:12 -03:00
parent 744ab48f1c
commit 18717aa6b8
4 changed files with 54 additions and 32 deletions

View File

@@ -381,7 +381,7 @@
"tests/unit/stream-utils.test.ts": 2445,
"tests/unit/token-refresh-service.test.ts": 1378,
"tests/unit/translator-openai-responses-req.test.ts": 1194,
"tests/unit/translator-openai-to-gemini.test.ts": 1616,
"tests/unit/translator-openai-to-gemini.test.ts": 1619,
"tests/unit/translator-openai-to-kiro.test.ts": 1275,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1483,
@@ -608,5 +608,6 @@
"_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.",
"_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).",
"_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel.",
"_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque."
"_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque.",
"_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente."
}

View File

@@ -1,6 +1,10 @@
import { providerUsesAuthoritativeLiveCatalog } from "@omniroute/open-sse/config/providerRegistry";
import { PROVIDER_ID_TO_ALIAS } from "@omniroute/open-sse/config/providerModels.ts";
import { getSyncedAvailableModelsByConnection, type SyncedAvailableModel } from "../models";
import {
getSyncedAvailableModels,
getSyncedAvailableModelsByConnection,
type SyncedAvailableModel,
} from "../models";
import { getRawProviderConnections } from "../providers";
export type ActiveSyncedCatalog = {
@@ -102,11 +106,26 @@ export async function getActiveSyncedCatalog(providerId: string): Promise<Active
.map((connection) => connection.id);
const models = collectModelsForConnections(modelsByConnection, activeConnectionIds);
if (models.length > 0) {
return {
authoritative: providerUsesAuthoritativeLiveCatalog(providerId),
models,
};
}
return {
authoritative: models.length > 0 && providerUsesAuthoritativeLiveCatalog(providerId),
models,
};
// No ACTIVE CONNECTION carries a catalog for this provider — but a provider
// NODE can: nodes live in `provider_nodes`, never in `provider_connections`,
// so filtering by active connection ids drops their synced catalog entirely.
// Before #9294 this path read the provider-wide key_value set, and losing it
// took every node's runtime metadata with it (supportedThinkingEfforts, so
// `-high`/`-low` effort suffixes stopped resolving, plus contextWindow /
// maxInputTokens used by the combo context-window filter).
//
// Fall back to that provider-wide set, and deliberately keep it
// NON-authoritative: #9294's live-catalog gating is about what an active
// connection actually serves, so a node-backed catalog must inform metadata
// without ever being used to reject a model as unavailable.
return { authoritative: false, models: await getSyncedAvailableModels(storedProviderId) };
} catch {
return { authoritative: false, models: [] };
}

View File

@@ -56,7 +56,11 @@ function capabilityEntry(limitContext: number | null) {
};
}
function capabilityEntryWithLimits(limitInput: number | null, limitContext: number | null, limitOutput = 4096) {
function capabilityEntryWithLimits(
limitInput: number | null,
limitContext: number | null,
limitOutput = 4096
) {
return {
...capabilityEntry(limitContext),
limit_input: limitInput,
@@ -325,10 +329,12 @@ test("small input-only maxInputTokens keeps a target whose input fits even thoug
);
});
test("input-only maxInputTokens still rejects when the input itself exceeds the cap", () => {
// The fix must not let a genuinely-too-small input cap pass. `too-small` has
// maxInputTokens = 1, which cannot even hold the ~11-token input, so it must
// still be dropped while the compatible target survives.
test("input-only maxInputTokens is demoted when the input itself exceeds the cap", () => {
// #8944 made context metadata ADVISORY: a catalog-too-small target is no longer
// removed (a stale catalog entry must never delete the only target that could
// accept the request at runtime), it is ordered AFTER the known-fitting ones.
// `too-small` has maxInputTokens = 1, which cannot hold the ~11-token input, so
// it must lose the ordering to `huge` while remaining available as a fallback.
saveModelsDevCapabilities({
"unit-7039-too-small": {
"too-small": capabilityEntryWithLimits(1, 1_000_000, 500),
@@ -344,14 +350,14 @@ test("input-only maxInputTokens still rejects when the input itself exceeds the
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-7039-too-small/huge"]
["unit-7039-too-small/huge", "unit-7039-too-small/too-small"]
);
});
test("maxInputTokens defaulting to contextWindow still rejects when input + output exceeds the total window (#7039 follow-up)", () => {
// Shared-window model where maxInputTokens equals the total window size.
// The input alone fits the input cap, but input + output overflows the
// window, so the target must be rejected instead of passing on the input cap.
test("maxInputTokens defaulting to contextWindow is demoted when input + output exceeds the total window (#7039 follow-up)", () => {
// Shared-window model where maxInputTokens equals the total window size. The
// input alone fits the input cap but input + output overflows the window, so the
// target must not be PREFERRED — since #8944 it is demoted rather than dropped.
saveModelsDevCapabilities({
"unit-7039-window": {
"shared-window": capabilityEntryWithLimits(400_000, 400_000, 200_000),
@@ -367,7 +373,7 @@ test("maxInputTokens defaulting to contextWindow still rejects when input + outp
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-7039-window/huge"]
["unit-7039-window/huge", "unit-7039-window/shared-window"]
);
});
@@ -391,16 +397,18 @@ test("model_context_override lets a small-catalog target survive a large-context
largeContextBody(),
noopLog
);
assert.deepEqual(
out.map((entry) => entry.modelStr).sort(),
["unit-override/big", "unit-override/capped"]
);
assert.deepEqual(out.map((entry) => entry.modelStr).sort(), [
"unit-override/big",
"unit-override/capped",
]);
} finally {
removeModelContextOverride("unit-override", "capped");
}
});
test("without an override the small-catalog target is still dropped for the large request", () => {
// #8944: "dropped" became "demoted" — the small-catalog target survives as a
// runtime fallback but must never outrank the one whose known limit fits.
test("without an override the small-catalog target is ordered last for the large request", () => {
saveModelsDevCapabilities({
"unit-override": {
big: capabilityEntry(1_000_000),
@@ -417,6 +425,6 @@ test("without an override the small-catalog target is still dropped for the larg
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-override/big"]
["unit-override/big", "unit-override/capped"]
);
});

View File

@@ -582,16 +582,10 @@ test("OpenAI -> Antigravity wraps Gemini requests in a Cloud Code envelope", ()
"model",
"userAgent",
"requestType",
// #9568 (c9a3361e5a): buildChangedToolNameMap now emits IDENTITY entries too,
// because Gemini lowercases tool names in functionCall responses and the
// response translator needs a key to map them back. So any request carrying
// tools now carries `_toolNameMap` in the envelope.
// #9568: identity entries are emitted too (Gemini lowercases tool names in responses).
"_toolNameMap",
]);
assert.deepEqual(
[...(result._toolNameMap as Map<string, string>).entries()],
[["weather", "weather"]]
);
assert.equal((result._toolNameMap as Map<string, string>).get("weather"), "weather");
assert.equal(result.userAgent, "antigravity");
assert.equal(result.requestType, "agent");
assert.match(result.requestId, /^agent\/\d+\/[0-9a-f]{8}$/);