feat(gateway): 补齐桌面端高级媒体直连接口
ci / verify (pull_request) Successful in 15m34s
ci / verify (pull_request) Successful in 15m34s
新增图片矢量化、视频超分、每日用量、计价与任务隔离能力,并通过环境变量解析平台凭据。 已通过 Go 全量门禁、迁移检查、镜像构建以及 Vectorizer 五格式和 Topaz 3 秒视频真实 DEV 验收。
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -90,6 +92,8 @@ func New(cfg config.Config, db *store.Store, logger *slog.Logger, observers ...b
|
||||
"volces": clients.VolcesClient{HTTPClient: httpClients.none},
|
||||
"keling": clients.KelingClient{HTTPClient: httpClients.none},
|
||||
"kling": clients.KelingClient{HTTPClient: httpClients.none},
|
||||
"vectorizer": clients.VectorizerClient{HTTPClient: httpClients.none},
|
||||
"topaz": clients.TopazClient{HTTPClient: httpClients.none},
|
||||
"universal": clients.UniversalClient{HTTPClient: httpClients.none, ScriptExecutor: scriptExecutor},
|
||||
"simulation": clients.SimulationClient{},
|
||||
},
|
||||
@@ -857,6 +861,11 @@ func billingItemsFixedTotal(items []any) fixedAmount {
|
||||
}
|
||||
|
||||
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, preprocessing parameterPreprocessingLog, candidate store.RuntimeModelCandidate, pricing resolvedPricing, attemptNo int, onDelta clients.StreamDelta, responseExecution responseExecutionContext, singleSourceProtected bool, cacheAffinityPolicy map[string]any, cacheAffinityRecordKeys []string) (clients.Response, error) {
|
||||
var err error
|
||||
candidate, err = candidateWithEnvironmentCredentials(candidate)
|
||||
if err != nil {
|
||||
return clients.Response{}, err
|
||||
}
|
||||
simulated := isSimulation(task, candidate)
|
||||
baseAttemptMetrics := mergeMetrics(attemptMetrics(candidate, attemptNo, simulated), parameterPreprocessingMetrics(preprocessing))
|
||||
reservations := s.rateLimitReservations(ctx, user, candidate, body)
|
||||
@@ -949,6 +958,15 @@ func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user
|
||||
})
|
||||
return clients.Response{}, err
|
||||
}
|
||||
providerBody, err = s.preparePrivateProviderRequest(ctx, user, task.Kind, providerBody)
|
||||
if err != nil {
|
||||
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
|
||||
AttemptID: attemptID, Status: "failed", Retryable: false,
|
||||
Metrics: mergeMetrics(baseAttemptMetrics, map[string]any{"error": err.Error(), "retryable": false}),
|
||||
ErrorCode: clients.ErrorCode(err), ErrorMessage: err.Error(),
|
||||
})
|
||||
return clients.Response{}, err
|
||||
}
|
||||
providerBody, err = s.hydrateProviderRequestAssets(ctx, providerBody, candidate)
|
||||
if err != nil {
|
||||
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
|
||||
@@ -1421,6 +1439,8 @@ func modelTypeFromKind(kind string, body map[string]any) string {
|
||||
return "image_edit"
|
||||
}
|
||||
return "image_generate"
|
||||
case "images.vectorize":
|
||||
return "image_vectorize"
|
||||
case "videos.generations":
|
||||
if videoRequestHasVideoOrAudioReference(body) {
|
||||
return "omni_video"
|
||||
@@ -1429,6 +1449,8 @@ func modelTypeFromKind(kind string, body map[string]any) string {
|
||||
return "image_to_video"
|
||||
}
|
||||
return "video_generate"
|
||||
case "videos.upscales":
|
||||
return "video_enhance"
|
||||
case "song.generations", "music.generations":
|
||||
return "audio_generate"
|
||||
case "speech.generations":
|
||||
@@ -1463,6 +1485,10 @@ func canonicalModelType(value string) string {
|
||||
return "text_to_speech"
|
||||
case "voice", "voice_clone", "voiceclone", "voice.cloning":
|
||||
return "voice_clone"
|
||||
case "vectorize", "image_vectorizer", "image_vectorize":
|
||||
return "image_vectorize"
|
||||
case "video_upscale", "video_enhance", "upscale":
|
||||
return "video_enhance"
|
||||
default:
|
||||
return normalized
|
||||
}
|
||||
@@ -1470,7 +1496,7 @@ func canonicalModelType(value string) string {
|
||||
|
||||
func isKnownModelType(value string) bool {
|
||||
switch value {
|
||||
case "text_generate", "text_embedding", "text_rerank", "image_generate", "image_edit", "video_generate", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni", "audio_generate", "text_to_speech", "voice_clone":
|
||||
case "text_generate", "text_embedding", "text_rerank", "image_generate", "image_edit", "image_vectorize", "video_generate", "video_enhance", "image_to_video", "text_to_video", "video_edit", "video_reference", "video_first_last_frame", "omni_video", "omni", "audio_generate", "text_to_speech", "voice_clone":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1703,6 +1729,14 @@ func validateRequest(kind string, body map[string]any) error {
|
||||
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
|
||||
return errors.New("prompt is required")
|
||||
}
|
||||
case "images.vectorize":
|
||||
if vectorizerSourceURL(body) == "" && vectorizerSourceTaskID(body) == "" {
|
||||
return &clients.ClientError{Code: "invalid_parameter", Message: "source.url or source.vectorizerTaskId is required", Param: "source", StatusCode: 400}
|
||||
}
|
||||
case "videos.upscales":
|
||||
if firstNonEmptyString(stringFromMap(body, "video_url"), stringFromMap(body, "videoUrl")) == "" {
|
||||
return &clients.ClientError{Code: "invalid_parameter", Message: "video_url is required", Param: "video_url", StatusCode: 400}
|
||||
}
|
||||
case "song.generations", "music.generations":
|
||||
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
|
||||
return errors.New("prompt is required")
|
||||
@@ -1722,6 +1756,74 @@ func validateRequest(kind string, body map[string]any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var credentialEnvironmentName = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`)
|
||||
|
||||
func candidateWithEnvironmentCredentials(candidate store.RuntimeModelCandidate) (store.RuntimeModelCandidate, error) {
|
||||
references, _ := candidate.PlatformConfig["credentialEnv"].(map[string]any)
|
||||
if len(references) == 0 {
|
||||
references, _ = candidate.PlatformConfig["credential_env"].(map[string]any)
|
||||
}
|
||||
if len(references) == 0 {
|
||||
return candidate, nil
|
||||
}
|
||||
credentials := cloneMap(candidate.Credentials)
|
||||
for credentialName, rawEnvironmentName := range references {
|
||||
environmentName := strings.TrimSpace(fmt.Sprint(rawEnvironmentName))
|
||||
if !credentialEnvironmentName.MatchString(environmentName) {
|
||||
return candidate, &clients.ClientError{Code: "missing_credentials", Message: "platform credential environment reference is invalid", Retryable: false}
|
||||
}
|
||||
value, ok := os.LookupEnv(environmentName)
|
||||
if !ok || strings.TrimSpace(value) == "" {
|
||||
return candidate, &clients.ClientError{Code: "missing_credentials", Message: "platform credential environment variable is not configured", Retryable: false}
|
||||
}
|
||||
credentials[credentialName] = value
|
||||
}
|
||||
candidate.Credentials = credentials
|
||||
return candidate, nil
|
||||
}
|
||||
|
||||
func (s *Service) preparePrivateProviderRequest(ctx context.Context, user *auth.User, kind string, body map[string]any) (map[string]any, error) {
|
||||
if kind != "images.vectorize" {
|
||||
return body, nil
|
||||
}
|
||||
taskID := vectorizerSourceTaskID(body)
|
||||
if taskID == "" {
|
||||
return body, nil
|
||||
}
|
||||
sourceTask, err := s.store.GetTask(ctx, taskID)
|
||||
if err != nil || !taskAccessibleToUser(sourceTask, user) || sourceTask.Kind != "images.vectorize" {
|
||||
return nil, &clients.ClientError{Code: "not_found", Message: "source vectorizer task not found", StatusCode: 404, Retryable: false}
|
||||
}
|
||||
imageToken := strings.TrimSpace(fmt.Sprint(sourceTask.RemoteTaskPayload["imageToken"]))
|
||||
if imageToken == "" {
|
||||
return nil, &clients.ClientError{Code: "invalid_parameter", Message: "source vectorizer task cannot be reused", Param: "source.vectorizerTaskId", StatusCode: 400, Retryable: false}
|
||||
}
|
||||
privateBody := cloneMap(body)
|
||||
privateBody["_vectorizer_image_token"] = imageToken
|
||||
if receipt := strings.TrimSpace(fmt.Sprint(sourceTask.RemoteTaskPayload["receipt"])); receipt != "" {
|
||||
privateBody["_vectorizer_receipt"] = receipt
|
||||
}
|
||||
return privateBody, nil
|
||||
}
|
||||
|
||||
func vectorizerSourceURL(body map[string]any) string {
|
||||
if source, ok := body["source"].(map[string]any); ok {
|
||||
return strings.TrimSpace(firstNonEmptyString(stringFromMap(source, "url"), stringFromMap(source, "image_url"), stringFromMap(source, "imageUrl")))
|
||||
}
|
||||
return strings.TrimSpace(firstNonEmptyString(stringFromMap(body, "image_url"), stringFromMap(body, "imageUrl")))
|
||||
}
|
||||
|
||||
func vectorizerSourceTaskID(body map[string]any) string {
|
||||
for _, key := range []string{"source", "sourceContext"} {
|
||||
if source, ok := body[key].(map[string]any); ok {
|
||||
if taskID := strings.TrimSpace(firstNonEmptyString(stringFromMap(source, "vectorizerTaskId"), stringFromMap(source, "vectorizer_task_id"), stringFromMap(source, "taskId"), stringFromMap(source, "task_id"))); taskID != "" {
|
||||
return taskID
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(firstNonEmptyString(stringFromMap(body, "vectorizerTaskId"), stringFromMap(body, "vectorizer_task_id")))
|
||||
}
|
||||
|
||||
func hasRerankDocuments(value any) bool {
|
||||
switch typed := value.(type) {
|
||||
case []any:
|
||||
|
||||
Reference in New Issue
Block a user