diff --git a/llm-preset-utils.js b/llm-preset-utils.js index 1e02859..22ab264 100644 --- a/llm-preset-utils.js +++ b/llm-preset-utils.js @@ -2,14 +2,26 @@ function normalizeLlmConfigValue(value) { return String(value || "").trim(); } +export function createLlmConfigSnapshot(source = {}) { + return { + llmApiUrl: normalizeLlmConfigValue(source?.llmApiUrl), + llmApiKey: normalizeLlmConfigValue(source?.llmApiKey), + llmModel: normalizeLlmConfigValue(source?.llmModel), + }; +} + +export function isUsableLlmConfigSnapshot(snapshot = {}) { + const normalized = createLlmConfigSnapshot(snapshot); + return Boolean(normalized.llmApiUrl && normalized.llmModel); +} + export function isSameLlmConfigSnapshot(left = {}, right = {}) { + const normalizedLeft = createLlmConfigSnapshot(left); + const normalizedRight = createLlmConfigSnapshot(right); return ( - normalizeLlmConfigValue(left?.llmApiUrl) === - normalizeLlmConfigValue(right?.llmApiUrl) && - normalizeLlmConfigValue(left?.llmApiKey) === - normalizeLlmConfigValue(right?.llmApiKey) && - normalizeLlmConfigValue(left?.llmModel) === - normalizeLlmConfigValue(right?.llmModel) + normalizedLeft.llmApiUrl === normalizedRight.llmApiUrl && + normalizedLeft.llmApiKey === normalizedRight.llmApiKey && + normalizedLeft.llmModel === normalizedRight.llmModel ); } @@ -84,11 +96,7 @@ export function sanitizeLlmPresetSettings(settings = {}) { 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), - }; + const snapshot = createLlmConfigSnapshot(normalized); if ( activePreset && @@ -108,3 +116,50 @@ export function resolveActiveLlmPresetName(settings = {}) { return ""; } + +export function resolveLlmConfigSelection(settings = {}, selectedPresetName = "") { + const normalized = settings && typeof settings === "object" ? settings : {}; + const { presets } = sanitizeLlmPresetSettings(normalized); + const globalConfig = createLlmConfigSnapshot(normalized); + const requestedPresetName = normalizeLlmConfigValue(selectedPresetName); + + if (!requestedPresetName) { + return { + source: "global", + config: globalConfig, + requestedPresetName: "", + presetName: "", + fallbackReason: "", + }; + } + + const presetConfig = presets[requestedPresetName]; + if (!presetConfig) { + return { + source: "global-fallback-missing-task-preset", + config: globalConfig, + requestedPresetName, + presetName: "", + fallbackReason: "selected_task_preset_missing", + }; + } + + const normalizedPresetConfig = createLlmConfigSnapshot(presetConfig); + if (!isUsableLlmConfigSnapshot(normalizedPresetConfig)) { + return { + source: "global-fallback-invalid-task-preset", + config: globalConfig, + requestedPresetName, + presetName: "", + fallbackReason: "selected_task_preset_incomplete", + }; + } + + return { + source: "task-preset", + config: normalizedPresetConfig, + requestedPresetName, + presetName: requestedPresetName, + fallbackReason: "", + }; +} diff --git a/llm.js b/llm.js index c07dae8..d0b7b9e 100644 --- a/llm.js +++ b/llm.js @@ -6,6 +6,8 @@ import { extension_settings } from "../../../extensions.js"; import { chat_completion_sources, sendOpenAIRequest } from "../../../openai.js"; import { debugLog, debugWarn } from "./debug-logging.js"; import { resolveTaskGenerationOptions } from "./generation-options.js"; +import { resolveLlmConfigSelection } from "./llm-preset-utils.js"; +import { getActiveTaskProfile } from "./prompt-profiles.js"; import { resolveConfiguredTimeoutMs } from "./request-timeout.js"; import { applyTaskRegex } from "./task-regex.js"; @@ -122,13 +124,41 @@ function getLlmTestOverride(name) { return typeof override === "function" ? override : null; } -function getMemoryLLMConfig() { +function formatLlmConfigSourceLabel(source = "") { + switch (String(source || "").trim()) { + case "task-preset": + return "任务专用模板"; + case "global-fallback-missing-task-preset": + return "任务模板缺失,已回退当前 API"; + case "global-fallback-invalid-task-preset": + return "任务模板不完整,已回退当前 API"; + case "global": + default: + return "跟随当前 API"; + } +} + +function getMemoryLLMConfig(taskType = "") { const settings = extension_settings[MODULE_NAME] || {}; + const normalizedTaskType = String(taskType || "").trim(); + const activeProfile = normalizedTaskType + ? getActiveTaskProfile(settings, normalizedTaskType) + : null; + const selectedPresetName = + typeof activeProfile?.generation?.llm_preset === "string" + ? activeProfile.generation.llm_preset + : ""; + const selection = resolveLlmConfigSelection(settings, selectedPresetName); return { - apiUrl: normalizeOpenAICompatibleBaseUrl(settings.llmApiUrl), - apiKey: String(settings.llmApiKey || "").trim(), - model: String(settings.llmModel || "").trim(), + apiUrl: normalizeOpenAICompatibleBaseUrl(selection.config?.llmApiUrl), + apiKey: String(selection.config?.llmApiKey || "").trim(), + model: String(selection.config?.llmModel || "").trim(), timeoutMs: getConfiguredTimeoutMs(settings), + llmConfigSource: selection.source || "global", + llmConfigSourceLabel: formatLlmConfigSourceLabel(selection.source), + llmPresetName: selection.presetName || "", + requestedLlmPresetName: selection.requestedPresetName || "", + llmPresetFallbackReason: selection.fallbackReason || "", }; } @@ -1296,9 +1326,15 @@ async function callDedicatedOpenAICompatible( taskType, requestSource, ); - const config = getMemoryLLMConfig(); + const config = getMemoryLLMConfig(taskType); const settings = extension_settings[MODULE_NAME] || {}; const hasDedicatedConfig = hasDedicatedLLMConfig(config); + if (taskType && config.llmPresetFallbackReason) { + debugWarn( + `[ST-BME] 任务 ${taskType} 指定的 API 模板不可用,已回退当前 API: ` + + `${config.requestedLlmPresetName || "(empty)"} / ${config.llmPresetFallbackReason}`, + ); + } const generationResolved = taskType ? resolveTaskGenerationOptions(settings, taskType, { max_completion_tokens: Number.isFinite(maxCompletionTokens) @@ -1332,6 +1368,11 @@ async function callDedicatedOpenAICompatible( : "sillytavern-current-model", model: hasDedicatedConfig ? config.model : "sillytavern-current-model", apiUrl: hasDedicatedConfig ? config.apiUrl : "", + llmConfigSource: config.llmConfigSource || "global", + llmConfigSourceLabel: config.llmConfigSourceLabel || "", + llmPresetName: config.llmPresetName || "", + requestedLlmPresetName: config.requestedLlmPresetName || "", + llmPresetFallbackReason: config.llmPresetFallbackReason || "", messages, generation: generationResolved.generation || {}, filteredGeneration: generationResolved.filtered || {}, @@ -1435,6 +1476,11 @@ async function callDedicatedOpenAICompatible( route: "dedicated-openai-compatible", model: config.model, apiUrl: config.apiUrl, + llmConfigSource: config.llmConfigSource || "global", + llmConfigSourceLabel: config.llmConfigSourceLabel || "", + llmPresetName: config.llmPresetName || "", + requestedLlmPresetName: config.requestedLlmPresetName || "", + llmPresetFallbackReason: config.llmPresetFallbackReason || "", messages, generation: generationResolved.generation || {}, filteredGeneration, diff --git a/panel.js b/panel.js index 5a8739f..902ed45 100644 --- a/panel.js +++ b/panel.js @@ -103,6 +103,18 @@ const GRAPH_WRITE_ACTION_IDS = [ ]; const TASK_PROFILE_GENERATION_GROUPS = [ + { + title: "API 配置", + fields: [ + { + key: "llm_preset", + label: "API 配置模板", + type: "llm_preset", + defaultValue: "", + help: "留空表示跟随当前 API;选中已保存模板后,这个任务会独立使用那套 URL / Key / Model。", + }, + ], + }, { title: "基础生成参数", fields: [ @@ -2587,7 +2599,7 @@ function _bindConfigControls() { const settings = _normalizeLlmPresetSettings(_getSettings?.() || {}); const preset = settings.llmPresets?.[selectedName]; if (!preset) { - _patchSettings({ llmActivePreset: "" }); + _patchSettings({ llmActivePreset: "" }, { refreshTaskWorkspace: true }); _populateLlmPresetSelect(settings.llmPresets || {}, ""); _syncLlmPresetControls(""); toastr.warning("选中的模板不存在,已切回手动模式", "ST-BME"); @@ -2623,7 +2635,7 @@ function _bindConfigControls() { ...(settings.llmPresets || {}), [activePreset]: _getLlmConfigInputSnapshot(), }; - _patchSettings({ llmPresets: nextPresets }); + _patchSettings({ llmPresets: nextPresets }, { refreshTaskWorkspace: true }); _populateLlmPresetSelect(nextPresets, activePreset); _syncLlmPresetControls(activePreset); toastr.success("当前模板已保存", "ST-BME"); @@ -2659,7 +2671,7 @@ function _bindConfigControls() { _patchSettings({ llmPresets: nextPresets, llmActivePreset: trimmedName, - }); + }, { refreshTaskWorkspace: true }); _populateLlmPresetSelect(nextPresets, trimmedName); _syncLlmPresetControls(trimmedName); toastr.success("已另存为新模板", "ST-BME"); @@ -2687,7 +2699,7 @@ function _bindConfigControls() { _patchSettings({ llmPresets: nextPresets, llmActivePreset: "", - }); + }, { refreshTaskWorkspace: true }); _populateLlmPresetSelect(nextPresets, ""); _syncLlmPresetControls(""); toastr.success("模板已删除", "ST-BME"); @@ -3861,7 +3873,11 @@ function _renderTaskGenerationTab(state) {