feat(aiCore): enhance dynamic provider registration and refactor HubProvider

- Introduced dynamic provider registration functionality, allowing for flexible management of providers through a new registry system.
- Refactored HubProvider to streamline model resolution and improve error handling for unsupported models.
- Added utility functions for managing dynamic providers, including registration, cleanup, and alias resolution.
- Updated index exports to include new dynamic provider APIs, enhancing overall usability and integration.
- Removed outdated provider files and simplified the provider management structure for better maintainability.
This commit is contained in:
MyPrototypeWhat
2025-08-26 16:17:01 +08:00
parent 53dcda6942
commit 84eef25ff9
18 changed files with 551 additions and 256 deletions
@@ -284,6 +284,12 @@ export class AiSdkToChunkAdapter {
}
})
break
case 'abort':
this.onChunk({
type: ChunkType.ERROR,
error: new DOMException('Request was aborted', 'AbortError')
})
break
case 'error':
this.onChunk({
type: ChunkType.ERROR,
@@ -6,7 +6,7 @@ import {
InvokeModelWithResponseStreamCommand
} from '@aws-sdk/client-bedrock-runtime'
import { loggerService } from '@logger'
import { GenericChunk } from '@renderer/aiCore/middleware/schemas'
import { GenericChunk } from '@renderer/aiCore/legacy/middleware/schemas'
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
import { findTokenLimit, isReasoningModel } from '@renderer/config/models'
import {
@@ -1,109 +1,124 @@
import { AiCore, ProviderConfigFactory, type ProviderId, type ProviderSettingsMap } from '@cherrystudio/ai-core'
import { isDedicatedImageGenerationModel } from '@renderer/config/models'
import { createVertexProvider, isVertexAIConfigured, isVertexProvider } from '@renderer/hooks/useVertexAI'
import { getProviderByModel } from '@renderer/services/AssistantService'
import type { Model, Provider } from '@renderer/types'
import { formatApiHost } from '@renderer/utils/api'
import { cloneDeep } from 'lodash'
import { createAihubmixProvider } from './aihubmix'
import { aihubmixProviderCreator, newApiResolverCreator } from './config'
import { getAiSdkProviderId } from './factory'
export function getActualProvider(model: Model): Provider {
const provider = getProviderByModel(model)
// 如果是 vertexai 类型且没有 googleCredentials,转换为 VertexProvider
let actualProvider = cloneDeep(provider)
/**
* 处理特殊provider的转换逻辑
*/
function handleSpecialProviders(model: Model, provider: Provider): Provider {
if (provider.type === 'vertexai' && !isVertexProvider(provider)) {
if (!isVertexAIConfigured()) {
throw new Error('VertexAI is not configured. Please configure project, location and service account credentials.')
}
actualProvider = createVertexProvider(provider)
return createVertexProvider(provider)
}
if (provider.id === 'aihubmix') {
actualProvider = createAihubmixProvider(model, actualProvider)
return aihubmixProviderCreator(model, provider)
}
if (actualProvider.type === 'gemini') {
actualProvider.apiHost = formatApiHost(actualProvider.apiHost, 'v1beta')
if (provider.id === 'newapi') {
return newApiResolverCreator(model, provider)
}
return provider
}
/**
* 格式化provider的API Host
*/
function formatProviderApiHost(provider: Provider): Provider {
const formatted = { ...provider }
if (formatted.type === 'gemini') {
formatted.apiHost = formatApiHost(formatted.apiHost, 'v1beta')
} else {
actualProvider.apiHost = formatApiHost(actualProvider.apiHost)
formatted.apiHost = formatApiHost(formatted.apiHost)
}
return formatted
}
/**
* 获取实际的Provider配置
* 简化版:将逻辑分解为小函数
*/
export function getActualProvider(model: Model): Provider {
const baseProvider = getProviderByModel(model)
// 按顺序处理各种转换
let actualProvider = cloneDeep(baseProvider)
actualProvider = handleSpecialProviders(model, actualProvider)
actualProvider = formatProviderApiHost(actualProvider)
return actualProvider
}
/**
* 将 Provider 配置转换为新 AI SDK 格式
* 简化版:利用新的别名映射系统
*/
export function providerToAiSdkConfig(actualProvider: Provider): {
providerId: ProviderId | 'openai-compatible'
options: ProviderSettingsMap[keyof ProviderSettingsMap]
} {
const aiSdkProviderId = getAiSdkProviderId(actualProvider)
const actualProviderType = actualProvider.type
const openaiResponseOptions =
actualProviderType === 'openai-response'
? {
mode: 'responses'
}
: aiSdkProviderId === 'openai'
? {
mode: 'chat'
}
: undefined
console.log('openaiResponseOptions', openaiResponseOptions)
console.log('actualProvider', actualProvider)
console.log('aiSdkProviderId', aiSdkProviderId)
if (AiCore.isSupported(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
const options = ProviderConfigFactory.fromProvider(
aiSdkProviderId,
{
baseURL: actualProvider.apiHost,
apiKey: actualProvider.apiKey
},
{ ...openaiResponseOptions, headers: actualProvider.extra_headers }
)
// 构建基础配置
const baseConfig = {
baseURL: actualProvider.apiHost,
apiKey: actualProvider.apiKey
}
// 处理OpenAI模式(简化逻辑)
const extraOptions: any = {}
if (actualProvider.type === 'openai-response') {
extraOptions.mode = 'responses'
} else if (aiSdkProviderId === 'openai') {
extraOptions.mode = 'chat'
}
// 添加额外headers
if (actualProvider.extra_headers) {
extraOptions.headers = actualProvider.extra_headers
}
// 如果AI SDK支持该provider,使用原生配置
if (AiCore.isSupported(aiSdkProviderId) && aiSdkProviderId !== 'openai-compatible') {
const options = ProviderConfigFactory.fromProvider(aiSdkProviderId, baseConfig, extraOptions)
return {
providerId: aiSdkProviderId as ProviderId,
options
}
} else {
console.log(`Using openai-compatible fallback for provider: ${actualProvider.type}`)
const options = ProviderConfigFactory.createOpenAICompatible(actualProvider.apiHost, actualProvider.apiKey)
}
return {
providerId: 'openai-compatible',
options: {
...options,
name: actualProvider.id
}
// 否则fallback到openai-compatible
const options = ProviderConfigFactory.createOpenAICompatible(baseConfig.baseURL, baseConfig.apiKey)
return {
providerId: 'openai-compatible',
options: {
...options,
name: actualProvider.id,
...extraOptions
}
}
}
/**
* 检查是否支持使用新的AI SDK
* 简化版:利用新的别名映射和动态provider系统
*/
export function isModernSdkSupported(provider: Provider, model?: Model): boolean {
// 目前支持主要的providers
const supportedProviders = ['openai', 'anthropic', 'gemini', 'azure-openai', 'vertexai']
// 检查provider类型
if (!supportedProviders.includes(provider.type)) {
return false
}
// 对于 vertexai,检查配置是否完整
export function isModernSdkSupported(provider: Provider): boolean {
// 特殊检查:vertexai需要配置完整
if (provider.type === 'vertexai' && !isVertexAIConfigured()) {
return false
}
// 图像生成模型现在支持新的 AI SDK
// (但需要确保 provider 是支持的
// 使用getAiSdkProviderId获取映射后的providerId,然后检查AI SDK是否支持
const aiSdkProviderId = getAiSdkProviderId(provider)
if (model && isDedicatedImageGenerationModel(model)) {
return true
}
return true
// 如果映射到了支持的provider,则支持现代SDK
return AiCore.isSupported(aiSdkProviderId)
}
@@ -1,56 +0,0 @@
import { ProviderId } from '@cherrystudio/ai-core/types'
import { isOpenAIModel } from '@renderer/config/models'
import { Model, Provider } from '@renderer/types'
export function getAiSdkProviderIdForAihubmix(model: Model): ProviderId | 'openai-compatible' {
console.log('getAiSdkProviderIdForAihubmix', model)
const id = model.id.toLowerCase()
if (id.startsWith('claude')) {
return 'anthropic'
}
// TODO:暂时注释,不清楚为什么排除,webSearch时会导致gemini模型走openai的逻辑
if ((id.startsWith('gemini') || id.startsWith('imagen')) && !id.endsWith('-nothink') && !id.endsWith('-search')) {
return 'google'
}
if (isOpenAIModel(model)) {
return 'openai'
}
return 'openai-compatible'
}
export function createAihubmixProvider(model: Model, provider: Provider): Provider {
const providerId = getAiSdkProviderIdForAihubmix(model)
provider = {
...provider,
extra_headers: {
...provider.extra_headers,
'APP-Code': 'MLTG2087'
}
}
if (providerId === 'google') {
return {
...provider,
type: 'gemini',
apiHost: 'https://aihubmix.com/gemini'
}
}
if (providerId === 'openai') {
return {
...provider,
type: 'openai-response'
}
}
if (providerId === 'anthropic') {
return {
...provider,
type: 'anthropic'
}
}
return provider
}
@@ -0,0 +1,57 @@
/**
* AiHubMix规则集
*/
import { isOpenAIModel } from '@renderer/config/models'
import { Provider } from '@renderer/types'
import { startsWith } from './helper'
import { provider2Provider } from './helper'
import type { ModelRule } from './types'
const extraProviderConfig = (provider: Provider) => {
return {
...provider,
extra_headers: {
...provider.extra_headers,
'APP-Code': 'MLTG2087'
}
}
}
const AIHUBMIX_RULES: ModelRule[] = [
{
name: 'claude',
match: startsWith('claude'),
provider: (provider: Provider) => {
return extraProviderConfig({
...provider,
type: 'anthropic'
})
}
},
{
name: 'gemini',
match: (model) =>
(startsWith('gemini')(model) || startsWith('imagen')(model)) &&
!model.id.endsWith('-nothink') &&
!model.id.endsWith('-search'),
provider: (provider: Provider) => {
return extraProviderConfig({
...provider,
apiHost: 'https://aihubmix.com/gemini'
})
}
},
{
name: 'openai',
match: isOpenAIModel,
provider: (provider: Provider) => {
return extraProviderConfig({
...provider,
type: 'openai-response'
})
}
}
]
export const aihubmixProviderCreator = provider2Provider.bind(null, AIHUBMIX_RULES)
@@ -0,0 +1,22 @@
import type { Model, Provider } from '@renderer/types'
import type { ModelRule } from './types'
export const startsWith = (prefix: string) => (model: Model) => model.id.toLowerCase().startsWith(prefix.toLowerCase())
export const endpointIs = (type: string) => (model: Model) => model.endpoint_type === type
/**
* 解析模型对应的Provider ID
* @param model 模型对象
* @param rules 匹配规则数组
* @param fallback 默认fallback的providerId
* @returns 解析出的providerId
*/
export function provider2Provider(rules: ModelRule[], model: Model, provider: Provider): Provider {
for (const rule of rules) {
if (rule.match(model)) {
return rule.provider(provider)
}
}
return provider
}
@@ -0,0 +1,16 @@
// /**
// * Provider解析规则模块导出
// */
// // 导出类型
// export type { ModelRule } from './types'
// // 导出匹配函数和解析器
// export { endpointIs, resolveProvider, startsWith } from './helper'
// // 导出规则集
// export { AIHUBMIX_RULES } from './aihubmix'
// export { NEWAPI_RULES } from './newApi'
export { aihubmixProviderCreator } from './aihubmix'
export { newApiResolverCreator } from './newApi'
@@ -0,0 +1,52 @@
/**
* NewAPI规则集
*/
import { Provider } from '@renderer/types'
import { endpointIs, provider2Provider } from './helper'
import type { ModelRule } from './types'
const NEWAPI_RULES: ModelRule[] = [
{
name: 'anthropic',
match: endpointIs('anthropic'),
provider: (provider: Provider) => {
return {
...provider,
type: 'anthropic'
}
}
},
{
name: 'gemini',
match: endpointIs('gemini'),
provider: (provider: Provider) => {
return {
...provider,
type: 'gemini'
}
}
},
{
name: 'openai-response',
match: endpointIs('openai-response'),
provider: (provider: Provider) => {
return {
...provider,
type: 'openai-response'
}
}
},
{
name: 'openai',
match: (model) => endpointIs('openai')(model) || endpointIs('image-generation')(model),
provider: (provider: Provider) => {
return {
...provider,
type: 'openai'
}
}
}
]
export const newApiResolverCreator = provider2Provider.bind(null, NEWAPI_RULES)
@@ -0,0 +1,7 @@
import type { Model, Provider } from '@renderer/types'
export interface ModelRule {
name: string
match: (model: Model) => boolean
provider: (provider: Provider) => Provider
}
+64 -38
View File
@@ -1,49 +1,75 @@
import { AiCore, type ProviderId } from '@cherrystudio/ai-core'
import { AiCore, getProviderMapping, type ProviderId } from '@cherrystudio/ai-core'
import { loggerService } from '@logger'
import { Provider } from '@renderer/types'
// TODO
// 初始化新的Provider注册系统
// initializeNewProviders()
import { initializeNewProviders } from './providerConfigs'
// 静态Provider映射 - 核心providers
const logger = loggerService.withContext('ProviderFactory')
/**
* 初始化动态Provider系统
* 在模块加载时自动注册新的providers
*/
;(async () => {
try {
await initializeNewProviders()
} catch (error) {
logger.warn('Failed to initialize new providers:', error as Error)
}
})()
/**
* 静态Provider映射表
* 处理Cherry Studio特有的provider ID到AI SDK标准ID的映射
*/
const STATIC_PROVIDER_MAPPING: Record<string, ProviderId> = {
// anthropic: 'anthropic',
gemini: 'google',
'azure-openai': 'azure',
'openai-response': 'openai',
grok: 'xai'
gemini: 'google', // Google Gemini -> google
'azure-openai': 'azure', // Azure OpenAI -> azure
'openai-response': 'openai', // OpenAI Responses -> openai
grok: 'xai' // Grok -> xai
}
/**
* 尝试解析provider标识符(支持静态映射和动态映射)
*/
function tryResolveProviderId(identifier: string): ProviderId | null {
// 1. 检查静态映射
const staticMapping = STATIC_PROVIDER_MAPPING[identifier]
if (staticMapping) {
return staticMapping
}
// 2. 检查动态映射
const dynamicMapping = getProviderMapping(identifier)
if (dynamicMapping && dynamicMapping !== identifier) {
return dynamicMapping as ProviderId
}
// 3. 检查AiCore是否直接支持
if (AiCore.isSupported(identifier)) {
return identifier as ProviderId
}
return null
}
/**
* 获取AI SDK Provider ID
* 简化版:减少重复逻辑,利用通用解析函数
*/
export function getAiSdkProviderId(provider: Provider): ProviderId | 'openai-compatible' {
// 1. 首先检查静态映射
const staticProviderId = STATIC_PROVIDER_MAPPING[provider.id]
if (staticProviderId) {
return staticProviderId
}
// TODO
// 2. 检查动态注册的provider映射(使用aiCore的函数)
// const dynamicProviderId = getProviderMapping(provider.id)
// if (dynamicProviderId) {
// return dynamicProviderId as ProviderId
// }
// 3. 检查provider.type的静态映射
const staticProviderType = STATIC_PROVIDER_MAPPING[provider.type]
if (staticProviderType) {
return staticProviderType
}
// TODO
// 4. 检查provider.type的动态映射
// const dynamicProviderType = getProviderMapping(provider.type)
// if (dynamicProviderType) {
// return dynamicProviderType as ProviderId
// }
// 5. 检查AiCore是否直接支持
if (AiCore.isSupported(provider.id)) {
return provider.id as ProviderId
// 1. 尝试解析provider.id
const resolvedFromId = tryResolveProviderId(provider.id)
if (resolvedFromId) {
return resolvedFromId
}
// 6. 最后的fallback
// 2. 尝试解析provider.type
const resolvedFromType = tryResolveProviderId(provider.type)
if (resolvedFromType) {
return resolvedFromType
}
// 3. 最后的fallback(通常会成为openai-compatible
return provider.id as ProviderId
}
@@ -1,4 +1,4 @@
import { type ProviderConfig } from '@cherrystudio/ai-core'
import { type ProviderConfig, registerMultipleProviders } from '@cherrystudio/ai-core'
import { loggerService } from '@logger'
const logger = loggerService.withContext('ProviderConfigs')
@@ -43,19 +43,29 @@ export const NEW_PROVIDER_CONFIGS: (ProviderConfig & {
}
] as const
// TODO
// /**
// * 初始化新的Providers
// * 使用aiCore的动态注册功能
// */
// export async function initializeNewProviders(): Promise<void> {
// try {
// const successCount = registerMultipleProviders(NEW_PROVIDER_CONFIGS)
/**
* 初始化新的Providers
* 使用aiCore的动态注册功能
*/
export async function initializeNewProviders(): Promise<void> {
try {
logger.info('Starting to register new providers', {
providerCount: NEW_PROVIDER_CONFIGS.length,
providerIds: NEW_PROVIDER_CONFIGS.map((p) => p.id)
})
// if (successCount < NEW_PROVIDER_CONFIGS.length) {
// logger.warn('Some providers failed to register. Check previous error logs.')
// }
// } catch (error) {
// logger.error('Failed to initialize new providers:', error as Error)
// }
// }
const successCount = registerMultipleProviders(NEW_PROVIDER_CONFIGS)
logger.info('Provider registration completed', {
successCount,
totalCount: NEW_PROVIDER_CONFIGS.length,
failedCount: NEW_PROVIDER_CONFIGS.length - successCount
})
if (successCount < NEW_PROVIDER_CONFIGS.length) {
logger.warn('Some providers failed to register. Check previous error logs.')
}
} catch (error) {
logger.error('Failed to initialize new providers:', error as Error)
}
}
+1 -5
View File
@@ -28,7 +28,6 @@ export function buildProviderOptions(
}
): Record<string, any> {
const providerId = getAiSdkProviderId(actualProvider)
// 构建 provider 特定的选项
let providerSpecificOptions: Record<string, any> = {}
@@ -37,11 +36,8 @@ export function buildProviderOptions(
case 'openai':
case 'azure':
providerSpecificOptions = {
...buildOpenAIProviderOptions(assistant, model, capabilities),
// 函数内有对于真实provider.id的判断,应该不会影响原生provider
...buildGenericProviderOptions(assistant, model, capabilities)
...buildOpenAIProviderOptions(assistant, model, capabilities)
}
break
case 'anthropic':