diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 7e65489368..2770b5f690 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -2027,7 +2027,7 @@ "description": "温度参数", "hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。", "type": "float", - "default": 0.6, + "default": 1.0, "slider": {"min": 0, "max": 2, "step": 0.1}, }, "top_p": { @@ -2035,7 +2035,7 @@ "description": "Top-p 采样", "hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。", "type": "float", - "default": 1.0, + "default": 0.95, "slider": {"min": 0, "max": 1, "step": 0.01}, }, "max_tokens": { @@ -2046,6 +2046,11 @@ "default": 8192, }, }, + "default_disabled_keys": [ + "temperature", + "top_p", + "max_tokens", + ], }, "provider": { "type": "string", diff --git a/astrbot/core/provider/provider.py b/astrbot/core/provider/provider.py index efe9e2e47e..227e95614e 100644 --- a/astrbot/core/provider/provider.py +++ b/astrbot/core/provider/provider.py @@ -74,6 +74,35 @@ def __init__( super().__init__(provider_config) self.provider_settings = provider_settings + def get_effective_custom_extra_body(self) -> dict: + """Get custom_extra_body with disabled keys filtered out. + + Returns the custom_extra_body dict excluding any keys that are + listed in the _disabled_keys metadata field within the dict. + """ + custom_extra_body = self.provider_config.get("custom_extra_body", {}) + if not isinstance(custom_extra_body, dict): + return {} + disabled_keys = custom_extra_body.get("_disabled_keys", []) + if not isinstance(disabled_keys, list): + disabled_keys = [] + return { + k: v + for k, v in custom_extra_body.items() + if k != "_disabled_keys" and k not in disabled_keys + } + + def _merge_custom_extra_body(self, payloads: dict) -> dict: + """Merge effective custom_extra_body into payloads without overwriting existing keys. + + Returns a shallow copy of payloads with custom_extra_body values merged in. + """ + merged = dict(payloads) + for k, v in self.get_effective_custom_extra_body().items(): + if k not in merged: + merged[k] = v + return merged + @abc.abstractmethod def get_current_key(self) -> str: raise NotImplementedError diff --git a/astrbot/core/provider/sources/anthropic_source.py b/astrbot/core/provider/sources/anthropic_source.py index 5caef0b263..57378ca351 100644 --- a/astrbot/core/provider/sources/anthropic_source.py +++ b/astrbot/core/provider/sources/anthropic_source.py @@ -328,7 +328,14 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: else "auto" } - extra_body = self.provider_config.get("custom_extra_body", {}) + extra_body = self.get_effective_custom_extra_body() + # Anthropic requires max_tokens - always include it even if disabled + full_custom_extra_body = self.provider_config.get("custom_extra_body", {}) + if ( + isinstance(full_custom_extra_body, dict) + and "max_tokens" in full_custom_extra_body + ): + extra_body["max_tokens"] = full_custom_extra_body["max_tokens"] if "max_tokens" not in payloads: payloads["max_tokens"] = 65536 @@ -422,7 +429,14 @@ async def _query_stream( final_tool_calls = [] id = None usage = TokenUsage() - extra_body = self.provider_config.get("custom_extra_body", {}) + extra_body = self.get_effective_custom_extra_body() + # Anthropic requires max_tokens - always include it even if disabled + full_custom_extra_body = self.provider_config.get("custom_extra_body", {}) + if ( + isinstance(full_custom_extra_body, dict) + and "max_tokens" in full_custom_extra_body + ): + extra_body["max_tokens"] = full_custom_extra_body["max_tokens"] reasoning_content = "" reasoning_signature = "" diff --git a/astrbot/core/provider/sources/gemini_source.py b/astrbot/core/provider/sources/gemini_source.py index f38fcfc359..bad05de93e 100644 --- a/astrbot/core/provider/sources/gemini_source.py +++ b/astrbot/core/provider/sources/gemini_source.py @@ -605,6 +605,9 @@ def _process_content_parts( async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: """非流式请求 Gemini API""" + # Merge effective custom_extra_body into payloads + payloads = self._merge_custom_extra_body(payloads) + system_instruction = next( (msg["content"] for msg in payloads["messages"] if msg["role"] == "system"), None, @@ -694,6 +697,9 @@ async def _query_stream( tools: ToolSet | None, ) -> AsyncGenerator[LLMResponse, None]: """流式请求 Gemini API""" + # Merge effective custom_extra_body into payloads + payloads = self._merge_custom_extra_body(payloads) + system_instruction = next( (msg["content"] for msg in payloads["messages"] if msg["role"] == "system"), None, diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 1585846987..ba57b8c588 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -585,9 +585,8 @@ async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse: del payloads[key] # 读取并合并 custom_extra_body 配置 - custom_extra_body = self.provider_config.get("custom_extra_body", {}) - if isinstance(custom_extra_body, dict): - extra_body.update(custom_extra_body) + custom_extra_body = self.get_effective_custom_extra_body() + extra_body.update(custom_extra_body) self._apply_provider_specific_extra_body_overrides(extra_body) model = payloads.get("model", "").lower() @@ -631,9 +630,7 @@ async def _query_stream( extra_body = {} # 读取并合并 custom_extra_body 配置 - custom_extra_body = self.provider_config.get("custom_extra_body", {}) - if isinstance(custom_extra_body, dict): - extra_body.update(custom_extra_body) + extra_body.update(self.get_effective_custom_extra_body()) to_del = [] for key in payloads: diff --git a/dashboard/src/components/shared/ObjectEditor.vue b/dashboard/src/components/shared/ObjectEditor.vue index a57a606a56..3b0432112d 100644 --- a/dashboard/src/components/shared/ObjectEditor.vue +++ b/dashboard/src/components/shared/ObjectEditor.vue @@ -110,7 +110,7 @@
{{ t('core.common.objectEditor.presets') }}
-
+
@@ -118,7 +118,7 @@ {{ resolveTemplateText(templateKey, 'hint', template.hint) }}
- +
@@ -139,6 +140,7 @@ color="primary" density="compact" hide-details + :disabled="isTemplateKeyDisabled(templateKey)" class="flex-grow-1" > @@ -158,20 +161,39 @@ @update:model-value="updateTemplateValue(templateKey, $event)" density="compact" hide-details + :disabled="isTemplateKeyDisabled(templateKey)" color="primary" > - - - mdi-close - + + + + + + +
@@ -277,6 +299,10 @@ const newKey = ref('') const newValueType = ref('string') const nextPairId = ref(0) +// Disabled keys tracking +const localDisabledKeys = ref([]) +const originalDisabledKeys = ref([]) + // Template schema support const templateSchema = computed(() => { return props.itemMeta?.template_schema || {} @@ -286,9 +312,20 @@ const hasTemplateSchema = computed(() => { return Object.keys(templateSchema.value).length > 0 }) -// 计算要显示的键名 +// Default disabled keys from metadata +const defaultDisabledKeys = computed(() => { + return props.itemMeta?.default_disabled_keys || [] +}) + +// Keys that cannot be disabled +const nonDisableableKeys = computed(() => { + return props.itemMeta?.non_disableable_keys || [] +}) + +// 计算要显示的键名 (exclude _disabled_keys from display) const displayKeys = computed(() => { - return Object.keys(props.modelValue).slice(0, props.maxDisplayItems) + if (!props.modelValue) return [] + return Object.keys(props.modelValue).filter(k => k !== '_disabled_keys').slice(0, props.maxDisplayItems) }) // 分离模板字段和普通字段 @@ -318,7 +355,23 @@ function createPair({ key, value, type, slider, template, jsonError = '', _origi function initializeLocalKeyValuePairs() { localKeyValuePairs.value = [] nextPairId.value = 0 - for (const [key, value] of Object.entries(props.modelValue)) { + + // Initialize disabled keys from modelValue or defaults + const existingDisabled = props.modelValue?._disabled_keys + if (Array.isArray(existingDisabled)) { + localDisabledKeys.value = existingDisabled.filter(k => !nonDisableableKeys.value.includes(k)) + } else if (Object.keys(props.modelValue || {}).filter(k => k !== '_disabled_keys').length === 0 && defaultDisabledKeys.value.length > 0) { + // New/empty config: use default disabled keys + localDisabledKeys.value = defaultDisabledKeys.value.filter(k => !nonDisableableKeys.value.includes(k)) + } else { + localDisabledKeys.value = [] + } + originalDisabledKeys.value = [...localDisabledKeys.value] + + for (const [key, value] of Object.entries(props.modelValue || {})) { + // Skip the internal _disabled_keys field + if (key === '_disabled_keys') continue + let _type = (typeof value) === 'object' ? 'json':(typeof value) let _value = _type === 'json' ? JSON.stringify(value) : value @@ -431,6 +484,43 @@ function isTemplateKeyAdded(templateKey) { return localKeyValuePairs.value.some(pair => pair.key === templateKey) } +function isTemplateKeyDisabled(templateKey) { + return localDisabledKeys.value.includes(templateKey) +} + +function isTemplateValueModified(templateKey) { + const template = templateSchema.value[templateKey] + if (!template || template.default === undefined) return false + const pair = localKeyValuePairs.value.find(p => p.key === templateKey) + if (!pair) return false + const type = template.type || 'string' + if (type === 'number' || type === 'float' || type === 'int') { + const pairNum = Number(pair.value) + const defaultNum = Number(template.default) + if (isNaN(pairNum) && isNaN(defaultNum)) return false + return pairNum !== defaultNum + } + return String(pair.value) !== String(template.default) +} + +function toggleTemplateKeyDisabled(templateKey) { + const index = localDisabledKeys.value.indexOf(templateKey) + if (index >= 0) { + // Enable: remove from disabled list + localDisabledKeys.value.splice(index, 1) + } else { + // Disable: add to disabled list + localDisabledKeys.value.push(templateKey) + } +} + +function resetTemplateKey(templateKey) { + const template = templateSchema.value[templateKey] + if (template && template.default !== undefined) { + updateTemplateValue(templateKey, template.default) + } +} + function getTemplateValue(templateKey) { const pair = localKeyValuePairs.value.find(pair => pair.key === templateKey) if (pair) { @@ -496,29 +586,25 @@ function confirmDialog() { break case 'float': case 'number': - // 尝试转换为数字,如果失败则保持原值(或设为默认值0) convertedValue = Number(pair.value) - // 可选:检查是否为有效数字,无效则设为0或报错 - // if (isNaN(convertedValue)) convertedValue = 0; break case 'bool': case 'boolean': - // 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换 - // 注意:在 JavaScript 中,只有严格的 false, 0, '', null, undefined, NaN 会被转换为 false - // 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值 - // convertedValue = Boolean(pair.value) break case 'json': convertedValue = JSON.parse(pair.value) break case 'string': default: - // 默认转换为字符串 convertedValue = String(pair.value) break } updatedValue[pair.key] = convertedValue } + // Store disabled keys in the value if there are any + if (localDisabledKeys.value.length > 0) { + updatedValue['_disabled_keys'] = [...localDisabledKeys.value] + } emit('update:modelValue', updatedValue) dialog.value = false } @@ -526,6 +612,7 @@ function confirmDialog() { function cancelDialog() { // Reset to original state localKeyValuePairs.value = originalKeyValuePairs.value.map(pair => ({ ...pair })) + localDisabledKeys.value = [...originalDisabledKeys.value] dialog.value = false } @@ -550,7 +637,7 @@ function resolveTemplateText(templateKey, attr, fallback) { transition: opacity 0.2s; } -.template-field-inactive { - opacity: 0.8; +.template-field-disabled { + opacity: 0.5; } diff --git a/dashboard/src/composables/useProviderModelConfigDialog.ts b/dashboard/src/composables/useProviderModelConfigDialog.ts index 4d041423ed..bc29ed64ad 100644 --- a/dashboard/src/composables/useProviderModelConfigDialog.ts +++ b/dashboard/src/composables/useProviderModelConfigDialog.ts @@ -46,6 +46,12 @@ export function useProviderModelConfigDialog(options: UseProviderModelConfigDial schema.provider.items[key].invisible = true } } + + // Anthropic requires max_tokens - mark it as non-disableable + if (sourceType === 'anthropic_chat_completion' && schema.provider.items.custom_extra_body) { + schema.provider.items.custom_extra_body.non_disableable_keys = ['max_tokens'] + } + return schema }) diff --git a/dashboard/src/i18n/locales/en-US/core/common.json b/dashboard/src/i18n/locales/en-US/core/common.json index 973feea542..cbdff33da9 100644 --- a/dashboard/src/i18n/locales/en-US/core/common.json +++ b/dashboard/src/i18n/locales/en-US/core/common.json @@ -115,6 +115,9 @@ "valueTypeLabel": "Value type", "keyExists": "Key already exists", "invalidJson": "Invalid JSON format", + "resetToDefault": "Reset to default", + "disableParam": "Disable injection", + "enableParam": "Enable injection", "placeholders": { "keyName": "Key", "stringValue": "String value", diff --git a/dashboard/src/i18n/locales/ru-RU/core/common.json b/dashboard/src/i18n/locales/ru-RU/core/common.json index e9f0d4a112..59ea0b73a3 100644 --- a/dashboard/src/i18n/locales/ru-RU/core/common.json +++ b/dashboard/src/i18n/locales/ru-RU/core/common.json @@ -115,6 +115,9 @@ "valueTypeLabel": "Тип значения", "keyExists": "Ключ уже существует", "invalidJson": "Некорректный формат JSON", + "resetToDefault": "Сбросить по умолчанию", + "disableParam": "Отключить инъекцию", + "enableParam": "Включить инъекцию", "placeholders": { "keyName": "Ключ", "stringValue": "Строка", diff --git a/dashboard/src/i18n/locales/zh-CN/core/common.json b/dashboard/src/i18n/locales/zh-CN/core/common.json index fa1a8bf8b6..fc389a7294 100644 --- a/dashboard/src/i18n/locales/zh-CN/core/common.json +++ b/dashboard/src/i18n/locales/zh-CN/core/common.json @@ -115,6 +115,9 @@ "valueTypeLabel": "值类型", "keyExists": "键名已存在", "invalidJson": "JSON 格式错误", + "resetToDefault": "还原默认值", + "disableParam": "禁用注入", + "enableParam": "启用注入", "placeholders": { "keyName": "键名", "stringValue": "字符串值",