feat(assistant): add configurable max tool calls setting (#13398)

### What this PR does

**Before this PR:**
The MCP tool call limit was hard-coded to 20 in `parameterBuilder.ts`:
```typescript
stopWhen: stepCountIs(20)
```
Users could not customize this limit, which was reported in #13370 as a
limitation for workflows requiring more tool calls.

**After this PR:**
- Added configurable `maxToolCalls` setting (range: 1-100) in Assistant
Settings → Model Settings
- Added `enableMaxToolCalls` toggle to control the feature
- Default behavior unchanged: 20 tool calls limit (matching AI SDK
default)
- Users can increase the limit if their workflow requires more tool
calls
- All i18n translations added for 11 languages

Fixes #13370

### Why we need it and why it was done in this way

**Tradeoffs:**
- Default value kept at 20 to prevent infinite loops and maintain
backward compatibility
- Range limited to 1-100 to prevent excessive token usage and API costs

**Alternatives considered:**
- Setting default to "unlimited" - rejected because AI SDK designed 20
as default for safety
- Making it a global setting - rejected because different assistants may
need different limits

### Breaking changes

None. Default behavior remains unchanged (20 tool calls). Existing users
will continue to have the same experience.

### Special notes for your reviewer

- The `enableMaxToolCalls` field defaults to `true` to maintain the
original 20-call behavior
- When disabled, it still uses 20 (AI SDK's default), not unlimited
- `getAssistantSettings()` properly returns the new fields for
consistent access

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. Check this only
when the PR introduces or changes a user-facing feature or behavior.
- [x] Self-review: I have reviewed my own code (e.g., via
[`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`,
or GitHub UI) before requesting review from others

### Release note

```release-note
Added configurable maximum tool calls setting for assistants. Users can now customize the tool call limit (1-100) in Assistant Settings → Model Settings. Default remains at 20 calls to prevent infinite loops, matching AI SDK's design.
```

---------

Co-authored-by: icarus <eurfelux@gmail.com>
This commit is contained in:
George·Dong
2026-03-13 23:39:31 +08:00
committed by GitHub
parent 97011c8bb2
commit ea694c630c
17 changed files with 383 additions and 29 deletions

View File

@@ -0,0 +1,225 @@
/**
* Tests for parameterBuilder maxToolCalls functionality
* These tests verify the maxToolCalls calculation and validation logic in isolation
*/
import { describe, expect, it } from 'vitest'
// Mirror the constants from parameterBuilder.ts
const MIN_TOOL_CALLS = 1
const MAX_TOOL_CALLS = 100
const DEFAULT_MAX_TOOL_CALLS = 20
const DEFAULT_ENABLE_MAX_TOOL_CALLS = true
/**
* Validates and clamps maxToolCalls to valid range
* Mirrors the logic in parameterBuilder.ts
*/
function validateMaxToolCalls(value: number | undefined): number {
if (value === undefined || value < MIN_TOOL_CALLS || value > MAX_TOOL_CALLS) {
return DEFAULT_MAX_TOOL_CALLS
}
return value
}
/**
* Calculate the effective max tool calls based on assistant settings
* Mirrors the logic in parameterBuilder.ts
*/
function calculateEffectiveMaxToolCalls(settings?: { maxToolCalls?: number; enableMaxToolCalls?: boolean }): {
stopWhen: number | null
maxToolCalls: number
} {
const enableMaxToolCalls = settings?.enableMaxToolCalls ?? DEFAULT_ENABLE_MAX_TOOL_CALLS
if (!enableMaxToolCalls) {
// When disabled, don't pass stopWhen (return null to indicate no stopWhen)
return { stopWhen: null, maxToolCalls: DEFAULT_MAX_TOOL_CALLS }
}
// When enabled, validate and use user-defined value
const maxToolCalls = validateMaxToolCalls(settings?.maxToolCalls)
return { stopWhen: maxToolCalls, maxToolCalls }
}
describe('validateMaxToolCalls', () => {
it('returns valid values as-is', () => {
expect(validateMaxToolCalls(1)).toBe(1)
expect(validateMaxToolCalls(50)).toBe(50)
expect(validateMaxToolCalls(100)).toBe(100)
})
it('clamps values above 100 to default', () => {
const result = validateMaxToolCalls(999)
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('clamps zero to default', () => {
const result = validateMaxToolCalls(0)
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('clamps negative values to default', () => {
const result = validateMaxToolCalls(-5)
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('returns default when value is undefined', () => {
const result = validateMaxToolCalls(undefined)
expect(result).toBe(DEFAULT_MAX_TOOL_CALLS)
})
})
describe('maxToolCalls calculation logic', () => {
describe('default behavior', () => {
it('uses default value 20 when settings are undefined', () => {
const result = calculateEffectiveMaxToolCalls(undefined)
expect(result.maxToolCalls).toBe(20)
expect(result.stopWhen).toBe(20)
})
it('uses default value 20 when settings is empty object', () => {
const result = calculateEffectiveMaxToolCalls({})
expect(result.maxToolCalls).toBe(20)
expect(result.stopWhen).toBe(20)
})
it('uses default value 20 when maxToolCalls is undefined', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true
// maxToolCalls is undefined
})
expect(result.maxToolCalls).toBe(20)
expect(result.stopWhen).toBe(20)
})
it('uses custom value when maxToolCalls is set and enabled', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 50
})
expect(result.maxToolCalls).toBe(50)
expect(result.stopWhen).toBe(50)
})
})
describe('custom values when enabled', () => {
it('uses custom value when enableMaxToolCalls is true and maxToolCalls is set', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 50
})
expect(result.stopWhen).toBe(50)
})
it('uses custom value at minimum boundary (1)', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 1
})
expect(result.stopWhen).toBe(1)
})
it('uses custom value at maximum boundary (100)', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 100
})
expect(result.stopWhen).toBe(100)
})
it('clamps values above 100 to default when enabled', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 999
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('clamps zero to default when enabled', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 0
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('clamps negative values to default when enabled', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: -5
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
})
describe('disabled behavior', () => {
it('does not pass stopWhen when enableMaxToolCalls is false', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: false,
maxToolCalls: 50
})
// When disabled, stopWhen should be null (indicating no stopWhen passed)
expect(result.stopWhen).toBeNull()
})
it('does not pass stopWhen when both enableMaxToolCalls is false and maxToolCalls is undefined', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: false
})
expect(result.stopWhen).toBeNull()
})
it('falls back to default when disabled with invalid maxToolCalls', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: false,
maxToolCalls: 999
})
// When disabled, maxToolCalls should still be default (for reference)
expect(result.maxToolCalls).toBe(DEFAULT_MAX_TOOL_CALLS)
expect(result.stopWhen).toBeNull()
})
})
describe('backward compatibility', () => {
it('maintains backward compatibility - existing assistants without new fields use default', () => {
// Simulate an old assistant without the new fields
const oldSettings = {
// Old assistants don't have enableMaxToolCalls or maxToolCalls
temperature: 0.7,
contextCount: 10
}
const result = calculateEffectiveMaxToolCalls(
oldSettings as { maxToolCalls?: number; enableMaxToolCalls?: boolean }
)
// Should default to enabled with 20 for backward compatibility
expect(result.maxToolCalls).toBe(20)
expect(result.stopWhen).toBe(20)
})
})
describe('security - invalid values from imported/migrated settings', () => {
it('validates extremely large values from imported settings', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 999999
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('validates negative values from imported settings', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: -100
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
it('validates zero from imported settings', () => {
const result = calculateEffectiveMaxToolCalls({
enableMaxToolCalls: true,
maxToolCalls: 0
})
expect(result.stopWhen).toBe(DEFAULT_MAX_TOOL_CALLS)
})
})
})

View File

@@ -13,6 +13,7 @@ import type { AnthropicSearchConfig, WebSearchPluginConfig } from '@cherrystudio
import { isBaseProvider } from '@cherrystudio/ai-core/core/providers/schemas'
import type { BaseProviderId } from '@cherrystudio/ai-core/provider'
import { loggerService } from '@logger'
import { MAX_TOOL_CALLS, MIN_TOOL_CALLS } from '@renderer/config/constant'
import {
isAnthropicModel,
isFixedReasoningModel,
@@ -27,7 +28,7 @@ import {
isWebSearchModel
} from '@renderer/config/models'
import { getHubModeSystemPrompt } from '@renderer/config/prompts-code-mode'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { DEFAULT_ASSISTANT_SETTINGS, getDefaultModel } from '@renderer/services/AssistantService'
import store from '@renderer/store'
import type { CherryWebSearchConfig } from '@renderer/store/websearch'
import type { Model } from '@renderer/types'
@@ -49,6 +50,19 @@ import { getMaxTokens, getTemperature, getTopP } from './modelParameters'
const logger = loggerService.withContext('parameterBuilder')
/**
* Validates and clamps maxToolCalls to valid range
* Falls back to DEFAULT_ASSISTANT_SETTINGS.maxToolCalls if invalid
* @param value - The maxToolCalls value from settings
* @returns Validated maxToolCalls value
*/
function validateMaxToolCalls(value: number | undefined): number {
if (value === undefined || value < MIN_TOOL_CALLS || value > MAX_TOOL_CALLS) {
return DEFAULT_ASSISTANT_SETTINGS.maxToolCalls
}
return value
}
type ProviderDefinedTool = Extract<Tool<any, any>, { type: 'provider' }>
function mapVertexAIGatewayModelToProviderId(model: Model): BaseProviderId | undefined {
@@ -229,6 +243,12 @@ export async function buildStreamTextParams(
// Note: standardParams (topK, frequencyPenalty, presencePenalty, stopSequences, seed)
// are extracted from custom parameters and passed directly to streamText()
// instead of being placed in providerOptions
// Get max tool calls from assistant settings
// When enabled, validate and use user-defined value (1-100)
// When disabled, don't pass stopWhen - let AI SDK use its own default
const enableMaxToolCalls = assistant.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls
const params: StreamTextParams = {
messages: sdkMessages,
maxOutputTokens: getMaxTokens(assistant, model),
@@ -239,10 +259,16 @@ export async function buildStreamTextParams(
abortSignal: finalSignal,
headers,
providerOptions,
stopWhen: stepCountIs(20),
maxRetries: 0
}
// Only add stopWhen when explicitly enabled and validated
if (enableMaxToolCalls) {
const maxToolCalls = validateMaxToolCalls(assistant.settings?.maxToolCalls)
params.stopWhen = stepCountIs(maxToolCalls)
}
// When disabled, don't pass stopWhen - let AI SDK use its own default
if (tools) {
params.tools = tools
}

View File

@@ -7,6 +7,10 @@ export const DEFAULT_KNOWLEDGE_THRESHOLD = 0.0
export const DEFAULT_WEBSEARCH_RAG_DOCUMENT_COUNT = 1
export const DEFAULT_STREAM_OPTIONS_INCLUDE_USAGE = true
// Max tool calls validation constants
export const MIN_TOOL_CALLS = 1
export const MAX_TOOL_CALLS = 100
export const platform = window.electron?.process?.platform
export const isMac = platform === 'darwin'
export const isWin = platform === 'win32' || platform === 'win64'

View File

@@ -621,6 +621,10 @@
"tip": "The assistant will use the large model's intent recognition capability to determine whether to use the knowledge base for answering. This feature will depend on the model's capabilities"
}
},
"max_tool_calls": {
"label": "Max Tool Calls",
"tip": "Maximum number of tool calls allowed in a single conversation. Higher values allow more complex multi-step operations but may increase token usage and response time."
},
"mcp": {
"description": "Default enabled MCP servers",
"enableFirst": "Enable this server in MCP settings first",

View File

@@ -621,6 +621,10 @@
"tip": "助手将调用大模型的意图识别能力,判断是否需要调用知识库进行回答,该功能将依赖模型的能力"
}
},
"max_tool_calls": {
"label": "最大工具调用次数",
"tip": "单次对话中允许的最大工具调用次数。较高的值支持更复杂的多步操作,但可能增加 Token 消耗和响应时间。"
},
"mcp": {
"description": "默认启用的 MCP 服务器",
"enableFirst": "请先在 MCP 设置中启用此服务器",

View File

@@ -621,6 +621,10 @@
"tip": "助手會使用大型語言模型的意圖識別能力,判斷是否需要查詢知識庫;此功能仰賴模型能力"
}
},
"max_tool_calls": {
"label": "最大工具呼叫次數",
"tip": "單次對話中允許的最大工具呼叫次數。較高的值支援更複雜的多步操作,但可能增加 Token 消耗和回應時間。"
},
"mcp": {
"description": "預設啟用的 MCP 伺服器",
"enableFirst": "請先在 MCP 設定中啟用此伺服器",

View File

@@ -621,6 +621,10 @@
"tip": "Der Assistent verwendet die Absichtserkennungsfähigkeit des großen Modells, um zu bestimmen, ob die Wissensdatenbank für die Antwort aufgerufen werden muss. Diese Funktion hängt von den Fähigkeiten des Modells ab"
}
},
"max_tool_calls": {
"label": "Maximale Werkzeugaufrufe",
"tip": "Maximale Anzahl von Tool-Aufrufen, die in einer einzelnen Konversation erlaubt sind. Höhere Werte ermöglichen komplexere, mehrstufige Operationen, können aber den Token-Verbrauch und die Antwortzeit erhöhen."
},
"mcp": {
"description": "Standardmäßig aktivierte MCP-Server",
"enableFirst": "Bitte aktivieren Sie diesen Server zuerst in den MCP-Einstellungen",

View File

@@ -621,6 +621,10 @@
"tip": "Ο πράκτορας θα καλέσει τη δυνατότητα αναγνώρισης πρόθεσης του μεγάλου μοντέλου για να αποφασίσει αν χρειάζεται να κληθεί η βάση γνώσης για να απαντηθεί, και αυτή η λειτουργία θα εξαρτηθεί από τις δυνατότητες του μοντέλου"
}
},
"max_tool_calls": {
"label": "Μέγιστος Αριθμός Κλήσεων Εργαλείων",
"tip": "Μέγιστος αριθμός κλήσεων εργαλείων που επιτρέπονται σε μία συνομιλία. Υψηλότερες τιμές επιτρέπουν πιο σύνθετες πολυσταδιακές λειτουργίες, αλλά ενδέχεται να αυξήσουν τη χρήση token και τον χρόνο απόκρισης."
},
"mcp": {
"description": "Διακομιστής MCP που είναι ενεργοποιημένος εξ ορισμού",
"enableFirst": "Πρώτα ενεργοποιήστε αυτόν τον διακομιστή στις ρυθμίσεις MCP",

View File

@@ -621,6 +621,10 @@
"tip": "El agente utilizará la capacidad del modelo grande para el reconocimiento de intenciones y decidirá si necesita invocar la base de conocimientos para responder. Esta función dependerá de las capacidades del modelo"
}
},
"max_tool_calls": {
"label": "Llamadas Máximas de Herramientas",
"tip": "Número máximo de llamadas a herramientas permitidas en una sola conversación. Valores más altos permiten operaciones de múltiples pasos más complejas, pero pueden incrementar el uso de tokens y el tiempo de respuesta."
},
"mcp": {
"description": "Servidor MCP habilitado por defecto",
"enableFirst": "Habilite este servidor en la configuración de MCP primero",

View File

@@ -621,6 +621,10 @@
"tip": "L'agent utilisera la capacité du grand modèle à reconnaître les intentions afin de déterminer si la base de connaissances doit être utilisée pour répondre. Cette fonctionnalité dépend des capacités du modèle"
}
},
"max_tool_calls": {
"label": "Maximiser les appels d'outils",
"tip": "Nombre maximum d'appels d'outils autorisés dans une seule conversation. Des valeurs plus élevées permettent des opérations multi-étapes plus complexes, mais peuvent augmenter l'utilisation de tokens et le temps de réponse."
},
"mcp": {
"description": "Serveur MCP activé par défaut",
"enableFirst": "Veuillez d'abord activer ce serveur dans les paramètres MCP",

View File

@@ -621,6 +621,10 @@
"tip": "アシスタントは大規模言語モデルの意図認識能力を使用して、ナレッジベースを参照する必要があるかどうかを判断します。この機能はモデルの能力に依存します"
}
},
"max_tool_calls": {
"label": "最大ツール呼び出し数",
"tip": "1回の会話で許可される最大のツール呼び出し回数。高い値に設定すると、より複雑な多段階操作が可能になりますが、トークン使用量と応答時間が増加する可能性があります。"
},
"mcp": {
"description": "デフォルトで有効な MCP サーバー",
"enableFirst": "まず MCP 設定でこのサーバーを有効にしてください",

View File

@@ -621,6 +621,10 @@
"tip": "O agente usará a capacidade de reconhecimento de intenção do grande modelo para decidir se deve chamar a base de conhecimento para responder. Esta função depende da capacidade do modelo"
}
},
"max_tool_calls": {
"label": "Máximo de Chamadas de Ferramentas",
"tip": "Número máximo de chamadas de ferramentas permitidas em uma única conversa. Valores mais altos permitem operações mais complexas de múltiplas etapas, mas podem aumentar o uso de tokens e o tempo de resposta."
},
"mcp": {
"description": "Servidor MCP ativado por padrão",
"enableFirst": "Por favor, ative este servidor nas configurações do MCP primeiro",

View File

@@ -621,6 +621,10 @@
"tip": "Asistentul va folosi capacitatea de recunoaștere a intenției modelului mare pentru a determina dacă să folosească baza de cunoștințe pentru a răspunde. Această funcție depinde de capacitățile modelului"
}
},
"max_tool_calls": {
"label": "Apeluri Maxime de Instrumente",
"tip": "Numărul maxim de apeluri de instrumente permise într-o singură conversație. Valori mai mari permit operații mai complexe, în mai multe etape, dar pot crește consumul de tokeni și timpul de răspuns."
},
"mcp": {
"description": "Servere MCP activate implicit",
"enableFirst": "Activează mai întâi acest server în setările MCP",

View File

@@ -621,6 +621,10 @@
"tip": "Ассистент будет использовать возможности большой модели для распознавания намерений, чтобы определить, нужно ли обращаться к базе знаний для ответа. Эта функция будет зависеть от возможностей модели"
}
},
"max_tool_calls": {
"label": "Максимальное количество вызовов инструментов",
"tip": "Максимальное количество вызовов инструментов, разрешённых в одном диалоге. Более высокие значения позволяют выполнять более сложные многошаговые операции, но могут увеличить использование токенов и время ответа."
},
"mcp": {
"description": "Серверы MCP, включенные по умолчанию",
"enableFirst": "Сначала включите этот сервер в настройках MCP",

View File

@@ -6,7 +6,13 @@ import { DeleteIcon, ResetIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import { SelectModelPopup } from '@renderer/components/Popups/SelectModelPopup'
import Selector from '@renderer/components/Selector'
import { DEFAULT_CONTEXTCOUNT, DEFAULT_TEMPERATURE, MAX_CONTEXT_COUNT } from '@renderer/config/constant'
import {
DEFAULT_CONTEXTCOUNT,
DEFAULT_TEMPERATURE,
MAX_CONTEXT_COUNT,
MAX_TOOL_CALLS,
MIN_TOOL_CALLS
} from '@renderer/config/constant'
import { isEmbeddingModel, isRerankModel } from '@renderer/config/models'
import { useTimer } from '@renderer/hooks/useTimer'
import { SettingRow } from '@renderer/pages/settings'
@@ -17,7 +23,7 @@ import { Button, Col, Divider, Input, InputNumber, Row, Select, Slider, Switch,
import { isNull } from 'lodash'
import { PlusIcon } from 'lucide-react'
import type { FC } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -30,19 +36,40 @@ interface Props {
const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateAssistantSettings }) => {
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setContextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT)
const [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput)
const [toolUseMode, setToolUseMode] = useState<AssistantSettings['toolUseMode']>(
assistant?.settings?.toolUseMode ?? 'function'
const enableMaxTokens = useMemo(
() => assistant?.settings?.enableMaxTokens ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens,
[assistant?.settings?.enableMaxTokens]
)
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
const streamOutput = useMemo(
() => assistant?.settings?.streamOutput ?? DEFAULT_ASSISTANT_SETTINGS.streamOutput,
[assistant?.settings?.streamOutput]
)
const toolUseMode = useMemo(
() => assistant?.settings?.toolUseMode ?? DEFAULT_ASSISTANT_SETTINGS.toolUseMode,
[assistant?.settings?.toolUseMode]
)
const [maxToolCalls, setMaxToolCalls] = useState(assistant?.settings?.maxToolCalls ?? 20)
const enableMaxToolCalls = useMemo(
() => assistant?.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls,
[assistant?.settings?.enableMaxToolCalls]
)
const defaultModel = useMemo(
() => assistant?.defaultModel ?? DEFAULT_ASSISTANT_SETTINGS.defaultModel,
[assistant?.defaultModel]
)
const [defaultModel, setDefaultModel] = useState(assistant?.defaultModel)
const [topP, setTopP] = useState(assistant?.settings?.topP ?? 1)
const [enableTopP, setEnableTopP] = useState(assistant?.settings?.enableTopP ?? false)
const enableTopP = useMemo(
() => assistant?.settings?.enableTopP ?? DEFAULT_ASSISTANT_SETTINGS.enableTopP,
[assistant?.settings?.enableTopP]
)
const [customParameters, setCustomParameters] = useState<AssistantSettingCustomParameters[]>(
assistant?.settings?.customParameters ?? []
)
const [enableTemperature, setEnableTemperature] = useState(assistant?.settings?.enableTemperature ?? false)
const enableTemperature = useMemo(
() => assistant?.settings?.enableTemperature ?? DEFAULT_ASSISTANT_SETTINGS.enableTemperature,
[assistant?.settings?.enableTemperature]
)
const customParametersRef = useRef(customParameters)
@@ -184,15 +211,11 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
const onReset = () => {
setTemperature(DEFAULT_ASSISTANT_SETTINGS.temperature)
setEnableTemperature(DEFAULT_ASSISTANT_SETTINGS.enableTemperature ?? false)
setContextCount(DEFAULT_ASSISTANT_SETTINGS.contextCount)
setEnableMaxTokens(DEFAULT_ASSISTANT_SETTINGS.enableMaxTokens ?? false)
setMaxTokens(DEFAULT_ASSISTANT_SETTINGS.maxTokens ?? 0)
setStreamOutput(DEFAULT_ASSISTANT_SETTINGS.streamOutput)
setMaxTokens(DEFAULT_ASSISTANT_SETTINGS.maxTokens)
setTopP(DEFAULT_ASSISTANT_SETTINGS.topP)
setEnableTopP(DEFAULT_ASSISTANT_SETTINGS.enableTopP ?? false)
setCustomParameters(DEFAULT_ASSISTANT_SETTINGS.customParameters ?? [])
setToolUseMode(DEFAULT_ASSISTANT_SETTINGS.toolUseMode)
setCustomParameters(DEFAULT_ASSISTANT_SETTINGS.customParameters)
setMaxToolCalls(DEFAULT_ASSISTANT_SETTINGS.maxToolCalls)
updateAssistantSettings(DEFAULT_ASSISTANT_SETTINGS)
}
const modelFilter = (model: Model) => !isEmbeddingModel(model) && !isRerankModel(model)
@@ -201,13 +224,12 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
const currentModel = defaultModel ? assistant?.model : undefined
const selectedModel = await SelectModelPopup.show({ model: currentModel, filter: modelFilter })
if (selectedModel) {
setDefaultModel(selectedModel)
updateAssistant({
...assistant,
model: selectedModel,
defaultModel: selectedModel
})
// TODO: 需要根据配置来设置默认值
// TODO: 移除根据模型自动修改参数的逻辑
if (selectedModel.name.includes('kimi-k2')) {
setTemperature(0.6)
setTimeoutTimer('onSelectModel_1', () => updateAssistantSettings({ temperature: 0.6 }), 500)
@@ -244,7 +266,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
variant="filled"
icon={<DeleteIcon size={14} className="lucide-custom" />}
onClick={() => {
setDefaultModel(undefined)
updateAssistant({ ...assistant, defaultModel: undefined })
}}
danger
@@ -266,7 +287,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<Switch
checked={enableTemperature}
onChange={(enabled) => {
setEnableTemperature(enabled)
updateAssistantSettings({ enableTemperature: enabled })
}}
/>
@@ -314,7 +334,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<Switch
checked={enableTopP}
onChange={(enabled) => {
setEnableTopP(enabled)
updateAssistantSettings({ enableTopP: enabled })
}}
/>
@@ -422,8 +441,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
})
if (!confirmed) return
}
setEnableMaxTokens(enabled)
updateAssistantSettings({ enableMaxTokens: enabled })
}}
/>
@@ -455,7 +472,6 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
<Switch
checked={streamOutput}
onChange={(checked) => {
setStreamOutput(checked)
updateAssistantSettings({ streamOutput: checked })
}}
/>
@@ -470,13 +486,46 @@ const AssistantModelSettings: FC<Props> = ({ assistant, updateAssistant, updateA
{ label: t('assistants.settings.tool_use_mode.function'), value: 'function' }
]}
onChange={(value) => {
setToolUseMode(value)
updateAssistantSettings({ toolUseMode: value })
}}
size={14}
/>
</SettingRow>
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<HStack alignItems="center">
<Label>{t('assistants.settings.max_tool_calls.label')}</Label>
<Tooltip title={t('assistants.settings.max_tool_calls.tip')}>
<QuestionIcon />
</Tooltip>
</HStack>
<Switch
checked={enableMaxToolCalls}
onChange={(enabled) => {
updateAssistantSettings({ enableMaxToolCalls: enabled })
}}
/>
</SettingRow>
{enableMaxToolCalls && (
<Row align="middle" style={{ marginTop: 5, marginBottom: 5 }}>
<Col span={24}>
<InputNumber
min={MIN_TOOL_CALLS}
max={MAX_TOOL_CALLS}
step={1}
value={maxToolCalls}
onChange={(value) => {
if (!isNull(value)) {
setMaxToolCalls(value)
setTimeoutTimer('maxToolCalls_onChange', () => updateAssistantSettings({ maxToolCalls: value }), 500)
}
}}
style={{ width: '100%' }}
/>
</Col>
</Row>
)}
<Divider style={{ margin: '10px 0' }} />
<SettingRow style={{ minHeight: 30 }}>
<Label>{t('models.custom_parameters')}</Label>
<Button icon={<PlusIcon size={18} />} onClick={onAddCustomParameter}>

View File

@@ -55,7 +55,9 @@ export const DEFAULT_ASSISTANT_SETTINGS = {
reasoning_effort_cache: undefined,
qwenThinkMode: undefined,
// It would gracefully fallback to prompt if not supported by model.
toolUseMode: 'function'
toolUseMode: 'function',
maxToolCalls: 20,
enableMaxToolCalls: true
} as const satisfies AssistantSettings
/**
@@ -248,6 +250,8 @@ export const getAssistantSettings = (assistant: Assistant): AssistantSettings =>
maxTokens: getAssistantMaxTokens(),
streamOutput: assistant?.settings?.streamOutput ?? DEFAULT_ASSISTANT_SETTINGS.streamOutput,
toolUseMode: assistant?.settings?.toolUseMode ?? DEFAULT_ASSISTANT_SETTINGS.toolUseMode,
maxToolCalls: assistant?.settings?.maxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.maxToolCalls,
enableMaxToolCalls: assistant?.settings?.enableMaxToolCalls ?? DEFAULT_ASSISTANT_SETTINGS.enableMaxToolCalls,
defaultModel: assistant?.defaultModel ?? DEFAULT_ASSISTANT_SETTINGS.defaultModel,
reasoning_effort: assistant?.settings?.reasoning_effort ?? DEFAULT_ASSISTANT_SETTINGS.reasoning_effort,
customParameters: assistant?.settings?.customParameters ?? DEFAULT_ASSISTANT_SETTINGS.customParameters

View File

@@ -188,6 +188,8 @@ export type AssistantSettings = {
reasoning_effort_cache?: ReasoningEffortOption
qwenThinkMode?: boolean
toolUseMode: 'function' | 'prompt'
maxToolCalls?: number
enableMaxToolCalls?: boolean
}
export type AssistantPreset = Omit<Assistant, 'model'> & {