fix(models): preserve keys after reverted connection edits (#6813)

This commit is contained in:
WuWenbo 2026-10-19 16:29:46 +08:00 committed by GitHub
parent b74e3842f7
commit 294de7cefd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 47 additions and 10 deletions

View File

@ -212,7 +212,6 @@ function ModelEditor({
const [draft, setDraft] = useState(() => modelDraft(model));
const [key, setKey] = useState("");
const [clearKey, setClearKey] = useState(false);
const [connectionChanged, setConnectionChanged] = useState(false);
const [pending, setPending] = useState(false);
const [result, setResult] = useState("");
const active = useRef(true);
@ -229,7 +228,7 @@ function ModelEditor({
model?.provider === draft.provider &&
(draft.provider !== "openai-compatible" ||
model?.base_url === draft.base_url);
const canKeepKey = sameConnection && !connectionChanged && model?.has_api_key;
const canKeepKey = sameConnection && model?.has_api_key;
const requiresKey =
provider?.requires_api_key ?? draft.provider !== "openai-compatible";
const missingKey = requiresKey && ((!key.trim() && !canKeepKey) || clearKey);
@ -246,7 +245,6 @@ function ModelEditor({
...(value === "openai-compatible" ? { base_url: "" } : {}),
});
setKey("");
setConnectionChanged(true);
setClearKey(false);
setResult("");
abort.current?.abort();
@ -255,11 +253,7 @@ function ModelEditor({
return {
config: {
...draft,
...(clearKey || (connectionChanged && !key)
? { api_key: "" }
: key
? { api_key: key }
: {}),
...(clearKey ? { api_key: "" } : key ? { api_key: key } : {}),
},
expected_revision: model?.revision ?? null,
};
@ -345,14 +339,13 @@ function ModelEditor({
onChange={(e) => {
setDraft({ ...draft, base_url: e.target.value });
setKey("");
setConnectionChanged(true);
setClearKey(false);
setResult("");
}}
/>
</label>
)}
{model && (connectionChanged || !sameConnection) && (
{model && !sameConnection && (
<p role="status">{text.providerChanged}</p>
)}
<label className="block space-y-1">

View File

@ -264,6 +264,27 @@ test("switching providers clears test status and cannot reuse a saved key", asyn
).not.toHaveProperty("base_url");
});
test("reverting a provider switch preserves the saved key", async () => {
rs.mocked(loadManagedModels).mockResolvedValue({
models: [{ ...existing, provider: "openai", base_url: null }],
});
mount();
fireEvent.click(await screen.findByRole("button", { name: "Edit model" }));
await selectProvider("Anthropic");
await selectProvider("OpenAI");
expect(screen.queryByText(enUS.settings.models.providerChanged)).toBeNull();
expect(
screen.getByPlaceholderText(enUS.settings.models.keepKey),
).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(saveManagedModel).toHaveBeenCalledTimes(1));
expect(
rs.mocked(saveManagedModel).mock.calls[0]![0].config,
).not.toHaveProperty("api_key");
});
test("custom endpoint is separate and allows no authentication", async () => {
mount();
await waitFor(() =>
@ -289,6 +310,29 @@ test("custom endpoint is separate and allows no authentication", async () => {
});
});
test("reverting a custom endpoint edit preserves the saved key", async () => {
mount();
fireEvent.click(await screen.findByRole("button", { name: "Edit model" }));
const endpoint = screen.getByLabelText("Base URL");
fireEvent.change(endpoint, {
target: { value: "https://other.example.com/v1" },
});
fireEvent.change(endpoint, {
target: { value: existing.base_url },
});
expect(screen.queryByText(enUS.settings.models.providerChanged)).toBeNull();
expect(
screen.getByPlaceholderText(enUS.settings.models.keepKey),
).toBeTruthy();
fireEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => expect(saveManagedModel).toHaveBeenCalledTimes(1));
expect(
rs.mocked(saveManagedModel).mock.calls[0]![0].config,
).not.toHaveProperty("api_key");
});
test("preset edits retain stable ID, leave key blank and preserve advanced overrides", async () => {
rs.mocked(loadManagedModels).mockResolvedValue({
models: [