tasks/prd-provider-system-refactor.md
重构 AI Provider 系统,将分散在 4 个位置的注册逻辑统一到单一的 defineProvider() 定义中。保留现有的 AbstractAISDKModel 继承结构和每个 provider 独立 class 的方式,只解决注册分散的问题。
添加一个 provider 需要改 4 个地方:
1. src/shared/models/xxx.ts - Model class (继承 AbstractAISDKModel)
2. src/shared/models/index.ts - 在 400 行 switch 中加 case
3. src/renderer/.../xxx-setting-util.ts - SettingUtil class
4. src/shared/defaults.ts - 在 SystemProviders[] 加配置
问题:
- 同一个 provider 的信息分散在 4 个文件
- getModel() switch 语句 400 行,难以维护
- Model class 和 SettingUtil class 有重复逻辑(如 listModels)
- 添加/修改 provider 容易遗漏某个文件
添加一个 provider 只需 1 个文件:
src/shared/providers/
├── registry.ts # 注册中心 + getModel() 实现
├── types.ts # ProviderDefinition 类型
├── definitions/ # Provider 定义(每个 1 文件,包含所有信息)
│ ├── openai.ts # Model class + 配置 + 元数据
│ ├── claude.ts
│ ├── groq.ts
│ └── ...
└── index.ts # 导出
改变:
- 4 个文件 → 1 个 defineProvider() 定义
- getModel() 400 行 switch → ~10 行 registry lookup
- SettingUtil 合并到 provider 定义中
- SystemProviders 从 registry 生成
保留:
- AbstractAISDKModel 基类不变
- 每个 provider 独立的 Model class
- 继承 + 覆盖方法的扩展方式
defineProvider() 调用getModel() 从 400 行 switch 简化为 <20 行AbstractAISDKModel 继承结构,无需学习新模式AbstractAISDKModel 基类// src/shared/providers/types.ts
import type { ModelInterface } from '../models/types'
import type { ModelProvider, ModelProviderType, ProviderModelInfo, ProviderSettings, SessionType } from '../types'
import type { ModelDependencies } from '../types/adapters'
export interface ProviderDefinition {
// === 基本信息 (原 SystemProviders) ===
id: ModelProvider
name: string
type: ModelProviderType
urls?: {
website?: string
apiKey?: string
docs?: string
models?: string
}
defaultSettings?: ProviderSettings
// === 创建 Model 实例(合并 modelClass + buildModelOptions)===
// 直接调用 new XxxModel(...),TypeScript 自动检查构造函数参数类型
createModel: (ctx: CreateModelContext) => ModelInterface
// === SettingUtil 功能 (原 model-setting-utils/xxx.ts) ===
getDisplayName?: (modelId: string, sessionType: SessionType, providerSettings?: ProviderSettings) => string
// listModels 已在 Model class 中,不需要重复
}
export interface CreateModelContext {
sessionSettings: SessionSettings
globalSettings: Settings
providerSettings: ProviderSettings
providerBaseInfo: ProviderBaseInfo
model: ProviderModelInfo
dependencies: ModelDependencies
}
Simple Provider (Groq):
// src/shared/providers/definitions/groq.ts
import { ModelProviderEnum, ModelProviderType } from '../../types'
import Groq from './models/groq' // Model class 保持不变
export default defineProvider({
id: ModelProviderEnum.Groq,
name: 'Groq',
type: ModelProviderType.OpenAI,
urls: {
website: 'https://groq.com/',
},
defaultSettings: {
apiHost: 'https://api.groq.com/openai',
models: [
{ modelId: 'llama-3.3-70b-versatile', contextWindow: 131072, capabilities: ['tool_use'] },
],
},
// 直接创建 Model 实例,TypeScript 检查构造函数参数
createModel: (ctx) => new Groq({
apiKey: ctx.providerSettings.apiKey || '',
model: ctx.model,
temperature: ctx.sessionSettings.temperature,
topP: ctx.sessionSettings.topP,
maxOutputTokens: ctx.sessionSettings.maxTokens,
stream: ctx.sessionSettings.stream,
}, ctx.dependencies),
getDisplayName: (modelId) => `Groq API (${modelId})`,
})
Complex Provider (OpenAI):
// src/shared/providers/definitions/openai.ts
import { ModelProviderEnum, ModelProviderType } from '../../types'
import OpenAI from './models/openai'
export default defineProvider({
id: ModelProviderEnum.OpenAI,
name: 'OpenAI',
type: ModelProviderType.OpenAI,
urls: {
website: 'https://openai.com',
},
defaultSettings: {
apiHost: 'https://api.openai.com',
models: [
{ modelId: 'gpt-4o', capabilities: ['vision', 'tool_use'], contextWindow: 128000 },
{ modelId: 'o3-mini', capabilities: ['vision', 'tool_use', 'reasoning'], contextWindow: 200000 },
{ modelId: 'text-embedding-3-small', type: 'embedding' },
],
},
// 直接创建 Model 实例,TypeScript 检查构造函数参数
createModel: (ctx) => new OpenAI({
apiKey: ctx.providerSettings.apiKey || '',
apiHost: ctx.providerSettings.apiHost || 'https://api.openai.com',
model: ctx.model,
dalleStyle: ctx.sessionSettings.dalleStyle || 'vivid',
temperature: ctx.sessionSettings.temperature,
topP: ctx.sessionSettings.topP,
maxOutputTokens: ctx.sessionSettings.maxTokens,
injectDefaultMetadata: ctx.globalSettings.injectDefaultMetadata,
useProxy: false,
stream: ctx.sessionSettings.stream,
}, ctx.dependencies),
getDisplayName: (modelId, sessionType, providerSettings) => {
if (sessionType === 'picture') {
return 'OpenAI API (DALL-E-3)'
}
const nickname = providerSettings?.models?.find(m => m.modelId === modelId)?.nickname
return `OpenAI API (${nickname || modelId})`
},
})
// src/shared/providers/registry.ts
const providers = new Map<string, ProviderDefinition>()
export function defineProvider<T>(definition: ProviderDefinition<T>): ProviderDefinition<T> {
providers.set(definition.id, definition)
return definition
}
export function getProviderDefinition(id: string): ProviderDefinition | undefined {
return providers.get(id)
}
export function getAllProviders(): ProviderDefinition[] {
return Array.from(providers.values())
}
// 替代原来的 SystemProviders
export function getSystemProviders(): ProviderBaseInfo[] {
return getAllProviders().map(p => ({
id: p.id,
name: p.name,
type: p.type,
urls: p.urls,
defaultSettings: p.defaultSettings,
}))
}
// src/shared/providers/index.ts
export function getModel(
settings: SessionSettings,
globalSettings: Settings,
config: Config,
dependencies: ModelDependencies
): ModelInterface {
const provider = settings.provider
if (!provider) {
throw new Error('Model provider must not be empty.')
}
// 获取 provider 定义
const definition = getProviderDefinition(provider)
if (!definition) {
// 处理 custom provider(见 US-013)
return createCustomProviderModel(settings, globalSettings, config, dependencies)
}
// 构建 context 并创建 model
const { providerSettings, model } = resolveProviderContext(settings, globalSettings, definition)
return definition.createModel({
sessionSettings: settings,
globalSettings,
providerSettings,
providerBaseInfo: definition,
model,
dependencies,
})
}
src/shared/
├── providers/
│ ├── index.ts # 导出 getModel, registry functions
│ ├── registry.ts # defineProvider, getProviderDefinition
│ ├── types.ts # ProviderDefinition 类型
│ ├── utils.ts # resolveProviderContext, createCustomProviderModel
│ └── definitions/
│ ├── index.ts # 自动导入所有定义
│ ├── openai.ts
│ ├── claude.ts
│ ├── gemini.ts
│ ├── deepseek.ts
│ ├── azure.ts
│ ├── chatboxai.ts
│ ├── ollama.ts
│ ├── groq.ts
│ ├── perplexity.ts
│ ├── xai.ts
│ ├── mistral-ai.ts
│ ├── siliconflow.ts
│ ├── volcengine.ts
│ ├── chatglm.ts
│ ├── lmstudio.ts
│ ├── openrouter.ts
│ ├── openai-responses.ts
│ └── models/ # Model classes (从 src/shared/models/ 移动)
│ ├── abstract-ai-sdk.ts
│ ├── openai.ts
│ ├── claude.ts
│ └── ...
│
├── models/ # 保留,逐步迁移到 providers/definitions/models/
│ └── index.ts # 改为从 providers 重新导出(兼容)
│
└── defaults.ts # SystemProviders 改为从 registry 获取
Description: 创建 provider 定义的类型系统和注册中心。
Acceptance Criteria:
src/shared/providers/types.ts,定义 ProviderDefinition 接口src/shared/providers/registry.ts,实现 defineProvider(), getProviderDefinition(), getAllProviders()src/shared/providers/index.ts,导出公共 APInpm run check)Description: 基于 registry 实现简化的 getModel() 函数。
Acceptance Criteria:
src/shared/providers/index.ts 实现新的 getModel()getModel() 可以共存(渐进迁移)Description: 将 Groq 作为第一个迁移的 provider,验证新架构。
Acceptance Criteria:
src/shared/providers/definitions/groq.tssrc/shared/models/groq.ts 到 src/shared/providers/definitions/models/groq.tssrc/shared/models/index.ts 的 switch 中移除 Groq casesrc/renderer/packages/model-setting-utils/groq-setting-util.tssrc/shared/defaults.ts 的 SystemProviders 中移除 Groqnpm run test:integration)Description: 迁移其他简单的 OpenAI-compatible providers。
Acceptance Criteria:
providers/definitions/models/Description: 迁移 OpenAI provider。
Acceptance Criteria:
src/shared/providers/definitions/openai.tsopenai-setting-util.tsDescription: 迁移 Claude provider,保留 temperature/topP 约束逻辑。
Acceptance Criteria:
src/shared/providers/definitions/claude.tsclaude-setting-util.tsDescription: 迁移 Gemini provider,保留自定义 paint() 和 isSupportSystemMessage()。
Acceptance Criteria:
src/shared/providers/definitions/gemini.tsgemini-setting-util.tsDescription: 迁移 DeepSeek provider,保留 isSupportToolUse() scope 限制。
Acceptance Criteria:
src/shared/providers/definitions/deepseek.tsdeepseek-setting-util.tsDescription: 迁移 Azure provider,处理特殊的 endpoint/deployment 配置。
Acceptance Criteria:
src/shared/providers/definitions/azure.tscreateModel 正确处理 endpoint, deploymentName, apiVersionazure-setting-util.tsDescription: 迁移 ChatboxAI provider,处理 license 相关逻辑。
Acceptance Criteria:
src/shared/providers/definitions/chatboxai.tscreateModel 正确处理 licenseKey, licenseInstances, licenseDetailchatboxai-setting-util.tsDescription: 迁移 Ollama provider。
Acceptance Criteria:
src/shared/providers/definitions/ollama.tsollama-setting-util.tsDescription: 迁移 OpenAI Responses API provider。
Acceptance Criteria:
src/shared/providers/definitions/openai-responses.tsopenai-responses-setting-util.tsDescription: 迁移 CustomOpenAI, CustomClaude, CustomGemini, CustomOpenAIResponses。
Acceptance Criteria:
createCustomProviderModel() 函数处理 custom providerDescription: 移除 src/shared/models/index.ts 中的旧 switch 语句。
Acceptance Criteria:
src/shared/models/index.ts 中的 switch 语句getModel() 从 src/shared/providers 重新导出Description: 移除 model-setting-utils 目录,用 registry 替代。
Acceptance Criteria:
getModelDisplayName() 使用 registry 的 getDisplayNamegetMergeOptionGroups() 使用 Model class 的 listModels()src/renderer/packages/model-setting-utils/index.ts 改为从 registry 调用*-setting-util.ts 文件(在各 US 中逐步完成)base-config.ts(保留,因为仍被 RegistrySettingUtil 和 CustomProviderSettingUtil 使用)Description: 将 SystemProviders 改为从 registry 生成。
Acceptance Criteria:
src/shared/defaults.ts 中的 SystemProviders 改为调用 getSystemProviders()SystemProviders())Description: 更新 AGENTS.md 和添加开发者文档。
Acceptance Criteria:
AGENTS.md 中的 Provider 架构描述docs/adding-new-provider.md,包含:
getModel() 的调用方式保持不变SystemProviders 的导出保持不变getModel() 从 ~400 行减少到 <30 行*-setting-util.ts 文件(~20 个)每个 provider 迁移后:
npm run checknpm run testnpm run test:integration(如有对应 API key)Model class 是否应该移动到
决定: 是,保持定义文件和 model class 在同一目录结构下。providers/definitions/models/?
是否需要保留 src/shared/models/ 目录作为别名?
建议: 保留 index.ts 重新导出,确保外部 import 路径兼容。
Status: ALL 17 USER STORIES COMPLETED (Sat Jan 24, 2026)
| Metric | Before | After |
|---|---|---|
| Files to modify for new provider | 4 | 1 |
getModel() lines | ~400 | ~30 |
| Setting-util files | ~20 | 6 (consolidated) |
| User data migration required | - | None |
SystemProviders() instead of using as arrayimport { getModel } from '@shared/models' still works via re-exportscreateCustomProviderModel() in src/shared/providers/utils.tssrc/shared/providers/
├── index.ts # getModel(), getProviderSettings()
├── registry.ts # defineProvider(), getProviderDefinition(), getAllProviders()
├── types.ts # ProviderDefinition, CreateModelConfig
├── utils.ts # createCustomProviderModel()
└── definitions/
├── groq.ts, openai.ts, claude.ts, ... # 17 provider definitions
└── models/
├── groq.ts, openai.ts, claude.ts, ... # Model classes
└── custom-*.ts # Custom provider model classes
AGENTS.md - Updated with new provider architecturedocs/adding-new-provider.md - Step-by-step guide for adding providers