From b20c10f8200afc50203f39063a6aecb126ef48f8 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Wed, 26 Aug 2026 22:53:49 +0800 Subject: [PATCH] fix: separate Anthropic output and context limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direct Anthropic connections reused the configured context window as max_tokens, which could impose an unintended output value or send a provider-invalid request. Anthropic connections now expose an independent per-response maximum output setting, defaulting to 32K for the protocol-required field, while OpenAI direct requests remain without an added output token cap and direct tool rounds remain unmetered. Release note: Anthropic 直连模型现在可单独配置单次最大输出,不再复用上下文窗口;OpenAI 直连请求和工具调用轮次不增加额外配额。 --- README.md | 2 +- .../computer-control-implementation-status.md | 4 +-- src/main/agent/create-runtime.ts | 5 ++- src/main/agent/model-runtime.test.ts | 3 ++ src/main/agent/model-runtime.ts | 13 ++++--- src/main/agent/remote-model-gateway.ts | 1 + src/main/runtime-settings-store.test.ts | 7 ++-- src/main/runtime-settings-store.ts | 16 +++++++++ src/renderer/src/SettingsPanel.test.tsx | 7 +++- src/renderer/src/SettingsPanel.tsx | 34 +++++++++++++++++++ .../src/i18n/locales/en-US/settings.ts | 3 ++ .../src/i18n/locales/zh-CN/settings.ts | 3 ++ src/shared/contracts.ts | 10 ++++++ 13 files changed, 96 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 84916322..b9ec17e8 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ GoodBuddy是一个开箱即用的 AI 生产平台,安全、跨平台、本地 - **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。 - **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。 - **多 Runtime**:支持直连模型、OpenCode、Continue 和预览版 DeepSeek Harness,统一处理取消、超时、输出限制和进程退出;原生能力清单将 Tools 与 Commands、LSP、Formatters 分开,并显示来源及 Ask/Execute 可用性。内置 OpenCode 提供 Agent、Tool、Command 与原生 Compact,Continue 提供 Rules、Prompt 预设、结构化提问与 GoodBuddy 手动摘要压缩,并明确标记当前版本无法静态发现原生 Tools。 -- **开放连接**:支持 OpenAI Responses、OpenAI 兼容 Chat Completions、Anthropic Messages、OpenAI Images、Embeddings、跨 Runtime Skills 与自定义 MCP,以及默认关闭、由用户显式开启的 DeepSeek Harness npm 插件市场。MCP 测试可读取有界 Prompt/Resource 元数据,但不会读取 Resource 内容。 +- **开放连接**:支持 OpenAI Responses、OpenAI 兼容 Chat Completions、Anthropic Messages、OpenAI Images、Embeddings、跨 Runtime Skills 与自定义 MCP,以及默认关闭、由用户显式开启的 DeepSeek Harness npm 插件市场。Anthropic 连接可独立配置协议必填的单次最大输出,不再复用上下文窗口;OpenAI 直连请求不额外设置输出 Token 上限。MCP 测试可读取有界 Prompt/Resource 元数据,但不会读取 Resource 内容。 - **知识工作区**:支持文件、目录和网页导入,以及全文、词组、向量和图谱混合检索。 - **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳;可从稳定的本地会话复制当前聊天内容并创建互不影响的会话分支。 - **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。 diff --git a/docs/status/computer-control-implementation-status.md b/docs/status/computer-control-implementation-status.md index b71aa6a0..c090c540 100644 --- a/docs/status/computer-control-implementation-status.md +++ b/docs/status/computer-control-implementation-status.md @@ -28,8 +28,8 @@ Linux x64 和 Linux arm64。 导航及重定向仍经过 DNS 解析和目标地址固定。 - 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、 空闲回收和应用退出清理。 -- 直连模型工具循环已提高到适合浏览器任务的有界上限,并包含重复调用和 - 无进展保护。 +- 直连模型工具循环不设置工具次数或模型轮次配额;取消、请求超时、工具结果大小和 + 累计输出边界继续防止失控执行。 ### MCP diff --git a/src/main/agent/create-runtime.ts b/src/main/agent/create-runtime.ts index 31d63f2c..0e5fbcc1 100644 --- a/src/main/agent/create-runtime.ts +++ b/src/main/agent/create-runtime.ts @@ -96,7 +96,8 @@ function resolveContextCompression( 'openai-images-generations' >, authentication: summaryProfile.authentication, - contextWindowTokens: summaryProfile.contextWindowTokens + contextWindowTokens: summaryProfile.contextWindowTokens, + maximumOutputTokens: summaryProfile.maximumOutputTokens } } : {}) @@ -120,6 +121,7 @@ export function createDefaultModelRuntime( protocol: settings.modelProtocol, authentication: settings.modelAuthentication, supportsImageInput: settings.supportsImageInput, + maximumOutputTokens: currentProfile?.maximumOutputTokens, defaultWorkspace: settings.workspacePath || defaultWorkspace, contextCompression: resolveContextCompression( settings, @@ -141,6 +143,7 @@ export function createModelProfileRuntime( protocol: profile.protocol, authentication: profile.authentication, supportsImageInput: profile.supportsImageInput, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality ?? defaultRuntimeSettings.imageGenerationQuality, diff --git a/src/main/agent/model-runtime.test.ts b/src/main/agent/model-runtime.test.ts index 03d92cd1..ec5b495b 100644 --- a/src/main/agent/model-runtime.test.ts +++ b/src/main/agent/model-runtime.test.ts @@ -476,6 +476,7 @@ describe('ModelAgentRuntime', () => { model: 'sonnet-5', protocol: 'anthropic-messages', authentication: 'api-key', + maximumOutputTokens: 48_000, skillInstructions: '# 文档写作', fetcher }) @@ -500,11 +501,13 @@ describe('ModelAgentRuntime', () => { const body = JSON.parse(init?.body as string) as { model: string + max_tokens: number stream: boolean system: string } expect(body).toMatchObject({ model: 'sonnet-5', + max_tokens: 48_000, stream: true }) expect(body.system).toMatch( diff --git a/src/main/agent/model-runtime.ts b/src/main/agent/model-runtime.ts index 19ba84f7..db511ec5 100644 --- a/src/main/agent/model-runtime.ts +++ b/src/main/agent/model-runtime.ts @@ -8,6 +8,7 @@ import type { ModelProtocol, RuntimeConversationCompactInput } from '../../shared/contracts' +import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts' import type { ResolvedMcpServer } from '../capabilities/capability-service' import type { BrowserToolService } from '../browser/browser-model-tools' import type { WorkspaceAccess } from '../workspace' @@ -233,8 +234,10 @@ export type ModelRuntimeOptions = { protocol: Exclude authentication: ModelAuthentication contextWindowTokens?: number + maximumOutputTokens?: number } } + maximumOutputTokens?: number } function getErrorMessage(value: unknown): string | undefined { @@ -1794,10 +1797,9 @@ export class ModelAgentRuntime implements AgentRuntime { } private get anthropicProtocolMaximumTokens(): number { - return Math.max( - this.options.contextCompression?.contextWindowTokens ?? - minimumModelContextWindowTokens, - minimumModelContextWindowTokens + return ( + this.options.maximumOutputTokens ?? + defaultAnthropicMaximumOutputTokens ) } @@ -1877,7 +1879,8 @@ export class ModelAgentRuntime implements AgentRuntime { ModelProtocol, 'openai-images-generations' >, - authentication: this.options.authentication + authentication: this.options.authentication, + maximumOutputTokens: this.options.maximumOutputTokens } const summaryRuntime = new ModelAgentRuntime({ ...summaryModel, diff --git a/src/main/agent/remote-model-gateway.ts b/src/main/agent/remote-model-gateway.ts index dfb4b01e..840bdc06 100644 --- a/src/main/agent/remote-model-gateway.ts +++ b/src/main/agent/remote-model-gateway.ts @@ -386,6 +386,7 @@ export function createResolvedModelProfileDigest( authentication: profile.authentication, supportsImageInput: profile.supportsImageInput === true, contextWindowTokens: profile.contextWindowTokens ?? null, + maximumOutputTokens: profile.maximumOutputTokens ?? null, imageGenerationQuality: profile.imageGenerationQuality ?? null }) ) diff --git a/src/main/runtime-settings-store.test.ts b/src/main/runtime-settings-store.test.ts index 6be88d0d..740f4cf1 100644 --- a/src/main/runtime-settings-store.test.ts +++ b/src/main/runtime-settings-store.test.ts @@ -283,6 +283,7 @@ describe('RuntimeSettingsStore', () => { authentication: 'none', supportsImageInput: false, contextWindowTokens: 256_000, + maximumOutputTokens: 48_000, imageGenerationQuality: 'auto', apiKey: { action: 'keep' } } @@ -302,7 +303,8 @@ describe('RuntimeSettingsStore', () => { modelProfiles: [ { id: profileId, - contextWindowTokens: 256_000 + contextWindowTokens: 256_000, + maximumOutputTokens: 48_000 } ], contextCompression: { @@ -316,7 +318,8 @@ describe('RuntimeSettingsStore', () => { modelProfiles: [ { id: profileId, - contextWindowTokens: 256_000 + contextWindowTokens: 256_000, + maximumOutputTokens: 48_000 } ], contextCompression: { diff --git a/src/main/runtime-settings-store.ts b/src/main/runtime-settings-store.ts index 270c2e57..44610357 100644 --- a/src/main/runtime-settings-store.ts +++ b/src/main/runtime-settings-store.ts @@ -170,6 +170,12 @@ const currentStoredModelProfileSchema = storedModelProfileSchema.extend({ .int() .min(8_000) .max(10_000_000) + .optional(), + maximumOutputTokens: z + .number() + .int() + .min(1) + .max(10_000_000) .optional() }) @@ -434,6 +440,7 @@ export type ResolvedModelProfile = { authentication: RuntimeSettings['modelAuthentication'] supportsImageInput?: boolean contextWindowTokens?: number + maximumOutputTokens?: number imageGenerationQuality?: RuntimeSettings['imageGenerationQuality'] apiKey?: string } @@ -1468,6 +1475,7 @@ export class RuntimeSettingsStore { authentication: RuntimeSettings['modelAuthentication'] supportsImageInput: boolean contextWindowTokens?: number + maximumOutputTokens?: number imageGenerationQuality: RuntimeSettings['imageGenerationQuality'] credentialSource: RuntimeSettings['credentialSource'] } { @@ -1529,6 +1537,7 @@ export class RuntimeSettingsStore { : profile.authentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality, credentialSource: credential.source } @@ -1552,6 +1561,7 @@ export class RuntimeSettingsStore { authentication: effective.authentication, supportsImageInput: effective.supportsImageInput, contextWindowTokens: effective.contextWindowTokens, + maximumOutputTokens: effective.maximumOutputTokens, imageGenerationQuality: effective.imageGenerationQuality, apiKey: effective.apiKey } @@ -1569,6 +1579,7 @@ export class RuntimeSettingsStore { authentication: profile.authentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality, apiKey: credential.activeApiKey } @@ -1651,6 +1662,7 @@ export class RuntimeSettingsStore { authentication: resolved.authentication, supportsImageInput: resolved.supportsImageInput, contextWindowTokens: resolved.contextWindowTokens, + maximumOutputTokens: resolved.maximumOutputTokens, imageGenerationQuality: resolved.imageGenerationQuality ?? defaultRuntimeSettings.imageGenerationQuality, @@ -1672,6 +1684,7 @@ export class RuntimeSettingsStore { authentication: profile.authentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality ?? defaultRuntimeSettings.imageGenerationQuality, @@ -1966,6 +1979,7 @@ export class RuntimeSettingsStore { authentication: input.modelAuthentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: input.imageGenerationQuality, apiKey: input.apiKey } @@ -1978,6 +1992,7 @@ export class RuntimeSettingsStore { authentication: profile.authentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality, apiKey: { action: 'keep' as const } } @@ -2029,6 +2044,7 @@ export class RuntimeSettingsStore { authentication: profile.authentication, supportsImageInput: profile.supportsImageInput ?? false, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality } if ( diff --git a/src/renderer/src/SettingsPanel.test.tsx b/src/renderer/src/SettingsPanel.test.tsx index e7c5fcb7..c9bd956f 100644 --- a/src/renderer/src/SettingsPanel.test.tsx +++ b/src/renderer/src/SettingsPanel.test.tsx @@ -1112,6 +1112,10 @@ describe('SettingsPanel runtime files', () => { expect(contextWindow).toHaveValue(null) expect(contextWindow).toHaveAttribute('min', '32') fireEvent.change(contextWindow, { target: { value: '256' } }) + const maximumOutput = screen.getByLabelText('Anthropic 最大输出') + expect(maximumOutput).toHaveValue(32) + expect(maximumOutput).toHaveAttribute('min', '1') + fireEvent.change(maximumOutput, { target: { value: '48' } }) fireEvent.click(screen.getByRole('button', { name: '保存设置' })) await waitFor(() => @@ -1120,7 +1124,8 @@ describe('SettingsPanel runtime files', () => { modelProfiles: expect.arrayContaining([ expect.objectContaining({ id: modelProfileId, - contextWindowTokens: 256_000 + contextWindowTokens: 256_000, + maximumOutputTokens: 48_000 }) ]) }) diff --git a/src/renderer/src/SettingsPanel.tsx b/src/renderer/src/SettingsPanel.tsx index 4275b4b4..38d62cf2 100644 --- a/src/renderer/src/SettingsPanel.tsx +++ b/src/renderer/src/SettingsPanel.tsx @@ -34,6 +34,7 @@ import type { } from '../../shared/contracts' import { defaultContextCompressionSettings, + defaultAnthropicMaximumOutputTokens, builtinEmbeddingConnectionId, defaultModelProfileId as builtInDefaultModelProfileId, defaultRuntimeSettings, @@ -257,6 +258,9 @@ function toModelProfileDrafts( return settings.modelProfiles.map((profile) => ({ ...profile, supportsImageInput: profile.supportsImageInput ?? false, + maximumOutputTokens: + profile.maximumOutputTokens ?? + defaultAnthropicMaximumOutputTokens, apiKey: '', clearApiKey: false })) @@ -1285,6 +1289,7 @@ export function SettingsPanel({ authentication: profile.authentication, supportsImageInput: profile.supportsImageInput, contextWindowTokens: profile.contextWindowTokens, + maximumOutputTokens: profile.maximumOutputTokens, imageGenerationQuality: profile.imageGenerationQuality, apiKey: profile.clearApiKey ? ({ action: 'clear' } as const) @@ -1670,6 +1675,7 @@ export function SettingsPanel({ protocol: 'openai-chat-completions', authentication: defaultRuntimeSettings.modelAuthentication, supportsImageInput: defaultRuntimeSettings.supportsImageInput, + maximumOutputTokens: defaultAnthropicMaximumOutputTokens, imageGenerationQuality: defaultRuntimeSettings.imageGenerationQuality, apiKeyConfigured: false, @@ -3136,6 +3142,34 @@ export function SettingsPanel({ {t('model.profile.contextWindowDescription')} + {profile.protocol === 'anthropic-messages' && ( + + )} )} {profile.protocol === diff --git a/src/renderer/src/i18n/locales/en-US/settings.ts b/src/renderer/src/i18n/locales/en-US/settings.ts index 2f56f650..dbbb1c24 100644 --- a/src/renderer/src/i18n/locales/en-US/settings.ts +++ b/src/renderer/src/i18n/locales/en-US/settings.ts @@ -721,6 +721,9 @@ export const settings = { contextWindow: 'Context window (optional)', contextWindowDescription: 'Enter 32K–10000K tokens. Leave blank when unknown. This value is used only for GoodBuddy local budget calculations.', + maximumOutput: 'Anthropic maximum output', + maximumOutputDescription: + 'The Anthropic Messages protocol requires this value on every request. Enter 1K–10000K tokens. It limits only one model response, not tool rounds or cumulative usage.', imageQuality: 'Image quality', imageQualityAriaLabel: 'Image quality for {{name}}', quality: { diff --git a/src/renderer/src/i18n/locales/zh-CN/settings.ts b/src/renderer/src/i18n/locales/zh-CN/settings.ts index e6a13117..a53ea42c 100644 --- a/src/renderer/src/i18n/locales/zh-CN/settings.ts +++ b/src/renderer/src/i18n/locales/zh-CN/settings.ts @@ -662,6 +662,9 @@ export const settings = { contextWindow: '上下文上限(可选)', contextWindowDescription: '以 K tokens 填写,范围为 32K–10000K。留空表示未知;此值仅用于 GoodBuddy 本地预算计算。', + maximumOutput: 'Anthropic 最大输出', + maximumOutputDescription: + 'Anthropic Messages 协议要求每次请求提供该值。填写 1K–10000K tokens;它只限制单次模型响应,不限制工具调用轮次或累计用量。', imageQuality: '图片质量', imageQualityAriaLabel: '图片质量 {{name}}', quality: { diff --git a/src/shared/contracts.ts b/src/shared/contracts.ts index 0e94458d..21fcfae4 100644 --- a/src/shared/contracts.ts +++ b/src/shared/contracts.ts @@ -568,6 +568,9 @@ export { minimumModelContextWindowTokens } from './context-window' +export const defaultAnthropicMaximumOutputTokens = 32_000 +export const maximumModelOutputTokens = 10_000_000 + const modelProfileInputSchema = z .object({ id: modelProfileIdSchema, @@ -588,6 +591,12 @@ const modelProfileInputSchema = z .min(minimumModelContextWindowTokens) .max(maximumModelContextWindowTokens) .optional(), + maximumOutputTokens: z + .number() + .int() + .min(1) + .max(maximumModelOutputTokens) + .optional(), imageGenerationQuality: imageGenerationQualitySchema, apiKey: modelApiKeyUpdateSchema }) @@ -968,6 +977,7 @@ export type ModelConnectionSettings = { authentication: ModelAuthentication supportsImageInput?: boolean contextWindowTokens?: number + maximumOutputTokens?: number imageGenerationQuality: ImageGenerationQuality apiKeyConfigured: boolean credentialSource: 'none' | 'encrypted' | 'environment' | 'unreadable'