Files
cherry-studio/src/main/apiServer/utils/index.ts
T
Pleasure1234 cb12bb5137 fix: v2 merge error (#10658)
* fix: v2 merge error

* fix: review improve

* fix: ci error

* fix: review

* Update index.tsx

* Update index.tsx
2025-10-13 09:24:32 +08:00

282 lines
8.0 KiB
TypeScript

import { cacheService } from '@data/CacheService'
import { loggerService } from '@logger'
import { reduxService } from '@main/services/ReduxService'
import type { ApiModel, Model, Provider } from '@types'
const logger = loggerService.withContext('ApiServerUtils')
// Cache configuration
const PROVIDERS_CACHE_KEY = 'api-server:providers'
const PROVIDERS_CACHE_TTL = 10 * 1000 // 10 seconds
export async function getAvailableProviders(): Promise<Provider[]> {
try {
// Try to get from cache first (faster)
const cachedSupportedProviders = cacheService.get<Provider[]>(PROVIDERS_CACHE_KEY)
if (cachedSupportedProviders && cachedSupportedProviders.length > 0) {
logger.debug('Providers resolved from cache', {
count: cachedSupportedProviders.length
})
return cachedSupportedProviders
}
// If cache is not available, get fresh data from Redux
const providers = await reduxService.select('state.llm.providers')
if (!providers || !Array.isArray(providers)) {
logger.warn('No providers found in Redux store')
return []
}
// Support OpenAI and Anthropic type providers for API server
const supportedProviders = providers.filter(
(p: Provider) => p.enabled && (p.type === 'openai' || p.type === 'anthropic')
)
// Cache the filtered results
cacheService.set(PROVIDERS_CACHE_KEY, supportedProviders, PROVIDERS_CACHE_TTL)
logger.info('Providers filtered', {
supported: supportedProviders.length,
total: providers.length
})
return supportedProviders
} catch (error: any) {
logger.error('Failed to get providers from Redux store', { error })
return []
}
}
export async function listAllAvailableModels(providers?: Provider[]): Promise<Model[]> {
try {
if (!providers) {
providers = await getAvailableProviders()
}
return providers.map((p: Provider) => p.models || []).flat()
} catch (error: any) {
logger.error('Failed to list available models', { error })
return []
}
}
export async function getProviderByModel(model: string): Promise<Provider | undefined> {
try {
if (!model || typeof model !== 'string') {
logger.warn('Invalid model parameter', { model })
return undefined
}
// Validate model format first
if (!model.includes(':')) {
logger.warn('Invalid model format missing separator', { model })
return undefined
}
const providers = await getAvailableProviders()
const modelInfo = model.split(':')
if (modelInfo.length < 2 || modelInfo[0].length === 0 || modelInfo[1].length === 0) {
logger.warn('Invalid model format with empty parts', { model })
return undefined
}
const providerId = modelInfo[0]
const provider = providers.find((p: Provider) => p.id === providerId)
if (!provider) {
logger.warn('Provider not found for model', {
providerId,
available: providers.map((p) => p.id)
})
return undefined
}
logger.debug('Provider resolved for model', { providerId, model })
return provider
} catch (error: any) {
logger.error('Failed to get provider by model', { error, model })
return undefined
}
}
export function getRealProviderModel(modelStr: string): string {
return modelStr.split(':').slice(1).join(':')
}
export interface ModelValidationError {
type: 'invalid_format' | 'provider_not_found' | 'model_not_available' | 'unsupported_provider_type'
message: string
code: string
}
export async function validateModelId(model: string): Promise<{
valid: boolean
error?: ModelValidationError
provider?: Provider
modelId?: string
}> {
try {
if (!model || typeof model !== 'string') {
return {
valid: false,
error: {
type: 'invalid_format',
message: 'Model must be a non-empty string',
code: 'invalid_model_parameter'
}
}
}
if (!model.includes(':')) {
return {
valid: false,
error: {
type: 'invalid_format',
message: "Invalid model format. Expected format: 'provider:model_id' (e.g., 'my-openai:gpt-4')",
code: 'invalid_model_format'
}
}
}
const modelInfo = model.split(':')
if (modelInfo.length < 2 || modelInfo[0].length === 0 || modelInfo[1].length === 0) {
return {
valid: false,
error: {
type: 'invalid_format',
message: "Invalid model format. Both provider and model_id must be non-empty. Expected: 'provider:model_id'",
code: 'invalid_model_format'
}
}
}
const providerId = modelInfo[0]
const modelId = getRealProviderModel(model)
const provider = await getProviderByModel(model)
if (!provider) {
return {
valid: false,
error: {
type: 'provider_not_found',
message: `Provider '${providerId}' not found, not enabled, or not supported. Only OpenAI providers are currently supported.`,
code: 'provider_not_found'
}
}
}
// Check if model exists in provider
const modelExists = provider.models?.some((m) => m.id === modelId)
if (!modelExists) {
const availableModels = provider.models?.map((m) => m.id).join(', ') || 'none'
return {
valid: false,
error: {
type: 'model_not_available',
message: `Model '${modelId}' not available in provider '${providerId}'. Available models: ${availableModels}`,
code: 'model_not_available'
}
}
}
return {
valid: true,
provider,
modelId
}
} catch (error: any) {
logger.error('Error validating model ID', { error, model })
return {
valid: false,
error: {
type: 'invalid_format',
message: 'Failed to validate model ID',
code: 'validation_error'
}
}
}
}
export function transformModelToOpenAI(model: Model, provider?: Provider): ApiModel {
const providerDisplayName = provider?.name
return {
id: `${model.provider}:${model.id}`,
object: 'model',
name: model.name,
created: Math.floor(Date.now() / 1000),
owned_by: model.owned_by || providerDisplayName || model.provider,
provider: model.provider,
provider_name: providerDisplayName,
provider_type: provider?.type,
provider_model_id: model.id
}
}
export async function getProviderById(providerId: string): Promise<Provider | undefined> {
try {
if (!providerId || typeof providerId !== 'string') {
logger.warn('Invalid provider ID parameter', { providerId })
return undefined
}
const providers = await getAvailableProviders()
const provider = providers.find((p: Provider) => p.id === providerId)
if (!provider) {
logger.warn('Provider not found by ID', {
providerId,
available: providers.map((p) => p.id)
})
return undefined
}
logger.debug('Provider found by ID', { providerId })
return provider
} catch (error: any) {
logger.error('Failed to get provider by ID', { error, providerId })
return undefined
}
}
export function validateProvider(provider: Provider): boolean {
try {
if (!provider) {
return false
}
// Check required fields
if (!provider.id || !provider.type || !provider.apiKey || !provider.apiHost) {
logger.warn('Provider missing required fields', {
id: !!provider.id,
type: !!provider.type,
apiKey: !!provider.apiKey,
apiHost: !!provider.apiHost
})
return false
}
// Check if provider is enabled
if (!provider.enabled) {
logger.debug('Provider is disabled', { providerId: provider.id })
return false
}
// Support OpenAI and Anthropic type providers
if (provider.type !== 'openai' && provider.type !== 'anthropic') {
logger.debug('Provider type not supported', {
providerId: provider.id,
providerType: provider.type
})
return false
}
return true
} catch (error: any) {
logger.error('Error validating provider', {
error,
providerId: provider?.id
})
return false
}
}