Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2027,15 +2027,15 @@
"description": "温度参数",
"hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。",
"type": "float",
"default": 0.6,
"default": 1.0,
"slider": {"min": 0, "max": 2, "step": 0.1},
},
"top_p": {
"name": "Top-p",
"description": "Top-p 采样",
"hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。",
"type": "float",
"default": 1.0,
"default": 0.95,
"slider": {"min": 0, "max": 1, "step": 0.01},
},
"max_tokens": {
Expand All @@ -2046,6 +2046,11 @@
"default": 8192,
},
},
"default_disabled_keys": [
"temperature",
"top_p",
"max_tokens",
],
},
"provider": {
"type": "string",
Expand Down
29 changes: 29 additions & 0 deletions astrbot/core/provider/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions astrbot/core/provider/sources/anthropic_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = ""

Expand Down
6 changes: 6 additions & 0 deletions astrbot/core/provider/sources/gemini_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 3 additions & 6 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
139 changes: 113 additions & 26 deletions dashboard/src/components/shared/ObjectEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,23 @@
<div v-if="hasTemplateSchema" class="mt-4">
<v-divider class="mb-3"></v-divider>
<div class="text-caption text-grey mb-2">{{ t('core.common.objectEditor.presets') }}</div>
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-inactive': !isTemplateKeyAdded(templateKey) }">
<div v-for="(template, templateKey) in templateSchema" :key="templateKey" class="template-field" :class="{ 'template-field-disabled': isTemplateKeyDisabled(templateKey) }">
<v-row no-gutters align="center" class="mb-2">
<v-col cols="4">
<div class="d-flex flex-column">
<span class="text-caption font-weight-medium">{{ getTemplateTitle(template, templateKey) }}</span>
<span v-if="template.hint" class="text-caption text-grey" style="font-size: 0.7rem;">{{ resolveTemplateText(templateKey, 'hint', template.hint) }}</span>
</div>
</v-col>
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
<v-col cols="6" class="pl-2 d-flex align-center justify-end">
<v-text-field
v-if="template.type === 'string'"
:model-value="getTemplateValue(templateKey)"
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
variant="outlined"
hide-details
:disabled="isTemplateKeyDisabled(templateKey)"
:placeholder="t('core.common.objectEditor.placeholders.stringValue')"
></v-text-field>
<div v-else-if="template.type === 'number' || template.type === 'float' || template.type === 'int'" class="d-flex align-center ga-4 flex-grow-1">
Expand All @@ -139,6 +140,7 @@
color="primary"
density="compact"
hide-details
:disabled="isTemplateKeyDisabled(templateKey)"
class="flex-grow-1"
></v-slider>
<v-text-field
Expand All @@ -148,6 +150,7 @@
density="compact"
variant="outlined"
hide-details
:disabled="isTemplateKeyDisabled(templateKey)"
:placeholder="t('core.common.objectEditor.placeholders.numberValue')"
:style="template.slider ? 'max-width: 120px;' : ''"
></v-text-field>
Expand All @@ -158,20 +161,39 @@
@update:model-value="updateTemplateValue(templateKey, $event)"
density="compact"
hide-details
:disabled="isTemplateKeyDisabled(templateKey)"
color="primary"
></v-switch>
</v-col>
<v-col cols="1" class="pl-2">
<v-btn
v-if="isTemplateKeyAdded(templateKey)"
icon
variant="text"
size="small"
color="error"
@click="removeTemplateKey(templateKey)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
<v-col cols="2" class="pl-2 d-flex align-center justify-end">
<v-tooltip :text="t('core.common.objectEditor.resetToDefault')" location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-btn
v-bind="tooltipProps"
icon
variant="text"
size="small"
:disabled="!isTemplateValueModified(templateKey)"
@click="resetTemplateKey(templateKey)"
>
<v-icon>mdi-restore</v-icon>
</v-btn>
</template>
</v-tooltip>
<v-tooltip :text="isTemplateKeyDisabled(templateKey) ? t('core.common.objectEditor.enableParam') : t('core.common.objectEditor.disableParam')" location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-checkbox
v-bind="tooltipProps"
:model-value="!isTemplateKeyDisabled(templateKey)"
@update:model-value="toggleTemplateKeyDisabled(templateKey)"
density="compact"
hide-details
color="success"
:disabled="nonDisableableKeys.includes(templateKey)"
class="ma-0 pa-0"
></v-checkbox>
</template>
</v-tooltip>
</v-col>
</v-row>
</div>
Expand Down Expand Up @@ -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 || {}
Expand All @@ -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)
})
Comment on lines 326 to 329
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

如果 props.modelValue 为 null 或 undefined,调用 Object.keys(props.modelValue) 将会抛出 TypeError 并导致组件崩溃。添加空值保护可以确保组件在模型值暂不可用时也能安全渲染。

const displayKeys = computed(() => {
  if (!props.modelValue) return []
  return Object.keys(props.modelValue).filter(k => k !== '_disabled_keys').slice(0, props.maxDisplayItems)
})


// 分离模板字段和普通字段
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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') {
Comment on lines +491 to +497
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

当前 isTemplateValueModified 的实现对所有模板参数都使用了 Number(pair.value) !== Number(template.default)。然而,ObjectEditor 是一个通用组件,可以处理非数值类型(如字符串或 JSON)。对于非数值字符串,Number() 会返回 NaN。由于在 JavaScript 中 NaN !== NaN 总是为 true,这会导致该函数在字符串值未被修改时也错误地返回 true,从而使非数值参数的“还原默认值”按钮一直处于启用状态。我们应该根据模板的类型进行针对性的类型安全比较。

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
  if (template.type === 'int' || template.type === 'float' || template.type === 'number') {
    return Number(pair.value) !== Number(template.default)
  }
  if (template.type === 'bool' || template.type === 'boolean') {
    return Boolean(pair.value) !== Boolean(template.default)
  }
  return pair.value !== template.default
}

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) {
Expand Down Expand Up @@ -496,36 +586,33 @@ 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
}

function cancelDialog() {
// Reset to original state
localKeyValuePairs.value = originalKeyValuePairs.value.map(pair => ({ ...pair }))
localDisabledKeys.value = [...originalDisabledKeys.value]
dialog.value = false
}

Expand All @@ -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;
}
</style>
Loading
Loading