- ModelChannel 新增 proxy_url 字段,v48 迁移自动加列 - outbound 新增 OutboundHTTPClientWithProxy,socks5/socks5h/http/https 均支持; 目标主机名不做本地解析,直接交给代理服务器远端解析,本地开启 TUN/VPN 时仍可用 - 系统代理、Provider 出站、管理员模型测试路径均使用渠道级代理 - 渠道管理表单新增"代理地址"输入框,留空回退全局代理
176 lines
5.9 KiB
Go
176 lines
5.9 KiB
Go
// 管理端「测试模型」:按能力构造最小测试请求并真实调用一次上游。
|
|
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"yingce/backend/internal/model"
|
|
)
|
|
|
|
func (s *Service) TestAdminChannelModel(ctx context.Context, actor *model.User, channelID string, req ChannelModelRequest) (*AdminChannelModelTestResult, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
channel, err := s.adminSystemChannel(channelID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modelKey, providerModelKey, capability, protocol, err := s.normalizeChannelModelContract(channel, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if capability == "text" || capability == "image" || capability == "video" {
|
|
if _, err := NormalizeModelCapabilityConfigForModel(capability, string(protocol), providerModelKey, req.CapabilityConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if strings.TrimSpace(channel.BaseURL) == "" || strings.TrimSpace(channel.APIKey) == "" {
|
|
return nil, BadAuthRequest("请先在渠道中配置 Base URL 和 API Key")
|
|
}
|
|
if _, err := ValidateOutboundURL(channel.BaseURL); err != nil {
|
|
return nil, err
|
|
}
|
|
headers, err := ParseOutboundHeadersJSON(channel.HeadersJSON)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prompt := map[string]string{
|
|
"text": "Reply with OK.",
|
|
"image": "A simple gray circle on a white background.",
|
|
"video": "A static gray circle on a white background.",
|
|
"audio": "Model test.",
|
|
}[capability]
|
|
videoSeconds := "6"
|
|
videoSecondsValue := 6
|
|
if protocol == model.ChannelInterfaceVolcengineJiMengVideo {
|
|
videoSeconds = "5"
|
|
videoSecondsValue = 5
|
|
}
|
|
imageSize, imageQuality := "", ""
|
|
var imageProfile *ImageCapabilityConfig
|
|
videoRatio, videoResolution := videoTestDefaults(nil)
|
|
var videoProfile *VideoCapabilityConfig
|
|
switch capability {
|
|
case "image":
|
|
profile, normalizeErr := NormalizeModelCapabilityConfigForModel(capability, string(protocol), providerModelKey, req.CapabilityConfig)
|
|
if normalizeErr != nil {
|
|
return nil, normalizeErr
|
|
}
|
|
imageProfile = profile.Image
|
|
imageSize, imageQuality = imageTestDefaults(imageProfile)
|
|
case "video":
|
|
// 视频测试必须带上模型能力画像:声明式协议只按画像里的枚举回填分辨率名(如 480 -> 480p),
|
|
// 没有画像时会把裸数字发给上游,火山方舟等供应商会直接拒绝。
|
|
profile, normalizeErr := NormalizeModelCapabilityConfigForModel(capability, string(protocol), providerModelKey, req.CapabilityConfig)
|
|
if normalizeErr != nil {
|
|
return nil, normalizeErr
|
|
}
|
|
videoProfile = profile.Video
|
|
videoRatio, videoResolution = videoTestDefaults(videoProfile)
|
|
}
|
|
input := canvasGenerationInput{
|
|
Mode: capability,
|
|
Prompt: prompt,
|
|
Config: providerConfig{
|
|
ChannelID: channel.ID,
|
|
APIFormat: channel.APIFormat,
|
|
InterfaceType: string(protocol),
|
|
BaseURL: channel.BaseURL,
|
|
APIKey: channel.APIKey,
|
|
SecretKey: channel.SecretKey,
|
|
Headers: headers,
|
|
Model: providerModelKey,
|
|
ChannelModelKey: modelKey,
|
|
Size: map[string]string{"image": imageSize, "video": videoRatio}[capability],
|
|
Quality: imageQuality,
|
|
Count: "1",
|
|
VideoSeconds: videoSeconds,
|
|
VQuality: videoResolution,
|
|
VideoGenerateAudio: "false",
|
|
VideoWatermark: "false",
|
|
AudioVoice: "alloy",
|
|
AudioFormat: "mp3",
|
|
AudioSpeed: "1",
|
|
},
|
|
Metadata: map[string]interface{}{},
|
|
}
|
|
if capability == "image" {
|
|
input.ImageCapability = imageProfile
|
|
}
|
|
if capability == "video" {
|
|
input.VideoCapability = videoProfile
|
|
}
|
|
|
|
// 测试复用真实生成协议、运行时并发和熔断策略,但不创建用户任务或计费订单。
|
|
testCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
|
|
defer cancel()
|
|
testCtx = context.WithValue(testCtx, providerAnalyticsKey{}, providerAnalyticsContext{
|
|
Service: s, Billing: s.taskBilling(), UserID: actor.ID, ChannelID: channel.ID, Capability: capability,
|
|
Operation: "admin_model_test", Model: modelKey, VideoSeconds: videoSecondsValue, ProxyURL: channel.ProxyURL,
|
|
})
|
|
testCtx = withProtocolRegistry(testCtx, s.protocolRegistry())
|
|
startedAt := time.Now()
|
|
switch capability {
|
|
case "text":
|
|
_, err = runTextTask(testCtx, input)
|
|
case "image":
|
|
_, err = runImageTask(testCtx, input)
|
|
case "video":
|
|
_, err = runVideoTask(testCtx, input)
|
|
case "audio":
|
|
_, err = runAudioTask(testCtx, input)
|
|
}
|
|
if err != nil {
|
|
status := http.StatusBadGateway
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
status = http.StatusGatewayTimeout
|
|
}
|
|
return nil, WrapAppError(status, "模型测试失败:"+providerUserFacingErrorMessage(err), err)
|
|
}
|
|
return &AdminChannelModelTestResult{DurationMs: time.Since(startedAt).Milliseconds()}, nil
|
|
}
|
|
|
|
// 模型测试必须使用当前模型声明的默认参数,避免固定分辨率 SKU 被通用 1K 测试值误伤。
|
|
// videoTestDefaults 从模型能力画像取测试用的比例和分辨率;画像缺失时回退到最通用的 16:9 / 720。
|
|
func videoTestDefaults(profile *VideoCapabilityConfig) (string, string) {
|
|
if profile == nil {
|
|
return "16:9", "720"
|
|
}
|
|
ratio := strings.TrimSpace(profile.DefaultRatio)
|
|
if ratio == "" && len(profile.Ratios) > 0 {
|
|
ratio = strings.TrimSpace(profile.Ratios[0])
|
|
}
|
|
if ratio == "" {
|
|
ratio = "16:9"
|
|
}
|
|
resolution := strings.TrimSpace(profile.DefaultResolution)
|
|
if resolution == "" && len(profile.Resolutions) > 0 {
|
|
resolution = strings.TrimSpace(profile.Resolutions[0])
|
|
}
|
|
if resolution == "" {
|
|
resolution = "720"
|
|
}
|
|
return ratio, resolution
|
|
}
|
|
|
|
func imageTestDefaults(profile *ImageCapabilityConfig) (string, string) {
|
|
if profile == nil {
|
|
return "1024x1024", "auto"
|
|
}
|
|
size := ""
|
|
if profile.Size.Parameter != "none" {
|
|
size = strings.TrimSpace(profile.Size.Default)
|
|
}
|
|
quality := ""
|
|
if profile.Quality.Supported {
|
|
quality = strings.TrimSpace(profile.Quality.Default)
|
|
}
|
|
return size, quality
|
|
}
|