From 4d78f9dc120a552775d5dd0dec3acd5a2fc0661f Mon Sep 17 00:00:00 2001 From: wizardchen Date: Sun, 30 Aug 2026 10:46:09 +0800 Subject: [PATCH] feat(agent): keep max completion tokens as default or a custom cap Unset (0) now resolves at call time (2048 / 4096 / 24576 with sandbox) so the editor can offer Default vs Custom without silently rewriting a saved value. --- config/builtin_agents.yaml | 12 +-- frontend/src/api/agent/index.ts | 2 +- frontend/src/i18n/locales/en-US.ts | 5 +- frontend/src/i18n/locales/ko-KR.ts | 5 +- frontend/src/i18n/locales/ru-RU.ts | 5 +- frontend/src/i18n/locales/zh-CN.ts | 5 +- frontend/src/views/agent/AgentEditorModal.vue | 73 +++++++++++++++++-- internal/agent/const.go | 8 +- internal/agent/engine_test.go | 17 +++++ internal/types/agent.go | 59 +++++++++++---- internal/types/agent_test.go | 10 ++- internal/types/custom_agent.go | 12 +-- internal/types/custom_agent_test.go | 11 +-- 13 files changed, 174 insertions(+), 50 deletions(-) diff --git a/config/builtin_agents.yaml b/config/builtin_agents.yaml index 494234366..9a9ea5a18 100644 --- a/config/builtin_agents.yaml +++ b/config/builtin_agents.yaml @@ -27,7 +27,7 @@ builtin_agents: system_prompt_id: "default_kb" context_template_id: "default_context" temperature: 0.7 - max_completion_tokens: 2048 + max_completion_tokens: 0 web_search_enabled: true web_search_max_results: 5 multi_turn_enabled: true @@ -70,7 +70,7 @@ builtin_agents: agent_type: "rag-qa" system_prompt: "" temperature: 0.7 - max_completion_tokens: 32768 + max_completion_tokens: 0 max_iterations: 50 kb_selection_mode: "all" retrieve_kb_only_when_mentioned: false @@ -118,7 +118,7 @@ builtin_agents: agent_type: "data-analysis" system_prompt_id: "data_analyst" temperature: 0.3 - max_completion_tokens: 32768 + max_completion_tokens: 0 max_iterations: 30 kb_selection_mode: "all" retrieve_kb_only_when_mentioned: false @@ -163,7 +163,7 @@ builtin_agents: agent_type: "wiki-qa" system_prompt_id: "wiki_researcher" temperature: 0.7 - max_completion_tokens: 32768 + max_completion_tokens: 0 max_iterations: 30 kb_selection_mode: "all" retrieve_kb_only_when_mentioned: false @@ -207,7 +207,7 @@ builtin_agents: agent_type: "custom" system_prompt_id: "wiki_fixer" temperature: 0.7 - max_completion_tokens: 32768 + max_completion_tokens: 0 max_iterations: 30 kb_selection_mode: "selected" retrieve_kb_only_when_mentioned: false @@ -257,7 +257,7 @@ builtin_agents: agent_type: "custom" system_prompt_id: "skill_installer" temperature: 0.2 - max_completion_tokens: 32768 + max_completion_tokens: 24576 max_iterations: 30 kb_selection_mode: "none" retrieve_kb_only_when_mentioned: false diff --git a/frontend/src/api/agent/index.ts b/frontend/src/api/agent/index.ts index 95a61b4ca..1ff670b7e 100644 --- a/frontend/src/api/agent/index.ts +++ b/frontend/src/api/agent/index.ts @@ -44,7 +44,7 @@ export interface CustomAgentConfig { model_id?: string; rerank_model_id?: string; // ReRank 模型 ID temperature?: number; - max_completion_tokens?: number; // 最大生成 token 数;智能推理 ReAct 轮次会抬到至少 32768,避免工具 JSON 被网关默认 4096 截断 + max_completion_tokens?: number; // 0 = 跟随系统默认(快速问答 2048;智能推理 4096,绑沙箱可写文件时 24576)。大于 0 为自定义上限 thinking?: boolean; // 是否启用思考模式(支持扩展思考的模型) citation_enabled?: boolean; // 是否在最终回答中输出知识库/网页来源引用(默认开启) diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index caedfe4c1..ae0cc1471 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -1008,6 +1008,8 @@ export default { rewritePromptUser: 'Rewrite User Prompt', rewritePromptUserPlaceholder: 'Leave empty to use default prompt', maxCompletionTokens: 'Max Completion Tokens', + maxCompletionTokensDefault: 'Default', + maxCompletionTokensCustom: 'Custom', fallbackStrategy: 'Fallback Strategy', fallbackResponse: 'Fixed Response', fallbackResponsePlaceholder: 'Sorry, I cannot answer this question.', @@ -5529,7 +5531,8 @@ export default { contextTemplate: 'Define how retrieved content is formatted before passing to the model', model: 'Select the LLM used by the agent', temperature: 'Control output randomness, 0 is most deterministic, 1 is most random', - maxTokens: 'Maximum number of tokens for model-generated responses', + maxTokens: 'Maximum tokens for the model reply. Default is 2048. Custom values are saved as entered.', + maxTokensAgent: 'Maximum tokens generated in each reasoning round, including tool-call JSON. Default is 4096 without a sandbox, or 24576 when a sandbox can write or edit files. A custom value is saved as entered and is not changed later.', thinking: 'Enable extended thinking capability (requires model support)', conversationSection: 'Configure multi-turn conversation and query rewriting parameters', conversationSectionAgent: 'How much earlier conversation each turn carries. Smart reasoning is always multi-turn', diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index 5cd901cb1..5fe976dd1 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -1100,7 +1100,8 @@ export default { contextTemplate: '검색된 콘텐츠를 모델에 전달하기 전에 형식을 정의합니다', model: '에이전트가 사용할 대규모 언어 모델을 선택하세요', temperature: '출력의 무작위성을 제어합니다. 0이 가장 확정적, 1이 가장 무작위', - maxTokens: '모델이 생성하는 응답의 최대 토큰 수', + maxTokens: '모델 응답의 최대 토큰 수. 「기본값」은 2048입니다. 「사용자 지정」은 입력한 값을 그대로 저장합니다.', + maxTokensAgent: '각 추론 라운드에서 생성할 최대 토큰 수(도구 호출 JSON 포함). 「기본값」은 샌드박스 없으면 4096, 파일 쓰기/편집이 가능하면 24576입니다. 「사용자 지정」은 입력한 값을 그대로 저장합니다.', thinking: '모델의 확장 사고 기능 활성화 (모델 지원 필요)', conversationSection: '다중 턴 대화 및 질문 재작성 관련 매개변수 설정', conversationSectionAgent: '매 턴에 실어 보낼 이전 대화 분량 설정 (스마트 추론은 항상 다중 턴)', @@ -5737,6 +5738,8 @@ export default { rewritePromptUser: '사용자 프롬프트 다시 작성', rewritePromptUserPlaceholder: '시스템 기본 프롬프트를 사용하려면 비워 두세요.', maxCompletionTokens: '생성된 토큰의 최대 수', + maxCompletionTokensDefault: '기본값', + maxCompletionTokensCustom: '사용자 지정', fallbackStrategy: '폴백 전략', fallbackResponse: '고정 응답 내용', fallbackResponsePlaceholder: '죄송합니다. 이 질문에는 답변해 드릴 수 없습니다.', diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index b94e68de7..b75346a35 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -1100,7 +1100,8 @@ export default { contextTemplate: 'Определите формат передачи найденного контента модели', model: 'Выберите LLM, используемую агентом', temperature: 'Контроль случайности выхода: 0 — наиболее детерминированный, 1 — наиболее случайный', - maxTokens: 'Максимальное количество токенов в ответе модели', + maxTokens: 'Максимум токенов в ответе. «По умолчанию» — 2048. «Своё» сохраняет введённое число.', + maxTokensAgent: 'Максимум токенов за один раунд рассуждения, включая JSON вызовов инструментов. «По умолчанию»: 4096 без песочницы, 24576 с записью/правкой файлов. «Своё» сохраняет введённое число и больше не меняется.', thinking: 'Включить расширенное мышление модели (требуется поддержка модели)', conversationSection: 'Настройка параметров многооборотного диалога и перефразирования вопросов', conversationSectionAgent: 'Объём предыдущего диалога в каждом ходе. Умные рассуждения всегда многооборотные', @@ -5737,6 +5738,8 @@ export default { rewritePromptUser: 'Rewrite User Prompt', rewritePromptUserPlaceholder: 'Leave empty to use default prompt', maxCompletionTokens: 'Max Completion Tokens', + maxCompletionTokensDefault: 'По умолчанию', + maxCompletionTokensCustom: 'Своё', fallbackStrategy: 'Fallback Strategy', fallbackResponse: 'Fixed Response', fallbackResponsePlaceholder: 'Sorry, I cannot answer this question.', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 01bbc5926..1fdf6e65e 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1102,7 +1102,8 @@ export default { contextTemplate: '定义如何将检索到的内容格式化后传递给模型', model: '选择智能体使用的大语言模型', temperature: '控制输出的随机性,0 最确定,1 最随机', - maxTokens: '模型生成回复的最大Token数量', + maxTokens: '模型生成回复的最大 Token 数。选「默认」时为 2048;选「自定义」后按你填的数保存。', + maxTokensAgent: '每一轮推理的最大生成 Token(含工具调用 JSON)。选「默认」时,未绑沙箱为 4096,绑了沙箱(可写/改文件)为 24576。选「自定义」后按你填的数保存,不再自动改。', thinking: '启用模型的扩展思考能力(需要模型支持)', conversationSection: '配置多轮对话开关与问题改写开关(改写提示词见「提示词」)', conversationSectionAgent: '配置每轮携带多少历史对话。智能推理始终为多轮模式。', @@ -5739,6 +5740,8 @@ export default { rewritePromptUser: '改写用户提示词', rewritePromptUserPlaceholder: '留空使用系统默认提示词', maxCompletionTokens: '最大生成Token数', + maxCompletionTokensDefault: '默认', + maxCompletionTokensCustom: '自定义', fallbackStrategy: '兜底策略', fallbackResponse: '固定回复内容', fallbackResponsePlaceholder: '抱歉,我无法回答这个问题。', diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue index 06d2c3704..c261d46a2 100644 --- a/frontend/src/views/agent/AgentEditorModal.vue +++ b/frontend/src/views/agent/AgentEditorModal.vue @@ -610,14 +610,25 @@ - -
+ +
-

{{ $t('agentEditor.desc.maxTokens') }}

+

{{ isAgentMode ? $t('agentEditor.desc.maxTokensAgent') : + $t('agentEditor.desc.maxTokens') }} +

-
- + + {{ $t('agent.editor.maxCompletionTokensDefault') + }} + {{ $t('agent.editor.maxCompletionTokensCustom') + }} + + + {{ effectiveDefaultMaxCompletionTokens }} + +
@@ -2202,9 +2213,20 @@ const defaultKeywordThreshold = ref(0.3); const defaultVectorThreshold = ref(0.5); const defaultRerankTopK = ref(5); const defaultRerankThreshold = ref(0.5); -const defaultMaxCompletionTokens = ref(2048); +const defaultQuickAnswerMaxCompletionTokens = 2048; +const defaultSmartReasoningMaxCompletionTokens = 4096; +const defaultSandboxWriteMaxCompletionTokens = 24576; const defaultTemperature = ref(0.7); +const defaultMaxCompletionTokensFor = (mode: string, sandboxConfigId?: string) => { + if (mode === 'smart-reasoning') { + return sandboxConfigId + ? defaultSandboxWriteMaxCompletionTokens + : defaultSmartReasoningMaxCompletionTokens; + } + return defaultQuickAnswerMaxCompletionTokens; +}; + // 知识库相关工具列表(用于 watch(hasKnowledgeBase) 从"无"变"有"时 seed 默认工具) const knowledgeBaseTools = ['grep_chunks', 'knowledge_search', 'list_knowledge_chunks', 'query_knowledge_graph', 'get_document_info', 'database_query']; @@ -2575,7 +2597,7 @@ const defaultFormData = { model_id: '', rerank_model_id: '', temperature: 0.7, - max_completion_tokens: 2048, + max_completion_tokens: 0, thinking: false, // 默认禁用思考模式 citation_enabled: true, // 默认输出知识库/网页来源引用 // Agent模式设置 @@ -2719,6 +2741,23 @@ const agentMode = computed({ const isAgentMode = computed(() => agentMode.value === 'smart-reasoning'); +const effectiveDefaultMaxCompletionTokens = computed(() => + defaultMaxCompletionTokensFor(agentMode.value, formData.value.config.sandbox_config_id), +); + +const maxCompletionTokensMode = computed({ + get: () => (formData.value.config.max_completion_tokens > 0 ? 'custom' : 'default'), + set: (mode: 'default' | 'custom') => { + if (mode === 'default') { + formData.value.config.max_completion_tokens = 0; + return; + } + if (!formData.value.config.max_completion_tokens) { + formData.value.config.max_completion_tokens = effectiveDefaultMaxCompletionTokens.value; + } + }, +}); + const currentIntentTemplate = computed(() => intentPromptTemplates.value.find((template) => template.id === selectedIntent.value), ); @@ -3250,6 +3289,7 @@ watch(() => props.visible, async (val) => { // 附件解析调优字段:旧数据缺省时置 0(表示使用全局默认) if (agentData.config.attachment_ocr_max_pages == null) agentData.config.attachment_ocr_max_pages = 0; if (agentData.config.attachment_parse_wait_timeout_sec == null) agentData.config.attachment_parse_wait_timeout_sec = 0; + if (agentData.config.max_completion_tokens == null) agentData.config.max_completion_tokens = 0; // 长期记忆:后端用 omitempty,跟随空间设置的智能体不带这个字段。 // 不补成 true 的话开关会显示为"关",用户随手一存就真的把记忆关了。 if (agentData.config.memory_enabled == null) agentData.config.memory_enabled = true; @@ -3285,7 +3325,7 @@ watch(() => props.visible, async (val) => { newFormData.config.vector_threshold = defaultVectorThreshold.value; newFormData.config.rerank_top_k = defaultRerankTopK.value; newFormData.config.rerank_threshold = defaultRerankThreshold.value; - newFormData.config.max_completion_tokens = defaultMaxCompletionTokens.value; + newFormData.config.max_completion_tokens = 0; newFormData.config.temperature = defaultTemperature.value; // 应用系统默认提示词(根据模式填充) const isAgent = newFormData.config.agent_mode === 'smart-reasoning'; @@ -5126,6 +5166,16 @@ const handleSave = async () => { justify-content: flex-start; } + &.max-tokens-control { + flex-direction: column; + align-items: flex-end; + gap: 8px; + + :deep(.t-input-number) { + width: 140px; + } + } + // 让 select 和 input 占满控件区域 :deep(.t-select), :deep(.t-input), @@ -5479,6 +5529,13 @@ const handleSave = async () => { color: var(--td-text-color-primary); } +.max-tokens-value { + font-family: var(--app-font-family-mono); + font-variant-numeric: tabular-nums; + font-size: 12px; + color: var(--td-text-color-placeholder); +} + // 推荐问题列表 .suggested-prompts-list { display: flex; diff --git a/internal/agent/const.go b/internal/agent/const.go index 47311da3c..125330ae1 100644 --- a/internal/agent/const.go +++ b/internal/agent/const.go @@ -85,14 +85,16 @@ func (e *AgentEngine) getLLMCallTimeout() time.Duration { } // getCompletionTokenBudget is the max_tokens / max_completion_tokens sent on -// each ReAct LLM round. Leaving it unset lets providers default to 4096 and -// truncate write_sandbox_file (and similar) tool-call JSON. +// each ReAct LLM round. Unset without a sandbox is 4096; unset with a +// sandbox (write_sandbox_file / edit_sandbox_file) is 24576. func (e *AgentEngine) getCompletionTokenBudget() int { configured := 0 + sandboxID := "" if e.config != nil { configured = e.config.MaxCompletionTokens + sandboxID = e.config.SandboxConfigID } - return types.AgentRoundMaxCompletionTokens(configured) + return types.AgentRoundMaxCompletionTokensFor(configured, sandboxID) } // generateEventID generates a unique event ID with type suffix for better traceability diff --git a/internal/agent/engine_test.go b/internal/agent/engine_test.go index e2fd0bf57..5823ff88e 100644 --- a/internal/agent/engine_test.go +++ b/internal/agent/engine_test.go @@ -446,6 +446,23 @@ func TestStreamThinkingToEventBus_SetsCompletionTokenBudget(t *testing.T) { []chat.Message{{Role: "user", Content: "test"}}, nil, 0, "sess-1") require.NoError(t, err) require.Len(t, mock.opts, 1) + assert.Equal(t, types.DefaultSmartReasoningMaxCompletionTokens, mock.opts[0].MaxTokens) + assert.Equal(t, types.DefaultSmartReasoningMaxCompletionTokens, mock.opts[0].MaxCompletionTokens) + }) + + t.Run("defaults to the write-file budget when a sandbox is bound", func(t *testing.T) { + mock := &mockChat{ + responses: []mockResponse{ + {chunks: []types.StreamResponse{{Content: "ok", Done: true, FinishReason: "stop"}}}, + }, + } + engine := newTestEngine(t, mock, func(cfg *types.AgentConfig) { + cfg.SandboxConfigID = "cfg-a" + }) + _, err := engine.streamThinkingToEventBus(context.Background(), + []chat.Message{{Role: "user", Content: "test"}}, nil, 0, "sess-1") + require.NoError(t, err) + require.Len(t, mock.opts, 1) assert.Equal(t, types.DefaultAgentMaxCompletionTokens, mock.opts[0].MaxTokens) assert.Equal(t, types.DefaultAgentMaxCompletionTokens, mock.opts[0].MaxCompletionTokens) }) diff --git a/internal/types/agent.go b/internal/types/agent.go index 9f4f4f331..cdbb16293 100644 --- a/internal/types/agent.go +++ b/internal/types/agent.go @@ -11,22 +11,54 @@ import ( // DefaultMaxContextTokens is the default context window budget for agent conversations (200k). const DefaultMaxContextTokens = 200000 -// DefaultAgentMaxCompletionTokens is the per-round completion budget for -// ReAct / tool-calling agents. When MaxTokens is omitted, OpenAI-compatible -// providers (and many Chinese gateways) default to 4096 — large tool-call JSON -// such as write_sandbox_file is then truncated mid-stream (finish_reason=length), -// which surfaces as missing required arguments. 32768 matches wiki ingest (#2604). -const DefaultAgentMaxCompletionTokens = 32768 +// DefaultSmartReasoningMaxCompletionTokens is the per-round budget when the +// agent cannot emit write_sandbox_file / edit_sandbox_file. 4096 matches +// typical OpenAI-compatible provider defaults and is enough for ordinary +// tool-call JSON. +const DefaultSmartReasoningMaxCompletionTokens = 4096 + +// DefaultAgentMaxCompletionTokens is the per-round budget when the agent +// can write or edit sandbox files. Those tool calls carry the file body in +// JSON; a 4096 cap truncates mid-stream (finish_reason=length). +const DefaultAgentMaxCompletionTokens = 24576 + +// DefaultQuickAnswerMaxCompletionTokens is the RAG answer budget. +const DefaultQuickAnswerMaxCompletionTokens = 2048 + +// NeedsSandboxWriteCompletionBudget reports whether this agent may register +// write_sandbox_file / edit_sandbox_file. Those tools follow the bound +// sandbox, not the allowed_tools checklist. +func NeedsSandboxWriteCompletionBudget(sandboxConfigID string) bool { + return strings.TrimSpace(sandboxConfigID) != "" +} + +// DefaultMaxCompletionTokens is the unset-config default for one agent: +// quick-answer 2048, smart-reasoning 4096, smart-reasoning with a sandbox 24576. +func DefaultMaxCompletionTokens(agentMode, sandboxConfigID string) int { + if agentMode == AgentModeSmartReasoning { + if NeedsSandboxWriteCompletionBudget(sandboxConfigID) { + return DefaultAgentMaxCompletionTokens + } + return DefaultSmartReasoningMaxCompletionTokens + } + return DefaultQuickAnswerMaxCompletionTokens +} // AgentRoundMaxCompletionTokens returns the completion-token budget for one -// ReAct LLM round. Zero (unset) uses DefaultAgentMaxCompletionTokens. An -// explicit configured value is honored as-is so a user who capped a small -// local model at 4096 is not silently sent 32768. +// ReAct LLM round when no sandbox is bound. Zero (unset) uses +// DefaultSmartReasoningMaxCompletionTokens. An explicit configured value is +// honored as-is. func AgentRoundMaxCompletionTokens(configured int) int { - if configured <= 0 { - return DefaultAgentMaxCompletionTokens + return AgentRoundMaxCompletionTokensFor(configured, "") +} + +// AgentRoundMaxCompletionTokensFor is AgentRoundMaxCompletionTokens with +// the agent's sandbox: unset + sandbox uses the large write-file budget. +func AgentRoundMaxCompletionTokensFor(configured int, sandboxConfigID string) int { + if configured > 0 { + return configured } - return configured + return DefaultMaxCompletionTokens(AgentModeSmartReasoning, sandboxConfigID) } // AgentConfig represents the full agent configuration (used at tenant level and runtime) @@ -90,7 +122,8 @@ type AgentConfig struct { LLMCallTimeout int `json:"llm_call_timeout,omitempty"` // Maximum completion tokens for each ReAct LLM round. Zero means - // DefaultAgentMaxCompletionTokens. Explicit values are sent as-is. + // DefaultMaxCompletionTokens for this agent's mode and sandbox. + // Explicit values are sent as-is. MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // Maximum character length for tool output (default: 16000). diff --git a/internal/types/agent_test.go b/internal/types/agent_test.go index ceb338f9a..47006e45d 100644 --- a/internal/types/agent_test.go +++ b/internal/types/agent_test.go @@ -77,8 +77,11 @@ func TestUseCustomSystemPromptFlag(t *testing.T) { } func TestAgentRoundMaxCompletionTokens(t *testing.T) { - if got := AgentRoundMaxCompletionTokens(0); got != DefaultAgentMaxCompletionTokens { - t.Fatalf("zero configured = %d, want %d", got, DefaultAgentMaxCompletionTokens) + if got := AgentRoundMaxCompletionTokens(0); got != DefaultSmartReasoningMaxCompletionTokens { + t.Fatalf("zero configured = %d, want %d", got, DefaultSmartReasoningMaxCompletionTokens) + } + if got := AgentRoundMaxCompletionTokensFor(0, "cfg-a"); got != DefaultAgentMaxCompletionTokens { + t.Fatalf("unset with sandbox = %d, want %d", got, DefaultAgentMaxCompletionTokens) } if got := AgentRoundMaxCompletionTokens(2048); got != 2048 { t.Fatalf("explicit 2048 = %d, want 2048", got) @@ -86,6 +89,9 @@ func TestAgentRoundMaxCompletionTokens(t *testing.T) { if got := AgentRoundMaxCompletionTokens(4096); got != 4096 { t.Fatalf("explicit 4096 = %d, want 4096", got) } + if got := AgentRoundMaxCompletionTokensFor(4096, "cfg-a"); got != 4096 { + t.Fatalf("explicit 4096 with sandbox must stay 4096, got %d", got) + } if got := AgentRoundMaxCompletionTokens(64000); got != 64000 { t.Fatalf("explicit high value = %d, want 64000", got) } diff --git a/internal/types/custom_agent.go b/internal/types/custom_agent.go index 9929e771d..b4a04ffaf 100644 --- a/internal/types/custom_agent.go +++ b/internal/types/custom_agent.go @@ -125,7 +125,7 @@ type CustomAgentConfig struct { Temperature float64 `yaml:"temperature" json:"temperature"` // Maximum completion tokens. Quick-answer uses this for the RAG answer. // Smart-reasoning ReAct rounds send this value as-is (zero becomes - // DefaultAgentMaxCompletionTokens at call time). + // DefaultMaxCompletionTokens at call time: 4096, or 24576 with a sandbox). MaxCompletionTokens int `yaml:"max_completion_tokens" json:"max_completion_tokens"` // Whether to enable thinking mode (for models that support extended thinking) Thinking *bool `yaml:"thinking" json:"thinking"` @@ -525,13 +525,9 @@ func (a *CustomAgent) EnsureDefaults() { if a.Config.FallbackStrategy == "" { a.Config.FallbackStrategy = "model" } - if a.Config.MaxCompletionTokens == 0 { - if a.Config.AgentMode == AgentModeSmartReasoning { - a.Config.MaxCompletionTokens = DefaultAgentMaxCompletionTokens - } else { - a.Config.MaxCompletionTokens = 2048 - } - } + // MaxCompletionTokens 0 means "use DefaultMaxCompletionTokens at call + // time". Do not materialize a number here — that would make the editor + // treat a chosen default as a custom cap. // Agent mode should always enable multi-turn conversation if a.Config.AgentMode == AgentModeSmartReasoning { a.Config.MultiTurnEnabled = true diff --git a/internal/types/custom_agent_test.go b/internal/types/custom_agent_test.go index 363f389eb..970c3d211 100644 --- a/internal/types/custom_agent_test.go +++ b/internal/types/custom_agent_test.go @@ -43,15 +43,16 @@ func TestEnsureDefaults_ThinkingPreservesTrue(t *testing.T) { func TestEnsureDefaults_MaxCompletionTokensByMode(t *testing.T) { qa := &CustomAgent{Config: CustomAgentConfig{AgentMode: AgentModeQuickAnswer}} qa.EnsureDefaults() - if qa.Config.MaxCompletionTokens != 2048 { - t.Fatalf("quick-answer default MaxCompletionTokens = %d, want 2048", qa.Config.MaxCompletionTokens) + if qa.Config.MaxCompletionTokens != 0 { + t.Fatalf("unset max_completion_tokens must stay 0 (follow default at call time), got %d", + qa.Config.MaxCompletionTokens) } sr := &CustomAgent{Config: CustomAgentConfig{AgentMode: AgentModeSmartReasoning}} sr.EnsureDefaults() - if sr.Config.MaxCompletionTokens != DefaultAgentMaxCompletionTokens { - t.Fatalf("smart-reasoning default MaxCompletionTokens = %d, want %d", - sr.Config.MaxCompletionTokens, DefaultAgentMaxCompletionTokens) + if sr.Config.MaxCompletionTokens != 0 { + t.Fatalf("unset smart-reasoning max_completion_tokens must stay 0, got %d", + sr.Config.MaxCompletionTokens) } explicit := &CustomAgent{Config: CustomAgentConfig{