fix: filter deleted providers out of /api/provider-metrics topology (#10714) (#10742)

Co-authored-by: Markus Hartung <mail@hartmark.se>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-19 11:08:12 -03:00
committed by GitHub
parent 41877978e0
commit b755dd5e74
5 changed files with 91 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(db): filter `getProviderMetrics()` to providers with a live `provider_connections` row so a deleted provider stops permanently ghost-haunting the Home "Provider Topology" widget (#10714)

View File

@@ -56,7 +56,10 @@ export interface SearchProviderCountRow {
/**
* Returns one row per provider with call-level aggregates plus last-status
* subselects. Excludes rows where provider is NULL or '-'.
* subselects. Excludes rows where provider is NULL or '-', and excludes
* providers with no live row in `provider_connections` — a deleted provider
* connection must not keep surfacing as a ghost topology node forever from
* its retained historical call_logs rows. See #10714.
*/
export function getProviderMetrics(): ProviderMetricRow[] {
const db = getDbInstance();
@@ -96,6 +99,9 @@ export function getProviderMetrics(): ProviderMetricRow[] {
) as lastErrorStatus
FROM call_logs c
WHERE c.provider IS NOT NULL AND c.provider != '-'
AND EXISTS (
SELECT 1 FROM provider_connections pc WHERE pc.provider = c.provider
)
GROUP BY c.provider`
)
.all() as ProviderMetricRow[];

View File

@@ -93,6 +93,17 @@ test.after(() => {
// ---------------------------------------------------------------------------
test("#3500 getProviderMetrics — aggregates totals and latency per provider", () => {
// #10714: getProviderMetrics() only surfaces providers with a live
// provider_connections row — seed openai/anthropic connections so their
// call_logs rows are not filtered out as ghost/deleted providers.
const db0 = core.getDbInstance();
db0.prepare(
`INSERT INTO provider_connections (id, provider, created_at, updated_at) VALUES (?, ?, ?, ?)`
).run("conn-3500-openai", "openai", new Date().toISOString(), new Date().toISOString());
db0.prepare(
`INSERT INTO provider_connections (id, provider, created_at, updated_at) VALUES (?, ?, ?, ?)`
).run("conn-3500-anthropic", "anthropic", new Date().toISOString(), new Date().toISOString());
// Two openai rows: one success, one error with error_summary
const ts1 = "2025-06-01T10:00:00.000Z";
const ts2 = "2025-06-01T11:00:00.000Z";

View File

@@ -0,0 +1,61 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(
path.join(os.tmpdir(), "omniroute-provider-metrics-deleted-")
);
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const providerMetricsRoute = await import("../../src/app/api/provider-metrics/route.ts");
type ProviderMetricsResponse = {
metrics: Record<string, unknown>;
topology: { providers: string[]; lastProvider: string; errorProvider: string };
};
function resetStorage() {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(() => {
resetStorage();
});
test.after(() => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
});
test("#10714: a provider deleted from provider_connections must NOT keep showing as an errored topology node", async () => {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO provider_connections (id, provider, created_at, updated_at) VALUES (?, ?, ?, ?)`
).run("conn-openai-1", "openai", "2026-08-19T09:00:00.000Z", "2026-08-19T09:00:00.000Z");
db.prepare(
`INSERT INTO call_logs (id, timestamp, provider, status, duration, error_summary) VALUES (?, ?, ?, ?, ?, ?)`
).run("openai-error", "2026-08-19T10:00:00.000Z", "openai", 500, 80, "upstream error");
// Deleted provider: historical call_logs rows exist, but no provider_connections row.
db.prepare(
`INSERT INTO call_logs (id, timestamp, provider, status, duration, error_summary) VALUES (?, ?, ?, ?, ?, ?)`
).run("g4f-pollinations-error", "2026-08-19T11:00:00.000Z", "g4f-pollinations", 402, 50, "payment required");
const response = await providerMetricsRoute.GET();
const body = (await response.json()) as ProviderMetricsResponse;
assert.equal(response.status, 200);
assert.equal(body.topology.providers.includes("g4f-pollinations"), false);
assert.equal(body.metrics["g4f-pollinations"], undefined);
assert.notEqual(body.topology.errorProvider, "g4f-pollinations");
assert.equal(body.topology.providers.includes("openai"), true);
assert.equal(body.topology.errorProvider, "openai");
});

View File

@@ -52,8 +52,17 @@ test.after(() => {
}
});
function seedProviderConnection(provider: string) {
const db = core.getDbInstance();
db.prepare(
`INSERT INTO provider_connections (id, provider, created_at, updated_at) VALUES (?, ?, ?, ?)`
).run(`conn-${provider}`, provider, new Date().toISOString(), new Date().toISOString());
}
test("GET /api/provider-metrics includes provider recency and error topology", async () => {
const db = core.getDbInstance();
seedProviderConnection("openai");
seedProviderConnection("anthropic");
db.prepare(
`INSERT INTO call_logs (id, timestamp, provider, status, duration, error_summary)
VALUES (?, ?, ?, ?, ?, ?)`
@@ -91,6 +100,8 @@ test("GET /api/provider-metrics errorProvider must NOT flag a provider whose mos
// Bug (pre-fix): errorProvider = "providerA" because lastErrorAt > 0.
// Fix (post-fix): errorProvider = "" because lastStatus for providerA is 200.
const db = core.getDbInstance();
seedProviderConnection("providerA");
seedProviderConnection("providerB");
db.prepare(
`INSERT INTO call_logs (id, timestamp, provider, status, duration, error_summary)
VALUES (?, ?, ?, ?, ?, ?)`