Model discovery currently assumes every provider exposes all models via
GET {baseURL}/models. Providers like Alibaba Cloud Bailian serve video,
3D and some image models through dedicated endpoints, so 21 models across
5 families (HappyHorse / Wan / Kling / PixVerse / Vidu / Tripo) are never
discoverable - a functional blocker for a video creation workbench.
This introduces a ModelDiscovery plugin interface with a registry that
matches vendor plugins by base URL and falls back to the standard OpenAI
implementation, keeping core discovery logic vendor-agnostic.
- add provider.ModelDiscovery interface and plugin registry
- add openai plugin (standard behaviour, default fallback)
- add bailian plugin (standard + 21 extended multimodal models)
- gate behind ENABLE_PROVIDER_PLUGINS, default false
- register plugins lazily from service layer to avoid import cycle
Verified locally: flag off -> 238 models (unchanged); flag on -> 257
models (238 + 19 net new after dedup). OpenAI and Gemini channels
unaffected in both modes.
139 lines
3.5 KiB
Go
139 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/provider"
|
|
"infinite-canvas/backend/internal/provider/bailian"
|
|
"infinite-canvas/backend/internal/provider/openai"
|
|
)
|
|
|
|
var (
|
|
pluginsInitialized bool
|
|
pluginsInitMutex sync.Mutex
|
|
)
|
|
|
|
// initPlugins 初始化并注册所有插件(只执行一次)
|
|
func initPlugins() {
|
|
pluginsInitMutex.Lock()
|
|
defer pluginsInitMutex.Unlock()
|
|
|
|
if pluginsInitialized {
|
|
return
|
|
}
|
|
|
|
// 注册 OpenAI 标准插件(默认)
|
|
provider.Register(openai.New())
|
|
|
|
// 注册阿里云百炼插件
|
|
provider.Register(bailian.New())
|
|
|
|
// 未来可以添加更多插件:
|
|
// provider.Register(gemini.New())
|
|
// provider.Register(anthropic.New())
|
|
|
|
pluginsInitialized = true
|
|
}
|
|
|
|
// FetchChannelModelCatalogWithPlugin 使用插件机制拉取模型(新实现)
|
|
func (s *Service) FetchChannelModelCatalogWithPlugin(ctx context.Context, actor *model.User, input ChannelModelsRequest) ([]ChannelModelCatalogItem, error) {
|
|
// 确保插件已初始化
|
|
initPlugins()
|
|
|
|
// 验证基本参数
|
|
if actor == nil || strings.TrimSpace(actor.ID) == "" {
|
|
return nil, Unauthorized("请先登录")
|
|
}
|
|
baseURL := strings.TrimRight(strings.TrimSpace(input.BaseURL), "/")
|
|
apiKey := strings.TrimSpace(input.APIKey)
|
|
if baseURL == "" {
|
|
return nil, BadAuthRequest("请填写 Base URL")
|
|
}
|
|
if apiKey == "" {
|
|
return nil, BadAuthRequest("请填写 API Key")
|
|
}
|
|
|
|
// 1. 匹配插件
|
|
headers := s.headersToMap(input.Headers)
|
|
discovery := provider.MatchProvider(baseURL, headers)
|
|
if discovery == nil {
|
|
return nil, fmt.Errorf("未找到匹配的服务商插件,请检查 Base URL")
|
|
}
|
|
|
|
// 2. 构建配置
|
|
config := provider.DiscoveryConfig{
|
|
BaseURL: baseURL,
|
|
APIKey: apiKey,
|
|
APIFormat: strings.ToLower(strings.TrimSpace(input.APIFormat)),
|
|
Headers: headers,
|
|
AllowLocalChannel: input.AllowLocalChannel,
|
|
Region: s.extractRegion(baseURL),
|
|
}
|
|
|
|
if config.APIFormat == "" {
|
|
config.APIFormat = "openai"
|
|
}
|
|
|
|
// 3. 调用插件发现模型
|
|
models, err := discovery.DiscoverModels(ctx, config)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("插件发现模型失败: %w", err)
|
|
}
|
|
|
|
// 4. 转换为内部格式
|
|
catalog := make([]ChannelModelCatalogItem, 0, len(models))
|
|
for _, m := range models {
|
|
catalog = append(catalog, ChannelModelCatalogItem{
|
|
ID: m.ID,
|
|
SupportedEndpointTypes: m.SupportedEndpointTypes,
|
|
})
|
|
}
|
|
|
|
// 5. 排序
|
|
sort.Slice(catalog, func(i, j int) bool {
|
|
return catalog[i].ID < catalog[j].ID
|
|
})
|
|
|
|
return catalog, nil
|
|
}
|
|
|
|
// headersToMap 将 OutboundHeader 数组转换为 map
|
|
func (s *Service) headersToMap(headers []OutboundHeader) map[string]string {
|
|
result := make(map[string]string, len(headers))
|
|
for _, h := range headers {
|
|
if h.Name != "" && h.Value != "" {
|
|
result[h.Name] = h.Value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// extractRegion 从 baseURL 提取地域信息
|
|
func (s *Service) extractRegion(baseURL string) string {
|
|
// 美国地域
|
|
if strings.Contains(baseURL, "dashscope-us") {
|
|
return "us-east-1"
|
|
}
|
|
|
|
// 亚太地域(新加坡)
|
|
if strings.Contains(baseURL, "ap-southeast-1") {
|
|
return "ap-southeast-1"
|
|
}
|
|
|
|
// 默认:中国北京
|
|
return "cn-beijing"
|
|
}
|
|
|
|
// isPluginEnabled 检查插件机制是否启用
|
|
func (s *Service) isPluginEnabled() bool {
|
|
// 通过环境变量控制
|
|
value := os.Getenv("ENABLE_PROVIDER_PLUGINS")
|
|
return strings.ToLower(value) == "true" || value == "1"
|
|
}
|