diff --git a/src/lib/openrouter.ts b/src/lib/openrouter.ts index 38e6616..6e42403 100644 --- a/src/lib/openrouter.ts +++ b/src/lib/openrouter.ts @@ -7,6 +7,8 @@ interface OpenRouterModel { modality: string tokenizer: string instruct_type?: string + input_modalities?: string[] + output_modalities?: string[] } pricing: { prompt: string @@ -63,12 +65,15 @@ export class OpenRouterService { // 将 OpenRouter 模型转换为我们数据库的格式 transformModelForDB(model: OpenRouterModel) { + const allOutputTypes = this.determineAllOutputTypes(model) + const primaryOutputType = this.determinePrimaryOutputType(allOutputTypes) + return { modelId: model.id, name: model.name, provider: this.extractProvider(model.id), serviceProvider: 'openrouter', - outputType: 'text', + outputType: primaryOutputType, // 主要输出类型 description: model.description || null, maxTokens: model.context_length || null, inputCostPer1k: parseFloat(model.pricing.prompt) * 1000 || null, @@ -77,6 +82,9 @@ export class OpenRouterService { modality: model.architecture.modality, tokenizer: model.architecture.tokenizer, instruct_type: model.architecture.instruct_type, + input_modalities: model.architecture.input_modalities, + output_modalities: model.architecture.output_modalities, + all_output_types: allOutputTypes, // 记录所有支持的输出类型 is_moderated: model.top_provider.is_moderated, max_completion_tokens: model.top_provider.max_completion_tokens, }, @@ -88,6 +96,83 @@ export class OpenRouterService { } } + // 根据模型的架构信息确定所有支持的输出类型 + private determineAllOutputTypes(model: OpenRouterModel): string[] { + const supportedTypes: Set = new Set() + + // 优先使用 output_modalities 字段 + if (model.architecture.output_modalities && Array.isArray(model.architecture.output_modalities)) { + model.architecture.output_modalities.forEach(modality => { + supportedTypes.add(modality) + }) + } + + // 如果没有 output_modalities,则通过 modality 字段推断 + if (model.architecture.modality) { + const modality = model.architecture.modality.toLowerCase() + + // 检查 modality 字段中的输出类型指示 + if (modality.includes('->image') || (modality.includes('image') && modality.includes('->'))) { + supportedTypes.add('image') + } + + if (modality.includes('->video') || (modality.includes('video') && modality.includes('->'))) { + supportedTypes.add('video') + } + + if (modality.includes('->audio') || (modality.includes('audio') && modality.includes('->'))) { + supportedTypes.add('audio') + } + + if (modality.includes('->text') || (modality.includes('text') && modality.includes('->'))) { + supportedTypes.add('text') + } + } + + // 通过模型名称进行启发式判断 + const modelName = model.name?.toLowerCase() || '' + const modelId = model.id?.toLowerCase() || '' + + // 图像生成模型关键词 + const imageKeywords = ['dall-e', 'dalle', 'midjourney', 'stable-diffusion', 'flux', 'imagen', 'ideogram'] + if (imageKeywords.some(keyword => modelName.includes(keyword) || modelId.includes(keyword))) { + supportedTypes.add('image') + } + + // 视频生成模型关键词 + const videoKeywords = ['runway', 'gen-2', 'pika', 'video', 'sora'] + if (videoKeywords.some(keyword => modelName.includes(keyword) || modelId.includes(keyword))) { + supportedTypes.add('video') + } + + // 音频生成模型关键词 + const audioKeywords = ['whisper', 'musicgen', 'bark', 'audio', 'speech', 'tts'] + if (audioKeywords.some(keyword => modelName.includes(keyword) || modelId.includes(keyword))) { + supportedTypes.add('audio') + } + + // 如果没有找到任何输出类型,默认为 text + if (supportedTypes.size === 0) { + supportedTypes.add('text') + } + + return Array.from(supportedTypes) + } + + // 确定主要输出类型(用于数据库的 outputType 字段) + private determinePrimaryOutputType(supportedTypes: string[]): string { + // 按优先级返回主要类型 + const priority = ['image', 'video', 'audio', 'text'] + + for (const type of priority) { + if (supportedTypes.includes(type)) { + return type + } + } + + return 'text' // 默认返回 text + } + private extractProvider(modelId: string): string { // 从模型 ID 中提取提供商名称,如 "openai/gpt-4" -> "OpenAI" const providerMap: Record = {