feat(model):阿里模型添加异步支持

This commit is contained in:
qian committed 2026-05-17 15:59:08 +08:00
1 parent c3c8448343
commit 42f2b2e1c0
8 files changed
+410

No files matched your search

+7
View File
@@ -358,6 +358,13 @@ func registerRelayRoutes(server *ghttp.Server) {
group.GET("/video/generations/{task_id}", relay.HandleTaskFetch)
})
// 异步图片生成端点(阿里云 DashScope 等)
server.Group("/v1", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.ApiMaintenance, middleware.MaintenanceMode, middleware.ApiKeyAuth, middleware.ContentFilter)
group.POST("/images/generations/async", relay.HandleAliImageSubmit)
group.GET("/images/generations/async/{task_id}", relay.HandleTaskFetch)
})
// Suno 端点
server.Group("/suno", func(group *ghttp.RouterGroup) {
group.Middleware(middleware.ApiMaintenance, middleware.MaintenanceMode, middleware.ApiKeyAuth, middleware.ContentFilter)
+34
View File
@@ -154,6 +154,40 @@ func HandleMjSubmit(r *ghttp.Request) {
)
}
// HandleAliImageSubmit 处理异步图片生成任务提交(POST /v1/images/generations/async)
func HandleAliImageSubmit(r *ghttp.Request) {
body := r.GetBody()
if len(body) == 0 {
r.Response.WriteStatus(400, g.Map{
"error": g.Map{"type": "invalid_request_error", "message": "request body is empty"},
})
return
}
rc := &relay_handler.TaskRelayContext{
TenantID: middleware.GetTenantID(r.Context()),
UserID: middleware.GetUserID(r.Context()),
ApiKeyID: middleware.GetApiKeyID(r.Context()),
RequestID: r.GetCtxVar("RequestId").String(),
Writer: r.Response.Writer,
Scope: r.GetCtxVar("ApiKeyScope").String(),
ClientIP: r.GetClientIp(),
}
channelMeta, err := selectTaskChannel(r, body)
if err != nil {
r.Response.WriteStatus(503, g.Map{
"error": g.Map{"type": "server_error", "message": err.Error()},
})
return
}
relay_handler.HandleTaskSubmit(
r.Context(), body, r.URL.Path, r.Header,
rc, taskDataProvider, taskBillingProvider, channelMeta,
)
}
// HandleMjFetch 处理 Midjourney 任务查询(GET /mj/task/:id/fetch)
func HandleMjFetch(r *ghttp.Request) {
taskID := r.Get("task_id").String()
+3
View File
@@ -9,6 +9,7 @@ const (
TaskPlatformSuno TaskPlatform = "suno"
TaskPlatformMidjourney TaskPlatform = "midjourney"
TaskPlatformVolcengine TaskPlatform = "volcengine"
TaskPlatformAli TaskPlatform = "ali"
)
// TaskAction 异步任务动作类型
@@ -81,6 +82,8 @@ func ProviderTypeToTaskPlatform(p ProviderType) (TaskPlatform, bool) {
return TaskPlatformMidjourney, true
case ProviderVolcengine:
return TaskPlatformVolcengine, true
case ProviderAli:
return TaskPlatformAli, true
default:
return "", false
}
+1
View File
@@ -13,6 +13,7 @@ import (
"github.com/qianfree/team-api/relay/common"
"github.com/qianfree/team-api/relay/constant"
"github.com/qianfree/team-api/relay/taskchannel"
_ "github.com/qianfree/team-api/relay/taskchannel/ali"
_ "github.com/qianfree/team-api/relay/taskchannel/kling"
"github.com/qianfree/team-api/relay/taskchannel/midjourney"
_ "github.com/qianfree/team-api/relay/taskchannel/sora"
+199
View File
@@ -0,0 +1,199 @@
package ali
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/qianfree/team-api/relay/common"
"github.com/qianfree/team-api/relay/constant"
"github.com/qianfree/team-api/relay/taskchannel"
)
func init() {
taskchannel.Register(constant.ProviderAli, func() common.TaskAdaptor {
return &AliImageAdaptor{}
})
}
// AliImageAdaptor 阿里云 DashScope 异步图片生成适配器
type AliImageAdaptor struct {
info *common.RelayInfo
}
func (a *AliImageAdaptor) Init(info *common.RelayInfo) {
a.info = info
}
func (a *AliImageAdaptor) ValidateRequest(_ context.Context, _ *common.RelayInfo, body []byte) *common.TaskError {
var req map[string]any
if err := json.Unmarshal(body, &req); err != nil {
return &common.TaskError{StatusCode: http.StatusBadRequest, Message: "invalid request body"}
}
if _, ok := req["model"]; !ok {
return &common.TaskError{StatusCode: http.StatusBadRequest, Message: "model is required"}
}
if _, ok := req["prompt"]; !ok {
return &common.TaskError{StatusCode: http.StatusBadRequest, Message: "prompt is required"}
}
return nil
}
func (a *AliImageAdaptor) EstimateBilling(_ context.Context, _ *common.RelayInfo, body []byte) map[string]float64 {
ratios := map[string]float64{"base": 1.0}
var req map[string]any
if json.Unmarshal(body, &req) != nil {
return ratios
}
if n, ok := req["n"].(float64); ok && n > 1 {
ratios["count"] = n
}
return ratios
}
func (a *AliImageAdaptor) AdjustBillingOnSubmit(_ *common.RelayInfo, _ []byte) map[string]float64 {
return nil
}
func (a *AliImageAdaptor) BuildRequestURL(info *common.RelayInfo) (string, error) {
baseURL := strings.TrimRight(info.ChannelMeta.BaseURL, "/")
return baseURL + "/api/v1/services/aigc/text2image/image-synthesis", nil
}
func (a *AliImageAdaptor) BuildRequestHeader(header http.Header, info *common.RelayInfo) error {
header.Set("Authorization", "Bearer "+info.ChannelMeta.ApiKey)
header.Set("Content-Type", "application/json")
header.Set("Accept", "application/json")
header.Set("X-DashScope-Async", "enable")
return nil
}
func (a *AliImageAdaptor) BuildRequestBody(_ context.Context, info *common.RelayInfo, body []byte) (io.Reader, error) {
converted, err := convertToDashScopeRequest(body, info)
if err != nil {
return nil, err
}
return strings.NewReader(string(converted)), nil
}
func (a *AliImageAdaptor) DoRequest(_ context.Context, info *common.RelayInfo, requestBody io.Reader) (*http.Response, error) {
url, err := a.BuildRequestURL(info)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, url, requestBody)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
if err := a.BuildRequestHeader(req.Header, info); err != nil {
return nil, fmt.Errorf("setup header: %w", err)
}
client := &http.Client{Timeout: 120 * 1e9}
return client.Do(req)
}
func (a *AliImageAdaptor) DoResponse(_ context.Context, resp *http.Response, _ *common.RelayInfo) (string, []byte, *common.TaskError) {
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, &common.TaskError{StatusCode: http.StatusInternalServerError, Message: "read response failed"}
}
if resp.StatusCode != http.StatusOK {
return "", body, &common.TaskError{
StatusCode: resp.StatusCode,
Message: string(body),
}
}
var result dashScopeSubmitResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", body, &common.TaskError{StatusCode: http.StatusInternalServerError, Message: "parse response failed"}
}
if result.Output.TaskID == "" {
// DashScope 有时在错误时返回 200 但带有 code/message
if result.Output.Code != "" {
return "", body, &common.TaskError{
StatusCode: resp.StatusCode,
Message: result.Output.Message,
ErrCode: result.Output.Code,
}
}
return "", body, &common.TaskError{StatusCode: http.StatusInternalServerError, Message: "upstream returned empty task id"}
}
return result.Output.TaskID, body, nil
}
func (a *AliImageAdaptor) FetchTask(baseURL, apiKey string, taskData []byte) (*http.Response, error) {
var data struct {
TaskID string `json:"task_id"`
}
if err := json.Unmarshal(taskData, &data); err != nil {
return nil, fmt.Errorf("ali: invalid task data: %w", err)
}
url := fmt.Sprintf("%s/api/v1/tasks/%s", strings.TrimRight(baseURL, "/"), data.TaskID)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{Timeout: 30 * 1e9}
return client.Do(req)
}
func (a *AliImageAdaptor) ParseTaskResult(body []byte) (*common.TaskInfo, error) {
var resp dashScopeTaskResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("ali: parse task result: %w", err)
}
info := &common.TaskInfo{Data: body}
switch resp.Output.TaskStatus {
case "PENDING":
info.Status = common.TaskStatusSubmitted
info.Progress = "10%"
case "RUNNING":
info.Status = common.TaskStatusInProgress
info.Progress = "50%"
case "SUCCEEDED":
info.Status = common.TaskStatusSuccess
info.Progress = "100%"
if len(resp.Output.Results) > 0 {
info.ResultURL = resp.Output.Results[0].URL
for i, r := range resp.Output.Results {
info.SubTasks = append(info.SubTasks, common.SubTask{
Index: i,
ResultURL: r.URL,
})
}
}
case "FAILED":
info.Status = common.TaskStatusFailure
info.FailReason = resp.Output.Message
case "CANCELED", "UNKNOWN":
info.Status = common.TaskStatusFailure
info.FailReason = fmt.Sprintf("task status: %s", resp.Output.TaskStatus)
default:
info.Status = common.TaskStatusSubmitted
info.Progress = "10%"
}
return info, nil
}
func (a *AliImageAdaptor) GetModelList() []string {
return ModelList
}
func (a *AliImageAdaptor) GetChannelName() string {
return channelName
}
+22
View File
@@ -0,0 +1,22 @@
package ali
const channelName = "AliImage"
// ModelList 支持异步图片生成的 DashScope 模型
var ModelList = []string{
"wanx-v1",
"wanx-v2",
"wanx2.1-t2i-ediff",
"wanx-sketch-to-image-v2",
"wanx-image-inpainting",
"wanx-style-repaint",
"wanx2.1-image-edit",
"wanx2.5-image-edit",
"wanx-image-outpainting",
"qwen-image-generation",
"qwen-image-edit",
"zimage",
"flux-dev",
"flux-schnell",
"stable-diffusion-xl",
}
+99
View File
@@ -0,0 +1,99 @@
package ali
import (
"encoding/json"
"fmt"
"strings"
"github.com/qianfree/team-api/relay/common"
)
// convertToDashScopeRequest 将 OpenAI Images 格式转换为 DashScope 原生格式
// OpenAI: {model, prompt, size, n, style, ...}
// DashScope: {model, input: {prompt, negative_prompt}, parameters: {size, n, style, ...}}
func convertToDashScopeRequest(body []byte, info *common.RelayInfo) ([]byte, error) {
var raw map[string]json.RawMessage
if err := json.Unmarshal(body, &raw); err != nil {
return nil, fmt.Errorf("parse request: %w", err)
}
req := dashScopeRequest{
Input: dashScopeInput{},
Parameters: make(map[string]any),
}
// model
if info.ChannelMeta.IsModelMapped && info.ChannelMeta.UpstreamModelName != "" {
req.Model = info.ChannelMeta.UpstreamModelName
} else if v, ok := raw["model"]; ok {
_ = json.Unmarshal(v, &req.Model)
}
// prompt → input.prompt
if v, ok := raw["prompt"]; ok {
_ = json.Unmarshal(v, &req.Input.Prompt)
}
// negative_prompt → input.negative_prompt
if v, ok := raw["negative_prompt"]; ok {
_ = json.Unmarshal(v, &req.Input.NegativePrompt)
}
// size: 1024x1024 → 1024*1024
if v, ok := raw["size"]; ok {
var size string
if err := json.Unmarshal(v, &size); err == nil && size != "" {
req.Parameters["size"] = strings.ReplaceAll(size, "x", "*")
}
}
// n
if v, ok := raw["n"]; ok {
var n int
if err := json.Unmarshal(v, &n); err == nil && n > 0 {
req.Parameters["n"] = n
}
}
// seed
if v, ok := raw["seed"]; ok {
var seed int
if err := json.Unmarshal(v, &seed); err == nil {
req.Parameters["seed"] = seed
}
}
// style
if v, ok := raw["style"]; ok {
var style string
if err := json.Unmarshal(v, &style); err == nil && style != "" {
req.Parameters["style"] = style
}
}
// ref_strength
if v, ok := raw["ref_strength"]; ok {
var strength float64
if err := json.Unmarshal(v, &strength); err == nil {
req.Parameters["ref_strength"] = strength
}
}
// ref_img
if v, ok := raw["ref_img"]; ok {
var refImg string
if err := json.Unmarshal(v, &refImg); err == nil && refImg != "" {
req.Parameters["ref_img"] = refImg
}
}
if len(req.Parameters) == 0 {
req.Parameters = nil
}
result, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal dashscope request: %w", err)
}
return result, nil
}
+45
View File
@@ -0,0 +1,45 @@
package ali
// dashScopeRequest DashScope 异步图片生成请求
type dashScopeRequest struct {
Model string `json:"model"`
Input dashScopeInput `json:"input"`
Parameters map[string]any `json:"parameters,omitempty"`
}
// dashScopeInput DashScope 请求输入
type dashScopeInput struct {
Prompt string `json:"prompt,omitempty"`
NegativePrompt string `json:"negative_prompt,omitempty"`
}
// dashScopeSubmitResponse 异步提交响应
type dashScopeSubmitResponse struct {
Output struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
} `json:"output"`
RequestID string `json:"request_id"`
}
// dashScopeTaskResponse 异步任务轮询响应
type dashScopeTaskResponse struct {
Output struct {
TaskID string `json:"task_id"`
TaskStatus string `json:"task_status"`
Results []dashScopeResult `json:"results"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
} `json:"output"`
Usage struct {
ImageCount int `json:"image_count"`
} `json:"usage,omitempty"`
RequestID string `json:"request_id,omitempty"`
}
// dashScopeResult 单个图片结果
type dashScopeResult struct {
URL string `json:"url"`
}