Fix failed model auto-hide defaults (#3930)

Integrated into release/v3.8.26
This commit is contained in:
Randi
2026-06-15 18:49:48 -04:00
committed by GitHub
parent 17fee53dbd
commit 15c8df4c78
6 changed files with 146 additions and 21 deletions

View File

@@ -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");
});
});

View File

@@ -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}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -4692,6 +4692,7 @@
"showHiddenOnly": "仅隐藏",
"filterByVisibility": "按可见性筛选",
"autoHideFailed": "自动隐藏失败模型",
"autoHideFailedHint": "启用后,测试全部会把非临时失败的模型从 /v1/models 等公共目录中隐藏。单模型测试永远不会自动隐藏。",
"testAllCompleted": "已测试 {total} 个模型:{ok} 个通过,{failed} 个失败",
"modelAutoHidden": "模型 {model} 已自动隐藏(测试失败)",
"filterVisible": "仅可见",