feat(dashboard): filter Free Provider Rankings by configured/available (#6150) (#6251)

feat(dashboard): configured-only / available-only filters on Free Provider Rankings (#6150) — server-side query params + tested lib helper; supersedes the client-side #6245 toggle with an available-only dimension. Lib logic 11/11 green; UI validated live on VPS. Base-reds only. Integrated into release/v3.8.45.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-05 10:52:14 -03:00
committed by GitHub
parent f237c07def
commit fc16dcd6ee
6 changed files with 334 additions and 82 deletions

View File

@@ -11,11 +11,10 @@
- **feat(combo):** add an option to **disable session stickiness**, per-combo or globally — round-robin / random combos can rotate to a different connection on every request instead of pinning a whole conversation to one connection by its first-message hash. Resolution precedence per-combo `config.disableSessionStickiness` → global `settings.disableSessionStickiness` → default `false` (preserves the #3825 prompt-cache/504 fix); gates **both** stickiness call sites in `open-sse/services/combo.ts`. Exposed as a global toggle (Combo Defaults) and a per-combo Inherit/on/off control. ([#6168](https://github.com/diegosouzapw/OmniRoute/issues/6168)) Regression guard: `tests/unit/combo-disable-session-stickiness.test.ts`. (thanks @RCrushMe)
- **feat(docker):** add the `OMNIROUTE_NO_SUDO` env flag for root-less / user-namespaced deployments — the MITM cert-trust command path (`resolveSudoSpawn` in `src/mitm/systemCommands.ts`) now strips the leading `sudo` when the flag is truthy, in addition to the existing root / sudo-missing cases, so the Proxy Agent runs without `sudo` (the operator trusts the CA manually, e.g. via `NODE_EXTRA_CA_CERTS`). Argv-array `spawn` preserved — no shell interpolation (Hard Rule #13). ([#6122](https://github.com/diegosouzapw/OmniRoute/issues/6122)) Regression guard: `tests/unit/mitm-systemCommands-no-sudo.test.ts`. (thanks @powellnorma)
- **feat(providers):** add **Requesty** as an OpenAI-compatible gateway provider (BYOK, base `https://router.requesty.ai/v1`, ~200 free requests/day) — wired through the shared OpenAI-compatible registry with full model passthrough (`open-sse/config/providers/registry/requesty/`, `src/shared/constants/providers/apikey/gateways.ts`). ([#6120](https://github.com/diegosouzapw/OmniRoute/issues/6120)) Regression guard: `tests/unit/requesty-provider.test.ts`. (thanks @chirag127)
- **feat(skills):** add a **GitHub skill-discovery** subsystem — search/score/scan/import agent skills from public GitHub repos that ship `SKILL.md` / `CLAUDE.md` / `.cursorrules` files, exposed as MCP tools (`omniroute_github_skills_search`/`scan`/`install`, gated behind `read:skills`/`write:skills` scopes) and a `GET/POST /api/github-skills` route (host-pinned to `api.github.com`, `encodeURIComponent`-escaped, error bodies sanitized). Registers `omni-github-skills` in the agent-skills catalog. Regression guards: `tests/unit/github-collector.test.ts` + the agent-skills catalog/routes/generator/mcp count suites. ([#6186](https://github.com/diegosouzapw/OmniRoute/pull/6186) — thanks @Moseyuh333)
- **feat(dashboard):** add **configured-only / available-only filters** to the Free Provider Rankings page ([#6150](https://github.com/diegosouzapw/OmniRoute/issues/6150)) — hide providers you haven't configured, or whose connections are all rate-limited / out of quota, via server-side query params (`?configuredOnly` / `?availableOnly` on `GET /api/free-provider-rankings`) backed by a testable lib helper reusing the in-process connection state (no Redis). Both filters default off, so the default view is unchanged; this supersedes the earlier client-side "Configured Only" toggle (#6245) with an available-only dimension and unit-tested logic. Regression guard: `tests/unit/freeProviderRankings-filters.test.ts`.
### 🐛 Bug Fixes
- **combo/streaming: fix intermittent 500s, corrupted SSE, and Gemini malformed-response handling ([#5976](https://github.com/diegosouzapw/OmniRoute/issues/5976)).** Five related fixes on the combo streaming path: (1) the quality check now reads a **clone** of the response so the original stream stays unlocked (kills the random `ERR_INVALID_STATE: ReadableStream is locked` 500s), and the abandoned clone branch is cancelled so it no longer buffers the whole body per request; (2) `withEarlyStreamKeepalive` only emits an in-band error frame when **no** bytes were forwarded yet, so a mid-flight upstream drop no longer corrupts a partially-delivered SSE stream; (3) Gemini `finishReason: "MALFORMED_RESPONSE"` maps to OpenAI `content_filter` and triggers combo failover instead of returning broken-but-"successful" output; (4) `/api/usage/call-logs?correlationId=` uses parameterized substring (`LIKE`) matching so partial IDs resolve; (5) per-model 500s skip model-lockout/cooldown for per-model-quota providers. Plus request-logger UI detail improvements. Regression guards: `tests/unit/{earlyStreamKeepalive,finishReason,combo-provider-cooldown-sibling,combo-context-relay,call-logs-correlation-substring,save-call-log-persistence,validate-response-quality}.test.ts`. ([#6216](https://github.com/diegosouzapw/OmniRoute/pull/6216) — thanks @hartmark)
- **chatcore (tools): stop the default 128-tool cap from silently dropping opencode's `task`/MCP tools.** opencode (used as an MCP/agent host) sends a large tool list; when it exceeds the speculative `MAX_TOOLS_LIMIT` (128) default, `truncateToolList` did a blind `tools.slice(0, 128)`, dropping every tool past index 128 — including opencode's built-in `task` tool (subagent launch) and many MCP tools, so models routed through OmniRoute could no longer spawn subagents or reach part of their tools. The cap exists to avoid upstream `400`s for providers with real hard limits (e.g. grok-cli 200), so it is kept for those: detection of the opencode client (`isOpencodeClient` — any `x-opencode-*` header, or `opencode` in the user-agent) now only bypasses the **speculative 128 default**, never a known provider ceiling. Precedence is explicit — a proactive/detected provider limit always truncates (even for opencode); otherwise opencode forwards its full tool list; otherwise the unchanged 128 default applies to every other client. Refactors `getEffectiveToolLimit` into `getKnownToolLimit(provider) ?? DEFAULT_LIMIT` (byte-identical for existing callers) and fixes a cosmetic debug-log that reported the truncated count instead of the original. Regression guard: `tests/unit/tool-limit-detector.test.ts`.
- **fix(mitm):** the macOS MITM-cert install check now matches the system keychain again. `security find-certificate -a -Z` prints the SHA-1 as a colon-less hex string, but the installed-check compared it against `getCertFingerprint()`'s colon-separated form, so the substring match never hit — the cert was reported as not-installed and re-prompted for the sudo install on every run. Fingerprints are now normalized (colons stripped, upper-cased) on both sides via the extracted `macCertOutputHasFingerprint` helper. Regression guard: `tests/unit/mitm-cert-mac-fingerprint.test.ts`. ([#6204](https://github.com/diegosouzapw/OmniRoute/pull/6204), closes [#6134](https://github.com/diegosouzapw/OmniRoute/issues/6134) — thanks @rianonehub)

View File

@@ -61,15 +61,20 @@ export default function FreeProviderRankingsPage() {
const [error, setError] = useState("");
const [filter, setFilter] = useState<string>("");
const [configuredOnly, setConfiguredOnly] = useState(false);
const [configuredProviderIds, setConfiguredProviderIds] = useState<Set<string>>(new Set());
const [availableOnly, setAvailableOnly] = useState(false);
const fetchRankings = useCallback(
async (category?: string) => {
async (category?: string, opts?: { configuredOnly?: boolean; availableOnly?: boolean }) => {
setLoading(true);
setError("");
try {
const url = category
? `/api/free-provider-rankings?category=${category}`
const params = new URLSearchParams();
if (category) params.set("category", category);
if (opts?.configuredOnly) params.set("configuredOnly", "1");
if (opts?.availableOnly) params.set("availableOnly", "1");
const qs = params.toString();
const url = qs
? `/api/free-provider-rankings?${qs}`
: "/api/free-provider-rankings";
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
@@ -85,30 +90,8 @@ export default function FreeProviderRankingsPage() {
);
useEffect(() => {
fetchRankings(filter || undefined);
}, [filter, fetchRankings]);
useEffect(() => {
let active = true;
fetch("/api/providers")
.then((res) => (res.ok ? res.json() : { connections: [] }))
.then((data) => {
if (!active) return;
const ids = new Set<string>();
for (const conn of data.connections || []) {
if (conn?.provider) ids.add(conn.provider);
}
setConfiguredProviderIds(ids);
})
.catch(() => {});
return () => {
active = false;
};
}, []);
const displayedRankings = configuredOnly
? rankings.filter((r) => configuredProviderIds.has(r.id))
: rankings;
fetchRankings(filter || undefined, { configuredOnly, availableOnly });
}, [filter, configuredOnly, availableOnly, fetchRankings]);
return (
<div className="flex flex-col gap-6">
@@ -135,30 +118,33 @@ export default function FreeProviderRankingsPage() {
{t(opt.labelKey)}
</button>
))}
<div className="ml-auto flex items-center gap-2">
<label
htmlFor="configured-only-toggle"
className="text-sm text-text-muted select-none cursor-pointer"
title={t("configuredOnlyHint")}
>
{t("configuredOnly")}
</label>
<button
id="configured-only-toggle"
role="switch"
aria-checked={configuredOnly}
onClick={() => setConfiguredOnly(!configuredOnly)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-500 focus-visible:ring-offset-2 ${
configuredOnly ? "bg-violet-500" : "bg-border"
}`}
>
<span
className={`pointer-events-none block h-4 w-4 rounded-full bg-white shadow-lg transition-transform ${
configuredOnly ? "translate-x-4" : "translate-x-0"
}`}
/>
</button>
</div>
</div>
{/* Availability toggles (default off → show all providers) */}
<div className="flex items-center gap-2 flex-wrap">
<button
onClick={() => setConfiguredOnly((v) => !v)}
aria-pressed={configuredOnly}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
configuredOnly
? "bg-emerald-500 border-emerald-500 text-white"
: "border-border text-text-muted hover:text-text-main hover:border-emerald-500/50"
}`}
>
{t("filterConfiguredOnly")}
</button>
<button
onClick={() => setAvailableOnly((v) => !v)}
aria-pressed={availableOnly}
title={t("filterAvailableOnlyHelp")}
className={`px-4 py-2 text-sm font-medium rounded-lg border transition-colors ${
availableOnly
? "bg-emerald-500 border-emerald-500 text-white"
: "border-border text-text-muted hover:text-text-main hover:border-emerald-500/50"
}`}
>
{t("filterAvailableOnly")}
</button>
</div>
{error && <div className="p-3 rounded-lg bg-red-500/10 text-red-400 text-sm">{error}</div>}
@@ -170,9 +156,9 @@ export default function FreeProviderRankingsPage() {
) : (
<>
{/* Top 3 Podium */}
{displayedRankings.length >= 3 && (
{rankings.length >= 3 && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{displayedRankings.slice(0, 3).map((provider, idx) => (
{rankings.slice(0, 3).map((provider, idx) => (
<Card key={provider.id} className="relative overflow-hidden">
<div
className={`absolute top-0 left-0 right-0 h-1 ${
@@ -216,7 +202,7 @@ export default function FreeProviderRankingsPage() {
)}
{/* Full List */}
{displayedRankings.length > 0 && (
{rankings.length > 0 && (
<Card>
<div className="overflow-x-auto">
<table className="w-full">
@@ -229,11 +215,10 @@ export default function FreeProviderRankingsPage() {
<th className="pb-3 font-medium text-right">{t("colAvgScore")}</th>
<th className="pb-3 font-medium text-right">{t("colModels")}</th>
<th className="pb-3 font-medium text-right">{t("colType")}</th>
<th className="pb-3 font-medium text-right">{t("colConfigured")}</th>
</tr>
</thead>
<tbody>
{displayedRankings.map((provider, idx) => (
{rankings.map((provider, idx) => (
<tr key={provider.id} className="border-b border-border/50 last:border-b-0">
<td className="py-3 text-text-muted font-mono">{idx + 1}</td>
<td className="py-3">
@@ -278,17 +263,6 @@ export default function FreeProviderRankingsPage() {
{provider.category.toUpperCase()}
</span>
</td>
<td className="py-3 text-right">
{configuredProviderIds.has(provider.id) ? (
<span className="text-xs px-2 py-1 rounded bg-green-500/10 text-green-500">
</span>
) : (
<span className="text-xs px-2 py-1 rounded bg-text-muted/10 text-text-muted">
</span>
)}
</td>
</tr>
))}
</tbody>
@@ -297,13 +271,9 @@ export default function FreeProviderRankingsPage() {
</Card>
)}
{displayedRankings.length === 0 && !error && (
{rankings.length === 0 && !error && (
<Card>
<div className="text-center py-12 text-text-muted">
{configuredOnly && rankings.length > 0
? t("noConfiguredProviders")
: t("emptyState")}
</div>
<div className="text-center py-12 text-text-muted">{t("emptyState")}</div>
</Card>
)}
</>

View File

@@ -3,6 +3,12 @@ import { z } from "zod";
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { computeFreeProviderRankings } from "@/lib/freeProviderRankings";
// Coerce common truthy query-string forms ("1", "true", "yes") to a boolean.
const boolParam = z
.string()
.optional()
.transform((val) => val === "1" || val === "true" || val === "yes");
const QuerySchema = z.object({
category: z.string().min(1).max(50).optional(),
limit: z
@@ -13,6 +19,9 @@ const QuerySchema = z.object({
const n = Number(val);
return Number.isFinite(n) && n >= 1 ? Math.min(Math.round(n), 100) : 50;
}),
// Additive filters (default off → current behavior). `availableOnly` implies configured.
configuredOnly: boolParam,
availableOnly: boolParam,
});
export async function OPTIONS() {
@@ -24,6 +33,8 @@ export async function GET(request: NextRequest) {
const parsed = QuerySchema.safeParse({
category: url.searchParams.get("category") || undefined,
limit: url.searchParams.get("limit") || undefined,
configuredOnly: url.searchParams.get("configuredOnly") || undefined,
availableOnly: url.searchParams.get("availableOnly") || undefined,
});
if (!parsed.success) {
@@ -33,8 +44,11 @@ export async function GET(request: NextRequest) {
);
}
const { category, limit } = parsed.data;
const rankings = computeFreeProviderRankings(category, limit);
const { category, limit, configuredOnly, availableOnly } = parsed.data;
const rankings = await computeFreeProviderRankings(category, limit, {
configuredOnly,
availableOnly,
});
return NextResponse.json({ rankings }, { headers: CORS_HEADERS });
}

View File

@@ -9040,6 +9040,9 @@
"colAvgScore": "Avg Score",
"colModels": "Models",
"colType": "Type",
"filterConfiguredOnly": "Configured only",
"filterAvailableOnly": "Available only",
"filterAvailableOnlyHelp": "Hide providers whose connections are all rate-limited or out of quota.",
"configuredOnly": "Configured Only",
"configuredOnlyHint": "Show only providers with active connections",
"noConfiguredProviders": "No configured providers found. Add a provider connection first.",

View File

@@ -11,6 +11,7 @@
import { NOAUTH_PROVIDERS, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers";
import { REGISTRY } from "@omniroute/open-sse/config/providerRegistry";
import { listModelIntelligence } from "./db/modelIntelligence";
import { getProviderConnections } from "./db/providers";
export interface ProviderModelScore {
modelId: string;
@@ -160,16 +161,108 @@ export function findMatchingIntelligence(
return bestPrefixMatch;
}
/**
* Minimal shape of a provider connection needed to decide "configured" /
* "non-exhausted". Matches the camelCase columns returned by
* `getProviderConnections()` (`provider`, `testStatus`, `rateLimitedUntil`).
*/
export interface ConnectionState {
provider: string;
testStatus?: string | null;
rateLimitedUntil?: string | null;
}
/**
* Options controlling the additive "configured" / "available" filters.
* Both default off (undefined/false) → output identical to current behavior.
*/
export interface FreeProviderRankingFilterOptions {
/** Keep only providers that have ≥1 (active) connection configured. */
configuredOnly?: boolean;
/** Keep only providers that have ≥1 non-exhausted, non-rate-limited connection (implies configured). */
availableOnly?: boolean;
}
// Terminal connection statuses — mirrors `isTerminalConnectionStatus`
// (`src/sse/services/auth.ts`). A connection in one of these states stays
// unavailable until credentials/settings change; it never self-recovers.
const TERMINAL_CONNECTION_STATUSES = new Set(["credits_exhausted", "banned", "expired"]);
/**
* Pure predicate: is at least one of a provider's connections usable *right now*?
*
* A connection is usable when it is neither terminal (`testStatus` ∉
* {credits_exhausted, banned, expired}) nor currently rate-limited
* (`rateLimitedUntil` null or in the past — lazy recovery, matching the
* Connection Cooldown rule in CLAUDE.md).
*
* NOTE: granularity is PROVIDER-level (connection = provider+account). Per-model
* quota lockout (model lockout, `open-sse/services/accountFallback.ts`) is a
* deferred Phase 3 and is intentionally NOT consulted here.
*/
export function isProviderUsable(connections: ConnectionState[], now: number = Date.now()): boolean {
return connections.some((conn) => {
const status = (conn.testStatus || "").trim().toLowerCase();
if (TERMINAL_CONNECTION_STATUSES.has(status)) return false;
if (conn.rateLimitedUntil) {
const until = new Date(conn.rateLimitedUntil).getTime();
if (Number.isFinite(until) && until > now) return false;
}
return true;
});
}
/**
* Pure filter over a ranking list + a snapshot of provider connections.
*
* - `configuredOnly`: keep only providers whose `id` appears in `connections`.
* - `availableOnly` (implies configured): additionally require ≥1 usable
* connection per `isProviderUsable`.
*
* With both flags off/absent the input list is returned unchanged.
* Fully synchronous + side-effect-free so it can be unit-tested without a DB.
*/
export function filterFreeProviderRankings(
rankings: FreeProviderRanking[],
connections: ConnectionState[],
opts: FreeProviderRankingFilterOptions = {},
now: number = Date.now()
): FreeProviderRanking[] {
const { configuredOnly, availableOnly } = opts;
if (!configuredOnly && !availableOnly) return rankings;
const byProvider = new Map<string, ConnectionState[]>();
for (const conn of connections) {
const list = byProvider.get(conn.provider);
if (list) {
list.push(conn);
} else {
byProvider.set(conn.provider, [conn]);
}
}
return rankings.filter((ranking) => {
const conns = byProvider.get(ranking.id);
if (!conns || conns.length === 0) return false; // not configured
if (availableOnly) return isProviderUsable(conns, now);
return true; // configuredOnly
});
}
/**
* Compute rankings for free providers based on ELO scores.
*
* @param category - Optional filter for task category (e.g., "coding", "default")
* @param limit - Maximum number of providers to return
* @param opts - Optional additive filters (configured-only / available-only).
* When set, live provider-connection state is read from the DB and providers
* with no configured / no usable connection are dropped. Both default off.
*/
export function computeFreeProviderRankings(
export async function computeFreeProviderRankings(
category?: string,
limit: number = 50
): FreeProviderRanking[] {
limit: number = 50,
opts: FreeProviderRankingFilterOptions = {}
): Promise<FreeProviderRanking[]> {
const freeProviders = getFreeProviders();
const intelligenceEntries = listModelIntelligence({
source: "arena_elo",
@@ -235,5 +328,13 @@ export function computeFreeProviderRankings(
return b.averageScore - a.averageScore;
});
return rankings.slice(0, limit);
// Apply the additive configured/available filters (if requested) BEFORE the
// limit slice, so `limit` counts providers that survive the filter.
let filtered = rankings;
if (opts.configuredOnly || opts.availableOnly) {
const connections = (await getProviderConnections({ isActive: true })) as ConnectionState[];
filtered = filterFreeProviderRankings(rankings, connections, opts);
}
return filtered.slice(0, limit);
}

View File

@@ -0,0 +1,165 @@
/**
* Unit tests for the #6150 "configured / non-exhausted" filters on the Free
* Provider Rankings page.
*
* Targets the PURE helpers `isProviderUsable` + `filterFreeProviderRankings`
* (no DB, no I/O) so the filter logic is exercised in isolation. The async
* `computeFreeProviderRankings` merely wraps these over `getProviderConnections`.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import {
isProviderUsable,
filterFreeProviderRankings,
type ConnectionState,
type FreeProviderRanking,
} from "../../src/lib/freeProviderRankings.ts";
const FIXED_NOW = 1_700_000_000_000; // deterministic "now" for rate-limit math
const future = () => new Date(FIXED_NOW + 60_000).toISOString();
const past = () => new Date(FIXED_NOW - 60_000).toISOString();
function ranking(id: string): FreeProviderRanking {
return {
id,
name: id,
icon: "",
color: "#000",
category: "apikey",
topModel: null,
averageScore: 0.5,
modelCount: 1,
};
}
function conn(provider: string, extra: Partial<ConnectionState> = {}): ConnectionState {
return { provider, testStatus: "active", rateLimitedUntil: null, ...extra };
}
// ──────────────── isProviderUsable ────────────────
test("isProviderUsable: healthy connection is usable", () => {
assert.equal(isProviderUsable([conn("glm")], FIXED_NOW), true);
});
test("isProviderUsable: empty connection list is not usable", () => {
assert.equal(isProviderUsable([], FIXED_NOW), false);
});
test("isProviderUsable: terminal statuses (credits_exhausted/banned/expired) are not usable", () => {
for (const status of ["credits_exhausted", "banned", "expired"]) {
assert.equal(
isProviderUsable([conn("glm", { testStatus: status })], FIXED_NOW),
false,
`expected ${status} to be unusable`
);
}
// case/whitespace-insensitive normalization
assert.equal(isProviderUsable([conn("glm", { testStatus: " BANNED " })], FIXED_NOW), false);
});
test("isProviderUsable: future rateLimitedUntil is not usable; past/null is usable", () => {
assert.equal(isProviderUsable([conn("glm", { rateLimitedUntil: future() })], FIXED_NOW), false);
assert.equal(isProviderUsable([conn("glm", { rateLimitedUntil: past() })], FIXED_NOW), true);
assert.equal(isProviderUsable([conn("glm", { rateLimitedUntil: null })], FIXED_NOW), true);
});
test("isProviderUsable: mixed — one usable connection makes the provider usable", () => {
const conns = [
conn("glm", { testStatus: "credits_exhausted" }),
conn("glm", { rateLimitedUntil: future() }),
conn("glm"), // healthy
];
assert.equal(isProviderUsable(conns, FIXED_NOW), true);
});
// ──────────────── filterFreeProviderRankings ────────────────
const RANKINGS = [ranking("glm"), ranking("groq"), ranking("cerebras")];
test("filter: both flags off returns the input unchanged (regression)", () => {
const out = filterFreeProviderRankings(RANKINGS, [], {}, FIXED_NOW);
assert.deepEqual(
out.map((r) => r.id),
["glm", "groq", "cerebras"]
);
// identical even when connections exist but no flag is set
const out2 = filterFreeProviderRankings(RANKINGS, [conn("glm")], {}, FIXED_NOW);
assert.equal(out2.length, 3);
});
test("filter: configuredOnly keeps only providers with ≥1 connection", () => {
const connections = [conn("glm"), conn("groq", { testStatus: "credits_exhausted" })];
const out = filterFreeProviderRankings(
RANKINGS,
connections,
{ configuredOnly: true },
FIXED_NOW
);
// cerebras has no connection → dropped; groq stays (configured, exhaustion ignored)
assert.deepEqual(
out.map((r) => r.id),
["glm", "groq"]
);
});
test("filter: availableOnly drops exhausted-only provider, keeps healthy", () => {
const connections = [conn("glm"), conn("groq", { testStatus: "credits_exhausted" })];
const out = filterFreeProviderRankings(
RANKINGS,
connections,
{ availableOnly: true },
FIXED_NOW
);
assert.deepEqual(
out.map((r) => r.id),
["glm"]
);
});
test("filter: availableOnly drops rate-limited-only provider; recovers when in the past", () => {
const dropped = filterFreeProviderRankings(
RANKINGS,
[conn("glm", { rateLimitedUntil: future() })],
{ availableOnly: true },
FIXED_NOW
);
assert.deepEqual(
dropped.map((r) => r.id),
[]
);
const recovered = filterFreeProviderRankings(
RANKINGS,
[conn("glm", { rateLimitedUntil: past() })],
{ availableOnly: true },
FIXED_NOW
);
assert.deepEqual(
recovered.map((r) => r.id),
["glm"]
);
});
test("filter: availableOnly keeps a provider that has at least one usable connection", () => {
const connections = [
conn("glm", { testStatus: "banned" }),
conn("glm"), // second connection is healthy
];
const out = filterFreeProviderRankings(
RANKINGS,
connections,
{ availableOnly: true },
FIXED_NOW
);
assert.deepEqual(
out.map((r) => r.id),
["glm"]
);
});
test("filter: availableOnly implies configured (unconfigured provider excluded)", () => {
// no connections at all → nothing survives availableOnly
const out = filterFreeProviderRankings(RANKINGS, [], { availableOnly: true }, FIXED_NOW);
assert.equal(out.length, 0);
});