feat(model):新增MiniMax-H3视频支持
This commit is contained in:
1 parent
26c53c2228
commit
5ce16daa46
29 files changed
+3736
-98
No files matched your search
@@ -579,6 +579,22 @@ func registerRelayRoutes(server *ghttp.Server) {
|
||||
group.POST("/fetch", relay.HandleSunoFetchBatch)
|
||||
group.GET("/fetch/{task_id}", relay.HandleTaskFetch)
|
||||
})
|
||||
|
||||
// MiniMax 官方视频协议端点(H3 v2,官方 SDK 换 base_url 直连;协议文档 docs/modeldocs/minimax/video/)
|
||||
server.Group("/v2", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(middleware.ApiMaintenance, middleware.MaintenanceMode, middleware.ApiKeyAuth, middleware.ContentFilter)
|
||||
group.POST("/video_generation", relay.HandleMiniMaxVideoSubmit)
|
||||
group.GET("/query/video_generation/{task_id}", relay.HandleMiniMaxVideoRetrieve)
|
||||
group.DELETE("/video_generation/{task_id}", relay.HandleMiniMaxVideoCancel)
|
||||
})
|
||||
|
||||
// MiniMax 官方视频协议端点(Hailuo 系列 v1,扁平字段协议;查询走 query 参数,
|
||||
// 成品经 files/retrieve 二跳由网关轮询时代取,官方无取消/删除端点)
|
||||
server.Group("/v1", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(middleware.ApiMaintenance, middleware.MaintenanceMode, middleware.ApiKeyAuth, middleware.ContentFilter)
|
||||
group.POST("/video_generation", relay.HandleMiniMaxVideoV1Submit)
|
||||
group.GET("/query/video_generation", relay.HandleMiniMaxVideoV1Retrieve)
|
||||
})
|
||||
}
|
||||
|
||||
// registerLandingPage 在未内嵌前端(非 embedweb 构建)时,为后端根路径注册项目介绍页,
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package relay
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
|
||||
"github.com/qianfree/team-api/internal/logic/monitor"
|
||||
"github.com/qianfree/team-api/internal/middleware"
|
||||
relay_constant "github.com/qianfree/team-api/relay/constant"
|
||||
relay_handler "github.com/qianfree/team-api/relay/handler"
|
||||
)
|
||||
|
||||
// MiniMax 官方视频协议入站胶水(/v2 端点):
|
||||
// 创建请求在此完成官方请求体解析与通用任务体转换,复用任务提交管线
|
||||
// (selectTaskChannel 渠道调度 + relay_handler.HandleTaskSubmit 计费/提交/落库);
|
||||
// 查询/删除直连 relay_handler 的 MiniMax 视频处理器。
|
||||
// 协议文档:docs/modeldocs/minimax/video/
|
||||
|
||||
// HandleMiniMaxVideoSubmit 处理 POST /v2/video_generation(创建视频生成任务)
|
||||
func HandleMiniMaxVideoSubmit(r *ghttp.Request) {
|
||||
body := r.GetBody()
|
||||
if len(body) == 0 {
|
||||
writeVideosError(r, 400, "request body is empty", "")
|
||||
return
|
||||
}
|
||||
|
||||
// 官方请求体 → 归一结构(content[] 原样保留,派生 prompt)
|
||||
req, taskErr := relay_handler.ParseMiniMaxVideoCreateRequest(body)
|
||||
if taskErr != nil {
|
||||
writeVideosError(r, taskErr.StatusCode, taskErr.Message, taskErr.ErrCode)
|
||||
return
|
||||
}
|
||||
if taskErr := relay_handler.ValidateMiniMaxVideoCreateRequest(req); taskErr != nil {
|
||||
writeVideosError(r, taskErr.StatusCode, taskErr.Message, taskErr.ErrCode)
|
||||
return
|
||||
}
|
||||
|
||||
// 转成任务框架通用请求体(适配器消费 content/resolution/duration + 双写通用词汇)
|
||||
taskBody, err := relay_handler.BuildMiniMaxVideoTaskBody(req)
|
||||
if err != nil {
|
||||
writeVideosError(r, 500, "build task body failed", "")
|
||||
return
|
||||
}
|
||||
|
||||
rc := &relay_handler.TaskRelayContext{
|
||||
TenantID: middleware.GetTenantID(r.Context()),
|
||||
UserID: middleware.GetUserID(r.Context()),
|
||||
ApiKeyID: middleware.GetApiKeyID(r.Context()),
|
||||
ProjectID: middleware.GetProjectID(r.Context()),
|
||||
RequestID: r.GetCtxVar("RequestId").String(),
|
||||
Writer: r.Response.Writer,
|
||||
Scope: r.GetCtxVar("ApiKeyScope").String(),
|
||||
ClientIP: r.GetClientIp(),
|
||||
KeyRateLimitQps: r.GetCtxVar(middleware.CtxKeyApiKeyRateLimitQps).Int(),
|
||||
KeyConcurrency: r.GetCtxVar(middleware.CtxKeyApiKeyRateLimitConcurrency).Int(),
|
||||
KeyIpWhitelist: r.GetCtxVar(middleware.CtxKeyApiKeyIpWhitelist).String(),
|
||||
KeyTotalQuota: r.GetCtxVar(middleware.CtxKeyApiKeyTotalQuota).Float64(),
|
||||
KeyUsedQuota: r.GetCtxVar(middleware.CtxKeyApiKeyUsedQuota).Float64(),
|
||||
// MiniMax 官方协议:{"task_id"} 响应 + task_ 前缀 ID + 请求回显落库
|
||||
// (协议标识与 relay/handler.minimaxVideoProtocol 对齐,对齐 videos.go 胶水层字面量惯例)
|
||||
Protocol: "minimax_native",
|
||||
RelayMode: int(relay_constant.RelayModeMiniMaxVideo),
|
||||
RequestEcho: relay_handler.NewMiniMaxRequestEcho(req),
|
||||
}
|
||||
|
||||
// 选择渠道(模型权限校验 + 调度引擎,与 /v1/video/generations 同一链路)
|
||||
channelMeta, err := selectTaskChannel(r, taskBody)
|
||||
if err != nil {
|
||||
writeTaskChannelSelectionError(r, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 响应捕获用于审计(对齐 HandleTaskSubmit 模式)
|
||||
capture := NewResponseCaptureWriter(r.Response.Writer)
|
||||
rc.Writer = capture
|
||||
|
||||
registerAsyncTask(rc.RequestID, rc.TenantID, rc.UserID, rc.ProjectID, req.Model, channelMeta, r.URL.Path)
|
||||
|
||||
relay_handler.HandleTaskSubmit(
|
||||
r.Context(), taskBody, r.URL.Path, r.Header,
|
||||
rc, taskDataProvider, taskBillingProvider, channelMeta,
|
||||
)
|
||||
|
||||
// 提交成功后切换为任务 ID 跟踪(任务生命周期远超 HTTP 请求)
|
||||
if rc.TaskID != "" {
|
||||
monitor.SwitchToTaskID(rc.RequestID, rc.TaskID)
|
||||
} else {
|
||||
monitor.UnregisterRequest(rc.RequestID)
|
||||
}
|
||||
|
||||
rc.ForwardingTrace = buildTaskForwardingTrace(r.URL.Path, taskBody, channelMeta, capture.StatusCode())
|
||||
go recordTaskSubmitAudit(r, rc, capture, taskBody)
|
||||
}
|
||||
|
||||
// HandleMiniMaxVideoRetrieve 处理 GET /v2/query/video_generation/{task_id}(查询视频任务)
|
||||
func HandleMiniMaxVideoRetrieve(r *ghttp.Request) {
|
||||
taskID := r.Get("task_id").String()
|
||||
if taskID == "" {
|
||||
writeVideosError(r, 400, "task id is required", "")
|
||||
return
|
||||
}
|
||||
|
||||
rc := &relay_handler.TaskRelayContext{
|
||||
TenantID: middleware.GetTenantID(r.Context()),
|
||||
UserID: middleware.GetUserID(r.Context()),
|
||||
ApiKeyID: middleware.GetApiKeyID(r.Context()),
|
||||
ProjectID: middleware.GetProjectID(r.Context()),
|
||||
RequestID: r.GetCtxVar("RequestId").String(),
|
||||
Writer: r.Response.Writer,
|
||||
}
|
||||
|
||||
relay_handler.HandleMiniMaxVideoRetrieve(r.Context(), taskID, rc, taskDataProvider)
|
||||
}
|
||||
|
||||
// HandleMiniMaxVideoCancel 处理 DELETE /v2/video_generation/{task_id}(取消排队中的视频任务)
|
||||
func HandleMiniMaxVideoCancel(r *ghttp.Request) {
|
||||
taskID := r.Get("task_id").String()
|
||||
if taskID == "" {
|
||||
writeVideosError(r, 400, "task id is required", "")
|
||||
return
|
||||
}
|
||||
|
||||
rc := &relay_handler.TaskRelayContext{
|
||||
TenantID: middleware.GetTenantID(r.Context()),
|
||||
UserID: middleware.GetUserID(r.Context()),
|
||||
ApiKeyID: middleware.GetApiKeyID(r.Context()),
|
||||
ProjectID: middleware.GetProjectID(r.Context()),
|
||||
RequestID: r.GetCtxVar("RequestId").String(),
|
||||
Writer: r.Response.Writer,
|
||||
}
|
||||
|
||||
relay_handler.HandleMiniMaxVideoCancel(r.Context(), taskID, rc, taskDataProvider)
|
||||
}
|
||||
|
||||
// HandleMiniMaxVideoV1Submit 处理 POST /v1/video_generation(MiniMax v1 官方协议创建,
|
||||
// 文生/图生/首尾帧/主体参考四形态共用端点,扁平字段)
|
||||
func HandleMiniMaxVideoV1Submit(r *ghttp.Request) {
|
||||
body := r.GetBody()
|
||||
if len(body) == 0 {
|
||||
writeVideosError(r, 400, "request body is empty", "")
|
||||
return
|
||||
}
|
||||
|
||||
req, taskErr := relay_handler.ParseMiniMaxVideoV1CreateRequest(body)
|
||||
if taskErr != nil {
|
||||
writeVideosError(r, taskErr.StatusCode, taskErr.Message, taskErr.ErrCode)
|
||||
return
|
||||
}
|
||||
if taskErr := relay_handler.ValidateMiniMaxVideoV1CreateRequest(req); taskErr != nil {
|
||||
writeVideosError(r, taskErr.StatusCode, taskErr.Message, taskErr.ErrCode)
|
||||
return
|
||||
}
|
||||
|
||||
taskBody, err := relay_handler.BuildMiniMaxVideoV1TaskBody(req)
|
||||
if err != nil {
|
||||
writeVideosError(r, 500, "build task body failed", "")
|
||||
return
|
||||
}
|
||||
|
||||
rc := &relay_handler.TaskRelayContext{
|
||||
TenantID: middleware.GetTenantID(r.Context()),
|
||||
UserID: middleware.GetUserID(r.Context()),
|
||||
ApiKeyID: middleware.GetApiKeyID(r.Context()),
|
||||
ProjectID: middleware.GetProjectID(r.Context()),
|
||||
RequestID: r.GetCtxVar("RequestId").String(),
|
||||
Writer: r.Response.Writer,
|
||||
Scope: r.GetCtxVar("ApiKeyScope").String(),
|
||||
ClientIP: r.GetClientIp(),
|
||||
KeyRateLimitQps: r.GetCtxVar(middleware.CtxKeyApiKeyRateLimitQps).Int(),
|
||||
KeyConcurrency: r.GetCtxVar(middleware.CtxKeyApiKeyRateLimitConcurrency).Int(),
|
||||
KeyIpWhitelist: r.GetCtxVar(middleware.CtxKeyApiKeyIpWhitelist).String(),
|
||||
KeyTotalQuota: r.GetCtxVar(middleware.CtxKeyApiKeyTotalQuota).Float64(),
|
||||
KeyUsedQuota: r.GetCtxVar(middleware.CtxKeyApiKeyUsedQuota).Float64(),
|
||||
// MiniMax v1 官方协议:{task_id, base_resp} 响应 + task_ 前缀 ID + 请求回显落库
|
||||
Protocol: "minimax_native_v1",
|
||||
RelayMode: int(relay_constant.RelayModeMiniMaxVideo),
|
||||
RequestEcho: relay_handler.NewMiniMaxV1RequestEcho(req),
|
||||
}
|
||||
|
||||
// 选择渠道(模型权限校验 + 调度引擎,与 /v1/video/generations 同一链路)
|
||||
channelMeta, err := selectTaskChannel(r, taskBody)
|
||||
if err != nil {
|
||||
writeTaskChannelSelectionError(r, err)
|
||||
return
|
||||
}
|
||||
|
||||
capture := NewResponseCaptureWriter(r.Response.Writer)
|
||||
rc.Writer = capture
|
||||
|
||||
registerAsyncTask(rc.RequestID, rc.TenantID, rc.UserID, rc.ProjectID, req.Model, channelMeta, r.URL.Path)
|
||||
|
||||
relay_handler.HandleTaskSubmit(
|
||||
r.Context(), taskBody, r.URL.Path, r.Header,
|
||||
rc, taskDataProvider, taskBillingProvider, channelMeta,
|
||||
)
|
||||
|
||||
if rc.TaskID != "" {
|
||||
monitor.SwitchToTaskID(rc.RequestID, rc.TaskID)
|
||||
} else {
|
||||
monitor.UnregisterRequest(rc.RequestID)
|
||||
}
|
||||
|
||||
rc.ForwardingTrace = buildTaskForwardingTrace(r.URL.Path, taskBody, channelMeta, capture.StatusCode())
|
||||
go recordTaskSubmitAudit(r, rc, capture, taskBody)
|
||||
}
|
||||
|
||||
// HandleMiniMaxVideoV1Retrieve 处理 GET /v1/query/video_generation?task_id=(v1 官方协议任务查询)
|
||||
func HandleMiniMaxVideoV1Retrieve(r *ghttp.Request) {
|
||||
taskID := r.Get("task_id").String()
|
||||
if taskID == "" {
|
||||
writeVideosError(r, 400, "task id is required", "")
|
||||
return
|
||||
}
|
||||
|
||||
rc := &relay_handler.TaskRelayContext{
|
||||
TenantID: middleware.GetTenantID(r.Context()),
|
||||
UserID: middleware.GetUserID(r.Context()),
|
||||
ApiKeyID: middleware.GetApiKeyID(r.Context()),
|
||||
ProjectID: middleware.GetProjectID(r.Context()),
|
||||
RequestID: r.GetCtxVar("RequestId").String(),
|
||||
Writer: r.Response.Writer,
|
||||
}
|
||||
|
||||
relay_handler.HandleMiniMaxVideoV1Retrieve(r.Context(), taskID, rc, taskDataProvider)
|
||||
}
|
||||
@@ -344,10 +344,16 @@ func pollSingleTask(ctx context.Context, adaptor common.TaskAdaptor, channel *co
|
||||
return
|
||||
}
|
||||
|
||||
// 查询上游状态
|
||||
// 查询上游状态(model 供供应商适配器按模型分派协议端点,如 MiniMax v1/v2;
|
||||
// 其余适配器按结构体解析 taskData,自动忽略未知键)
|
||||
upstreamModel := task.UpstreamModel
|
||||
if upstreamModel == "" {
|
||||
upstreamModel = task.ModelName
|
||||
}
|
||||
taskData, _ := json.Marshal(map[string]any{
|
||||
"task_id": pd.UpstreamTaskID,
|
||||
"use_proxy": parseChannelUseProxy(channel.Settings),
|
||||
"model": upstreamModel,
|
||||
})
|
||||
|
||||
resp, err := adaptor.FetchTask(channel.BaseURL, channel.ApiKey, taskData)
|
||||
|
||||
@@ -226,6 +226,9 @@ func (s *sTenant) ExportTasks(ctx context.Context, req *v1.TenantTaskExportReq)
|
||||
"kling": "Kling",
|
||||
"midjourney": "Midjourney",
|
||||
"suno": "Suno",
|
||||
"volcengine": "火山引擎",
|
||||
"ali": "阿里",
|
||||
"minimax": "MiniMax",
|
||||
}
|
||||
|
||||
// 预扣/实际费用属 bil_ 记账层,币种 = 系统本位币
|
||||
|
||||
Reference in new issue
Block a user