Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 18 additions & 0 deletions astrbot/core/provider/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ 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
}

@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
12 changes: 12 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,12 @@ def _process_content_parts(

async def _query(self, payloads: dict, tools: ToolSet | None) -> LLMResponse:
"""非流式请求 Gemini API"""
# Merge effective custom_extra_body into payloads
custom_extra_body = self.get_effective_custom_extra_body()
for k, v in custom_extra_body.items():
if k not in payloads:
payloads[k] = v
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

直接在原处修改传入的 payloads 字典可能会对调用者或重试循环产生意料之外的副作用。更安全的做法是在合并自定义额外请求体参数之前,先对 payloads 进行浅拷贝。此外,由于此合并逻辑在多处重复,建议将其重构为一个共享的辅助函数以避免代码重复。

Suggested change
# Merge effective custom_extra_body into payloads
custom_extra_body = self.get_effective_custom_extra_body()
for k, v in custom_extra_body.items():
if k not in payloads:
payloads[k] = v
payloads = self._merge_custom_extra_body(payloads)
References
  1. When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.


system_instruction = next(
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
None,
Expand Down Expand Up @@ -694,6 +700,12 @@ async def _query_stream(
tools: ToolSet | None,
) -> AsyncGenerator[LLMResponse, None]:
"""流式请求 Gemini API"""
# Merge effective custom_extra_body into payloads
custom_extra_body = self.get_effective_custom_extra_body()
for k, v in custom_extra_body.items():
if k not in payloads:
payloads[k] = v
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

直接在原处修改传入的 payloads 字典可能会对调用者或重试循环产生意料之外的副作用。更安全的做法是在合并自定义额外请求体参数之前,先对 payloads 进行浅拷贝。此外,由于此合并逻辑在多处重复,建议将其重构为一个共享的辅助函数以避免代码重复。

Suggested change
# Merge effective custom_extra_body into payloads
custom_extra_body = self.get_effective_custom_extra_body()
for k, v in custom_extra_body.items():
if k not in payloads:
payloads[k] = v
payloads = self._merge_custom_extra_body(payloads)
References
  1. When implementing similar functionality for different cases (e.g., direct vs. quoted attachments), refactor the logic into a shared helper function to avoid code duplication.


system_instruction = next(
(msg["content"] for msg in payloads["messages"] if msg["role"] == "system"),
None,
Expand Down
4 changes: 2 additions & 2 deletions astrbot/core/provider/sources/openai_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ 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", {})
custom_extra_body = self.get_effective_custom_extra_body()
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
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

由于 get_effective_custom_extra_body() 保证会返回一个 dict(空字典或已过滤的字典),因此这里的 isinstance(custom_extra_body, dict) 类型检查是多余的。我们可以直接使用返回的字典来更新 extra_body,使代码更加简洁。

        # 读取并合并 custom_extra_body 配置
        extra_body.update(self.get_effective_custom_extra_body())

self._apply_provider_specific_extra_body_overrides(extra_body)
Expand Down Expand Up @@ -631,7 +631,7 @@ async def _query_stream(
extra_body = {}

# 读取并合并 custom_extra_body 配置
custom_extra_body = self.provider_config.get("custom_extra_body", {})
custom_extra_body = self.get_effective_custom_extra_body()
if isinstance(custom_extra_body, dict):
extra_body.update(custom_extra_body)
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

由于 get_effective_custom_extra_body() 保证会返回一个 dict,这里的 isinstance(custom_extra_body, dict) 类型检查是多余的。我们可以直接使用返回的字典来更新 extra_body,使代码更加简洁。

        # 读取并合并 custom_extra_body 配置
        extra_body.update(self.get_effective_custom_extra_body())


Expand Down
136 changes: 111 additions & 25 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,19 @@ 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)
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 +354,23 @@ function createPair({ key, value, type, slider, template, jsonError = '', _origi
function initializeLocalKeyValuePairs() {
localKeyValuePairs.value = []
nextPairId.value = 0

// 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)) {
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.entries(props.modelValue) 将会抛出 TypeError。建议在此处添加 props.modelValue || {} 的空值保护,以防止潜在的运行时崩溃。

  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 +483,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 +585,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 +636,7 @@ function resolveTemplateText(templateKey, attr, fallback) {
transition: opacity 0.2s;
}

.template-field-inactive {
opacity: 0.8;
.template-field-disabled {
opacity: 0.5;
}
</style>
6 changes: 6 additions & 0 deletions dashboard/src/composables/useProviderModelConfigDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand Down
3 changes: 3 additions & 0 deletions dashboard/src/i18n/locales/en-US/core/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading