Back to Chatbox

PRD: AI Provider System Refactor

tasks/prd-provider-system-refactor.md

1.20.119.6 KB
Original Source

PRD: AI Provider System Refactor

Introduction

重构 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
- 继承 + 覆盖方法的扩展方式

Goals

  • 将 4 个注册点统一为 1 个 defineProvider() 调用
  • getModel() 从 400 行 switch 简化为 <20 行
  • 消除 SettingUtil 冗余,合并到 provider 定义
  • 保留 AbstractAISDKModel 继承结构,无需学习新模式
  • 维持 100% 向后兼容(用户设置、session 数据不变)

Non-Goals

  • 不改变 AbstractAISDKModel 基类
  • 不改变 Model class 的继承方式
  • 不新增 provider(专注于架构重构)
  • 不改变 UI 组件
  • 不改变用户设置格式

Technical Design

ProviderDefinition 类型

typescript
// 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
}

Provider 定义示例

Simple Provider (Groq)

typescript
// 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)

typescript
// 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})`
  },
})

Registry 实现

typescript
// 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,
  }))
}

简化后的 getModel()

typescript
// 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 获取

User Stories

Phase 1: 基础设施 ✅ COMPLETED

US-001: 创建 ProviderDefinition 类型和 registry ✅

Description: 创建 provider 定义的类型系统和注册中心。

Acceptance Criteria:

  • 创建 src/shared/providers/types.ts,定义 ProviderDefinition 接口
  • 创建 src/shared/providers/registry.ts,实现 defineProvider(), getProviderDefinition(), getAllProviders()
  • 创建 src/shared/providers/index.ts,导出公共 API
  • 类型检查通过 (npm run check)
  • 单元测试覆盖 registry 操作

US-002: 实现新的 getModel() 函数 ✅

Description: 基于 registry 实现简化的 getModel() 函数。

Acceptance Criteria:

  • src/shared/providers/index.ts 实现新的 getModel()
  • 支持从 registry 查找 provider definition
  • 支持 custom provider fallback(暂时调用原有逻辑)
  • 新旧 getModel() 可以共存(渐进迁移)
  • 类型检查通过

Phase 2: 迁移 Providers ✅ COMPLETED

US-003: 迁移 Groq provider(验证方案) ✅

Description: 将 Groq 作为第一个迁移的 provider,验证新架构。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/groq.ts
  • 移动 src/shared/models/groq.tssrc/shared/providers/definitions/models/groq.ts
  • src/shared/models/index.ts 的 switch 中移除 Groq case
  • 删除 src/renderer/packages/model-setting-utils/groq-setting-util.ts
  • src/shared/defaults.ts 的 SystemProviders 中移除 Groq
  • 所有现有功能正常工作
  • 集成测试通过 (npm run test:integration)
  • 类型检查通过

US-004: 迁移简单 providers ✅

Description: 迁移其他简单的 OpenAI-compatible providers。

Acceptance Criteria:

  • 迁移: Perplexity, XAI, MistralAI, SiliconFlow, VolcEngine, ChatGLM, LMStudio, OpenRouter
  • 每个 provider 创建对应的 definition 文件
  • 移动 Model class 到 providers/definitions/models/
  • 删除对应的 setting-util 文件
  • 从 SystemProviders 和 getModel() switch 中移除
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-005: 迁移 OpenAI provider ✅

Description: 迁移 OpenAI provider。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/openai.ts
  • 移动 Model class
  • 处理 embedding、image generation 能力
  • 删除 openai-setting-util.ts
  • 所有现有功能正常工作(包括 DALL-E)
  • 集成测试通过
  • 类型检查通过

US-006: 迁移 Claude provider ✅

Description: 迁移 Claude provider,保留 temperature/topP 约束逻辑。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/claude.ts
  • 移动 Model class(保留 getCallSettings 中的 temperature XOR topP ��辑)
  • 删除 claude-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-007: 迁移 Gemini provider ✅

Description: 迁移 Gemini provider,保留自定义 paint() 和 isSupportSystemMessage()。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/gemini.ts
  • 移动 Model class(保留 paint()、isSupportSystemMessage()、safety settings)
  • 删除 gemini-setting-util.ts
  • 所有现有功能正常工作(包括图片生成)
  • 集成测试通过
  • 类型检查通过

US-008: 迁移 DeepSeek provider ✅

Description: 迁移 DeepSeek provider,保留 isSupportToolUse() scope 限制。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/deepseek.ts
  • 移动 Model class(保留 isSupportToolUse 的 v3/r1 scope 限制)
  • 删除 deepseek-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-009: 迁移 Azure provider ✅

Description: 迁移 Azure provider,处理特殊的 endpoint/deployment 配置。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/azure.ts
  • 移动 Model class
  • createModel 正确处理 endpoint, deploymentName, apiVersion
  • 删除 azure-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-010: 迁移 ChatboxAI provider ✅

Description: 迁移 ChatboxAI provider,处理 license 相关逻辑。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/chatboxai.ts
  • 移动 Model class
  • createModel 正确处理 licenseKey, licenseInstances, licenseDetail
  • 删除 chatboxai-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-011: 迁移 Ollama provider ✅

Description: 迁移 Ollama provider。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/ollama.ts
  • 移动 Model class
  • 删除 ollama-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-012: 迁移 OpenAI Responses provider ✅

Description: 迁移 OpenAI Responses API provider。

Acceptance Criteria:

  • 创建 src/shared/providers/definitions/openai-responses.ts
  • 移动 Model class
  • 删除 openai-responses-setting-util.ts
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

US-013: 迁移 Custom providers ✅

Description: 迁移 CustomOpenAI, CustomClaude, CustomGemini, CustomOpenAIResponses。

Acceptance Criteria:

  • 实现 createCustomProviderModel() 函数处理 custom provider
  • Custom provider 根据 type 字段选择对应的 Model class
  • 移动 custom Model classes
  • 删除 custom setting-util 文件
  • 所有现有功能正常工作
  • 集成测试通过
  • 类型检查通过

Phase 3: 清理和兼容 ✅ COMPLETED

US-014: 清理旧的 getModel() switch ✅

Description: 移除 src/shared/models/index.ts 中的旧 switch 语句。

Acceptance Criteria:

  • 所有 provider 已迁移到新架构
  • 删除 src/shared/models/index.ts 中的 switch 语句
  • getModel()src/shared/providers 重新导出
  • 保持向后兼容(import 路径不变)
  • 类型检查通过

US-015: 清理 SettingUtil 系统 ✅

Description: 移除 model-setting-utils 目录,用 registry 替代。

Acceptance Criteria:

  • 实现 getModelDisplayName() 使用 registry 的 getDisplayName
  • 实现 getMergeOptionGroups() 使用 Model class 的 listModels()
  • src/renderer/packages/model-setting-utils/index.ts 改为从 registry 调用
  • 删除所有 *-setting-util.ts 文件(在各 US 中逐步完成)
  • 删除 base-config.ts(保留,因为仍被 RegistrySettingUtil 和 CustomProviderSettingUtil 使用)
  • 所有 UI 功能正常工作
  • 类型检查通过

US-016: 清理 SystemProviders ✅

Description: 将 SystemProviders 改为从 registry 生成。

Acceptance Criteria:

  • src/shared/defaults.ts 中的 SystemProviders 改为调用 getSystemProviders()
  • 保持向后兼容(导出名称不变,但调用方式改为 SystemProviders()
  • 所有使用 SystemProviders 的代码正常工作
  • 类型检查通过

US-017: 更新文档 ✅

Description: 更新 AGENTS.md 和添加开发者文档。

Acceptance Criteria:

  • 更新 AGENTS.md 中的 Provider 架构描述
  • 创建 docs/adding-new-provider.md,包含:
    • 添加新 provider 的步骤
    • ProviderDefinition 字段说明
    • 示例代码
  • 类型检查通过

Functional Requirements

  • FR-1: 所有 17+ 内置 provider 在重构后功能完全一致
  • FR-2: Custom provider 支持所有现有配置选项(apiPath, useProxy 等)
  • FR-3: 用户设置(config.json)无需迁移
  • FR-4: 现有 session 数据完全兼容
  • FR-5: getModel() 的调用方式保持不变
  • FR-6: SystemProviders 的导出保持不变
  • FR-7: UI 组件无需修改

Success Metrics

  • getModel() 从 ~400 行减少到 <30 行
  • 添加新 provider: 从改 4 个文件 → 改 1 个文件
  • 删除所有 *-setting-util.ts 文件(~20 个)
  • 零用户影响(无需数据迁移)
  • 测试覆盖率保持不变

Migration Strategy

  1. Phase 1: 创建新架构,新旧共存
  2. Phase 2: 逐个迁移 provider,每个 PR 一个 provider
  3. Phase 3: 所有 provider 迁移完成后,删除旧代码

每个 provider 迁移后:

  • 运行 npm run check
  • 运行 npm run test
  • 运行 npm run test:integration(如有对应 API key)

Open Questions

  1. Model class 是否应该移动到 providers/definitions/models/ 决定: 是,保持定义文件和 model class 在同一目录结构下。

  2. 是否需要保留 src/shared/models/ 目录作为别名? 建议: 保留 index.ts 重新导出,确保外部 import 路径兼容。


🎉 Completion Summary

Status: ALL 17 USER STORIES COMPLETED (Sat Jan 24, 2026)

Results Achieved

MetricBeforeAfter
Files to modify for new provider41
getModel() lines~400~30
Setting-util files~206 (consolidated)
User data migration required-None

Key Implementation Notes

  1. SystemProviders is now a function: Call as SystemProviders() instead of using as array
  2. base-config.ts retained: Still used by RegistrySettingUtil and CustomProviderSettingUtil for shared logic
  3. Backward compatible imports: import { getModel } from '@shared/models' still works via re-exports
  4. Custom providers: Handled via createCustomProviderModel() in src/shared/providers/utils.ts

Files Structure After Refactor

src/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

Documentation

  • AGENTS.md - Updated with new provider architecture
  • docs/adding-new-provider.md - Step-by-step guide for adding providers