fix(sse): exclude search providers from credential-health scheduler sweep

The credential-health scheduler's sweep() tested every active connection
every 5 minutes with no exclusion for search providers. For providers in
SEARCH_VALIDATOR_CONFIGS (tavily-search, exa-search, serper-search,
brave-search, google-pse-search, linkup-search, searchapi-search,
youcom-search), "validation" fires a real billed upstream query
(e.g. POST api.tavily.com/search), so the periodic sweep silently burned
quota with no user-initiated search.

Exclude connections whose provider id is registered in
SEARCH_VALIDATOR_CONFIGS from the sweep's connection-selection filter.
Non-search API-key/OAuth connections remain monitored (#9180, #9289
regressions verified green).

Closes #9970
This commit is contained in:
adevwithpurpose
2026-08-14 21:32:59 -03:00
parent abd4df63dc
commit ef3c1e985a
4 changed files with 106 additions and 2 deletions

View File

@@ -0,0 +1 @@
- fix(sse): exclude search providers from credential-health scheduler sweep to stop burning billed API queries (#9970)

View File

@@ -146,7 +146,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| `OMNIROUTE_SKIP_DB_HEALTHCHECK` | _(unset)_ | `src/lib/db/core.ts` / `src/lib/db/healthCheck.ts` | Set to `1` to skip the SQLite integrity health check on startup. Useful for faster boot on large databases. |
| `CREDENTIAL_HEALTH_CHECK_INTERVAL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/scheduler.ts` | Interval (ms) for the background credential health check scheduler. Minimum: 10000 (10s). |
| `CREDENTIAL_HEALTH_CACHE_TTL` | `300000` | `open-sse/config/constants.ts` / `src/lib/credentialHealth/cache.ts` | TTL (ms) for cached credential health status. |
| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. |
| `OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK` | `false` | `src/lib/credentialHealth/scheduler.ts` | Set to `1` or `true` to disable background periodic testing of provider connections. Search providers (`SEARCH_VALIDATOR_CONFIGS` in `src/lib/providers/validation/searchProviders.ts`, e.g. `tavily-search`) are always excluded from the sweep — their "validation" is a real billed upstream query, so they are never health-checked on a timer (#9970). |
| `HOST` | `0.0.0.0` | `scripts/dev/run-next.mjs` | Bind address for the Next.js dev/start server. Overrides the default `0.0.0.0` when set. |
| `HOSTNAME` | `127.0.0.1` | `scripts/dev/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. **Do not use for `omniroute serve`** — use `OMNIROUTE_SERVER_HOST` instead (POSIX shells auto-set `HOSTNAME` to the machine name; `.env` cannot override it). |
| `OMNIROUTE_SERVER_HOST` | `0.0.0.0` | `bin/cli/commands/serve.mjs` | Bind address for `omniroute serve`. Avoids collision with the POSIX shell `HOSTNAME` variable (always set to the machine name by bash/zsh). Falls back to `0.0.0.0` when unset. (#6194) |

View File

@@ -25,6 +25,7 @@ import {
} from "@/lib/credentialHealth/cache";
import { emit } from "@/lib/events/eventBus";
import { isAutomatedTestProcess } from "@/shared/utils/testProcess";
import { SEARCH_VALIDATOR_CONFIGS } from "@/lib/providers/validation/searchProviders";
// ── Config ────────────────────────────────────────────────────────────────
@@ -230,7 +231,13 @@ export async function sweep(): Promise<void> {
try {
const raw = await getProviderConnections({ isActive: true });
connections = (Array.isArray(raw) ? raw : []).filter(
(conn: any) => conn && conn.id && (conn.authType === "apikey" || conn.authType === "oauth")
(conn: any) =>
conn &&
conn.id &&
(conn.authType === "apikey" || conn.authType === "oauth") &&
// #9970: search-provider "validation" fires a REAL billed upstream
// query (e.g. POST api.tavily.com/search) — never sweep these.
!(conn.provider in SEARCH_VALIDATOR_CONFIGS)
) as Array<{
id: string;
provider: string;

View File

@@ -0,0 +1,96 @@
/**
* Regression test for #9970 — credential-health scheduler burns real billed
* API queries for search providers.
*
* Search-provider "validation" (SEARCH_VALIDATOR_CONFIGS, e.g. tavily-search)
* issues a real upstream query (POST api.tavily.com/search) rather than a
* cheap auth probe. The scheduler's periodic sweep() must exclude connections
* whose provider id is registered in SEARCH_VALIDATOR_CONFIGS so it never
* fires a billed query on a timer.
*
* Mirrors the source-inspection style of
* tests/unit/credential-health-active-connections-9180.test.ts.
*/
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
const schedulerSource = fs.readFileSync(
new URL("../../src/lib/credentialHealth/scheduler.ts", import.meta.url),
"utf8"
);
const searchProvidersSource = fs.readFileSync(
new URL("../../src/lib/providers/validation/searchProviders.ts", import.meta.url),
"utf8"
);
function getSweepConnectionSelection(): string {
const start = schedulerSource.indexOf("export async function sweep(): Promise<void>");
assert.notEqual(start, -1, "credential-health sweep must exist");
const end = schedulerSource.indexOf("\n if (connections.length === 0) return;", start);
assert.notEqual(end, -1, "credential-health connection-selection block must exist");
return schedulerSource.slice(start, end);
}
test("#9970 scheduler imports SEARCH_VALIDATOR_CONFIGS to classify billed-query providers", () => {
assert.match(
schedulerSource,
/import\s*\{\s*SEARCH_VALIDATOR_CONFIGS\s*\}\s*from\s*"@\/lib\/providers\/validation\/searchProviders"/,
"scheduler.ts must import SEARCH_VALIDATOR_CONFIGS from the search-provider validators module"
);
});
test("#9970 sweep() excludes search providers from the connection-selection filter", () => {
const selection = getSweepConnectionSelection();
assert.match(
selection,
/SEARCH_VALIDATOR_CONFIGS/,
"sweep()'s connection-selection block must reference SEARCH_VALIDATOR_CONFIGS to exclude search providers"
);
assert.match(
selection,
/!\(conn\.provider in SEARCH_VALIDATOR_CONFIGS\)/,
"sweep() must filter out connections whose provider id is a registered search-validator provider"
);
});
test("#9970 sweep() still keeps API-key + OAuth eligibility intact (no regression on #9180)", () => {
const selection = getSweepConnectionSelection();
assert.match(
selection,
/getProviderConnections\(\{\s*isActive:\s*true\s*\}\)/,
"the scheduler must still request only active provider connections"
);
assert.match(
selection,
/conn\.authType === "apikey"/,
"API-key connections must remain eligible"
);
assert.match(selection, /conn\.authType === "oauth"/, "OAuth connections must remain eligible");
});
test("#9970 trust anchor: SEARCH_VALIDATOR_CONFIGS providers target real billed upstream endpoints", () => {
// Sanity-check the assumption driving the fix: the search validators really
// do fire live upstream queries (not just an auth ping), so excluding them
// from the periodic sweep is the correct trade-off.
assert.match(
searchProvidersSource,
/api\.tavily\.com\/search/,
"tavily-search validator must target the real Tavily search endpoint"
);
assert.match(
searchProvidersSource,
/export const SEARCH_VALIDATOR_CONFIGS/,
"SEARCH_VALIDATOR_CONFIGS must be exported so the scheduler can reference it"
);
});