fix: separate Anthropic output and context limits

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 直连请求和工具调用轮次不增加额外配额。
This commit is contained in:
mesalogo
2026-08-26 22:53:49 +08:00
parent 49700bc7cf
commit b20c10f820
13 changed files with 96 additions and 12 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ GoodBuddy是一个开箱即用的 AI 生产平台,安全、跨平台、本地
- **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。 - **安全执行**:`Ask` 保持只读;`Execute` 仅运行已启用且受边界约束的工具,并保留活动记录。
- **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。 - **本地优先**:会话、任务、成果、记忆、知识库和图谱保存在本地 SQLite;API Key 由系统安全存储加密。
- **多 Runtime**:支持直连模型、OpenCode、Continue 和预览版 DeepSeek Harness,统一处理取消、超时、输出限制和进程退出;原生能力清单将 Tools 与 Commands、LSP、Formatters 分开,并显示来源及 Ask/Execute 可用性。内置 OpenCode 提供 Agent、Tool、Command 与原生 Compact,Continue 提供 Rules、Prompt 预设、结构化提问与 GoodBuddy 手动摘要压缩,并明确标记当前版本无法静态发现原生 Tools。 - **多 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、对话、任务、活动、成果、记忆、魔法笔记和智能心跳;可从稳定的本地会话复制当前聊天内容并创建互不影响的会话分支。 - **工作管理**:集中管理 Projects、对话、任务、活动、成果、记忆、魔法笔记和智能心跳;可从稳定的本地会话复制当前聊天内容并创建互不影响的会话分支。
- **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。 - **远程通道**:支持微信 ClawBot、企业微信和钉钉,每个发送者使用独立远程会话。
@@ -28,8 +28,8 @@ Linux x64 和 Linux arm64。
导航及重定向仍经过 DNS 解析和目标地址固定。 导航及重定向仍经过 DNS 解析和目标地址固定。
- 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、 - 已实现回环过滤代理、下载和文件选择器阻止、权限拒绝、会话取消、
空闲回收和应用退出清理。 空闲回收和应用退出清理。
- 直连模型工具循环已提高到适合浏览器任务的有界上限,并包含重复调用和 - 直连模型工具循环不设置工具次数或模型轮次配额;取消、请求超时、工具结果大小和
无进展保护。 累计输出边界继续防止失控执行。
### MCP ### MCP
+4 -1
View File
@@ -96,7 +96,8 @@ function resolveContextCompression(
'openai-images-generations' 'openai-images-generations'
>, >,
authentication: summaryProfile.authentication, authentication: summaryProfile.authentication,
contextWindowTokens: summaryProfile.contextWindowTokens contextWindowTokens: summaryProfile.contextWindowTokens,
maximumOutputTokens: summaryProfile.maximumOutputTokens
} }
} }
: {}) : {})
@@ -120,6 +121,7 @@ export function createDefaultModelRuntime(
protocol: settings.modelProtocol, protocol: settings.modelProtocol,
authentication: settings.modelAuthentication, authentication: settings.modelAuthentication,
supportsImageInput: settings.supportsImageInput, supportsImageInput: settings.supportsImageInput,
maximumOutputTokens: currentProfile?.maximumOutputTokens,
defaultWorkspace: settings.workspacePath || defaultWorkspace, defaultWorkspace: settings.workspacePath || defaultWorkspace,
contextCompression: resolveContextCompression( contextCompression: resolveContextCompression(
settings, settings,
@@ -141,6 +143,7 @@ export function createModelProfileRuntime(
protocol: profile.protocol, protocol: profile.protocol,
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: imageGenerationQuality:
profile.imageGenerationQuality ?? profile.imageGenerationQuality ??
defaultRuntimeSettings.imageGenerationQuality, defaultRuntimeSettings.imageGenerationQuality,
+3
View File
@@ -476,6 +476,7 @@ describe('ModelAgentRuntime', () => {
model: 'sonnet-5', model: 'sonnet-5',
protocol: 'anthropic-messages', protocol: 'anthropic-messages',
authentication: 'api-key', authentication: 'api-key',
maximumOutputTokens: 48_000,
skillInstructions: '# 文档写作', skillInstructions: '# 文档写作',
fetcher fetcher
}) })
@@ -500,11 +501,13 @@ describe('ModelAgentRuntime', () => {
const body = JSON.parse(init?.body as string) as { const body = JSON.parse(init?.body as string) as {
model: string model: string
max_tokens: number
stream: boolean stream: boolean
system: string system: string
} }
expect(body).toMatchObject({ expect(body).toMatchObject({
model: 'sonnet-5', model: 'sonnet-5',
max_tokens: 48_000,
stream: true stream: true
}) })
expect(body.system).toMatch( expect(body.system).toMatch(
+8 -5
View File
@@ -8,6 +8,7 @@ import type {
ModelProtocol, ModelProtocol,
RuntimeConversationCompactInput RuntimeConversationCompactInput
} from '../../shared/contracts' } from '../../shared/contracts'
import { defaultAnthropicMaximumOutputTokens } from '../../shared/contracts'
import type { ResolvedMcpServer } from '../capabilities/capability-service' import type { ResolvedMcpServer } from '../capabilities/capability-service'
import type { BrowserToolService } from '../browser/browser-model-tools' import type { BrowserToolService } from '../browser/browser-model-tools'
import type { WorkspaceAccess } from '../workspace' import type { WorkspaceAccess } from '../workspace'
@@ -233,8 +234,10 @@ export type ModelRuntimeOptions = {
protocol: Exclude<ModelProtocol, 'openai-images-generations'> protocol: Exclude<ModelProtocol, 'openai-images-generations'>
authentication: ModelAuthentication authentication: ModelAuthentication
contextWindowTokens?: number contextWindowTokens?: number
maximumOutputTokens?: number
} }
} }
maximumOutputTokens?: number
} }
function getErrorMessage(value: unknown): string | undefined { function getErrorMessage(value: unknown): string | undefined {
@@ -1794,10 +1797,9 @@ export class ModelAgentRuntime implements AgentRuntime {
} }
private get anthropicProtocolMaximumTokens(): number { private get anthropicProtocolMaximumTokens(): number {
return Math.max( return (
this.options.contextCompression?.contextWindowTokens ?? this.options.maximumOutputTokens ??
minimumModelContextWindowTokens, defaultAnthropicMaximumOutputTokens
minimumModelContextWindowTokens
) )
} }
@@ -1877,7 +1879,8 @@ export class ModelAgentRuntime implements AgentRuntime {
ModelProtocol, ModelProtocol,
'openai-images-generations' 'openai-images-generations'
>, >,
authentication: this.options.authentication authentication: this.options.authentication,
maximumOutputTokens: this.options.maximumOutputTokens
} }
const summaryRuntime = new ModelAgentRuntime({ const summaryRuntime = new ModelAgentRuntime({
...summaryModel, ...summaryModel,
+1
View File
@@ -386,6 +386,7 @@ export function createResolvedModelProfileDigest(
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput === true, supportsImageInput: profile.supportsImageInput === true,
contextWindowTokens: profile.contextWindowTokens ?? null, contextWindowTokens: profile.contextWindowTokens ?? null,
maximumOutputTokens: profile.maximumOutputTokens ?? null,
imageGenerationQuality: profile.imageGenerationQuality ?? null imageGenerationQuality: profile.imageGenerationQuality ?? null
}) })
) )
+5 -2
View File
@@ -283,6 +283,7 @@ describe('RuntimeSettingsStore', () => {
authentication: 'none', authentication: 'none',
supportsImageInput: false, supportsImageInput: false,
contextWindowTokens: 256_000, contextWindowTokens: 256_000,
maximumOutputTokens: 48_000,
imageGenerationQuality: 'auto', imageGenerationQuality: 'auto',
apiKey: { action: 'keep' } apiKey: { action: 'keep' }
} }
@@ -302,7 +303,8 @@ describe('RuntimeSettingsStore', () => {
modelProfiles: [ modelProfiles: [
{ {
id: profileId, id: profileId,
contextWindowTokens: 256_000 contextWindowTokens: 256_000,
maximumOutputTokens: 48_000
} }
], ],
contextCompression: { contextCompression: {
@@ -316,7 +318,8 @@ describe('RuntimeSettingsStore', () => {
modelProfiles: [ modelProfiles: [
{ {
id: profileId, id: profileId,
contextWindowTokens: 256_000 contextWindowTokens: 256_000,
maximumOutputTokens: 48_000
} }
], ],
contextCompression: { contextCompression: {
+16
View File
@@ -170,6 +170,12 @@ const currentStoredModelProfileSchema = storedModelProfileSchema.extend({
.int() .int()
.min(8_000) .min(8_000)
.max(10_000_000) .max(10_000_000)
.optional(),
maximumOutputTokens: z
.number()
.int()
.min(1)
.max(10_000_000)
.optional() .optional()
}) })
@@ -434,6 +440,7 @@ export type ResolvedModelProfile = {
authentication: RuntimeSettings['modelAuthentication'] authentication: RuntimeSettings['modelAuthentication']
supportsImageInput?: boolean supportsImageInput?: boolean
contextWindowTokens?: number contextWindowTokens?: number
maximumOutputTokens?: number
imageGenerationQuality?: RuntimeSettings['imageGenerationQuality'] imageGenerationQuality?: RuntimeSettings['imageGenerationQuality']
apiKey?: string apiKey?: string
} }
@@ -1468,6 +1475,7 @@ export class RuntimeSettingsStore {
authentication: RuntimeSettings['modelAuthentication'] authentication: RuntimeSettings['modelAuthentication']
supportsImageInput: boolean supportsImageInput: boolean
contextWindowTokens?: number contextWindowTokens?: number
maximumOutputTokens?: number
imageGenerationQuality: RuntimeSettings['imageGenerationQuality'] imageGenerationQuality: RuntimeSettings['imageGenerationQuality']
credentialSource: RuntimeSettings['credentialSource'] credentialSource: RuntimeSettings['credentialSource']
} { } {
@@ -1529,6 +1537,7 @@ export class RuntimeSettingsStore {
: profile.authentication, : profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality, imageGenerationQuality: profile.imageGenerationQuality,
credentialSource: credential.source credentialSource: credential.source
} }
@@ -1552,6 +1561,7 @@ export class RuntimeSettingsStore {
authentication: effective.authentication, authentication: effective.authentication,
supportsImageInput: effective.supportsImageInput, supportsImageInput: effective.supportsImageInput,
contextWindowTokens: effective.contextWindowTokens, contextWindowTokens: effective.contextWindowTokens,
maximumOutputTokens: effective.maximumOutputTokens,
imageGenerationQuality: effective.imageGenerationQuality, imageGenerationQuality: effective.imageGenerationQuality,
apiKey: effective.apiKey apiKey: effective.apiKey
} }
@@ -1569,6 +1579,7 @@ export class RuntimeSettingsStore {
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality, imageGenerationQuality: profile.imageGenerationQuality,
apiKey: credential.activeApiKey apiKey: credential.activeApiKey
} }
@@ -1651,6 +1662,7 @@ export class RuntimeSettingsStore {
authentication: resolved.authentication, authentication: resolved.authentication,
supportsImageInput: resolved.supportsImageInput, supportsImageInput: resolved.supportsImageInput,
contextWindowTokens: resolved.contextWindowTokens, contextWindowTokens: resolved.contextWindowTokens,
maximumOutputTokens: resolved.maximumOutputTokens,
imageGenerationQuality: imageGenerationQuality:
resolved.imageGenerationQuality ?? resolved.imageGenerationQuality ??
defaultRuntimeSettings.imageGenerationQuality, defaultRuntimeSettings.imageGenerationQuality,
@@ -1672,6 +1684,7 @@ export class RuntimeSettingsStore {
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: imageGenerationQuality:
profile.imageGenerationQuality ?? profile.imageGenerationQuality ??
defaultRuntimeSettings.imageGenerationQuality, defaultRuntimeSettings.imageGenerationQuality,
@@ -1966,6 +1979,7 @@ export class RuntimeSettingsStore {
authentication: input.modelAuthentication, authentication: input.modelAuthentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: input.imageGenerationQuality, imageGenerationQuality: input.imageGenerationQuality,
apiKey: input.apiKey apiKey: input.apiKey
} }
@@ -1978,6 +1992,7 @@ export class RuntimeSettingsStore {
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality, imageGenerationQuality: profile.imageGenerationQuality,
apiKey: { action: 'keep' as const } apiKey: { action: 'keep' as const }
} }
@@ -2029,6 +2044,7 @@ export class RuntimeSettingsStore {
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput ?? false, supportsImageInput: profile.supportsImageInput ?? false,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality imageGenerationQuality: profile.imageGenerationQuality
} }
if ( if (
+6 -1
View File
@@ -1112,6 +1112,10 @@ describe('SettingsPanel runtime files', () => {
expect(contextWindow).toHaveValue(null) expect(contextWindow).toHaveValue(null)
expect(contextWindow).toHaveAttribute('min', '32') expect(contextWindow).toHaveAttribute('min', '32')
fireEvent.change(contextWindow, { target: { value: '256' } }) 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: '保存设置' })) fireEvent.click(screen.getByRole('button', { name: '保存设置' }))
await waitFor(() => await waitFor(() =>
@@ -1120,7 +1124,8 @@ describe('SettingsPanel runtime files', () => {
modelProfiles: expect.arrayContaining([ modelProfiles: expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: modelProfileId, id: modelProfileId,
contextWindowTokens: 256_000 contextWindowTokens: 256_000,
maximumOutputTokens: 48_000
}) })
]) ])
}) })
+34
View File
@@ -34,6 +34,7 @@ import type {
} from '../../shared/contracts' } from '../../shared/contracts'
import { import {
defaultContextCompressionSettings, defaultContextCompressionSettings,
defaultAnthropicMaximumOutputTokens,
builtinEmbeddingConnectionId, builtinEmbeddingConnectionId,
defaultModelProfileId as builtInDefaultModelProfileId, defaultModelProfileId as builtInDefaultModelProfileId,
defaultRuntimeSettings, defaultRuntimeSettings,
@@ -257,6 +258,9 @@ function toModelProfileDrafts(
return settings.modelProfiles.map((profile) => ({ return settings.modelProfiles.map((profile) => ({
...profile, ...profile,
supportsImageInput: profile.supportsImageInput ?? false, supportsImageInput: profile.supportsImageInput ?? false,
maximumOutputTokens:
profile.maximumOutputTokens ??
defaultAnthropicMaximumOutputTokens,
apiKey: '', apiKey: '',
clearApiKey: false clearApiKey: false
})) }))
@@ -1285,6 +1289,7 @@ export function SettingsPanel({
authentication: profile.authentication, authentication: profile.authentication,
supportsImageInput: profile.supportsImageInput, supportsImageInput: profile.supportsImageInput,
contextWindowTokens: profile.contextWindowTokens, contextWindowTokens: profile.contextWindowTokens,
maximumOutputTokens: profile.maximumOutputTokens,
imageGenerationQuality: profile.imageGenerationQuality, imageGenerationQuality: profile.imageGenerationQuality,
apiKey: profile.clearApiKey apiKey: profile.clearApiKey
? ({ action: 'clear' } as const) ? ({ action: 'clear' } as const)
@@ -1670,6 +1675,7 @@ export function SettingsPanel({
protocol: 'openai-chat-completions', protocol: 'openai-chat-completions',
authentication: defaultRuntimeSettings.modelAuthentication, authentication: defaultRuntimeSettings.modelAuthentication,
supportsImageInput: defaultRuntimeSettings.supportsImageInput, supportsImageInput: defaultRuntimeSettings.supportsImageInput,
maximumOutputTokens: defaultAnthropicMaximumOutputTokens,
imageGenerationQuality: imageGenerationQuality:
defaultRuntimeSettings.imageGenerationQuality, defaultRuntimeSettings.imageGenerationQuality,
apiKeyConfigured: false, apiKeyConfigured: false,
@@ -3136,6 +3142,34 @@ export function SettingsPanel({
{t('model.profile.contextWindowDescription')} {t('model.profile.contextWindowDescription')}
</small> </small>
</label> </label>
{profile.protocol === 'anthropic-messages' && (
<label className="field">
<span>{t('model.profile.maximumOutput')}</span>
<input
aria-label={t('model.profile.maximumOutput')}
inputMode="numeric"
max={10_000}
min={1}
onChange={(event) => {
const value = event.target.valueAsNumber
updateModelProfile(profile.id, {
maximumOutputTokens: Number.isFinite(value)
? Math.round(value * 1_000)
: defaultAnthropicMaximumOutputTokens
})
}}
placeholder="32"
type="number"
value={
(profile.maximumOutputTokens ??
defaultAnthropicMaximumOutputTokens) / 1_000
}
/>
<small>
{t('model.profile.maximumOutputDescription')}
</small>
</label>
)}
</> </>
)} )}
{profile.protocol === {profile.protocol ===
@@ -721,6 +721,9 @@ export const settings = {
contextWindow: 'Context window (optional)', contextWindow: 'Context window (optional)',
contextWindowDescription: contextWindowDescription:
'Enter 32K–10000K tokens. Leave blank when unknown. This value is used only for GoodBuddy local budget calculations.', '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', imageQuality: 'Image quality',
imageQualityAriaLabel: 'Image quality for {{name}}', imageQualityAriaLabel: 'Image quality for {{name}}',
quality: { quality: {
@@ -662,6 +662,9 @@ export const settings = {
contextWindow: '上下文上限(可选)', contextWindow: '上下文上限(可选)',
contextWindowDescription: contextWindowDescription:
'以 K tokens 填写,范围为 32K–10000K。留空表示未知;此值仅用于 GoodBuddy 本地预算计算。', '以 K tokens 填写,范围为 32K–10000K。留空表示未知;此值仅用于 GoodBuddy 本地预算计算。',
maximumOutput: 'Anthropic 最大输出',
maximumOutputDescription:
'Anthropic Messages 协议要求每次请求提供该值。填写 1K–10000K tokens;它只限制单次模型响应,不限制工具调用轮次或累计用量。',
imageQuality: '图片质量', imageQuality: '图片质量',
imageQualityAriaLabel: '图片质量 {{name}}', imageQualityAriaLabel: '图片质量 {{name}}',
quality: { quality: {
+10
View File
@@ -568,6 +568,9 @@ export {
minimumModelContextWindowTokens minimumModelContextWindowTokens
} from './context-window' } from './context-window'
export const defaultAnthropicMaximumOutputTokens = 32_000
export const maximumModelOutputTokens = 10_000_000
const modelProfileInputSchema = z const modelProfileInputSchema = z
.object({ .object({
id: modelProfileIdSchema, id: modelProfileIdSchema,
@@ -588,6 +591,12 @@ const modelProfileInputSchema = z
.min(minimumModelContextWindowTokens) .min(minimumModelContextWindowTokens)
.max(maximumModelContextWindowTokens) .max(maximumModelContextWindowTokens)
.optional(), .optional(),
maximumOutputTokens: z
.number()
.int()
.min(1)
.max(maximumModelOutputTokens)
.optional(),
imageGenerationQuality: imageGenerationQualitySchema, imageGenerationQuality: imageGenerationQualitySchema,
apiKey: modelApiKeyUpdateSchema apiKey: modelApiKeyUpdateSchema
}) })
@@ -968,6 +977,7 @@ export type ModelConnectionSettings = {
authentication: ModelAuthentication authentication: ModelAuthentication
supportsImageInput?: boolean supportsImageInput?: boolean
contextWindowTokens?: number contextWindowTokens?: number
maximumOutputTokens?: number
imageGenerationQuality: ImageGenerationQuality imageGenerationQuality: ImageGenerationQuality
apiKeyConfigured: boolean apiKeyConfigured: boolean
credentialSource: 'none' | 'encrypted' | 'environment' | 'unreadable' credentialSource: 'none' | 'encrypted' | 'environment' | 'unreadable'