Fix memory LLM preset auto detection

This commit is contained in:
Youzini-afk
2026-04-07 11:20:15 +08:00
parent 83dc205869
commit 95205de1df
3 changed files with 266 additions and 77 deletions

110
llm-preset-utils.js Normal file
View File

@@ -0,0 +1,110 @@
function normalizeLlmConfigValue(value) {
return String(value || "").trim();
}
export function isSameLlmConfigSnapshot(left = {}, right = {}) {
return (
normalizeLlmConfigValue(left?.llmApiUrl) ===
normalizeLlmConfigValue(right?.llmApiUrl) &&
normalizeLlmConfigValue(left?.llmApiKey) ===
normalizeLlmConfigValue(right?.llmApiKey) &&
normalizeLlmConfigValue(left?.llmModel) ===
normalizeLlmConfigValue(right?.llmModel)
);
}
export function normalizeLlmPresetMap(rawPresets = {}) {
const normalizedPresets = {};
let changed =
!rawPresets ||
typeof rawPresets !== "object" ||
Array.isArray(rawPresets);
if (!changed) {
for (const [name, preset] of Object.entries(rawPresets)) {
const normalizedName = String(name || "").trim();
if (!normalizedName) {
changed = true;
continue;
}
if (
!preset ||
typeof preset !== "object" ||
Array.isArray(preset) ||
typeof preset.llmApiUrl !== "string" ||
typeof preset.llmApiKey !== "string" ||
typeof preset.llmModel !== "string"
) {
changed = true;
continue;
}
normalizedPresets[normalizedName] = {
llmApiUrl: normalizeLlmConfigValue(preset.llmApiUrl),
llmApiKey: normalizeLlmConfigValue(preset.llmApiKey),
llmModel: normalizeLlmConfigValue(preset.llmModel),
};
if (normalizedName !== name) {
changed = true;
}
}
}
return {
presets: normalizedPresets,
changed,
};
}
export function sanitizeLlmPresetSettings(settings = {}) {
const normalized = settings && typeof settings === "object" ? settings : {};
const { presets, changed: presetChanged } = normalizeLlmPresetMap(
normalized.llmPresets,
);
let activePreset =
typeof normalized.llmActivePreset === "string"
? normalized.llmActivePreset
: "";
let changed = presetChanged || typeof normalized.llmActivePreset !== "string";
if (
activePreset &&
!Object.prototype.hasOwnProperty.call(presets, activePreset)
) {
activePreset = "";
changed = true;
}
return {
presets,
activePreset,
changed,
};
}
export function resolveActiveLlmPresetName(settings = {}) {
const normalized = settings && typeof settings === "object" ? settings : {};
const { presets, activePreset } = sanitizeLlmPresetSettings(normalized);
const snapshot = {
llmApiUrl: normalizeLlmConfigValue(normalized.llmApiUrl),
llmApiKey: normalizeLlmConfigValue(normalized.llmApiKey),
llmModel: normalizeLlmConfigValue(normalized.llmModel),
};
if (
activePreset &&
presets[activePreset] &&
isSameLlmConfigSnapshot(snapshot, presets[activePreset])
) {
return activePreset;
}
const matchingPresets = Object.keys(presets).filter((name) =>
isSameLlmConfigSnapshot(snapshot, presets[name]),
);
if (matchingPresets.length === 1) {
return matchingPresets[0];
}
return "";
}

View File

@@ -10,6 +10,10 @@ import {
buildScopeBadgeText,
normalizeMemoryScope,
} from "./memory-scope.js";
import {
resolveActiveLlmPresetName,
sanitizeLlmPresetSettings,
} from "./llm-preset-utils.js";
import {
cloneTaskProfile,
createBuiltinPromptBlock,
@@ -1866,11 +1870,8 @@ function _bindActions() {
}
function _refreshConfigTab() {
let settings = _normalizeLlmPresetSettings(_getSettings?.() || {});
const resolvedActiveLlmPreset = _resolveActiveLlmPresetName(settings);
if (resolvedActiveLlmPreset !== String(settings.llmActivePreset || "")) {
settings = _patchSettings({ llmActivePreset: resolvedActiveLlmPreset });
}
const settings = _resolveAndPersistActiveLlmPreset(_getSettings?.() || {});
const resolvedActiveLlmPreset = String(settings.llmActivePreset || "");
_refreshPlannerLauncher();
_setCheckboxValue("bme-setting-enabled", settings.enabled ?? true);
@@ -5736,81 +5737,28 @@ function _patchSettings(patch = {}, options = {}) {
}
function _normalizeLlmPresetSettings(settings = _getSettings?.() || {}) {
const rawPresets = settings?.llmPresets;
const normalizedPresets = {};
let changed =
!rawPresets ||
typeof rawPresets !== "object" ||
Array.isArray(rawPresets);
const normalized = sanitizeLlmPresetSettings(settings);
if (!changed) {
for (const [name, preset] of Object.entries(rawPresets)) {
if (!String(name || "").trim()) {
changed = true;
continue;
}
if (
!preset ||
typeof preset !== "object" ||
Array.isArray(preset) ||
typeof preset.llmApiUrl !== "string" ||
typeof preset.llmApiKey !== "string" ||
typeof preset.llmModel !== "string"
) {
changed = true;
continue;
}
normalizedPresets[name] = {
llmApiUrl: preset.llmApiUrl,
llmApiKey: preset.llmApiKey,
llmModel: preset.llmModel,
};
}
}
let activePreset =
typeof settings?.llmActivePreset === "string" ? settings.llmActivePreset : "";
if (activePreset && !Object.prototype.hasOwnProperty.call(normalizedPresets, activePreset)) {
activePreset = "";
changed = true;
}
if (typeof settings?.llmActivePreset !== "string") {
changed = true;
}
if (!changed) {
if (!normalized.changed) {
return settings;
}
return _patchSettings({
llmPresets: normalizedPresets,
llmActivePreset: activePreset,
llmPresets: normalized.presets,
llmActivePreset: normalized.activePreset,
});
}
function _resolveActiveLlmPresetName(settings = _getSettings?.() || {}) {
const activePreset = String(settings?.llmActivePreset || "");
if (!activePreset) return "";
const preset = settings?.llmPresets?.[activePreset];
if (!preset) return "";
return _isSameLlmConfigSnapshot(
{
llmApiUrl: String(settings?.llmApiUrl || ""),
llmApiKey: String(settings?.llmApiKey || ""),
llmModel: String(settings?.llmModel || ""),
},
preset,
)
? activePreset
: "";
}
function _isSameLlmConfigSnapshot(left = {}, right = {}) {
return (
String(left?.llmApiUrl || "") === String(right?.llmApiUrl || "") &&
String(left?.llmApiKey || "") === String(right?.llmApiKey || "") &&
String(left?.llmModel || "") === String(right?.llmModel || "")
);
function _resolveAndPersistActiveLlmPreset(settings = _getSettings?.() || {}) {
const normalizedSettings = _normalizeLlmPresetSettings(settings);
const resolvedActivePreset = resolveActiveLlmPresetName(normalizedSettings);
if (
resolvedActivePreset !==
String(normalizedSettings?.llmActivePreset || "")
) {
return _patchSettings({ llmActivePreset: resolvedActivePreset });
}
return normalizedSettings;
}
function _getLlmConfigInputSnapshot() {
@@ -5877,11 +5825,8 @@ function _markLlmPresetDirty(options = {}) {
_clearFetchedLlmModels();
}
const activePreset = String((_getSettings?.() || {}).llmActivePreset || "");
if (activePreset) {
_patchSettings({ llmActivePreset: "" });
}
_syncLlmPresetControls("");
const settings = _resolveAndPersistActiveLlmPreset(_getSettings?.() || {});
_syncLlmPresetControls(String(settings?.llmActivePreset || ""));
}
function _highlightThemeChoice(themeName) {

134
tests/llm-preset-utils.mjs Normal file
View File

@@ -0,0 +1,134 @@
import assert from "node:assert/strict";
import {
isSameLlmConfigSnapshot,
normalizeLlmPresetMap,
resolveActiveLlmPresetName,
sanitizeLlmPresetSettings,
} from "../llm-preset-utils.js";
assert.equal(
isSameLlmConfigSnapshot(
{
llmApiUrl: " https://example.com/v1 ",
llmApiKey: " sk-test ",
llmModel: " model-a ",
},
{
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-test",
llmModel: "model-a",
},
),
true,
);
const normalizedMap = normalizeLlmPresetMap({
Alpha: {
llmApiUrl: " https://example.com/v1 ",
llmApiKey: " sk-alpha ",
llmModel: " model-a ",
},
"": {
llmApiUrl: "https://bad.example/v1",
llmApiKey: "sk-bad",
llmModel: "bad-model",
},
Broken: {
llmApiUrl: "https://broken.example/v1",
llmApiKey: 42,
llmModel: "broken",
},
});
assert.equal(normalizedMap.changed, true);
assert.deepEqual(normalizedMap.presets, {
Alpha: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-alpha",
llmModel: "model-a",
},
});
const sanitized = sanitizeLlmPresetSettings({
llmPresets: {
Alpha: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-alpha",
llmModel: "model-a",
},
},
llmActivePreset: "Missing",
});
assert.equal(sanitized.changed, true);
assert.equal(sanitized.activePreset, "");
const uniqueMatchSettings = {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-alpha",
llmModel: "model-a",
llmPresets: {
Alpha: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-alpha",
llmModel: "model-a",
},
Beta: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-beta",
llmModel: "model-b",
},
},
llmActivePreset: "",
};
assert.equal(resolveActiveLlmPresetName(uniqueMatchSettings), "Alpha");
const preservedActiveSettings = {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
llmPresets: {
Alpha: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
},
Beta: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
},
},
llmActivePreset: "Beta",
};
assert.equal(resolveActiveLlmPresetName(preservedActiveSettings), "Beta");
const ambiguousSettings = {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
llmPresets: {
Alpha: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
},
Beta: {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-shared",
llmModel: "shared-model",
},
},
llmActivePreset: "",
};
assert.equal(resolveActiveLlmPresetName(ambiguousSettings), "");
const noMatchSettings = {
llmApiUrl: "https://example.com/v1",
llmApiKey: "sk-gamma",
llmModel: "model-gamma",
llmPresets: uniqueMatchSettings.llmPresets,
llmActivePreset: "",
};
assert.equal(resolveActiveLlmPresetName(noMatchSettings), "");
console.log("llm-preset-utils tests passed");