mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Fix failed model auto-hide defaults (#3930)
Integrated into release/v3.8.26
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
useModelVisibilityHandlers,
|
||||
type UseModelVisibilityHandlersReturn,
|
||||
} from "../hooks/useModelVisibilityHandlers";
|
||||
|
||||
type HookResult = UseModelVisibilityHandlersReturn;
|
||||
|
||||
const t = ((key: string) => key) as ((key: string) => string) & {
|
||||
has: (key: string) => boolean;
|
||||
};
|
||||
t.has = () => false;
|
||||
|
||||
const notify = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
providerId: "anthropic-compatible-cc-test",
|
||||
modelAliases: {},
|
||||
customMap: new Map<string, unknown>(),
|
||||
providerStorageAlias: "anthropic-compatible-cc-test",
|
||||
fetchProviderModelMeta: vi.fn().mockResolvedValue(undefined),
|
||||
fetchAliases: vi.fn().mockResolvedValue(undefined),
|
||||
notify,
|
||||
t,
|
||||
selectedConnection: { id: "conn-1", provider: "anthropic-compatible-cc-test" },
|
||||
providerNode: { id: "anthropic-compatible-cc-test" },
|
||||
};
|
||||
|
||||
function renderHook(): { get: () => HookResult } {
|
||||
let latestResult: HookResult | null = null;
|
||||
|
||||
function Wrapper() {
|
||||
const result = useModelVisibilityHandlers(baseProps);
|
||||
React.useEffect(() => {
|
||||
latestResult = result;
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
const root = createRoot(el);
|
||||
|
||||
act(() => {
|
||||
root.render(<Wrapper />);
|
||||
});
|
||||
|
||||
roots.push({ root, el });
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
if (!latestResult) throw new Error("Hook did not render");
|
||||
return latestResult;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const roots: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
vi.stubGlobal("fetch", vi.fn());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const { root, el } of roots.splice(0)) {
|
||||
act(() => root.unmount());
|
||||
el.remove();
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("useModelVisibilityHandlers", () => {
|
||||
it("defaults auto-hide failed models to off", () => {
|
||||
const hook = renderHook();
|
||||
|
||||
expect(hook.get().autoHideFailed).toBe(false);
|
||||
});
|
||||
|
||||
it("does not hide a model when a single-model test fails", async () => {
|
||||
const fetchMock = vi.mocked(fetch);
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
status: "error",
|
||||
error: "model unavailable",
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
const hook = renderHook();
|
||||
|
||||
await act(async () => {
|
||||
await hook
|
||||
.get()
|
||||
.onTestModel("claude-opus-4-8", "anthropic-compatible-cc-test/claude-opus-4-8");
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
"/api/models/test",
|
||||
expect.objectContaining({ method: "POST" })
|
||||
);
|
||||
expect(
|
||||
fetchMock.mock.calls.some(([url]) => String(url).startsWith("/api/provider-models"))
|
||||
).toBe(false);
|
||||
expect(hook.get().modelTestStatus["claude-opus-4-8"]).toBe("error");
|
||||
});
|
||||
});
|
||||
@@ -149,7 +149,14 @@ export function ModelVisibilityToolbar({
|
||||
</div>
|
||||
)}
|
||||
{onAutoHideFailedChange && (
|
||||
<label className="flex items-center gap-1.5 text-xs text-text-muted">
|
||||
<label
|
||||
className="flex items-center gap-1.5 text-xs text-text-muted"
|
||||
title={providerText(
|
||||
t,
|
||||
"autoHideFailedHint",
|
||||
"When enabled, Test all hides non-transient failures from public catalogs such as /v1/models. Single-model tests never auto-hide."
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoHideFailed ?? false}
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function PassthroughModelsSection({
|
||||
const [modelFilter, setModelFilter] = useState("");
|
||||
const [testingAll, setTestingAll] = useState(false);
|
||||
const [testProgress, setTestProgress] = useState<{ done: number; total: number } | null>(null);
|
||||
const [localAutoHideFailed, setLocalAutoHideFailed] = useState(true);
|
||||
const [localAutoHideFailed, setLocalAutoHideFailed] = useState(false);
|
||||
const autoHideFailed =
|
||||
autoHideFailedProp !== undefined ? autoHideFailedProp : localAutoHideFailed;
|
||||
const setAutoHideFailed = onAutoHideFailedChange ?? setLocalAutoHideFailed;
|
||||
|
||||
@@ -78,11 +78,7 @@ export interface UseModelVisibilityHandlersReturn {
|
||||
setAutoHideFailed: (v: boolean) => void;
|
||||
setVisibilityFilter: (v: "all" | "visible" | "hidden") => void;
|
||||
saveModelCompatFlags: (modelId: string, patch: ModelCompatSavePatch) => Promise<void>;
|
||||
handleToggleModelHidden: (
|
||||
providerKey: string,
|
||||
modelId: string,
|
||||
hidden: boolean
|
||||
) => Promise<void>;
|
||||
handleToggleModelHidden: (providerKey: string, modelId: string, hidden: boolean) => Promise<void>;
|
||||
handleBulkToggleModelHidden: (
|
||||
providerKey: string,
|
||||
modelIds: string[],
|
||||
@@ -112,16 +108,16 @@ export function useModelVisibilityHandlers({
|
||||
}: UseModelVisibilityHandlersParams): UseModelVisibilityHandlersReturn {
|
||||
const [compatSavingModelId, setCompatSavingModelId] = useState<string | null>(null);
|
||||
const [togglingModelId, setTogglingModelId] = useState<string | null>(null);
|
||||
const [bulkVisibilityAction, setBulkVisibilityAction] = useState<
|
||||
"select" | "deselect" | null
|
||||
>(null);
|
||||
const [bulkVisibilityAction, setBulkVisibilityAction] = useState<"select" | "deselect" | null>(
|
||||
null
|
||||
);
|
||||
const [clearingModels, setClearingModels] = useState(false);
|
||||
const [modelFilter, setModelFilter] = useState("");
|
||||
const [testingModelId, setTestingModelId] = useState<string | null>(null);
|
||||
const [modelTestStatus, setModelTestStatus] = useState<Record<string, "ok" | "error">>({});
|
||||
const [testingAll, setTestingAll] = useState(false);
|
||||
const [testProgress, setTestProgress] = useState<{ done: number; total: number } | null>(null);
|
||||
const [autoHideFailed, setAutoHideFailed] = useState(true);
|
||||
const [autoHideFailed, setAutoHideFailed] = useState(false);
|
||||
const [visibilityFilter, setVisibilityFilter] = useState<"all" | "visible" | "hidden">("all");
|
||||
|
||||
const providerAliasEntries = useMemo(
|
||||
@@ -303,25 +299,24 @@ export function useModelVisibilityHandlers({
|
||||
const data = await res.json();
|
||||
if (res.ok && data.status === "ok") {
|
||||
notify.success(
|
||||
providerText(t, "testModelSuccess", `Model ${modelId} is working. Latency: ${data.latencyMs}ms`, {
|
||||
modelId,
|
||||
latencyMs: data.latencyMs,
|
||||
})
|
||||
providerText(
|
||||
t,
|
||||
"testModelSuccess",
|
||||
`Model ${modelId} is working. Latency: ${data.latencyMs}ms`,
|
||||
{
|
||||
modelId,
|
||||
latencyMs: data.latencyMs,
|
||||
}
|
||||
)
|
||||
);
|
||||
setModelTestStatus((prev) => ({ ...prev, [modelId]: "ok" }));
|
||||
} else {
|
||||
notify.error(data.error || "Model test failed");
|
||||
setModelTestStatus((prev) => ({ ...prev, [modelId]: "error" }));
|
||||
// Hidden flag keyed by providerId — same as the manual eye toggle and the read
|
||||
// (fetchProviderModelMeta). providerStorageAlias wrote it under the alias while the
|
||||
// read looked under the canonical id, so auto-hide never reflected.
|
||||
await handleToggleModelHidden(providerId, modelId, true);
|
||||
}
|
||||
} catch (err) {
|
||||
notify.error("Network error testing model");
|
||||
setModelTestStatus((prev) => ({ ...prev, [modelId]: "error" }));
|
||||
// Hidden flag keyed by providerId (see the test-failure branch above).
|
||||
await handleToggleModelHidden(providerId, modelId, true);
|
||||
} finally {
|
||||
setTestingModelId(null);
|
||||
}
|
||||
|
||||
@@ -3921,6 +3921,7 @@
|
||||
"showHiddenOnly": "Hidden only",
|
||||
"filterByVisibility": "Filter by visibility",
|
||||
"autoHideFailed": "Auto-hide failed models",
|
||||
"autoHideFailedHint": "When enabled, Test all hides non-transient failures from public catalogs such as /v1/models. Single-model tests never auto-hide.",
|
||||
"connected": "{count} Connected",
|
||||
"errorCount": "{count} Error ({code})",
|
||||
"errorCountNoCode": "{count} Error",
|
||||
|
||||
@@ -4692,6 +4692,7 @@
|
||||
"showHiddenOnly": "仅隐藏",
|
||||
"filterByVisibility": "按可见性筛选",
|
||||
"autoHideFailed": "自动隐藏失败模型",
|
||||
"autoHideFailedHint": "启用后,测试全部会把非临时失败的模型从 /v1/models 等公共目录中隐藏。单模型测试永远不会自动隐藏。",
|
||||
"testAllCompleted": "已测试 {total} 个模型:{ok} 个通过,{failed} 个失败",
|
||||
"modelAutoHidden": "模型 {model} 已自动隐藏(测试失败)",
|
||||
"filterVisible": "仅可见",
|
||||
|
||||
Reference in New Issue
Block a user