feat(model):阿里模型添加异步支持
This commit is contained in:
1 parent
c3c8448343
commit
42f2b2e1c0
8 files changed
+410
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
Reference in new issue
Block a user