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,
|
||
})
|
||
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
|
||
}
|