Files
goodbuddy/src/main/agent/runtime-selection.ts
T
mesalogo 9c88ca20f3 feat: layer runtime and model selection across conversation, project and global
- Store runtime selection as { provider?, model? } layers and resolve them per
  field (conversation -> project -> global); drop the legacy auto provider.
- Migrate stored selections (database v49) and clear deleted profile ids.
- Re-resolve selections in Main per request and at queue dispatch.
- Add ProjectRuntimeSelector and a two-column RuntimeModelPicker; manage
  link opens model connections; restore per-runtime default model tabs.
- Stop duplicating trusted instructions and history across OpenCode,
  Continue and DeepSeek Harness runtimes.
- Document the layered design, global provider removal plan and planned
  purpose-specific models.
2026-09-29 21:33:06 +08:00

220 lines
6.4 KiB
TypeScript

import {
isAgentRuntimeModelProtocol,
isDeepSeekHarnessModelProfile
} from '../../shared/contracts'
import {
resolveRuntimeChoice,
type AgentRuntimeSelection,
type ResolvedRuntimeChoice,
type RuntimeResolutionOptions,
type RuntimeResolutionSettings,
type RuntimeSelectionLayer
} from '../../shared/runtime-selection-contracts'
import type {
ResolvedModelProfile,
ResolvedRuntimeSettings
} from '../runtime-settings-store'
export type SelectedRuntimeTarget =
| 'model'
| 'opencode'
| 'continue'
| 'deepseek-harness'
function requireProfile(
settings: ResolvedRuntimeSettings,
profileId: string
): ResolvedModelProfile {
const profile = settings.modelProfiles.find(
(candidate) => candidate.id === profileId
)
if (!profile) {
throw new Error('所选模型连接不存在或已被删除')
}
return profile
}
export function getConfiguredRuntimeTarget(
settings: ResolvedRuntimeSettings
): SelectedRuntimeTarget {
if (settings.provider === 'continue') {
return 'continue'
}
if (settings.provider === 'deepseek-harness') {
return 'deepseek-harness'
}
if (settings.provider === 'opencode') {
return 'opencode'
}
return 'model'
}
/** Presents resolved Main settings in the shape used by the shared resolver. */
export function runtimeResolutionSettings(
settings: ResolvedRuntimeSettings
): RuntimeResolutionSettings {
// Partial settings (early startup, narrow callers) resolve with no connections.
const modelProfiles = settings.modelProfiles ?? []
const known = (profile?: ResolvedModelProfile) =>
profile && modelProfiles.some((candidate) => candidate.id === profile.id)
? ({ kind: 'profile', profileId: profile.id } as const)
: ({ kind: 'platform' } as const)
return {
provider: settings.provider,
defaultModelProfileId: settings.defaultModelProfileId,
modelProfiles: modelProfiles.map((profile) => ({
id: profile.id,
name: profile.name,
modelName: profile.modelName,
baseUrl: profile.baseUrl,
protocol: profile.protocol,
authentication: profile.authentication,
apiKeyConfigured:
profile.authentication !== 'api-key' || Boolean(profile.apiKey)
})),
opencodeModelSource: known(settings.opencodeModelProfile),
continueModelSource: known(settings.continueModelProfile),
deepseekHarnessModelSource: known(settings.deepseekHarnessModelProfile),
opencodeBaseUrl: settings.opencodeBaseUrl
}
}
export function resolveLayeredRuntimeSelection(
settings: ResolvedRuntimeSettings,
layers: {
project?: RuntimeSelectionLayer
conversation?: RuntimeSelectionLayer
} = {},
options: RuntimeResolutionOptions = {}
): ResolvedRuntimeChoice {
return resolveRuntimeChoice(runtimeResolutionSettings(settings), layers, options)
}
/** Pins the model a Runtime selection implies so remote validation sees one concrete profile. */
export function resolveConfiguredAgentRuntimeSelection(
settings: ResolvedRuntimeSettings,
selection: AgentRuntimeSelection
): AgentRuntimeSelection {
if (
selection.provider === 'model' ||
selection.profileId ||
('runtimeConfig' in selection && selection.runtimeConfig)
) {
return selection
}
const profile =
selection.provider === 'opencode'
? settings.opencodeModelProfile
: selection.provider === 'continue'
? settings.continueModelProfile
: settings.deepseekHarnessModelProfile
return {
provider: selection.provider,
...(profile && settings.modelProfiles.some(
(candidate) => candidate.id === profile.id
)
? { profileId: profile.id }
: {})
}
}
export function applyRuntimeSelection(
settings: ResolvedRuntimeSettings,
selection: AgentRuntimeSelection
): {
settings: ResolvedRuntimeSettings
target: SelectedRuntimeTarget
} {
if (selection.provider === 'model') {
const profile = requireProfile(settings, selection.profileId ?? settings.defaultModelProfileId)
return {
target: 'model',
settings: {
...settings,
provider: 'model',
modelBaseUrl: profile.baseUrl,
modelName: profile.modelName,
modelProtocol: profile.protocol,
modelAuthentication: profile.authentication,
supportsImageInput: profile.supportsImageInput,
imageGenerationQuality:
profile.imageGenerationQuality ?? settings.imageGenerationQuality,
apiKey: profile.apiKey,
defaultModelProfileId: profile.id
}
}
}
const runtimeConfig = selection.runtimeConfig === true
const profile = selection.profileId
? requireProfile(settings, selection.profileId)
: runtimeConfig
? undefined
: selection.provider === 'opencode'
? settings.opencodeModelProfile
: selection.provider === 'continue'
? settings.continueModelProfile
: settings.deepseekHarnessModelProfile
if (selection.provider === 'opencode') {
if (profile && !isAgentRuntimeModelProtocol(profile.protocol)) {
throw new Error(
'OpenCode 独立模型连接仅支持文本对话协议,不支持图像生成协议'
)
}
if (profile && settings.opencodeBaseUrl) {
throw new Error(
'OpenCode 独立模型连接需要启用由 GoodBuddy 自动启动的本机 OpenCode'
)
}
return {
target: 'opencode',
settings: {
...settings,
provider: 'opencode',
opencodeEmbedded: !settings.opencodeBaseUrl,
opencodeModelProfile: profile
}
}
}
if (selection.provider === 'deepseek-harness') {
// DeepSeek Harness "own configuration" is the administrator-provided or fallback model.
const selectedProfile = runtimeConfig
? settings.deepseekHarnessPlatformModelProfile
: profile
if (
selectedProfile &&
!isDeepSeekHarnessModelProfile(selectedProfile)
) {
throw new Error(
'DeepSeek Harness 仅支持使用 API Key 的 OpenAI 兼容 Chat Completions 连接'
)
}
return {
target: 'deepseek-harness',
settings: {
...settings,
provider: 'deepseek-harness',
deepseekHarnessModelProfile: selectedProfile
}
}
}
if (
profile &&
!isAgentRuntimeModelProtocol(profile.protocol)
) {
throw new Error(
'Continue 独立模型连接仅支持文本对话协议,不支持图像生成协议'
)
}
return {
target: 'continue',
settings: {
...settings,
provider: 'continue',
continueModelProfile: profile
}
}
}