mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 06:32:16 +03:00
fix(auto-combo): respect model visibility (isHidden) in auto-combo candidate pool (#4558)
Integrated into release/v3.8.33
This commit is contained in:
committed by
GitHub
parent
cf6546e6d3
commit
6b5590e8f4
@@ -129,7 +129,7 @@
|
||||
"open-sse/services/batchProcessor.ts": 828,
|
||||
"open-sse/services/browserBackedChat.ts": 850,
|
||||
"open-sse/services/claudeCodeCompatible.ts": 1202,
|
||||
"open-sse/services/combo.ts": 2695,
|
||||
"open-sse/services/combo.ts": 2701,
|
||||
"open-sse/services/rateLimitManager.ts": 1035,
|
||||
"open-sse/services/tokenRefresh.ts": 1997,
|
||||
"open-sse/services/usage.ts": 3450,
|
||||
@@ -162,6 +162,7 @@
|
||||
"src/app/(dashboard)/dashboard/providers/page.tsx": 1927,
|
||||
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1198,
|
||||
"src/app/(dashboard)/dashboard/settings/components/AppearanceTab.tsx": 819,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ComboDefaultsTab.tsx": 846,
|
||||
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974,
|
||||
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
|
||||
@@ -181,7 +182,7 @@
|
||||
"src/lib/db/apiKeys.ts": 1662,
|
||||
"src/lib/db/core.ts": 1825,
|
||||
"src/lib/db/migrationRunner.ts": 1125,
|
||||
"src/lib/db/models.ts": 1221,
|
||||
"src/lib/db/models.ts": 1259,
|
||||
"src/lib/db/providers.ts": 1050,
|
||||
"src/lib/db/proxies.ts": 1048,
|
||||
"src/lib/db/settings.ts": 1149,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type AutoCategory,
|
||||
type AutoTier,
|
||||
} from "./suffixComposition";
|
||||
import { getHiddenModelsByProvider } from "@/models";
|
||||
|
||||
/** #4235 Phase B: optional category/tier overlay for `auto/<category>:<tier>` combos. */
|
||||
export interface AutoComboSpec {
|
||||
@@ -235,6 +236,7 @@ export async function createVirtualAutoCombo(
|
||||
const blockedProviders = new Set(
|
||||
Array.isArray(settings.blockedProviders) ? (settings.blockedProviders as string[]) : []
|
||||
);
|
||||
const hiddenModelsMap = getHiddenModelsByProvider();
|
||||
|
||||
const validConnections = connections.filter(hasUsableConnectionCredential);
|
||||
|
||||
@@ -250,6 +252,10 @@ export async function createVirtualAutoCombo(
|
||||
}
|
||||
if (!modelId) continue; // Skip providers without a model
|
||||
|
||||
// Skip models that the user has hidden in the dashboard
|
||||
const hiddenModels = hiddenModelsMap.get(conn.provider);
|
||||
if (hiddenModels?.has(modelId)) continue;
|
||||
|
||||
candidatePool.push({
|
||||
provider: conn.provider,
|
||||
connectionId: conn.id,
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
getHandoff,
|
||||
} from "../../src/lib/db/contextHandoffs.ts";
|
||||
import { extractSessionAffinityKey } from "@/sse/services/auth";
|
||||
import { getHiddenModelsByProvider } from "@/models";
|
||||
import { resolveModelLockoutSettings } from "../../src/lib/resilience/modelLockoutSettings";
|
||||
import { fetchCodexQuota } from "./codexQuotaFetcher.ts";
|
||||
import {
|
||||
@@ -318,6 +319,7 @@ export async function buildAutoCandidates(
|
||||
resetWindowConfig: ResetWindowConfig = resolveResetWindowConfig(null),
|
||||
resilienceSettings: ResilienceSettings | null = null
|
||||
): Promise<AutoProviderCandidate[]> {
|
||||
const hiddenModelsMap = getHiddenModelsByProvider();
|
||||
const metrics = getComboMetrics(comboName);
|
||||
// Opt-in hard quota cutoff (default OFF). When disabled, candidates are never
|
||||
// dropped for low quota here — the soft quota penalty + connection cooldown still
|
||||
@@ -513,7 +515,11 @@ export async function buildAutoCandidates(
|
||||
})
|
||||
);
|
||||
|
||||
return candidates;
|
||||
// Filter out candidates whose model is hidden by the user in the dashboard
|
||||
return candidates.filter((c) => {
|
||||
const hiddenModels = hiddenModelsMap.get(c.provider);
|
||||
return !hiddenModels?.has(c.model);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Card, Button, Input, Toggle } from "@/shared/components";
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
import {
|
||||
@@ -102,7 +102,13 @@ export default function ComboDefaultsTab() {
|
||||
});
|
||||
const [codexSessionAffinityTtlMs, setCodexSessionAffinityTtlMs] = useState(0);
|
||||
const [providerOverrides, setProviderOverrides] = useState<any>({});
|
||||
const [newOverrideProvider, setNewOverrideProvider] = useState("");
|
||||
const [availableProviders, setAvailableProviders] = useState<{ id: string; provider: string }[]>(
|
||||
[]
|
||||
);
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [highlightedIdx, setHighlightedIdx] = useState(0);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [status, setStatus] = useState<{ type: "success" | "error" | ""; message: string }>({
|
||||
type: "",
|
||||
@@ -129,6 +135,27 @@ export default function ComboDefaultsTab() {
|
||||
Promise.all([
|
||||
fetch("/api/settings/combo-defaults").then((res) => res.json()),
|
||||
fetch("/api/settings").then((res) => res.json()),
|
||||
fetch("/api/providers")
|
||||
.then((res) => res.json())
|
||||
.then((providers: any[]) => {
|
||||
// Filter: include a provider only if at least one of its connections is active.
|
||||
// Disabled providers (all connections inactive) are excluded.
|
||||
const byProvider = new Map<string, any[]>();
|
||||
for (const p of providers) {
|
||||
if (!p.provider) continue;
|
||||
const list = byProvider.get(p.provider) || [];
|
||||
list.push(p);
|
||||
byProvider.set(p.provider, list);
|
||||
}
|
||||
const activeProviders = Array.from(byProvider.entries())
|
||||
.filter(([, conns]) => conns.some((c) => c.isActive !== false))
|
||||
.map(([name]) => name)
|
||||
.sort();
|
||||
setAvailableProviders(activeProviders.map((p) => ({ id: p, provider: p })));
|
||||
})
|
||||
.catch(() => {
|
||||
/* providers fetch is non-critical */
|
||||
}),
|
||||
])
|
||||
.then(([comboData, settingsData]) => {
|
||||
setComboDefaults((prev) => ({
|
||||
@@ -153,6 +180,17 @@ export default function ComboDefaultsTab() {
|
||||
.catch((err) => console.error("Failed to fetch combo defaults:", err));
|
||||
}, []);
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const showStatus = (type: "success" | "error", message: string) => {
|
||||
setStatus({ type, message });
|
||||
setTimeout(() => setStatus({ type: "", message: "" }), 2500);
|
||||
@@ -204,14 +242,16 @@ export default function ComboDefaultsTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const addProviderOverride = () => {
|
||||
const name = newOverrideProvider.trim().toLowerCase();
|
||||
if (!name || providerOverrides[name]) return;
|
||||
setProviderOverrides((prev) => ({ ...prev, [name]: { maxRetries: 1 } }));
|
||||
setNewOverrideProvider("");
|
||||
const addProviderOverride = (name: string) => {
|
||||
const trimmed = name.trim().toLowerCase();
|
||||
if (!trimmed || providerOverrides[trimmed]) return;
|
||||
setProviderOverrides((prev) => ({ ...prev, [trimmed]: { maxRetries: 1 } }));
|
||||
setDropdownOpen(false);
|
||||
setSearchQuery("");
|
||||
setHighlightedIdx(0);
|
||||
};
|
||||
|
||||
const removeProviderOverride = (provider) => {
|
||||
const removeProviderOverride = (provider: string) => {
|
||||
setProviderOverrides((prev) => {
|
||||
const copy = { ...prev };
|
||||
delete copy[provider];
|
||||
@@ -219,6 +259,55 @@ export default function ComboDefaultsTab() {
|
||||
});
|
||||
};
|
||||
|
||||
// Reorder a provider override by rebuilding the object in the new order.
|
||||
// direction: -1 = move up, +1 = move down
|
||||
const moveProviderOverride = (provider: string, direction: -1 | 1) => {
|
||||
setProviderOverrides((prev) => {
|
||||
const keys = Object.keys(prev);
|
||||
const idx = keys.indexOf(provider);
|
||||
if (idx < 0) return prev;
|
||||
const target = idx + direction;
|
||||
if (target < 0 || target >= keys.length) return prev;
|
||||
// Swap positions
|
||||
[keys[idx], keys[target]] = [keys[target], keys[idx]];
|
||||
// Rebuild object in new order
|
||||
const reordered: Record<string, any> = {};
|
||||
for (const k of keys) {
|
||||
reordered[k] = prev[k];
|
||||
}
|
||||
return reordered;
|
||||
});
|
||||
};
|
||||
|
||||
// Filtered provider list — excludes already-added ones, filtered by search query
|
||||
const filteredProviders = availableProviders.filter(
|
||||
(p) =>
|
||||
!providerOverrides[p.provider] && p.provider.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleDropdownKeyDown = (e: React.KeyboardEvent) => {
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setHighlightedIdx((prev) => Math.min(prev + 1, filteredProviders.length - 1));
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setHighlightedIdx((prev) => Math.max(prev - 1, 0));
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (filteredProviders[highlightedIdx]) {
|
||||
addProviderOverride(filteredProviders[highlightedIdx].provider);
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
setDropdownOpen(false);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
@@ -625,57 +714,122 @@ export default function ComboDefaultsTab() {
|
||||
<p className="font-medium text-sm mb-2">{t("providerOverrides")}</p>
|
||||
<p className="text-xs text-text-muted mb-3">{t("providerOverridesDesc")}</p>
|
||||
|
||||
{Object.entries(providerOverrides).map(([provider, config]: [string, any]) => (
|
||||
<div
|
||||
key={provider}
|
||||
className="flex items-center gap-2 mb-2 p-2 rounded-lg bg-black/[0.02] dark:bg-white/[0.02]"
|
||||
>
|
||||
<span className="text-xs font-mono font-medium min-w-[80px]">{provider}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
value={config.maxRetries ?? 1}
|
||||
onChange={(e) =>
|
||||
setProviderOverrides((prev) => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], maxRetries: parseInt(e.target.value) || 0 },
|
||||
}))
|
||||
}
|
||||
className="text-xs w-16"
|
||||
aria-label={t("providerMaxRetriesAria", { provider })}
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">{t("retries")}</span>
|
||||
<button
|
||||
onClick={() => removeProviderOverride(provider)}
|
||||
className="ml-auto text-red-400 hover:text-red-500 transition-colors"
|
||||
aria-label={t("removeProviderOverrideAria", { provider })}
|
||||
{Object.entries(providerOverrides).map(
|
||||
([provider, config]: [string, any], index: number) => (
|
||||
<div
|
||||
key={provider}
|
||||
className="flex items-center gap-1.5 mb-2 p-2 rounded-lg bg-black/[0.02] dark:bg-white/[0.02]"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{/* Reorder arrows (combo-builder pattern) */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<button
|
||||
onClick={() => moveProviderOverride(provider, -1)}
|
||||
disabled={index === 0}
|
||||
className={`p-0.5 rounded ${index === 0 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||
title="Move up"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">arrow_upward</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveProviderOverride(provider, 1)}
|
||||
disabled={index === Object.keys(providerOverrides).length - 1}
|
||||
className={`p-0.5 rounded ${index === Object.keys(providerOverrides).length - 1 ? "text-text-muted/20 cursor-not-allowed" : "text-text-muted hover:text-primary hover:bg-black/5 dark:hover:bg-white/5"}`}
|
||||
title="Move down"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px]">arrow_downward</span>
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-medium min-w-[80px]">{provider}</span>
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
value={config.maxRetries ?? 1}
|
||||
onChange={(e) =>
|
||||
setProviderOverrides((prev) => ({
|
||||
...prev,
|
||||
[provider]: { ...prev[provider], maxRetries: parseInt(e.target.value) || 0 },
|
||||
}))
|
||||
}
|
||||
className="text-xs w-16"
|
||||
aria-label={t("providerMaxRetriesAria", { provider })}
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">{t("retries")}</span>
|
||||
<button
|
||||
onClick={() => removeProviderOverride(provider)}
|
||||
className="ml-auto text-red-400 hover:text-red-500 transition-colors"
|
||||
aria-label={t("removeProviderOverrideAria", { provider })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
|
||||
close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("newProviderNamePlaceholder")}
|
||||
value={newOverrideProvider}
|
||||
onChange={(e) => setNewOverrideProvider(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && addProviderOverride()}
|
||||
className="text-xs flex-1"
|
||||
aria-label={t("newProviderNameAria")}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addProviderOverride}
|
||||
disabled={!newOverrideProvider.trim()}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 text-xs rounded-lg border border-border/50 bg-black/[0.02] dark:bg-white/[0.02] hover:bg-black/[0.05] dark:hover:bg-white/[0.05] transition-colors w-full mt-2"
|
||||
>
|
||||
{tc("add")}
|
||||
</Button>
|
||||
<span className="flex-1 text-left text-text-muted">
|
||||
{t("selectProviderPlaceholder") || "Select provider..."}
|
||||
</span>
|
||||
<span
|
||||
className="material-symbols-outlined text-[16px] transition-transform"
|
||||
style={{ transform: dropdownOpen ? "rotate(180deg)" : "none" }}
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{dropdownOpen && (
|
||||
<div className="absolute z-50 mt-1 w-full rounded-lg border border-border/50 bg-white dark:bg-gray-900 shadow-lg overflow-hidden">
|
||||
<div className="p-2 border-b border-border/50">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setHighlightedIdx(0);
|
||||
}}
|
||||
className="w-full px-2 py-1.5 text-xs rounded-md border border-border/50 bg-transparent outline-none focus:border-amber-500 transition-colors"
|
||||
placeholder={t("searchProviderPlaceholder") || "Search providers..."}
|
||||
aria-label={t("searchProviderAria") || "Search providers"}
|
||||
onKeyDown={handleDropdownKeyDown}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<ul role="listbox" className="max-h-48 overflow-auto py-1">
|
||||
{filteredProviders.length === 0 ? (
|
||||
<li className="px-3 py-2 text-xs text-text-muted text-center">
|
||||
{availableProviders.filter((p) => !providerOverrides[p.provider]).length === 0
|
||||
? "All providers added"
|
||||
: "No providers found"}
|
||||
</li>
|
||||
) : (
|
||||
filteredProviders.map((p, idx) => (
|
||||
<li
|
||||
key={p.provider}
|
||||
role="option"
|
||||
aria-selected={idx === highlightedIdx}
|
||||
className={`px-3 py-2 text-xs cursor-pointer transition-colors ${
|
||||
idx === highlightedIdx
|
||||
? "bg-black/[0.05] dark:bg-white/[0.05] font-medium"
|
||||
: "hover:bg-black/[0.03] dark:hover:bg-white/[0.03]"
|
||||
}`}
|
||||
onClick={() => addProviderOverride(p.provider)}
|
||||
onMouseEnter={() => setHighlightedIdx(idx)}
|
||||
>
|
||||
{p.provider}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1101,6 +1101,44 @@ export function getModelIsHidden(providerId: string, modelId: string): boolean {
|
||||
return Boolean(co?.isHidden);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a map of provider ID → set of hidden model IDs from all modelCompatOverrides
|
||||
* and customModels. Used by auto-combo candidate building to skip user-hidden models.
|
||||
* Single bulk DB query — not N+1 per model.
|
||||
*/
|
||||
export function getHiddenModelsByProvider(): Map<string, Set<string>> {
|
||||
const db = getDbInstance();
|
||||
const result = new Map<string, Set<string>>();
|
||||
|
||||
// Query all rows from key_value for both namespaces
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT key, value FROM key_value WHERE namespace IN ('modelCompatOverrides', 'customModels')"
|
||||
)
|
||||
.all() as Array<{ key: string; value: string | null }>;
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row.value) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(row.value);
|
||||
if (!Array.isArray(parsed)) continue;
|
||||
for (const entry of parsed) {
|
||||
if (entry && typeof entry === "object" && entry.isHidden) {
|
||||
const modelId = entry.id;
|
||||
if (typeof modelId === "string" && modelId.length > 0) {
|
||||
if (!result.has(row.key)) result.set(row.key, new Set());
|
||||
result.get(row.key)!.add(modelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed entries
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* #3782 — Check if a model was DELETED (trash) rather than merely eye-hidden.
|
||||
*
|
||||
|
||||
@@ -64,6 +64,7 @@ export {
|
||||
getModelUpstreamExtraHeaders,
|
||||
getModelIsHidden,
|
||||
setModelIsHidden,
|
||||
getHiddenModelsByProvider,
|
||||
|
||||
// Synced Available Models
|
||||
getSyncedAvailableModels,
|
||||
|
||||
@@ -24,4 +24,5 @@ export {
|
||||
validateApiKey,
|
||||
isCloudEnabled,
|
||||
resolveProxyForProvider,
|
||||
getHiddenModelsByProvider,
|
||||
} from "@/lib/localDb";
|
||||
|
||||
81
tests/unit/auto-combo-hidden-models-4558.test.ts
Normal file
81
tests/unit/auto-combo-hidden-models-4558.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* #4558 — Auto-combo must respect model visibility (isHidden).
|
||||
*
|
||||
* When an operator hides a model with the EYE/visibility toggle
|
||||
* (`mergeModelCompatOverride(provider, model, { isHidden: true })`, written to
|
||||
* the `modelCompatOverrides` key_value namespace), that model must be excluded
|
||||
* from the AUTO-combo candidate pool. Both auto paths consume the same seam:
|
||||
* - `open-sse/services/combo.ts::buildAutoCandidates` (combo.ts:322,520-521)
|
||||
* - `open-sse/services/autoCombo/virtualFactory.ts` (virtualFactory.ts:239,256-257)
|
||||
* via the new bulk `getHiddenModelsByProvider()` map (single query, not N+1).
|
||||
*
|
||||
* This guards that seam: a hidden model is present in the map's per-provider set
|
||||
* while a visible sibling is not, and that toggling visibility back off
|
||||
* (`isHidden: null`) removes it from the map again. Without the fix the map is
|
||||
* empty and the auto pool would keep serving hidden models.
|
||||
*/
|
||||
import test, { before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
// Hermetic DB: this test writes overrides into the `modelCompatOverrides`
|
||||
// key_value namespace. Without an isolated DATA_DIR it would leak that state
|
||||
// into the shared dev/CI database. Point DATA_DIR at a throwaway dir before any
|
||||
// import that opens the SQLite handle (CLAUDE.md "Database Handles in Tests").
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-test-hidden-4558-"));
|
||||
process.env.DATA_DIR = tmpDir;
|
||||
|
||||
const { mergeModelCompatOverride, getHiddenModelsByProvider, getModelIsHidden } = await import(
|
||||
"../../src/lib/localDb.ts"
|
||||
);
|
||||
const { resetDbInstance } = await import("../../src/lib/db/core.ts");
|
||||
|
||||
before(() => {
|
||||
resetDbInstance();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
resetDbInstance();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const PROVIDER = "openai";
|
||||
const HIDDEN_MODEL = "gpt-hidden-preview";
|
||||
const VISIBLE_MODEL = "gpt-visible-4o";
|
||||
|
||||
test("getHiddenModelsByProvider: empty before any model is hidden", () => {
|
||||
const map = getHiddenModelsByProvider();
|
||||
assert.equal(map.get(PROVIDER)?.has(HIDDEN_MODEL) ?? false, false);
|
||||
});
|
||||
|
||||
test("a hidden model lands in the provider's hidden set; a visible sibling does not", () => {
|
||||
// Hide one model, leave a sibling visible (overridden for an unrelated reason).
|
||||
mergeModelCompatOverride(PROVIDER, HIDDEN_MODEL, { isHidden: true });
|
||||
mergeModelCompatOverride(PROVIDER, VISIBLE_MODEL, { normalizeToolCallId: true });
|
||||
|
||||
// Sanity: the per-model read agrees.
|
||||
assert.equal(getModelIsHidden(PROVIDER, HIDDEN_MODEL), true);
|
||||
assert.equal(getModelIsHidden(PROVIDER, VISIBLE_MODEL), false);
|
||||
|
||||
const map = getHiddenModelsByProvider();
|
||||
const hiddenForProvider = map.get(PROVIDER);
|
||||
assert.ok(hiddenForProvider, "expected an entry for the provider");
|
||||
assert.equal(hiddenForProvider.has(HIDDEN_MODEL), true, "hidden model must be in the set");
|
||||
assert.equal(
|
||||
hiddenForProvider.has(VISIBLE_MODEL),
|
||||
false,
|
||||
"visible model must NOT be in the hidden set"
|
||||
);
|
||||
});
|
||||
|
||||
test("un-hiding a model (isHidden: null) removes it from the map", () => {
|
||||
mergeModelCompatOverride(PROVIDER, HIDDEN_MODEL, { isHidden: null });
|
||||
const map = getHiddenModelsByProvider();
|
||||
assert.equal(
|
||||
map.get(PROVIDER)?.has(HIDDEN_MODEL) ?? false,
|
||||
false,
|
||||
"un-hidden model must drop out of the hidden set"
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user