Compare commits

...
12 Commits
Author SHA1 Message Date
wangbo 265b7e3cc6 docs(test): 添加 AI Gateway 回环测试任务清单
- 新增 loopback-test-checklist.md 文档
- 包含完整的测试原则和执行环境记录表格
- 提供测试数据准备、任务执行链路、历史记录验证流程
- 添加定价规则、权限控制、运行策略和限流控制测试用例
- 设计验收总表、失败处理记录和清理清单
- 支持 Chat、图像、视频等多模态功能验证
- 集成计费、认证、授权等核心业务逻辑测试
- 提供详细的测试状态跟踪和结果记录模板
2026-05-11 00:43:59 +08:00
wangbo 9f7c9f6581 feat(web): improve media playground controls 2026-05-11 00:40:02 +08:00
wangbo ada765d90e feat: improve simulation media tasks 2026-05-11 00:39:19 +08:00
wangbo d86651ff55 feat: refine api key permissions and admin routes 2026-05-10 23:22:26 +08:00
wangbo 0fc23d7eb8 feat(api): migrate volces client to gateway 2026-05-10 23:14:10 +08:00
wangbo d59756a27c chore: commit pending gateway changes 2026-05-10 22:34:15 +08:00
wangbo 53f8edfb67 feat: enrich task record details 2026-05-10 22:33:58 +08:00
wangbo 205a4b625e fix(web): align playground chat layout 2026-05-10 21:53:45 +08:00
wangbo fdcdcd477b feat: implement AI gateway phase one runtime 2026-05-09 21:18:32 +08:00
wangbo a5e66e79cd feat(web): add reusable admin form dialog 2026-05-09 20:15:35 +08:00
wangbo c0335bd5d0 feat: add ai gateway local core flow 2026-05-09 16:51:28 +08:00
wangbo 5b20f017eb feat: scaffold ai gateway identity and design 2026-05-09 16:01:32 +08:00
143 changed files with 37776 additions and 818 deletions
+16 -3
View File
@@ -1,7 +1,7 @@
APP_ENV=development
HTTP_ADDR=:8088
# Reuse the same PostgreSQL instance as Agent memory, but use an independent
# Reuse the same PostgreSQL 18 instance as Agent memory, but use an independent
# database. When running from the host, use the externally reachable host/port.
AI_GATEWAY_DATABASE_NAME=easyai_ai_gateway
AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@localhost:5432/easyai_ai_gateway?sslmode=disable
@@ -17,9 +17,22 @@ AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@localhost:5432/easyai_ai_
# Keep this aligned with easyai-server-main CONFIG_JWT_SECRET in the first migration phase.
CONFIG_JWT_SECRET=this is a very secret secret
# Used when the gateway delegates OpenAPI sk-* validation, file upload, and settlement callbacks.
# Identity mode:
# - standalone: Gateway owns users, groups, login/API keys, wallet, recharge, and local billing.
# - server-main: server-main owns users/API keys/billing; Gateway stores synced users/groups for policy execution.
# - hybrid: both sources are accepted and separated by gateway_users.source.
IDENTITY_MODE=hybrid
# Used when the gateway delegates OpenAPI sk-* validation, user/group sync, file upload, and settlement callbacks.
SERVER_MAIN_BASE_URL=http://localhost:3000
SERVER_MAIN_INTERNAL_TOKEN=change-me
CORS_ALLOWED_ORIGIN=http://localhost:5178
# Gateway writes progress events locally, then calls this server-main endpoint.
# server-main receives the callback and pushes it through the existing WebSocket gateway.
TASK_PROGRESS_CALLBACK_ENABLED=true
TASK_PROGRESS_CALLBACK_URL=http://localhost:3000/internal/platform/task-progress-callbacks
TASK_PROGRESS_CALLBACK_TIMEOUT_MS=5000
TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS=10
CORS_ALLOWED_ORIGIN=http://localhost:5178,http://127.0.0.1:5178
VITE_GATEWAY_API_BASE_URL=http://localhost:8088
+14 -6
View File
@@ -4,10 +4,10 @@
## 技术选型
- 后端:Go + PostgreSQL,复用 Agent memory 的 `easyai-pgvector`保留 `server-main` 的 JWT / API Key 授权语义。
- 后端:Go + PostgreSQL 18,复用 Agent memory 的 `easyai-pgvector`支持本地用户、可选邀请码、API Key、余额/充值闭环,也支持复用 `server-main` 的 JWT / API Key 授权语义。
- 前端:React + TypeScript + TSXUI 体系按 `shadcn-ui` / Radix / Tailwind 方向沉淀,先提供运维控制台骨架。
- MonorepoNx 负责任务编排,Go 使用 `go.work` 管理模块。
- 集成:完成后由 `easyai-server-main` 通过内部 HTTP SDK 直连本服务,前端经网关访问本服务
- 集成:完成后由 `easyai-server-main` 通过内部 HTTP SDK 直连本服务;任务实时进度由 Gateway 回调 `server-main`,再通过原 WebSocket 网关推送给业务前端
## 目录
@@ -33,7 +33,15 @@ pnpm dev
- API: `http://localhost:8088`
- Web: `http://localhost:5178`
- PostgreSQL: 默认使用宿主机 `localhost:5432` 上的 `postgres` 容器,并使用独立库 `easyai_ai_gateway`
- PostgreSQL: 目标版本 18默认使用宿主机 `localhost:5432` 上的 `easyai-pgvector` 实例,并使用独立库 `easyai_ai_gateway`
- 身份模式: 默认 `IDENTITY_MODE=hybrid`,可同时测试 Gateway 本地账号注册登录、可选邀请码和 `server-main` JWT / API Key 对接。
`pnpm dev` 会先创建数据库并执行 migration,然后并行启动:
- `api:dev`:通过 `scripts/go-watch.mjs` 运行 Go API,监听 `.go``go.mod``go.sum` 变化并自动重启后端进程;watcher 会按进程组终止旧的 `go run` 和其子进程,避免热更新时残留进程占用 API 端口。
- `web:dev`Vite React dev server。
后端热更新可通过 `GO_WATCH_SHUTDOWN_GRACE_MS``GO_WATCH_RESTART_DELAY_MS` 调整旧进程退出等待时间与重启间隔。
默认 EasyAI 部署里,`easyai-pgvector` 在容器网络内的连接串是:
@@ -52,9 +60,9 @@ AI_GATEWAY_DATABASE_URL=postgresql://easyai:easyai2025@localhost:5432/easyai_ai_
## 迁移原则
1. 新服务先并行运行,不直接删除 `easyai-server-main` 内现有模块。
2. 授权先复用 `server-main` 的 JWT secret、claim、角色权限模型
3. OpenAPI `sk-*` 校验、文件上传、扣费结算仍由 `server-main` 承担。
4. 网关服务负责基准模型库、平台模型路由、TPM/RPM/并发限流、任务队列、三方平台执行、任务进度推送
2. 身份域支持 `standalone``server-main``hybrid` 三种模式;独立模式由 Gateway 维护租户、用户、用户组、本地 API Key、余额和充值订单,接入模式从 `server-main` 同步租户、用户和用户组
3. OpenAPI `sk-*` 校验、文件上传、扣费结算在接入模式下仍由 `server-main` 承担;独立模式走 Gateway 本地闭环
4. 网关服务负责基准模型库、平台模型路由、用户组调用折扣、TPM/RPM/并发限流、任务队列、三方平台执行、任务进度事件和回调 outbox
5. 切流时优先让 `server-main``OpenaiService` 变成薄门面,内部调用本服务。
详细设计见 [docs/design.md](docs/design.md)。
+9
View File
@@ -5,4 +5,13 @@ go 1.23
require (
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jackc/pgx/v5 v5.7.2
golang.org/x/crypto v0.31.0
)
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
)
+30
View File
@@ -0,0 +1,30 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+106 -19
View File
@@ -24,14 +24,22 @@ const (
)
type User struct {
ID string `json:"sub"`
Username string `json:"username"`
Roles []string `json:"role,omitempty"`
TenantID string `json:"tenantId,omitempty"`
SSOID string `json:"sso_id,omitempty"`
APIKeyID string `json:"apiKeyId,omitempty"`
APIKeySecret string `json:"apiKeySecret,omitempty"`
APIKeyName string `json:"apiKeyName,omitempty"`
ID string `json:"sub"`
Username string `json:"username"`
Roles []string `json:"role,omitempty"`
TenantID string `json:"tenantId,omitempty"`
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
TenantKey string `json:"tenantKey,omitempty"`
SSOID string `json:"sso_id,omitempty"`
Source string `json:"source,omitempty"`
GatewayUserID string `json:"gatewayUserId,omitempty"`
UserGroupID string `json:"userGroupId,omitempty"`
UserGroupKey string `json:"userGroupKey,omitempty"`
UserGroupKeys []string `json:"userGroupKeys,omitempty"`
APIKeyID string `json:"apiKeyId,omitempty"`
APIKeySecret string `json:"apiKeySecret,omitempty"`
APIKeyName string `json:"apiKeyName,omitempty"`
APIKeyPrefix string `json:"apiKeyPrefix,omitempty"`
}
type contextKey string
@@ -41,15 +49,16 @@ const userContextKey contextKey = "easyai-auth-user"
var ErrUnauthorized = errors.New("unauthorized")
type Authenticator struct {
JWTSecret string
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
HTTPClient *http.Client
HTTPClient *http.Client
LocalAPIKeyVerifier func(ctx context.Context, apiKey string) (*User, error)
}
func New(jwtSecret string, serverMainBaseURL string, internalToken string) *Authenticator {
return &Authenticator{
JWTSecret: jwtSecret,
JWTSecret: jwtSecret,
ServerMainBaseURL: strings.TrimRight(serverMainBaseURL, "/"),
ServerMainInternalToken: internalToken,
HTTPClient: &http.Client{
@@ -112,14 +121,25 @@ func (a *Authenticator) verifyJWT(tokenString string) (*User, error) {
}
user := &User{
ID: stringClaim(claims, "sub"),
Username: stringClaim(claims, "username"),
Roles: stringSliceClaim(claims, "role"),
TenantID: stringClaim(claims, "tenantId"),
SSOID: stringClaim(claims, "sso_id"),
APIKeyID: stringClaim(claims, "apiKeyId"),
APIKeySecret: stringClaim(claims, "apiKeySecret"),
APIKeyName: stringClaim(claims, "apiKeyName"),
ID: stringClaim(claims, "sub"),
Username: stringClaim(claims, "username"),
Roles: stringSliceClaim(claims, "role"),
TenantID: stringClaim(claims, "tenantId"),
GatewayTenantID: stringClaim(claims, "gatewayTenantId"),
TenantKey: stringClaim(claims, "tenantKey"),
SSOID: stringClaim(claims, "sso_id"),
Source: stringClaim(claims, "source"),
GatewayUserID: stringClaim(claims, "gatewayUserId"),
UserGroupID: stringClaim(claims, "userGroupId"),
UserGroupKey: stringClaim(claims, "userGroupKey"),
UserGroupKeys: stringSliceClaim(claims, "userGroupKeys"),
APIKeyID: stringClaim(claims, "apiKeyId"),
APIKeySecret: stringClaim(claims, "apiKeySecret"),
APIKeyName: stringClaim(claims, "apiKeyName"),
APIKeyPrefix: stringClaim(claims, "apiKeyPrefix"),
}
if user.Source == "" {
user.Source = "gateway"
}
if user.ID == "" {
return nil, ErrUnauthorized
@@ -127,7 +147,46 @@ func (a *Authenticator) verifyJWT(tokenString string) (*User, error) {
return user, nil
}
func (a *Authenticator) SignJWT(user *User, ttl time.Duration) (string, error) {
if ttl <= 0 {
ttl = time.Hour
}
now := time.Now()
claims := jwt.MapClaims{
"sub": user.ID,
"username": user.Username,
"role": user.Roles,
"tenantId": user.TenantID,
"gatewayTenantId": user.GatewayTenantID,
"tenantKey": user.TenantKey,
"source": user.Source,
"gatewayUserId": user.GatewayUserID,
"userGroupId": user.UserGroupID,
"userGroupKey": user.UserGroupKey,
"userGroupKeys": user.UserGroupKeys,
"apiKeyId": user.APIKeyID,
"apiKeyName": user.APIKeyName,
"apiKeyPrefix": user.APIKeyPrefix,
"iat": now.Unix(),
"exp": now.Add(ttl).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(a.JWTSecret))
}
func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User, error) {
if a.LocalAPIKeyVerifier != nil {
user, err := a.LocalAPIKeyVerifier(ctx, apiKey)
if err == nil {
return user, nil
}
if !errors.Is(err, ErrUnauthorized) {
return nil, err
}
if strings.HasPrefix(apiKey, "sk-gw-") {
return nil, ErrUnauthorized
}
}
if a.ServerMainBaseURL == "" || a.ServerMainInternalToken == "" {
return nil, ErrUnauthorized
}
@@ -154,6 +213,9 @@ func (a *Authenticator) verifyAPIKey(ctx context.Context, apiKey string) (*User,
if user.ID == "" {
return nil, ErrUnauthorized
}
if user.Source == "" {
user.Source = "server-main"
}
return &user, nil
}
@@ -178,6 +240,31 @@ func hasPermission(roles []string, required Permission) bool {
return granted[required]
}
func PermissionLevel(roles []string) int {
level := 0
for _, role := range roles {
switch role {
case "admin", "manager":
if level < 4 {
level = 4
}
case "operator":
if level < 3 {
level = 3
}
case "creator":
if level < 2 {
level = 2
}
case "user":
if level < 1 {
level = 1
}
}
}
return level
}
func permissionsForRole(role string) []Permission {
switch role {
case "admin", "manager":
+405
View File
@@ -0,0 +1,405 @@
package clients
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func TestSimulationClientReturnsImageDemoAssets(t *testing.T) {
response, err := (SimulationClient{}).Run(context.Background(), Request{
Kind: "images.generations",
Model: "gpt-image-1",
Body: map[string]any{
"prompt": "demo image",
"n": 2,
"simulationDurationMs": 5,
},
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
})
if err != nil {
t.Fatalf("run simulation image client: %v", err)
}
data, _ := response.Result["data"].([]any)
if len(data) != 2 || response.ResponseDurationMS <= 0 {
t.Fatalf("unexpected simulated image response: %+v duration=%d", response.Result, response.ResponseDurationMS)
}
item, _ := data[0].(map[string]any)
if item["url"] != "/static/simulation/image.svg" || item["assetSource"] != "simulation" {
t.Fatalf("unexpected simulated image item: %+v", item)
}
}
func TestSimulationClientReturnsVideoDemoAssets(t *testing.T) {
response, err := (SimulationClient{}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "video_generate",
Model: "demo-video-model",
Body: map[string]any{
"prompt": "demo video",
"count": 2,
"duration": 6,
"simulationDurationMs": 5,
},
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
})
if err != nil {
t.Fatalf("run simulation video client: %v", err)
}
data, _ := response.Result["data"].([]any)
if len(data) != 2 || response.ResponseDurationMS <= 0 {
t.Fatalf("unexpected simulated video response: %+v duration=%d", response.Result, response.ResponseDurationMS)
}
item, _ := data[0].(map[string]any)
if item["video_url"] != "/static/simulation/video.mp4" || item["url"] != "/static/simulation/video.mp4" || item["poster"] != "/static/simulation/video-poster.svg" {
t.Fatalf("unexpected simulated video item: %+v", item)
}
if item["duration"] != 6 || item["assetSource"] != "simulation" {
t.Fatalf("unexpected simulated video metadata: %+v", item)
}
}
func TestSimulationDurationDefaultsByMediaType(t *testing.T) {
imageDuration := simulationDuration(Request{Kind: "images.generations"})
if imageDuration < 10*time.Second || imageDuration > 30*time.Second {
t.Fatalf("image simulation duration should default to 10-30s, got %s", imageDuration)
}
videoDuration := simulationDuration(Request{Kind: "videos.generations"})
if videoDuration < 2*time.Minute || videoDuration > 3*time.Minute {
t.Fatalf("video simulation duration should default to 2-3m, got %s", videoDuration)
}
textDuration := simulationDuration(Request{Kind: "chat.completions"})
if textDuration < 800*time.Millisecond || textDuration > 2400*time.Millisecond {
t.Fatalf("text simulation duration should keep short defaults, got %s", textDuration)
}
}
func TestSimulationDurationCanBeControlledByParams(t *testing.T) {
fixedDuration := simulationDuration(Request{Body: map[string]any{"simulationDurationSeconds": 7}})
if fixedDuration != 7*time.Second {
t.Fatalf("simulationDurationSeconds should set fixed duration, got %s", fixedDuration)
}
rangeDuration := simulationDuration(Request{
Kind: "videos.generations",
Body: map[string]any{
"simulationMinDurationSeconds": 1,
"simulationMaxDurationSeconds": 1,
},
})
if rangeDuration != time.Second {
t.Fatalf("simulation duration range params should override video defaults, got %s", rangeDuration)
}
}
func TestOpenAIClientChatContract(t *testing.T) {
var gotPath string
var gotAuth string
var gotModel string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
w.Header().Set("X-Request-Id", "req-chat-test")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "chatcmpl-test",
"object": "chat.completion",
"model": gotModel,
"choices": []any{map[string]any{
"message": map[string]any{"role": "assistant", "content": "ok"},
}},
"usage": map[string]any{"prompt_tokens": 3, "completion_tokens": 2, "total_tokens": 5},
})
}))
defer server.Close()
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "openai:gpt-4o-mini",
Body: map[string]any{"model": "openai:gpt-4o-mini", "messages": []any{map[string]any{"role": "user", "content": "ping"}}},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gpt-4o-mini",
Credentials: map[string]any{"apiKey": "test-key"},
},
})
if err != nil {
t.Fatalf("run openai client: %v", err)
}
if gotPath != "/chat/completions" || gotAuth != "Bearer test-key" || gotModel != "gpt-4o-mini" {
t.Fatalf("unexpected request path=%s auth=%s model=%s", gotPath, gotAuth, gotModel)
}
if response.Usage.TotalTokens != 5 || response.Result["id"] != "chatcmpl-test" {
t.Fatalf("unexpected response: %+v", response)
}
if response.RequestID != "req-chat-test" || response.ResponseStartedAt.IsZero() || response.ResponseFinishedAt.IsZero() {
t.Fatalf("response metadata was not captured: %+v", response)
}
}
func TestOpenAIClientChatStreamContract(t *testing.T) {
var gotStream bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotStream, _ = body["stream"].(bool)
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-stream\",\"object\":\"chat.completion.chunk\",\"model\":\"deepseek-v4-flash\",\"choices\":[{\"delta\":{\"content\":\"hello\"}}]}\n\n"))
_, _ = w.Write([]byte("data: {\"id\":\"chatcmpl-stream\",\"object\":\"chat.completion.chunk\",\"model\":\"deepseek-v4-flash\",\"choices\":[{\"delta\":{\"content\":\" world\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":1,\"completion_tokens\":2,\"total_tokens\":3}}\n\n"))
_, _ = w.Write([]byte("data: [DONE]\n\n"))
}))
defer server.Close()
response, err := (OpenAIClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "DeepSeek-V4-Flash",
Body: map[string]any{
"model": "DeepSeek-V4-Flash",
"messages": []any{map[string]any{"role": "user", "content": "ping"}},
"stream": true,
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "deepseek-v4-flash",
Credentials: map[string]any{"apiKey": "test-key"},
},
})
if err != nil {
t.Fatalf("run openai stream client: %v", err)
}
if !gotStream {
t.Fatalf("expected upstream stream request")
}
if response.Usage.TotalTokens != 3 {
t.Fatalf("unexpected usage: %+v", response.Usage)
}
choices, _ := response.Result["choices"].([]any)
choice, _ := choices[0].(map[string]any)
message, _ := choice["message"].(map[string]any)
if message["content"] != "hello world" {
t.Fatalf("unexpected stream response: %+v", response.Result)
}
}
func TestGeminiClientChatContract(t *testing.T) {
var gotPath string
var gotKey string
var gotText string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotKey = r.URL.Query().Get("key")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
contents, _ := body["contents"].([]any)
first, _ := contents[0].(map[string]any)
parts, _ := first["parts"].([]any)
part, _ := parts[0].(map[string]any)
gotText, _ = part["text"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"candidates": []any{map[string]any{
"content": map[string]any{
"parts": []any{map[string]any{"text": "gemini ok"}},
},
}},
"usageMetadata": map[string]any{
"promptTokenCount": 4,
"candidatesTokenCount": 6,
"totalTokenCount": 10,
},
})
}))
defer server.Close()
response, err := (GeminiClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "chat.completions",
Model: "gemini:gemini-2.5-flash",
Body: map[string]any{
"model": "gemini:gemini-2.5-flash",
"messages": []any{map[string]any{"role": "user", "content": "ping"}},
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "gemini-2.5-flash",
ModelType: "chat",
Credentials: map[string]any{"apiKey": "gemini-key"},
},
})
if err != nil {
t.Fatalf("run gemini client: %v", err)
}
if gotPath != "/v1beta/models/gemini-2.5-flash:generateContent" || gotKey != "gemini-key" || gotText != "ping" {
t.Fatalf("unexpected request path=%s key=%s text=%s", gotPath, gotKey, gotText)
}
if response.Usage.TotalTokens != 10 || extractText(response.Result) != "gemini ok" {
t.Fatalf("unexpected response: %+v", response)
}
}
func TestGeminiURLAcceptsVersionedBaseURL(t *testing.T) {
got := geminiURL("https://generativelanguage.googleapis.com/v1beta", "gemini-2.5-flash", "test-key")
want := "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=test-key"
if got != want {
t.Fatalf("unexpected gemini url: %s", got)
}
}
func TestVolcesClientImageEditUsesGenerationEndpoint(t *testing.T) {
var gotPath string
var gotAuth string
var gotModel string
var gotImage string
var gotSequential string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
gotImage, _ = body["image"].(string)
gotSequential, _ = body["sequential_image_generation"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "img-volces-edit",
"created": 123,
"data": []any{map[string]any{"url": "https://example.com/out.png"}},
})
}))
defer server.Close()
response, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "images.edits",
ModelType: "image_edit",
Model: "doubao-4.0图像编辑",
Body: map[string]any{
"model": "doubao-4.0图像编辑",
"prompt": "make it brighter",
"image": "https://example.com/source.png",
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "doubao-seedream-4-0-250828",
Credentials: map[string]any{"apiKey": "volces-key"},
Capabilities: map[string]any{
"image_edit": map[string]any{"output_multiple_images": true},
},
},
})
if err != nil {
t.Fatalf("run volces image edit: %v", err)
}
if gotPath != "/images/generations" || gotAuth != "Bearer volces-key" {
t.Fatalf("unexpected request path=%s auth=%s", gotPath, gotAuth)
}
if gotModel != "doubao-seedream-4-0-250828" || gotImage != "https://example.com/source.png" || gotSequential != "auto" {
t.Fatalf("unexpected body model=%s image=%s sequential=%s", gotModel, gotImage, gotSequential)
}
if response.Result["id"] != "img-volces-edit" {
t.Fatalf("unexpected response: %+v", response.Result)
}
}
func TestVolcesClientVideoSubmitsAndPollsTask(t *testing.T) {
var submitPath string
var pollPath string
var gotAuth string
var gotModel string
var gotText string
var gotFirstFrameRole string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotAuth = r.Header.Get("Authorization")
switch r.Method + " " + r.URL.Path {
case "POST /contents/generations/tasks":
submitPath = r.URL.Path
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
gotModel, _ = body["model"].(string)
if body["prompt"] != nil || body["first_frame"] != nil {
t.Fatalf("video convenience fields leaked upstream: %+v", body)
}
content, _ := body["content"].([]any)
textItem, _ := content[0].(map[string]any)
gotText, _ = textItem["text"].(string)
frameItem, _ := content[1].(map[string]any)
gotFirstFrameRole, _ = frameItem["role"].(string)
_ = json.NewEncoder(w).Encode(map[string]any{"id": "cgt-test"})
case "GET /contents/generations/tasks/cgt-test":
pollPath = r.URL.Path
_ = json.NewEncoder(w).Encode(map[string]any{
"id": "cgt-test",
"model": "doubao-seedance-2-0-260128",
"status": "succeeded",
"created_at": 456,
"content": map[string]any{"video_url": "https://example.com/out.mp4"},
"usage": map[string]any{"completion_tokens": 7, "total_tokens": 9},
})
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
}))
defer server.Close()
response, err := (VolcesClient{HTTPClient: server.Client()}).Run(context.Background(), Request{
Kind: "videos.generations",
ModelType: "video_generate",
Model: "豆包Seedance-2.0",
Body: map[string]any{
"model": "豆包Seedance-2.0",
"prompt": "A clean product reveal",
"first_frame": "https://example.com/first.png",
"duration": 6,
"aspect_ratio": "16:9",
},
Candidate: store.RuntimeModelCandidate{
BaseURL: server.URL,
ModelName: "doubao-seedance-2-0-260128",
Credentials: map[string]any{"apiKey": "volces-key"},
PlatformConfig: map[string]any{
"volcesPollIntervalMs": 100,
"volcesPollTimeoutSeconds": 1,
},
},
})
if err != nil {
t.Fatalf("run volces video: %v", err)
}
if submitPath != "/contents/generations/tasks" || pollPath != "/contents/generations/tasks/cgt-test" || gotAuth != "Bearer volces-key" {
t.Fatalf("unexpected paths/auth submit=%s poll=%s auth=%s", submitPath, pollPath, gotAuth)
}
if gotModel != "doubao-seedance-2-0-260128" || gotFirstFrameRole != "first_frame" {
t.Fatalf("unexpected submitted model=%s role=%s", gotModel, gotFirstFrameRole)
}
for _, fragment := range []string{"A clean product reveal", "--dur 6", "--ratio 16:9", "--watermark false", "--seed -1"} {
if !strings.Contains(gotText, fragment) {
t.Fatalf("expected text to contain %q, got %q", fragment, gotText)
}
}
data, _ := response.Result["data"].([]any)
item, _ := data[0].(map[string]any)
if item["url"] != "https://example.com/out.mp4" || response.Usage.TotalTokens != 9 {
t.Fatalf("unexpected response: %+v usage=%+v", response.Result, response.Usage)
}
}
func extractText(result map[string]any) string {
choices, _ := result["choices"].([]any)
choice, _ := choices[0].(map[string]any)
message, _ := choice["message"].(map[string]any)
text, _ := message["content"].(string)
return text
}
+189
View File
@@ -0,0 +1,189 @@
package clients
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"time"
)
type GeminiClient struct {
HTTPClient *http.Client
}
func (c GeminiClient) Run(ctx context.Context, request Request) (Response, error) {
apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
if apiKey == "" {
return Response{}, &ClientError{Code: "missing_credentials", Message: "gemini api key is required", Retryable: false}
}
body := geminiBody(request)
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, geminiURL(request.Candidate.BaseURL, request.Candidate.ModelName, apiKey), bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := httpClient(c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
responseFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
}
output := geminiResult(request, result)
if requestID == "" {
requestID = requestIDFromResult(output)
}
return Response{
Result: output,
RequestID: requestID,
Usage: geminiUsage(result),
Progress: providerProgress(request),
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
}, nil
}
func geminiURL(baseURL string, model string, apiKey string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
if base == "" {
base = "https://generativelanguage.googleapis.com"
}
if strings.HasSuffix(base, "/v1beta") {
base = strings.TrimSuffix(base, "/v1beta")
}
escapedModel := url.PathEscape(model)
return fmt.Sprintf("%s/v1beta/models/%s:generateContent?key=%s", base, escapedModel, url.QueryEscape(apiKey))
}
func geminiBody(request Request) map[string]any {
if contents, ok := request.Body["contents"]; ok {
return map[string]any{"contents": contents}
}
prompt := firstNonEmptyPrompt(request.Body, "")
if prompt == "" {
prompt = textFromMessages(request.Body)
}
return map[string]any{
"contents": []any{map[string]any{
"role": "user",
"parts": []any{map[string]any{"text": prompt}},
}},
}
}
func geminiResult(request Request, raw map[string]any) map[string]any {
if request.ModelType == "image" {
data := geminiImageData(raw)
if len(data) == 0 {
data = []any{map[string]any{"url": "/static/provider/gemini-image-placeholder.png"}}
}
return map[string]any{
"id": "gemini-image",
"created": nowUnix(),
"model": request.Model,
"data": data,
"raw": raw,
}
}
content := geminiText(raw)
return map[string]any{
"id": "gemini-chat",
"object": "chat.completion",
"created": nowUnix(),
"model": request.Model,
"choices": []any{map[string]any{
"index": 0,
"finish_reason": "stop",
"message": map[string]any{"role": "assistant", "content": content},
}},
"usage": geminiUsageMap(raw),
"raw": raw,
}
}
func textFromMessages(body map[string]any) string {
messages, _ := body["messages"].([]any)
parts := make([]string, 0, len(messages))
for _, message := range messages {
item, _ := message.(map[string]any)
content := item["content"]
switch typed := content.(type) {
case string:
parts = append(parts, typed)
case []any:
for _, part := range typed {
partMap, _ := part.(map[string]any)
if text, ok := partMap["text"].(string); ok {
parts = append(parts, text)
}
}
}
}
return strings.TrimSpace(strings.Join(parts, "\n"))
}
func geminiText(raw map[string]any) string {
candidates, _ := raw["candidates"].([]any)
for _, candidate := range candidates {
candidateMap, _ := candidate.(map[string]any)
content, _ := candidateMap["content"].(map[string]any)
parts, _ := content["parts"].([]any)
for _, part := range parts {
partMap, _ := part.(map[string]any)
if text, ok := partMap["text"].(string); ok && text != "" {
return text
}
}
}
return ""
}
func geminiImageData(raw map[string]any) []any {
candidates, _ := raw["candidates"].([]any)
out := []any{}
for _, candidate := range candidates {
candidateMap, _ := candidate.(map[string]any)
content, _ := candidateMap["content"].(map[string]any)
parts, _ := content["parts"].([]any)
for _, part := range parts {
partMap, _ := part.(map[string]any)
inline, _ := partMap["inlineData"].(map[string]any)
if inline == nil {
inline, _ = partMap["inline_data"].(map[string]any)
}
if data, ok := inline["data"].(string); ok && data != "" {
out = append(out, map[string]any{"b64_json": data, "mime_type": inline["mimeType"]})
}
}
}
return out
}
func geminiUsage(raw map[string]any) Usage {
usageMap := geminiUsageMap(raw)
input := intFromAny(usageMap["prompt_tokens"])
output := intFromAny(usageMap["completion_tokens"])
total := intFromAny(usageMap["total_tokens"])
return Usage{InputTokens: input, OutputTokens: output, TotalTokens: total}
}
func geminiUsageMap(raw map[string]any) map[string]any {
meta, _ := raw["usageMetadata"].(map[string]any)
input := intFromAny(meta["promptTokenCount"])
output := intFromAny(meta["candidatesTokenCount"])
total := intFromAny(meta["totalTokenCount"])
if total == 0 {
total = input + output
}
return map[string]any{"prompt_tokens": input, "completion_tokens": output, "total_tokens": total}
}
+302
View File
@@ -0,0 +1,302 @@
package clients
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"strings"
"time"
)
func credential(candidate map[string]any, keys ...string) string {
for _, key := range keys {
if value, ok := candidate[key].(string); ok && strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func boolValue(body map[string]any, key string) bool {
value, _ := body[key].(bool)
return value
}
func stringValue(body map[string]any, key string) string {
value, _ := body[key].(string)
return strings.TrimSpace(value)
}
func intValue(body map[string]any, key string, fallback int) int {
switch value := body[key].(type) {
case float64:
return int(math.Round(value))
case int:
return value
default:
return fallback
}
}
func decodeHTTPResponse(resp *http.Response) (map[string]any, error) {
defer resp.Body.Close()
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024*1024))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, &ClientError{
Code: statusCodeName(resp.StatusCode),
Message: errorMessage(raw, resp.Status),
StatusCode: resp.StatusCode,
RequestID: requestIDFromHTTPResponse(resp),
Retryable: HTTPRetryable(resp.StatusCode),
}
}
var out map[string]any
if len(raw) == 0 {
return map[string]any{}, nil
}
if err := json.Unmarshal(raw, &out); err != nil {
return nil, &ClientError{Code: "invalid_response", Message: err.Error(), Retryable: false}
}
return out, nil
}
func decodeOpenAIStreamResponse(resp *http.Response, onDelta StreamDelta) (map[string]any, error) {
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 16*1024*1024))
return nil, &ClientError{
Code: statusCodeName(resp.StatusCode),
Message: errorMessage(raw, resp.Status),
StatusCode: resp.StatusCode,
RequestID: requestIDFromHTTPResponse(resp),
Retryable: HTTPRetryable(resp.StatusCode),
}
}
if result, ok, err := decodeOpenAIStreamReader(resp.Body, onDelta); ok || err != nil {
return result, err
}
return map[string]any{}, nil
}
func decodeOpenAIStreamReader(reader io.Reader, onDelta StreamDelta) (map[string]any, bool, error) {
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
rawLines := make([]string, 0)
parts := make([]string, 0)
var last map[string]any
var usage Usage
for scanner.Scan() {
rawLine := scanner.Text()
rawLines = append(rawLines, rawLine)
line := strings.TrimSpace(rawLine)
if !strings.HasPrefix(line, "data:") {
continue
}
payload := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
if payload == "" || payload == "[DONE]" {
continue
}
var event map[string]any
if err := json.Unmarshal([]byte(payload), &event); err != nil {
continue
}
last = event
if text := streamEventText(event); text != "" {
parts = append(parts, text)
if onDelta != nil {
if err := onDelta(text); err != nil {
return nil, true, err
}
}
}
if eventUsage := usageFromOpenAI(event); eventUsage.TotalTokens > 0 {
usage = eventUsage
}
}
if err := scanner.Err(); err != nil {
return nil, true, &ClientError{Code: "stream_read_error", Message: err.Error(), Retryable: true}
}
if last == nil {
raw := []byte(strings.Join(rawLines, "\n"))
if len(raw) == 0 {
return map[string]any{}, true, nil
}
var out map[string]any
if err := json.Unmarshal(raw, &out); err != nil {
return nil, false, nil
}
return out, true, nil
}
return buildOpenAIStreamResult(last, parts, usage), true, nil
}
func decodeOpenAIStream(raw []byte) (map[string]any, bool) {
if !bytes.Contains(raw, []byte("data:")) {
return nil, false
}
result, ok, err := decodeOpenAIStreamReader(bytes.NewReader(raw), nil)
return result, ok && err == nil
}
func buildOpenAIStreamResult(last map[string]any, parts []string, usage Usage) map[string]any {
if len(parts) == 0 {
return last
}
var out map[string]any
out = map[string]any{
"id": stringFromAny(firstPresent(last["id"], "chatcmpl-stream")),
"object": "chat.completion",
"model": stringFromAny(last["model"]),
"choices": []any{map[string]any{
"index": 0,
"message": map[string]any{
"role": "assistant",
"content": strings.Join(parts, ""),
},
"finish_reason": "stop",
}},
}
if usage.TotalTokens > 0 {
out["usage"] = map[string]any{
"prompt_tokens": usage.InputTokens,
"completion_tokens": usage.OutputTokens,
"total_tokens": usage.TotalTokens,
}
}
return out
}
func streamEventText(event map[string]any) string {
if choices, ok := event["choices"].([]any); ok {
for _, rawChoice := range choices {
choice, _ := rawChoice.(map[string]any)
if delta, ok := choice["delta"].(map[string]any); ok {
if content, ok := delta["content"].(string); ok {
return content
}
}
if message, ok := choice["message"].(map[string]any); ok {
if content, ok := message["content"].(string); ok {
return content
}
}
}
}
if delta, ok := event["delta"].(string); ok {
return delta
}
if text, ok := event["output_text"].(string); ok {
return text
}
return ""
}
func usageFromOpenAI(result map[string]any) Usage {
usage, _ := result["usage"].(map[string]any)
input := intFromAny(firstPresent(usage["prompt_tokens"], usage["input_tokens"]))
output := intFromAny(firstPresent(usage["completion_tokens"], usage["output_tokens"]))
total := intFromAny(usage["total_tokens"])
if total == 0 {
total = input + output
}
return Usage{InputTokens: input, OutputTokens: output, TotalTokens: total}
}
func requestIDFromHTTPResponse(resp *http.Response) string {
if resp == nil {
return ""
}
for _, key := range []string{
"x-request-id",
"x-requestid",
"request-id",
"x-amzn-requestid",
"x-amz-request-id",
"cf-ray",
} {
if value := strings.TrimSpace(resp.Header.Get(key)); value != "" {
return value
}
}
return ""
}
func requestIDFromResult(result map[string]any) string {
for _, key := range []string{"request_id", "requestId", "id", "response_id", "responseId"} {
if value := strings.TrimSpace(stringFromAny(result[key])); value != "" {
return value
}
}
return ""
}
func intFromAny(value any) int {
switch typed := value.(type) {
case float64:
return int(math.Round(typed))
case int:
return typed
case int64:
return int(typed)
default:
return 0
}
}
func stringFromAny(value any) string {
if text, ok := value.(string); ok {
return text
}
return ""
}
func firstPresent(values ...any) any {
for _, value := range values {
if value != nil {
return value
}
}
return nil
}
func errorMessage(raw []byte, fallback string) string {
if len(raw) == 0 {
return fallback
}
var parsed map[string]any
if json.Unmarshal(raw, &parsed) == nil {
if errObj, ok := parsed["error"].(map[string]any); ok {
if message, ok := errObj["message"].(string); ok {
return message
}
}
if message, ok := parsed["message"].(string); ok {
return message
}
}
return string(raw)
}
func statusCodeName(status int) string {
switch status {
case http.StatusTooManyRequests:
return "rate_limit"
case http.StatusRequestTimeout:
return "timeout"
case http.StatusUnauthorized, http.StatusForbidden:
return "auth_failed"
default:
if status >= 500 {
return "server_error"
}
return fmt.Sprintf("http_%d", status)
}
}
func nowUnix() int64 {
return time.Now().Unix()
}
+114
View File
@@ -0,0 +1,114 @@
package clients
import (
"bytes"
"context"
"encoding/json"
"net/http"
"strings"
"time"
)
type OpenAIClient struct {
HTTPClient *http.Client
}
func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error) {
apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
if apiKey == "" {
return Response{}, &ClientError{Code: "missing_credentials", Message: "openai api key is required", Retryable: false}
}
endpoint := openAIEndpoint(request.Kind)
if endpoint == "" {
return Response{}, &ClientError{Code: "unsupported_kind", Message: "unsupported openai request kind", Retryable: false}
}
body := cloneBody(request.Body)
body["model"] = request.Candidate.ModelName
stream := request.Stream || boolValue(body, "stream")
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(request.Candidate.BaseURL, endpoint), bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := httpClient(c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeOpenAIResponse(resp, stream, request.StreamDelta)
responseFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
}
if requestID == "" {
requestID = requestIDFromResult(result)
}
return Response{
Result: result,
RequestID: requestID,
Usage: usageFromOpenAI(result),
Progress: providerProgress(request),
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
}, nil
}
func decodeOpenAIResponse(resp *http.Response, stream bool, onDelta StreamDelta) (map[string]any, error) {
if stream {
result, err := decodeOpenAIStreamResponse(resp, onDelta)
if err == nil {
return result, nil
}
return nil, err
}
return decodeHTTPResponse(resp)
}
func openAIEndpoint(kind string) string {
switch kind {
case "chat.completions":
return "/chat/completions"
case "responses":
return "/responses"
case "images.generations":
return "/images/generations"
case "images.edits":
return "/images/edits"
default:
return ""
}
}
func cloneBody(body map[string]any) map[string]any {
out := map[string]any{}
for key, value := range body {
out[key] = value
}
return out
}
func joinURL(base string, path string) string {
base = strings.TrimRight(strings.TrimSpace(base), "/")
if base == "" {
base = "https://api.openai.com/v1"
}
return base + path
}
func httpClient(client *http.Client) *http.Client {
if client != nil {
return client
}
return http.DefaultClient
}
func providerProgress(request Request) []Progress {
return []Progress{
{Phase: "submitting", Progress: 0.35, Message: "provider request submitted", Payload: map[string]any{"clientId": request.Candidate.ClientID}},
{Phase: "fetching_result", Progress: 0.8, Message: "provider response received", Payload: map[string]any{"provider": request.Candidate.Provider}},
}
}
+323
View File
@@ -0,0 +1,323 @@
package clients
import (
"context"
"fmt"
"math/rand"
"strings"
"time"
)
type SimulationClient struct{}
const (
defaultSimulationTextMinDuration = 800 * time.Millisecond
defaultSimulationTextMaxDuration = 2400 * time.Millisecond
defaultSimulationImageMinDuration = 10 * time.Second
defaultSimulationImageMaxDuration = 30 * time.Second
defaultSimulationVideoMinDuration = 2 * time.Minute
defaultSimulationVideoMaxDuration = 3 * time.Minute
maxSimulationDuration = 10 * time.Minute
)
func (c SimulationClient) Run(ctx context.Context, request Request) (Response, error) {
profile := simulationProfile(request)
responseStartedAt := time.Now()
duration := simulationDuration(request)
if duration > 0 {
timer := time.NewTimer(duration)
select {
case <-ctx.Done():
timer.Stop()
return Response{}, ctx.Err()
case <-timer.C:
}
}
responseFinishedAt := time.Now()
if profile == "retryable_failure" {
return Response{}, &ClientError{
Code: "server_error",
Message: "simulated retryable failure",
RequestID: "simulated-request",
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
Retryable: true,
}
}
if profile == "fatal_failure" || profile == "non_retryable_failure" {
return Response{}, &ClientError{
Code: "bad_request",
Message: "simulated non-retryable failure",
RequestID: "simulated-request",
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
Retryable: false,
}
}
result := simulatedResult(request)
return Response{
Result: result,
RequestID: requestIDFromResult(result),
Usage: simulatedUsage(request),
Progress: simulatedProgress(request),
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
}, nil
}
func simulationProfile(request Request) string {
if value := stringValue(request.Candidate.Credentials, "simulationFailure"); value != "" {
return value
}
if value := stringValue(request.Candidate.PlatformConfig, "simulationFailure"); value != "" {
return value
}
if value := stringValue(request.Body, "simulationProfile"); value != "" {
return value
}
if value := stringValue(request.Body, "testProfile"); value != "" {
return value
}
return "success"
}
func simulatedResult(request Request) map[string]any {
switch request.Kind {
case "chat.completions":
return map[string]any{
"id": "chatcmpl-simulated",
"object": "chat.completion",
"created": nowUnix(),
"model": request.Model,
"choices": []any{map[string]any{
"index": 0,
"finish_reason": "stop",
"message": map[string]any{
"role": "assistant",
"content": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
},
}},
"usage": map[string]any{"prompt_tokens": 12, "completion_tokens": 8, "total_tokens": 20},
}
case "responses":
return map[string]any{
"id": "resp-simulated",
"object": "response",
"created_at": nowUnix(),
"model": request.Model,
"output_text": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
"usage": map[string]any{"input_tokens": 12, "output_tokens": 8, "total_tokens": 20},
}
case "images.edits":
return map[string]any{
"id": "img-edit-simulated",
"created": nowUnix(),
"model": request.Model,
"data": simulatedImageData(request, "/static/simulation/image-edit.svg", "simulation image edit"),
}
case "images.generations":
return map[string]any{
"id": "img-simulated",
"created": nowUnix(),
"model": request.Model,
"data": simulatedImageData(request, "/static/simulation/image.svg", "simulation image"),
}
case "videos.generations":
return map[string]any{
"id": "video-simulated",
"created": nowUnix(),
"model": request.Model,
"data": simulatedVideoData(request),
}
default:
modelType := strings.ToLower(request.ModelType)
kind := strings.ToLower(request.Kind)
if strings.Contains(modelType, "video") || strings.Contains(kind, "video") {
return map[string]any{
"id": "video-simulated",
"created": nowUnix(),
"model": request.Model,
"data": simulatedVideoData(request),
}
}
return map[string]any{
"id": "img-simulated",
"created": nowUnix(),
"model": request.Model,
"data": simulatedImageData(request, "/static/simulation/image.svg", "simulation image"),
}
}
}
func simulatedImageData(request Request, url string, fallbackPrompt string) []any {
count := simulatedOutputCount(request.Body)
items := make([]any, 0, count)
for index := 0; index < count; index += 1 {
items = append(items, map[string]any{
"url": url,
"assetSource": "simulation",
"index": index,
"revised_prompt": firstNonEmptyPrompt(request.Body, fallbackPrompt),
})
}
return items
}
func simulatedVideoData(request Request) []any {
count := simulatedOutputCount(request.Body)
items := make([]any, 0, count)
for index := 0; index < count; index += 1 {
items = append(items, map[string]any{
"url": "/static/simulation/video.mp4",
"video_url": "/static/simulation/video.mp4",
"poster": "/static/simulation/video-poster.svg",
"duration": simulatedVideoDurationSeconds(request),
"assetSource": "simulation",
"index": index,
"revised_prompt": firstNonEmptyPrompt(request.Body, "simulation video"),
})
}
return items
}
func simulatedUsage(request Request) Usage {
if request.ModelType == "chat" || request.Kind == "responses" {
return Usage{InputTokens: 12, OutputTokens: 8, TotalTokens: 20}
}
return Usage{}
}
func simulatedProgress(request Request) []Progress {
provider := request.Candidate.Provider
if provider == "" {
provider = "simulation"
}
return []Progress{
{Phase: "normalizing", Progress: 0.2, Message: "request normalized", Payload: map[string]any{"provider": provider}},
{Phase: "submitting", Progress: 0.55, Message: "simulation client submitted", Payload: map[string]any{"clientId": request.Candidate.ClientID}},
{Phase: "fetching_result", Progress: 0.85, Message: "simulation result ready", Payload: map[string]any{"kind": request.Kind}},
}
}
func simulationDuration(request Request) time.Duration {
if fixedMS := simulationDurationMS(request, "simulationDurationMs", "testDurationMs"); fixedMS >= 0 {
return clampSimulationDuration(time.Duration(fixedMS) * time.Millisecond)
}
if fixedSeconds := simulationDurationSeconds(request, "simulationDurationSeconds", "testDurationSeconds"); fixedSeconds >= 0 {
return clampSimulationDuration(time.Duration(fixedSeconds) * time.Second)
}
minDuration, maxDuration := defaultSimulationDurationRange(request)
if minMS := simulationDurationMS(request, "simulationMinDurationMs", "simulationDurationMinMs", "testMinDurationMs", "testDurationMinMs"); minMS >= 0 {
minDuration = time.Duration(minMS) * time.Millisecond
}
if maxMS := simulationDurationMS(request, "simulationMaxDurationMs", "simulationDurationMaxMs", "testMaxDurationMs", "testDurationMaxMs"); maxMS >= 0 {
maxDuration = time.Duration(maxMS) * time.Millisecond
}
if minSeconds := simulationDurationSeconds(request, "simulationMinDurationSeconds", "simulationDurationMinSeconds", "testMinDurationSeconds", "testDurationMinSeconds"); minSeconds >= 0 {
minDuration = time.Duration(minSeconds) * time.Second
}
if maxSeconds := simulationDurationSeconds(request, "simulationMaxDurationSeconds", "simulationDurationMaxSeconds", "testMaxDurationSeconds", "testDurationMaxSeconds"); maxSeconds >= 0 {
maxDuration = time.Duration(maxSeconds) * time.Second
}
minDuration = clampSimulationDuration(minDuration)
maxDuration = clampSimulationDuration(maxDuration)
if maxDuration < minDuration {
maxDuration = minDuration
}
spread := maxDuration - minDuration
if spread <= 0 {
return minDuration
}
return minDuration + time.Duration(rand.Int63n(int64(spread)+1))
}
func defaultSimulationDurationRange(request Request) (time.Duration, time.Duration) {
if simulationVideoRequest(request) {
return defaultSimulationVideoMinDuration, defaultSimulationVideoMaxDuration
}
if simulationImageRequest(request) {
return defaultSimulationImageMinDuration, defaultSimulationImageMaxDuration
}
return defaultSimulationTextMinDuration, defaultSimulationTextMaxDuration
}
func simulationVideoRequest(request Request) bool {
kind := strings.ToLower(request.Kind)
modelType := strings.ToLower(request.ModelType)
return strings.Contains(kind, "video") || strings.Contains(modelType, "video")
}
func simulationImageRequest(request Request) bool {
kind := strings.ToLower(request.Kind)
modelType := strings.ToLower(request.ModelType)
return strings.Contains(kind, "image") || strings.Contains(modelType, "image")
}
func simulationDurationSeconds(request Request, keys ...string) int {
for _, source := range []map[string]any{request.Body, request.Candidate.PlatformConfig, request.Candidate.Credentials} {
for _, key := range keys {
value := intValue(source, key, -1)
if value >= 0 {
return value
}
}
}
return -1
}
func simulationDurationMS(request Request, keys ...string) int {
for _, source := range []map[string]any{request.Body, request.Candidate.PlatformConfig, request.Candidate.Credentials} {
for _, key := range keys {
value := intValue(source, key, -1)
if value >= 0 {
return value
}
}
}
return -1
}
func clampSimulationDuration(duration time.Duration) time.Duration {
if duration < 0 {
return 0
}
if duration > maxSimulationDuration {
return maxSimulationDuration
}
return duration
}
func simulatedOutputCount(body map[string]any) int {
count := intValue(body, "n", 0)
if count <= 0 {
count = intValue(body, "count", 0)
}
if count <= 0 {
return 1
}
if count > 20 {
return 20
}
return count
}
func simulatedVideoDurationSeconds(request Request) int {
for _, key := range []string{"duration", "duration_seconds", "durationSeconds"} {
if value := intValue(request.Body, key, 0); value > 0 {
return value
}
}
return 5
}
func firstNonEmptyPrompt(body map[string]any, fallback string) string {
for _, key := range []string{"prompt", "input"} {
if value := strings.TrimSpace(stringValue(body, key)); value != "" {
return value
}
}
return fallback
}
+138
View File
@@ -0,0 +1,138 @@
package clients
import (
"context"
"errors"
"net/http"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type Request struct {
Kind string
ModelType string
Model string
Body map[string]any
Candidate store.RuntimeModelCandidate
Stream bool
StreamDelta StreamDelta
}
type Response struct {
Result map[string]any
RequestID string
Usage Usage
Progress []Progress
Metrics map[string]any
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
}
type Usage struct {
InputTokens int
OutputTokens int
TotalTokens int
}
type Progress struct {
Phase string
Progress float64
Message string
Payload map[string]any
}
type StreamDelta func(text string) error
type Client interface {
Run(ctx context.Context, request Request) (Response, error)
}
type ClientError struct {
Code string
Message string
StatusCode int
RequestID string
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
Retryable bool
}
func (e *ClientError) Error() string {
if e.Message != "" {
return e.Message
}
return e.Code
}
func IsRetryable(err error) bool {
var clientErr *ClientError
return errors.As(err, &clientErr) && clientErr.Retryable
}
func ErrorCode(err error) string {
var clientErr *ClientError
if errors.As(err, &clientErr) && clientErr.Code != "" {
return clientErr.Code
}
return "client_error"
}
type ResponseMetadata struct {
RequestID string
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
StatusCode int
}
func ErrorResponseMetadata(err error) ResponseMetadata {
var clientErr *ClientError
if errors.As(err, &clientErr) {
return ResponseMetadata{
RequestID: clientErr.RequestID,
ResponseStartedAt: clientErr.ResponseStartedAt,
ResponseFinishedAt: clientErr.ResponseFinishedAt,
ResponseDurationMS: clientErr.ResponseDurationMS,
StatusCode: clientErr.StatusCode,
}
}
return ResponseMetadata{}
}
func annotateResponseError(err error, requestID string, startedAt time.Time, finishedAt time.Time) error {
var clientErr *ClientError
if !errors.As(err, &clientErr) {
return err
}
if clientErr.RequestID == "" {
clientErr.RequestID = requestID
}
if clientErr.ResponseStartedAt.IsZero() {
clientErr.ResponseStartedAt = startedAt
}
if clientErr.ResponseFinishedAt.IsZero() {
clientErr.ResponseFinishedAt = finishedAt
}
if clientErr.ResponseDurationMS == 0 {
clientErr.ResponseDurationMS = responseDurationMS(clientErr.ResponseStartedAt, clientErr.ResponseFinishedAt)
}
return err
}
func HTTPRetryable(status int) bool {
return status == http.StatusTooManyRequests || status == http.StatusRequestTimeout || status >= 500
}
func responseDurationMS(startedAt time.Time, finishedAt time.Time) int64 {
if startedAt.IsZero() || finishedAt.IsZero() {
return 0
}
duration := finishedAt.Sub(startedAt).Milliseconds()
if duration < 0 {
return 0
}
return duration
}
+668
View File
@@ -0,0 +1,668 @@
package clients
import (
"bytes"
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"strings"
"time"
)
type VolcesClient struct {
HTTPClient *http.Client
}
func (c VolcesClient) Run(ctx context.Context, request Request) (Response, error) {
apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
if apiKey == "" {
return Response{}, &ClientError{Code: "missing_credentials", Message: "volces api key is required", Retryable: false}
}
switch request.Kind {
case "images.generations", "images.edits":
return c.runImage(ctx, request, apiKey)
case "videos.generations":
return c.runVideo(ctx, request, apiKey)
default:
return Response{}, &ClientError{Code: "unsupported_kind", Message: "unsupported volces request kind", Retryable: false}
}
}
func (c VolcesClient) runImage(ctx context.Context, request Request, apiKey string) (Response, error) {
body := volcesImageBody(request)
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(request.Candidate.BaseURL, "/images/generations"), bytes.NewReader(raw))
if err != nil {
return Response{}, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := httpClient(c.HTTPClient).Do(req)
if err != nil {
return Response{}, &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
responseStartedAt := time.Now()
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
responseFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
}
if requestID == "" {
requestID = requestIDFromResult(result)
}
return Response{
Result: result,
RequestID: requestID,
Usage: usageFromOpenAI(result),
Progress: providerProgress(request),
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
}, nil
}
func (c VolcesClient) runVideo(ctx context.Context, request Request, apiKey string) (Response, error) {
body := volcesVideoBody(request)
submitStartedAt := time.Now()
submitResult, submitRequestID, err := c.postJSON(ctx, request.Candidate.BaseURL, "/contents/generations/tasks", apiKey, body)
submitFinishedAt := time.Now()
if err != nil {
return Response{}, annotateResponseError(err, submitRequestID, submitStartedAt, submitFinishedAt)
}
upstreamTaskID := strings.TrimSpace(stringFromAny(submitResult["id"]))
if upstreamTaskID == "" {
return Response{}, &ClientError{Code: "invalid_response", Message: "volces video task id is missing", RequestID: submitRequestID, Retryable: false}
}
interval := volcesPollInterval(request)
timeout := volcesPollTimeout(request)
deadline := time.NewTimer(timeout)
defer deadline.Stop()
ticker := time.NewTicker(interval)
defer ticker.Stop()
var lastResult map[string]any
for {
select {
case <-ctx.Done():
return Response{}, &ClientError{Code: "cancelled", Message: ctx.Err().Error(), RequestID: submitRequestID, Retryable: true}
default:
}
pollStartedAt := time.Now()
pollResult, pollRequestID, err := c.getJSON(ctx, request.Candidate.BaseURL, "/contents/generations/tasks/"+upstreamTaskID, apiKey)
pollFinishedAt := time.Now()
requestID := firstNonEmpty(pollRequestID, submitRequestID, upstreamTaskID)
if err != nil {
return Response{}, annotateResponseError(err, requestID, pollStartedAt, pollFinishedAt)
}
lastResult = pollResult
switch volcesTaskStatus(pollResult) {
case "succeeded":
result := volcesVideoSuccessResult(request, upstreamTaskID, pollResult)
return Response{
Result: result,
RequestID: requestID,
Usage: volcesVideoUsage(pollResult),
Progress: volcesVideoProgress(request, upstreamTaskID),
ResponseStartedAt: submitStartedAt,
ResponseFinishedAt: pollFinishedAt,
ResponseDurationMS: responseDurationMS(submitStartedAt, pollFinishedAt),
}, nil
case "failed", "cancelled":
return Response{}, &ClientError{
Code: volcesTaskErrorCode(pollResult),
Message: volcesTaskErrorMessage(pollResult),
RequestID: requestID,
ResponseStartedAt: submitStartedAt,
ResponseFinishedAt: pollFinishedAt,
ResponseDurationMS: responseDurationMS(submitStartedAt, pollFinishedAt),
Retryable: false,
}
}
select {
case <-ctx.Done():
return Response{}, &ClientError{Code: "cancelled", Message: ctx.Err().Error(), RequestID: requestID, Retryable: true}
case <-deadline.C:
return Response{}, &ClientError{
Code: "timeout",
Message: fmt.Sprintf("volces video task %s did not finish before timeout; last status: %s", upstreamTaskID, volcesTaskStatus(lastResult)),
RequestID: requestID,
Retryable: true,
}
case <-ticker.C:
}
}
}
func (c VolcesClient) postJSON(ctx context.Context, baseURL string, path string, apiKey string, body map[string]any) (map[string]any, string, error) {
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(baseURL, path), bytes.NewReader(raw))
if err != nil {
return nil, "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := httpClient(c.HTTPClient).Do(req)
if err != nil {
return nil, "", &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
return result, requestID, err
}
func (c VolcesClient) getJSON(ctx context.Context, baseURL string, path string, apiKey string) (map[string]any, string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, joinURL(baseURL, path), nil)
if err != nil {
return nil, "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := httpClient(c.HTTPClient).Do(req)
if err != nil {
return nil, "", &ClientError{Code: "network", Message: err.Error(), Retryable: true}
}
requestID := requestIDFromHTTPResponse(resp)
result, err := decodeHTTPResponse(resp)
return result, requestID, err
}
func volcesImageBody(request Request) map[string]any {
body := cleanProviderBody(request.Body)
body["model"] = request.Candidate.ModelName
if _, ok := body["watermark"]; !ok {
body["watermark"] = false
}
if request.Kind == "images.generations" {
if _, ok := body["seed"]; !ok {
body["seed"] = -1
}
}
if resolution := strings.TrimSpace(stringFromAny(body["resolution"])); resolution != "" {
body["size"] = resolution
}
if size := widthHeightSize(body); size != "" {
body["size"] = size
}
if supportsMultipleOutputs(request, request.ModelType) && body["sequential_image_generation"] == nil {
body["sequential_image_generation"] = "auto"
}
return body
}
func volcesVideoBody(request Request) map[string]any {
body := cleanProviderBody(request.Body)
body["model"] = request.Candidate.ModelName
content := contentItems(body["content"])
if len(content) == 0 {
content = buildVolcesContentFromBody(body)
}
appendMultiShotTimeline(&content)
normalizeVolcesContentRoles(content)
appendVolcesVideoParams(&content, body)
body["content"] = content
stripVolcesVideoConvenienceFields(body)
return body
}
func cleanProviderBody(body map[string]any) map[string]any {
out := cloneBody(body)
for _, key := range []string{
"runMode",
"mode",
"simulation",
"testMode",
"simulationProfile",
"testProfile",
"pollIntervalMs",
"poll_interval_ms",
"pollTimeoutSeconds",
"poll_timeout_seconds",
} {
delete(out, key)
}
return out
}
func buildVolcesContentFromBody(body map[string]any) []map[string]any {
content := make([]map[string]any, 0)
if prompt := firstNonEmptyStringValue(body, "prompt", "input"); prompt != "" {
content = append(content, map[string]any{"type": "text", "text": prompt})
}
appendURLContent := func(kind string, role string, url string) {
if strings.TrimSpace(url) == "" {
return
}
switch kind {
case "image_url":
content = append(content, map[string]any{"type": kind, "role": role, "image_url": map[string]any{"url": strings.TrimSpace(url)}})
case "video_url":
content = append(content, map[string]any{"type": kind, "role": role, "video_url": map[string]any{"url": strings.TrimSpace(url)}})
case "audio_url":
content = append(content, map[string]any{"type": kind, "role": role, "audio_url": map[string]any{"url": strings.TrimSpace(url)}})
}
}
appendURLContent("image_url", "first_frame", firstNonEmptyStringValue(body, "first_frame", "firstFrame"))
appendURLContent("image_url", "last_frame", firstNonEmptyStringValue(body, "last_frame", "lastFrame"))
for _, url := range firstNonEmptyStringListFromAny(body["image"], body["images"], body["image_url"], body["imageUrl"], body["image_urls"], body["imageUrls"], body["reference_image"], body["referenceImage"]) {
appendURLContent("image_url", "reference_image", url)
}
for _, url := range firstNonEmptyStringListFromAny(body["video"], body["video_url"], body["videoUrl"], body["reference_video"], body["referenceVideo"]) {
appendURLContent("video_url", "reference_video", url)
}
for _, url := range firstNonEmptyStringListFromAny(body["audio_url"], body["audioUrl"], body["reference_audio"], body["referenceAudio"]) {
appendURLContent("audio_url", "reference_audio", url)
}
if len(content) == 0 {
content = append(content, map[string]any{"type": "text", "text": ""})
}
return content
}
func stripVolcesVideoConvenienceFields(body map[string]any) {
for _, key := range []string{
"prompt",
"input",
"image",
"images",
"image_url",
"imageUrl",
"image_urls",
"imageUrls",
"reference_image",
"referenceImage",
"first_frame",
"firstFrame",
"last_frame",
"lastFrame",
"video",
"video_url",
"videoUrl",
"reference_video",
"referenceVideo",
"audio_url",
"audioUrl",
"reference_audio",
"referenceAudio",
} {
delete(body, key)
}
}
func contentItems(value any) []map[string]any {
rawItems, ok := value.([]any)
if !ok {
return nil
}
out := make([]map[string]any, 0, len(rawItems))
for _, raw := range rawItems {
item, ok := raw.(map[string]any)
if !ok {
continue
}
copied := map[string]any{}
for key, value := range item {
copied[key] = value
}
out = append(out, copied)
}
return out
}
func normalizeVolcesContentRoles(content []map[string]any) {
for _, item := range content {
itemType := strings.TrimSpace(stringFromAny(item["type"]))
role := strings.TrimSpace(stringFromAny(item["role"]))
switch itemType {
case "image_url":
if role != "first_frame" && role != "last_frame" {
item["role"] = "reference_image"
}
case "video_url":
item["role"] = "reference_video"
case "audio_url":
item["role"] = "reference_audio"
}
}
}
func appendVolcesVideoParams(content *[]map[string]any, body map[string]any) {
textItem := ensureTextContent(content)
current := strings.TrimSpace(stringFromAny(textItem["text"]))
values := []struct {
key string
value any
}{
{"dur", firstPresent(body["duration"], body["dur"])},
{"ratio", firstPresent(body["aspect_ratio"], body["aspectRatio"], body["ratio"])},
{"fps", firstPresent(body["framespersecond"], body["framesPerSecond"], body["fps"])},
{"watermark", firstPresent(body["watermark"], false)},
{"seed", firstPresent(body["seed"], -1)},
{"cf", firstPresent(body["camerafixed"], body["cameraFixed"])},
{"rs", firstPresent(body["resolution"], body["size"])},
}
for _, item := range values {
valueText := volcesParamString(item.value)
if valueText == "" || strings.Contains(current, "--"+item.key) {
continue
}
if current != "" {
current += " "
}
current += "--" + item.key + " " + valueText
}
textItem["text"] = current
}
func appendMultiShotTimeline(content *[]map[string]any) {
type shot struct {
index int
text string
duration float64
}
shots := make([]shot, 0)
items := *content
remaining := items[:0]
for index, item := range items {
if stringFromAny(item["type"]) != "text" {
remaining = append(remaining, item)
continue
}
role := stringFromAny(item["role"])
if role != "shot_prompt" && item["shot_index"] == nil {
remaining = append(remaining, item)
continue
}
text := strings.TrimSpace(stringFromAny(item["text"]))
if text == "" {
continue
}
shotIndex := numericValue(item["shot_index"], float64(index))
shots = append(shots, shot{index: int(math.Floor(shotIndex)), text: text, duration: numericValue(item["duration"], 5)})
}
if len(shots) == 0 {
return
}
*content = remaining
for i := 0; i < len(shots)-1; i++ {
for j := i + 1; j < len(shots); j++ {
if shots[j].index < shots[i].index {
shots[i], shots[j] = shots[j], shots[i]
}
}
}
cursor := 0.0
lines := make([]string, 0, len(shots))
for idx, shot := range shots {
start := cursor
duration := shot.duration
if duration <= 0 {
duration = 5
}
end := start + duration
cursor = end
shotNumber := shot.index + 1
if shotNumber <= 0 {
shotNumber = idx + 1
}
lines = append(lines, fmt.Sprintf("Shot %d, %gs~%gs: %s", shotNumber, start, end, shot.text))
}
textItem := ensureTextContent(content)
current := stringFromAny(textItem["text"])
const prefix = "Additional shot timeline (auto-generated):"
if strings.Contains(current, prefix) {
return
}
separator := ""
if strings.TrimSpace(current) != "" {
separator = "\n\n"
}
textItem["text"] = current + separator + prefix + "\n" + strings.Join(lines, "\n")
}
func ensureTextContent(content *[]map[string]any) map[string]any {
for _, item := range *content {
if stringFromAny(item["type"]) == "text" && item["shot_index"] == nil && stringFromAny(item["role"]) != "shot_prompt" {
return item
}
}
item := map[string]any{"type": "text", "text": ""}
*content = append([]map[string]any{item}, (*content)...)
return item
}
func supportsMultipleOutputs(request Request, capabilityName string) bool {
for _, key := range []string{capabilityName, request.ModelType, "image_generate", "image_edit"} {
if key == "" {
continue
}
capability, _ := request.Candidate.Capabilities[key].(map[string]any)
if boolFromAny(capability["output_multiple_images"]) {
return true
}
}
return false
}
func widthHeightSize(body map[string]any) string {
width := numericValue(body["width"], 0)
height := numericValue(body["height"], 0)
if width <= 0 || height <= 0 {
return ""
}
return fmt.Sprintf("%dx%d", int(math.Round(width)), int(math.Round(height)))
}
func volcesTaskStatus(result map[string]any) string {
return strings.ToLower(strings.TrimSpace(stringFromAny(result["status"])))
}
func volcesTaskErrorCode(result map[string]any) string {
errorObj, _ := result["error"].(map[string]any)
if code := strings.TrimSpace(stringFromAny(errorObj["code"])); code != "" {
return code
}
status := volcesTaskStatus(result)
if status != "" {
return status
}
return "volces_task_failed"
}
func volcesTaskErrorMessage(result map[string]any) string {
errorObj, _ := result["error"].(map[string]any)
if message := strings.TrimSpace(stringFromAny(errorObj["message"])); message != "" {
return message
}
if status := volcesTaskStatus(result); status != "" {
return "volces video task " + status
}
return "volces video task failed"
}
func volcesVideoSuccessResult(request Request, upstreamTaskID string, raw map[string]any) map[string]any {
content, _ := raw["content"].(map[string]any)
videoURL := strings.TrimSpace(stringFromAny(content["video_url"]))
created := intFromAny(raw["created_at"])
if created == 0 {
created = int(nowUnix())
}
data := []any{}
if videoURL != "" {
data = append(data, map[string]any{"url": videoURL, "type": "video"})
}
return map[string]any{
"id": upstreamTaskID,
"object": "video.generation",
"created": created,
"model": request.Candidate.ModelName,
"status": "succeeded",
"upstream_task_id": upstreamTaskID,
"data": data,
"raw": raw,
}
}
func volcesVideoUsage(raw map[string]any) Usage {
usage, _ := raw["usage"].(map[string]any)
output := intFromAny(usage["completion_tokens"])
total := intFromAny(usage["total_tokens"])
if total == 0 {
total = output
}
return Usage{OutputTokens: output, TotalTokens: total}
}
func volcesVideoProgress(request Request, upstreamTaskID string) []Progress {
progress := providerProgress(request)
progress = append(progress, Progress{
Phase: "polling_result",
Progress: 0.9,
Message: "volces video task completed",
Payload: map[string]any{"upstreamTaskId": upstreamTaskID},
})
return progress
}
func volcesPollInterval(request Request) time.Duration {
ms := numericValue(firstPresent(request.Candidate.PlatformConfig["volcesPollIntervalMs"], request.Body["pollIntervalMs"], request.Body["poll_interval_ms"]), 5000)
if ms < 100 {
ms = 100
}
return time.Duration(ms) * time.Millisecond
}
func volcesPollTimeout(request Request) time.Duration {
seconds := numericValue(firstPresent(request.Candidate.PlatformConfig["volcesPollTimeoutSeconds"], request.Body["pollTimeoutSeconds"], request.Body["poll_timeout_seconds"]), 600)
if seconds < 1 {
seconds = 600
}
return time.Duration(seconds) * time.Second
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func firstNonEmptyStringValue(body map[string]any, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
return value
}
}
return ""
}
func stringListFromAny(value any) []string {
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) == "" {
return nil
}
return []string{typed}
case []any:
out := make([]string, 0, len(typed))
for _, item := range typed {
if value := strings.TrimSpace(stringFromAny(item)); value != "" {
out = append(out, value)
}
}
return out
case []string:
out := make([]string, 0, len(typed))
for _, item := range typed {
if value := strings.TrimSpace(item); value != "" {
out = append(out, value)
}
}
return out
default:
return nil
}
}
func firstNonEmptyStringListFromAny(values ...any) []string {
for _, value := range values {
items := stringListFromAny(value)
if len(items) > 0 {
return items
}
}
return nil
}
func volcesParamString(value any) string {
switch typed := value.(type) {
case nil:
return ""
case string:
return strings.TrimSpace(typed)
case bool:
if typed {
return "true"
}
return "false"
case int:
return fmt.Sprintf("%d", typed)
case int64:
return fmt.Sprintf("%d", typed)
case float64:
if math.Mod(typed, 1) == 0 {
return fmt.Sprintf("%d", int64(typed))
}
return fmt.Sprintf("%g", typed)
default:
return fmt.Sprintf("%v", typed)
}
}
func numericValue(value any, fallback float64) float64 {
switch typed := value.(type) {
case int:
return float64(typed)
case int64:
return float64(typed)
case float64:
return typed
case string:
var parsed float64
if _, err := fmt.Sscanf(strings.TrimSpace(typed), "%f", &parsed); err == nil {
return parsed
}
return fallback
default:
return fallback
}
}
func boolFromAny(value any) bool {
switch typed := value.(type) {
case bool:
return typed
case string:
normalized := strings.ToLower(strings.TrimSpace(typed))
return normalized == "true" || normalized == "1"
case float64:
return typed == 1
case int:
return typed == 1
default:
return false
}
}
+23 -11
View File
@@ -8,14 +8,19 @@ import (
)
type Config struct {
AppEnv string
HTTPAddr string
DatabaseURL string
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
CORSAllowedOrigin string
LogLevel slog.Level
AppEnv string
HTTPAddr string
DatabaseURL string
IdentityMode string
JWTSecret string
ServerMainBaseURL string
ServerMainInternalToken string
TaskProgressCallbackEnabled bool
TaskProgressCallbackURL string
TaskProgressCallbackTimeoutMS string
TaskProgressCallbackMaxAttempts string
CORSAllowedOrigin string
LogLevel slog.Level
}
func Load() Config {
@@ -23,14 +28,21 @@ func Load() Config {
AppEnv: env("APP_ENV", "development"),
HTTPAddr: env("HTTP_ADDR", ":8088"),
DatabaseURL: gatewayDatabaseURL(),
IdentityMode: env("IDENTITY_MODE", "hybrid"),
JWTSecret: env("CONFIG_JWT_SECRET", "this is a very secret secret"),
ServerMainBaseURL: strings.TrimRight(
env("SERVER_MAIN_BASE_URL", "http://localhost:3000"),
"/",
),
ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""),
CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178"),
LogLevel: logLevel(env("LOG_LEVEL", "info")),
ServerMainInternalToken: env("SERVER_MAIN_INTERNAL_TOKEN", ""),
TaskProgressCallbackEnabled: env("TASK_PROGRESS_CALLBACK_ENABLED", "true") == "true",
TaskProgressCallbackURL: env("TASK_PROGRESS_CALLBACK_URL",
strings.TrimRight(env("SERVER_MAIN_BASE_URL", "http://localhost:3000"), "/")+"/internal/platform/task-progress-callbacks",
),
TaskProgressCallbackTimeoutMS: env("TASK_PROGRESS_CALLBACK_TIMEOUT_MS", "5000"),
TaskProgressCallbackMaxAttempts: env("TASK_PROGRESS_CALLBACK_MAX_ATTEMPTS", "10"),
CORSAllowedOrigin: env("CORS_ALLOWED_ORIGIN", "http://localhost:5178,http://127.0.0.1:5178"),
LogLevel: logLevel(env("LOG_LEVEL", "info")),
}
}
@@ -0,0 +1,188 @@
package httpapi
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) listAccessRules(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListAccessRules(r.Context())
if err != nil {
s.logger.Error("list access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "list access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listAPIKeyAccessRules(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
items, err := s.store.ListAPIKeyAccessRules(r.Context(), user)
if err != nil {
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
s.logger.Error("list api key access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "list api key access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createAccessRule(w http.ResponseWriter, r *http.Request) {
var input store.AccessRuleInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleInput(input) {
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
return
}
item, err := s.store.CreateAccessRule(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "access rule already exists")
return
}
s.logger.Error("create access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "create access rule failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) batchAccessRules(w http.ResponseWriter, r *http.Request) {
var input store.AccessRuleBatchInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleBatchInput(input) {
writeError(w, http.StatusBadRequest, "subject, effect and resources are required")
return
}
items, err := s.store.BatchAccessRules(r.Context(), input)
if err != nil {
s.logger.Error("batch access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "batch access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) batchAPIKeyAccessRules(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
var input store.AccessRuleBatchInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleBatchInput(input) || input.SubjectType != "api_key" {
writeError(w, http.StatusBadRequest, "api key subject, effect and resources are required")
return
}
items, err := s.store.BatchAPIKeyAccessRules(r.Context(), input, user)
if err != nil {
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "api key not found")
return
}
if errors.Is(err, store.ErrAccessRuleResourceDenied) {
writeError(w, http.StatusForbidden, "resource is not available for current user group")
return
}
s.logger.Error("batch api key access rules failed", "error", err)
writeError(w, http.StatusInternalServerError, "batch api key access rules failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) updateAccessRule(w http.ResponseWriter, r *http.Request) {
var input store.AccessRuleInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validAccessRuleInput(input) {
writeError(w, http.StatusBadRequest, "subject, resource and effect are required")
return
}
item, err := s.store.UpdateAccessRule(r.Context(), r.PathValue("ruleID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "access rule not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "access rule already exists")
return
}
s.logger.Error("update access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "update access rule failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteAccessRule(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteAccessRule(r.Context(), r.PathValue("ruleID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "access rule not found")
return
}
s.logger.Error("delete access rule failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete access rule failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validAccessRuleInput(input store.AccessRuleInput) bool {
return validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") &&
strings.TrimSpace(input.SubjectID) != "" &&
validOneOf(input.ResourceType, "platform", "platform_model", "base_model") &&
strings.TrimSpace(input.ResourceID) != "" &&
validOneOf(input.Effect, "allow", "deny") &&
(input.Status == "" || validOneOf(input.Status, "active", "disabled"))
}
func validAccessRuleBatchInput(input store.AccessRuleBatchInput) bool {
if !validOneOf(input.SubjectType, "user_group", "tenant", "user", "api_key") ||
strings.TrimSpace(input.SubjectID) == "" ||
!validOneOf(input.Effect, "allow", "deny") {
return false
}
if len(input.UpsertResources) == 0 && len(input.DeleteResources) == 0 {
return false
}
for _, resource := range append(input.UpsertResources, input.DeleteResources...) {
if !validOneOf(resource.ResourceType, "platform", "platform_model", "base_model") ||
strings.TrimSpace(resource.ResourceID) == "" ||
(resource.Status != "" && !validOneOf(resource.Status, "active", "disabled")) {
return false
}
}
return true
}
func validOneOf(value string, allowed ...string) bool {
value = strings.TrimSpace(value)
for _, item := range allowed {
if value == item {
return true
}
}
return false
}
@@ -0,0 +1,190 @@
package httpapi
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) listCatalogProviders(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListCatalogProviders(r.Context())
if err != nil {
s.logger.Error("list catalog providers failed", "error", err)
writeError(w, http.StatusInternalServerError, "list catalog providers failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createCatalogProvider(w http.ResponseWriter, r *http.Request) {
var input store.CatalogProviderInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(input.ProviderKey) == "" || strings.TrimSpace(input.DisplayName) == "" {
writeError(w, http.StatusBadRequest, "providerKey and displayName are required")
return
}
item, err := s.store.CreateCatalogProvider(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "provider key or code already exists")
return
}
s.logger.Error("create catalog provider failed", "error", err)
writeError(w, http.StatusInternalServerError, "create catalog provider failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateCatalogProvider(w http.ResponseWriter, r *http.Request) {
var input store.CatalogProviderInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if strings.TrimSpace(input.ProviderKey) == "" || strings.TrimSpace(input.DisplayName) == "" {
writeError(w, http.StatusBadRequest, "providerKey and displayName are required")
return
}
item, err := s.store.UpdateCatalogProvider(r.Context(), r.PathValue("providerID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "catalog provider not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "provider key or code already exists")
return
}
s.logger.Error("update catalog provider failed", "error", err)
writeError(w, http.StatusInternalServerError, "update catalog provider failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteCatalogProvider(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteCatalogProvider(r.Context(), r.PathValue("providerID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "catalog provider not found")
return
}
s.logger.Error("delete catalog provider failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete catalog provider failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) listBaseModels(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListBaseModels(r.Context())
if err != nil {
s.logger.Error("list base models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list base models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createBaseModel(w http.ResponseWriter, r *http.Request) {
var input store.BaseModelInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validBaseModelInput(input) {
writeError(w, http.StatusBadRequest, "providerKey, providerModelName and modelType are required")
return
}
item, err := s.store.CreateBaseModel(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "canonical model key already exists")
return
}
s.logger.Error("create base model failed", "error", err)
writeError(w, http.StatusInternalServerError, "create base model failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateBaseModel(w http.ResponseWriter, r *http.Request) {
var input store.BaseModelInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validBaseModelInput(input) {
writeError(w, http.StatusBadRequest, "providerKey, providerModelName and modelType are required")
return
}
item, err := s.store.UpdateBaseModel(r.Context(), r.PathValue("baseModelID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "canonical model key already exists")
return
}
s.logger.Error("update base model failed", "error", err)
writeError(w, http.StatusInternalServerError, "update base model failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) resetBaseModel(w http.ResponseWriter, r *http.Request) {
item, err := s.store.ResetBaseModelToDefault(r.Context(), r.PathValue("baseModelID"))
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
}
if errors.Is(err, store.ErrProtectedDefault) {
writeError(w, http.StatusConflict, "base model has no system default snapshot")
return
}
s.logger.Error("reset base model failed", "error", err)
writeError(w, http.StatusInternalServerError, "reset base model failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) resetAllBaseModels(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ResetAllBaseModelsToDefault(r.Context())
if err != nil {
s.logger.Error("reset all base models failed", "error", err)
writeError(w, http.StatusInternalServerError, "reset all base models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) deleteBaseModel(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteBaseModel(r.Context(), r.PathValue("baseModelID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
}
s.logger.Error("delete base model failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete base model failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validBaseModelInput(input store.BaseModelInput) bool {
return strings.TrimSpace(input.ProviderKey) != "" &&
strings.TrimSpace(input.ProviderModelName) != "" &&
len(input.ModelType) > 0
}
@@ -0,0 +1,510 @@
package httpapi
import (
"bytes"
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestCoreLocalFlow(t *testing.T) {
databaseURL := strings.TrimSpace(os.Getenv("AI_GATEWAY_TEST_DATABASE_URL"))
if databaseURL == "" {
t.Skip("set AI_GATEWAY_TEST_DATABASE_URL to run the PostgreSQL integration flow")
}
ctx := context.Background()
applyMigration(t, ctx, databaseURL)
db, err := store.Connect(ctx, databaseURL)
if err != nil {
t.Fatalf("connect store: %v", err)
}
defer db.Close()
handler := NewServer(config.Config{
AppEnv: "test",
HTTPAddr: ":0",
DatabaseURL: databaseURL,
IdentityMode: "hybrid",
JWTSecret: "test-secret",
TaskProgressCallbackEnabled: true,
TaskProgressCallbackURL: "http://callback.local/task-progress",
CORSAllowedOrigin: "*",
}, db, slog.New(slog.NewTextHandler(io.Discard, nil)))
server := httptest.NewServer(handler)
defer server.Close()
suffix := time.Now().UnixNano()
suffixText := strconv.FormatInt(suffix, 10)
username := "smoke_admin_" + suffixText
password := "password123"
var registerResponse struct {
AccessToken string `json:"accessToken"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/register", "", map[string]any{
"username": username,
"email": username + "@example.com",
"password": password,
"tenantKey": "manual-" + suffixText,
"tenantName": "Manual Tenant",
}, http.StatusCreated, &registerResponse)
if registerResponse.AccessToken == "" {
t.Fatal("register did not return access token")
}
var duplicateResponse map[string]any
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/register", "", map[string]any{
"username": username,
"email": username + "@example.com",
"password": password,
}, http.StatusConflict, &duplicateResponse)
if errorBody, ok := duplicateResponse["error"].(map[string]any); !ok || errorBody["message"] != "user already exists" {
t.Fatalf("unexpected duplicate response: %+v", duplicateResponse)
}
var loginResponse struct {
AccessToken string `json:"accessToken"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/login", "", map[string]any{
"account": username,
"password": password,
}, http.StatusOK, &loginResponse)
if loginResponse.AccessToken == "" {
t.Fatal("login did not return access token")
}
var apiKeyResponse struct {
Secret string `json:"secret"`
APIKey struct {
ID string `json:"id"`
Name string `json:"name"`
KeyPrefix string `json:"keyPrefix"`
Status string `json:"status"`
} `json:"apiKey"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/api-keys", loginResponse.AccessToken, map[string]any{
"name": "smoke key",
"scopes": []string{"chat", "image", "video"},
}, http.StatusCreated, &apiKeyResponse)
if !strings.HasPrefix(apiKeyResponse.Secret, "sk-gw-") || apiKeyResponse.APIKey.Status != "active" {
t.Fatalf("unexpected api key response: %+v", apiKeyResponse)
}
var me map[string]any
doJSON(t, server.URL, http.MethodGet, "/api/v1/me", apiKeyResponse.Secret, nil, http.StatusOK, &me)
if me["apiKeyId"] == "" {
t.Fatalf("api key auth did not expose apiKeyId: %+v", me)
}
if me["tenantKey"] != "default" {
t.Fatalf("register should ignore public tenant fields and use default tenant: %+v", me)
}
testPool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatalf("connect test pool: %v", err)
}
defer testPool.Close()
if _, err := testPool.Exec(ctx, `UPDATE gateway_users SET roles = '["admin"]'::jsonb WHERE username = $1`, username); err != nil {
t.Fatalf("promote smoke user: %v", err)
}
doJSON(t, server.URL, http.MethodGet, "/api/admin/models", apiKeyResponse.Secret, nil, http.StatusForbidden, nil)
inviteCode := "INVITE-" + suffixText
if _, err := testPool.Exec(ctx, `
INSERT INTO gateway_invitations (invite_code, max_uses, metadata)
VALUES ($1, 5, '{"purpose":"core-flow"}'::jsonb)`, inviteCode); err != nil {
t.Fatalf("insert invitation: %v", err)
}
invitedUsername := "smoke_invited_" + suffixText
var invitedRegisterResponse struct {
AccessToken string `json:"accessToken"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/auth/register", "", map[string]any{
"username": invitedUsername,
"email": invitedUsername + "@example.com",
"password": password,
"tenantKey": "manual-invited-" + suffixText,
"tenantName": "Manual Invited Tenant",
"invitationCode": inviteCode,
}, http.StatusCreated, &invitedRegisterResponse)
var invitedMe map[string]any
doJSON(t, server.URL, http.MethodGet, "/api/v1/me", invitedRegisterResponse.AccessToken, nil, http.StatusOK, &invitedMe)
if invitedMe["tenantKey"] != "default" {
t.Fatalf("invitation should not change tenant context: %+v", invitedMe)
}
var usedCount int
if err := testPool.QueryRow(ctx, `SELECT used_count FROM gateway_invitations WHERE invite_code = $1`, inviteCode).Scan(&usedCount); err != nil {
t.Fatalf("read invitation used_count: %v", err)
}
if usedCount != 1 {
t.Fatalf("invitation used_count = %d, want 1", usedCount)
}
var userMetadata []byte
if err := testPool.QueryRow(ctx, `SELECT metadata FROM gateway_users WHERE username = $1`, invitedUsername).Scan(&userMetadata); err != nil {
t.Fatalf("read invited user metadata: %v", err)
}
var metadata map[string]any
if err := json.Unmarshal(userMetadata, &metadata); err != nil {
t.Fatalf("decode invited user metadata: %v", err)
}
registration, ok := metadata["registration"].(map[string]any)
if !ok || registration["invitationCode"] != inviteCode {
t.Fatalf("invitation relationship was not recorded: %+v", metadata)
}
var platform struct {
ID string `json:"id"`
Provider string `json:"provider"`
PlatformKey string `json:"platformKey"`
Status string `json:"status"`
}
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
"provider": "openai",
"platformKey": "openai-smoke-" + suffixText,
"name": "OpenAI Smoke",
"baseUrl": "https://api.openai.com/v1",
"authType": "bearer",
"credentials": map[string]any{"mode": "simulation"},
"config": map[string]any{"testMode": true},
}, http.StatusCreated, &platform)
if platform.ID == "" || platform.Status != "enabled" {
t.Fatalf("unexpected platform response: %+v", platform)
}
var baseModels struct {
Items []struct {
ID string `json:"id"`
CanonicalModelKey string `json:"canonicalModelKey"`
ProviderModelName string `json:"providerModelName"`
ModelType []string `json:"modelType"`
} `json:"items"`
}
doJSON(t, server.URL, http.MethodGet, "/api/admin/catalog/base-models", loginResponse.AccessToken, nil, http.StatusOK, &baseModels)
if len(baseModels.Items) < 300 {
t.Fatalf("server-main seed should include the migrated base model catalog: got %d", len(baseModels.Items))
}
baseModelInput := map[string]any{
"providerKey": "openai",
"canonicalModelKey": "openai:smoke-base-" + suffixText,
"providerModelName": "smoke-base-" + suffixText,
"modelType": []string{"text_generate"},
"modelAlias": "Smoke Base Model",
"capabilities": map[string]any{"originalTypes": []string{"text_generate"}},
"metadata": map[string]any{"source": "test"},
}
var createdBaseModel struct {
ID string `json:"id"`
CanonicalModelKey string `json:"canonicalModelKey"`
ModelAlias string `json:"modelAlias"`
}
doJSON(t, server.URL, http.MethodPost, "/api/admin/catalog/base-models", loginResponse.AccessToken, baseModelInput, http.StatusCreated, &createdBaseModel)
if createdBaseModel.ID == "" || createdBaseModel.CanonicalModelKey != baseModelInput["canonicalModelKey"] {
t.Fatalf("unexpected created base model: %+v", createdBaseModel)
}
baseModelInput["modelAlias"] = "Smoke Base Model Updated"
var updatedBaseModel struct {
ModelAlias string `json:"modelAlias"`
}
doJSON(t, server.URL, http.MethodPatch, "/api/admin/catalog/base-models/"+createdBaseModel.ID, loginResponse.AccessToken, baseModelInput, http.StatusOK, &updatedBaseModel)
if updatedBaseModel.ModelAlias != "Smoke Base Model Updated" {
t.Fatalf("unexpected updated base model: %+v", updatedBaseModel)
}
doJSON(t, server.URL, http.MethodDelete, "/api/admin/catalog/base-models/"+createdBaseModel.ID, loginResponse.AccessToken, nil, http.StatusNoContent, nil)
var taskResponse struct {
Task struct {
ID string `json:"id"`
Status string `json:"status"`
RunMode string `json:"runMode"`
APIKeyID string `json:"apiKeyId"`
APIKeyName string `json:"apiKeyName"`
RequestID string `json:"requestId"`
ResolvedModel string `json:"resolvedModel"`
Usage map[string]any `json:"usage"`
Metrics map[string]any `json:"metrics"`
BillingSummary map[string]any `json:"billingSummary"`
FinalChargeAmount float64 `json:"finalChargeAmount"`
ResponseStartedAt string `json:"responseStartedAt"`
ResponseFinishedAt string `json:"responseFinishedAt"`
ResponseDurationMS int64 `json:"responseDurationMs"`
Result map[string]any `json:"result"`
} `json:"task"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/chat/completions", apiKeyResponse.Secret, map[string]any{
"model": "gpt-4o-mini",
"runMode": "simulation",
"simulation": true,
"simulationDurationMs": 5,
"messages": []map[string]any{{"role": "user", "content": "ping"}},
}, http.StatusAccepted, &taskResponse)
if taskResponse.Task.ID == "" || taskResponse.Task.Status != "succeeded" || taskResponse.Task.RunMode != "simulation" {
t.Fatalf("unexpected task response: %+v", taskResponse.Task)
}
if taskResponse.Task.APIKeyID != apiKeyResponse.APIKey.ID || taskResponse.Task.APIKeyName != apiKeyResponse.APIKey.Name {
t.Fatalf("task should record full api key identity: %+v key=%+v", taskResponse.Task, apiKeyResponse.APIKey)
}
if taskResponse.Task.RequestID == "" || taskResponse.Task.ResolvedModel == "" || taskResponse.Task.ResponseStartedAt == "" || taskResponse.Task.ResponseFinishedAt == "" {
t.Fatalf("task should record provider request and response timing: %+v", taskResponse.Task)
}
if taskResponse.Task.Usage["totalTokens"] == nil || taskResponse.Task.FinalChargeAmount <= 0 {
t.Fatalf("task should record token usage and final charge: %+v", taskResponse.Task)
}
if taskResponse.Task.BillingSummary["finalCharge"] == nil || taskResponse.Task.Metrics["requestedModel"] == nil {
t.Fatalf("task should record billing summary and task metrics: %+v", taskResponse.Task)
}
var compatChat map[string]any
doJSON(t, server.URL, http.MethodPost, "/v1/chat/completions", apiKeyResponse.Secret, map[string]any{
"model": "gpt-4o-mini",
"runMode": "simulation",
"messages": []map[string]any{{"role": "user", "content": "ping"}},
"simulation": true,
"simulationDurationMs": 5,
}, http.StatusOK, &compatChat)
if compatChat["object"] != "chat.completion" {
t.Fatalf("unexpected compatible chat response: %+v", compatChat)
}
var imageResponse struct {
Task struct {
ID string `json:"id"`
Status string `json:"status"`
Result map[string]any `json:"result"`
} `json:"task"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/images/generations", apiKeyResponse.Secret, map[string]any{
"model": "gpt-image-1",
"runMode": "simulation",
"prompt": "a tiny gateway console",
"size": "1024x1024",
"quality": "medium",
"simulation": true,
"simulationDurationMs": 5,
}, http.StatusAccepted, &imageResponse)
if imageResponse.Task.Status != "succeeded" || imageResponse.Task.Result["id"] == "" {
t.Fatalf("unexpected image generation task: %+v", imageResponse.Task)
}
var imageEditResponse struct {
Task struct {
ID string `json:"id"`
Status string `json:"status"`
Result map[string]any `json:"result"`
} `json:"task"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/images/edits", apiKeyResponse.Secret, map[string]any{
"model": "gpt-image-1",
"runMode": "simulation",
"prompt": "replace background with clean studio light",
"image": "https://example.com/source.png",
"mask": "https://example.com/mask.png",
"simulation": true,
"simulationDurationMs": 5,
}, http.StatusAccepted, &imageEditResponse)
if imageEditResponse.Task.Status != "succeeded" || imageEditResponse.Task.Result["id"] == "" {
t.Fatalf("unexpected image edit task: %+v", imageEditResponse.Task)
}
failoverModel := "phase1-failover-" + suffixText
var failedPlatform struct {
ID string `json:"id"`
}
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
"provider": "openai",
"platformKey": "openai-fail-" + suffixText,
"name": "OpenAI Retryable Failure",
"baseUrl": "https://api.openai.com/v1",
"authType": "bearer",
"credentials": map[string]any{"mode": "simulation", "simulationFailure": "retryable_failure"},
"priority": 10,
}, http.StatusCreated, &failedPlatform)
var successPlatform struct {
ID string `json:"id"`
}
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms", loginResponse.AccessToken, map[string]any{
"provider": "openai",
"platformKey": "openai-success-" + suffixText,
"name": "OpenAI Retry Success",
"baseUrl": "https://api.openai.com/v1",
"authType": "bearer",
"credentials": map[string]any{"mode": "simulation"},
"priority": 20,
}, http.StatusCreated, &successPlatform)
for _, platformID := range []string{failedPlatform.ID, successPlatform.ID} {
var platformModel map[string]any
doJSON(t, server.URL, http.MethodPost, "/api/admin/platforms/"+platformID+"/models", loginResponse.AccessToken, map[string]any{
"canonicalModelKey": "openai:gpt-4o-mini",
"modelName": failoverModel,
"modelAlias": failoverModel,
"modelType": "chat",
"displayName": "Failover Smoke",
"retryPolicy": map[string]any{"enabled": true, "maxAttempts": 2},
}, http.StatusCreated, &platformModel)
if platformModel["id"] == "" {
t.Fatalf("platform model was not created: %+v", platformModel)
}
}
var failoverTask struct {
Task struct {
ID string `json:"id"`
Status string `json:"status"`
} `json:"task"`
}
doJSON(t, server.URL, http.MethodPost, "/api/v1/chat/completions", apiKeyResponse.Secret, map[string]any{
"model": failoverModel,
"runMode": "simulation",
"messages": []map[string]any{{"role": "user", "content": "retry please"}},
}, http.StatusAccepted, &failoverTask)
if failoverTask.Task.Status != "succeeded" {
t.Fatalf("failover task should succeed through second client: %+v", failoverTask.Task)
}
var taskDetail struct {
ID string `json:"id"`
Status string `json:"status"`
APIKeyName string `json:"apiKeyName"`
RequestID string `json:"requestId"`
Usage map[string]any `json:"usage"`
BillingSummary map[string]any `json:"billingSummary"`
FinalChargeAmount float64 `json:"finalChargeAmount"`
Result map[string]any `json:"result"`
}
doJSON(t, server.URL, http.MethodGet, "/api/v1/tasks/"+taskResponse.Task.ID, apiKeyResponse.Secret, nil, http.StatusOK, &taskDetail)
if taskDetail.Status != "succeeded" || taskDetail.Result["id"] == "" {
t.Fatalf("unexpected task detail: %+v", taskDetail)
}
if taskDetail.APIKeyName != apiKeyResponse.APIKey.Name || taskDetail.RequestID == "" || taskDetail.Usage["totalTokens"] == nil || taskDetail.FinalChargeAmount <= 0 {
t.Fatalf("task detail should expose enriched record fields: %+v", taskDetail)
}
req, err := http.NewRequest(http.MethodGet, server.URL+"/api/v1/tasks/"+taskResponse.Task.ID+"/events", nil)
if err != nil {
t.Fatalf("build events request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+apiKeyResponse.Secret)
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("events request: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK || !bytes.Contains(body, []byte("task.completed")) {
t.Fatalf("unexpected events response status=%d body=%s", resp.StatusCode, string(body))
}
if !bytes.Contains(body, []byte("task.progress")) {
t.Fatalf("events response should include progress events body=%s", string(body))
}
req, err = http.NewRequest(http.MethodGet, server.URL+"/api/v1/tasks/"+failoverTask.Task.ID+"/events", nil)
if err != nil {
t.Fatalf("build failover events request: %v", err)
}
req.Header.Set("Authorization", "Bearer "+apiKeyResponse.Secret)
resp, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failover events request: %v", err)
}
defer resp.Body.Close()
body, _ = io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK || !bytes.Contains(body, []byte("task.retrying")) {
t.Fatalf("failover events should include retrying event status=%d body=%s", resp.StatusCode, string(body))
}
var callbackRows int
if err := testPool.QueryRow(ctx, `SELECT count(*) FROM gateway_task_callback_outbox WHERE task_id = $1::uuid`, taskResponse.Task.ID).Scan(&callbackRows); err != nil {
t.Fatalf("read callback outbox: %v", err)
}
if callbackRows == 0 {
t.Fatal("task progress callback outbox should receive events")
}
}
func TestOriginAllowedSupportsCommaSeparatedOrigins(t *testing.T) {
allowed := "http://localhost:5178, http://127.0.0.1:5178"
if !originAllowed("http://localhost:5178", allowed) {
t.Fatal("localhost origin should be allowed")
}
if !originAllowed("http://127.0.0.1:5178", allowed) {
t.Fatal("127.0.0.1 origin should be allowed")
}
if originAllowed("http://127.0.0.1:5179", allowed) {
t.Fatal("unexpected origin should not be allowed")
}
}
func applyMigration(t *testing.T, ctx context.Context, databaseURL string) {
t.Helper()
_, filename, _, _ := runtime.Caller(0)
migrationFiles, err := filepath.Glob(filepath.Join(filepath.Dir(filename), "..", "..", "migrations", "*.sql"))
if err != nil {
t.Fatalf("read migration files: %v", err)
}
sort.Strings(migrationFiles)
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatalf("connect migration db: %v", err)
}
defer pool.Close()
for _, migrationPath := range migrationFiles {
migration, err := os.ReadFile(migrationPath)
if err != nil {
t.Fatalf("read migration %s: %v", filepath.Base(migrationPath), err)
}
if _, err := pool.Exec(ctx, string(migration)); err != nil {
t.Fatalf("apply migration %s: %v", filepath.Base(migrationPath), err)
}
}
}
func doJSON(t *testing.T, baseURL string, method string, path string, token string, payload any, expectedStatus int, out any) {
t.Helper()
var body io.Reader
if payload != nil {
raw, err := json.Marshal(payload)
if err != nil {
t.Fatalf("marshal payload: %v", err)
}
body = bytes.NewReader(raw)
}
req, err := http.NewRequest(method, baseURL+path, body)
if err != nil {
t.Fatalf("build request: %v", err)
}
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("%s %s: %v", method, path, err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode != expectedStatus {
t.Fatalf("%s %s status=%d want=%d body=%s", method, path, resp.StatusCode, expectedStatus, string(raw))
}
if out != nil && len(raw) > 0 {
if err := json.Unmarshal(raw, out); err != nil {
t.Fatalf("decode %s %s response: %v body=%s", method, path, err, string(raw))
}
}
}
+472 -42
View File
@@ -2,8 +2,10 @@ package httpapi
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
@@ -12,9 +14,10 @@ import (
func (s *Server) health(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"service": "easyai-ai-gateway",
"env": s.cfg.AppEnv,
"ok": true,
"service": "easyai-ai-gateway",
"env": s.cfg.AppEnv,
"identityMode": s.cfg.IdentityMode,
})
}
@@ -31,6 +34,104 @@ func (s *Server) me(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, user)
}
func (s *Server) register(w http.ResponseWriter, r *http.Request) {
if !s.localIdentityEnabled() {
writeError(w, http.StatusForbidden, "local registration is disabled")
return
}
var input store.LocalRegisterInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
user, err := s.store.RegisterLocalUser(r.Context(), input)
if err != nil {
if errors.Is(err, store.ErrWeakPassword) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if errors.Is(err, store.ErrInvalidInvitation) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if errors.Is(err, store.ErrUserAlreadyExists) {
writeError(w, http.StatusConflict, err.Error())
return
}
s.logger.Error("register local user failed", "error", err)
writeError(w, http.StatusInternalServerError, "register local user failed")
return
}
s.writeAuthResponse(w, http.StatusCreated, user)
}
func (s *Server) login(w http.ResponseWriter, r *http.Request) {
if !s.localIdentityEnabled() {
writeError(w, http.StatusForbidden, "local login is disabled")
return
}
var input store.LocalLoginInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
user, err := s.store.AuthenticateLocalUser(r.Context(), input)
if err != nil {
if errors.Is(err, store.ErrInvalidCredentials) {
writeError(w, http.StatusUnauthorized, "invalid account or password")
return
}
s.logger.Error("login local user failed", "error", err)
writeError(w, http.StatusInternalServerError, "login failed")
return
}
s.writeAuthResponse(w, http.StatusOK, user)
}
func (s *Server) localIdentityEnabled() bool {
mode := strings.ToLower(strings.TrimSpace(s.cfg.IdentityMode))
return mode == "" || mode == "standalone" || mode == "hybrid"
}
func (s *Server) writeAuthResponse(w http.ResponseWriter, status int, user store.GatewayUser) {
authUser := authUserFromGatewayUser(user)
const ttl = 24 * time.Hour
token, err := s.auth.SignJWT(authUser, ttl)
if err != nil {
s.logger.Error("sign local jwt failed", "error", err)
writeError(w, http.StatusInternalServerError, "token sign failed")
return
}
writeJSON(w, status, map[string]any{
"accessToken": token,
"tokenType": "Bearer",
"expiresIn": int(ttl.Seconds()),
"user": authUser,
})
}
func authUserFromGatewayUser(user store.GatewayUser) *auth.User {
roles := user.Roles
if len(roles) == 0 {
roles = []string{"user"}
}
tenantID := user.TenantID
if tenantID == "" {
tenantID = user.TenantKey
}
return &auth.User{
ID: user.ID,
Username: user.Username,
Roles: roles,
TenantID: tenantID,
GatewayTenantID: user.GatewayTenantID,
TenantKey: user.TenantKey,
Source: "gateway",
GatewayUserID: user.ID,
UserGroupID: user.DefaultUserGroupID,
}
}
func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
platforms, err := s.store.ListPlatforms(r.Context())
if err != nil {
@@ -41,12 +142,42 @@ func (s *Server) listPlatforms(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"items": platforms})
}
func (s *Server) listPlayablePlatforms(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
if err != nil {
s.logger.Error("list playable platform models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list playable platforms failed")
return
}
allowedPlatformIDs := map[string]bool{}
for _, model := range models {
allowedPlatformIDs[model.PlatformID] = true
}
platforms, err := s.store.ListPlatforms(r.Context())
if err != nil {
s.logger.Error("list platforms failed", "error", err)
writeError(w, http.StatusInternalServerError, "list playable platforms failed")
return
}
filtered := platforms[:0]
for _, platform := range platforms {
if platform.Status == "enabled" && allowedPlatformIDs[platform.ID] {
filtered = append(filtered, platform)
}
}
writeJSON(w, http.StatusOK, map[string]any{"items": filtered})
}
func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
var input store.CreatePlatformInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
input.Provider = strings.TrimSpace(input.Provider)
input.Name = strings.TrimSpace(input.Name)
input.InternalName = strings.TrimSpace(input.InternalName)
if input.Provider == "" || input.Name == "" {
writeError(w, http.StatusBadRequest, "provider and name are required")
return
@@ -63,6 +194,119 @@ func (s *Server) createPlatform(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, platform)
}
func (s *Server) updatePlatform(w http.ResponseWriter, r *http.Request) {
var input store.CreatePlatformInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
input.Provider = strings.TrimSpace(input.Provider)
input.Name = strings.TrimSpace(input.Name)
input.InternalName = strings.TrimSpace(input.InternalName)
if input.Provider == "" || input.Name == "" {
writeError(w, http.StatusBadRequest, "provider and name are required")
return
}
if input.AuthType == "" {
input.AuthType = "bearer"
}
platform, err := s.store.UpdatePlatform(r.Context(), r.PathValue("platformID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "platform not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "platform key already exists")
return
}
s.logger.Error("update platform failed", "error", err)
writeError(w, http.StatusInternalServerError, "update platform failed")
return
}
writeJSON(w, http.StatusOK, platform)
}
func (s *Server) deletePlatform(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeletePlatform(r.Context(), r.PathValue("platformID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "platform not found")
return
}
s.logger.Error("delete platform failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete platform failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) createPlatformModel(w http.ResponseWriter, r *http.Request) {
var input store.CreatePlatformModelInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if pathPlatformID := r.PathValue("platformID"); pathPlatformID != "" {
input.PlatformID = pathPlatformID
}
if input.PlatformID == "" {
writeError(w, http.StatusBadRequest, "platformId is required")
return
}
model, err := s.store.CreatePlatformModel(r.Context(), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
}
s.logger.Error("create platform model failed", "error", err)
writeError(w, http.StatusInternalServerError, "create platform model failed")
return
}
writeJSON(w, http.StatusCreated, model)
}
func (s *Server) replacePlatformModels(w http.ResponseWriter, r *http.Request) {
platformID := r.PathValue("platformID")
if platformID == "" {
writeError(w, http.StatusBadRequest, "platformId is required")
return
}
var input struct {
Models []store.CreatePlatformModelInput `json:"models"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
models, err := s.store.ReplacePlatformModels(r.Context(), platformID, input.Models)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "base model not found")
return
}
s.logger.Error("replace platform models failed", "error", err)
writeError(w, http.StatusInternalServerError, "replace platform models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": models})
}
func (s *Server) deletePlatformModel(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeletePlatformModel(r.Context(), r.PathValue("modelID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "platform model not found")
return
}
s.logger.Error("delete platform model failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete platform model failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
models, err := s.store.ListModels(r.Context())
if err != nil {
@@ -73,24 +317,15 @@ func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"items": models})
}
func (s *Server) listCatalogProviders(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListCatalogProviders(r.Context())
func (s *Server) listPlayableModels(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
models, err := s.store.ListAccessiblePlatformModels(r.Context(), user)
if err != nil {
s.logger.Error("list catalog providers failed", "error", err)
writeError(w, http.StatusInternalServerError, "list catalog providers failed")
s.logger.Error("list playable models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list playable models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listBaseModels(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListBaseModels(r.Context())
if err != nil {
s.logger.Error("list base models failed", "error", err)
writeError(w, http.StatusInternalServerError, "list base models failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
writeJSON(w, http.StatusOK, map[string]any{"items": models})
}
func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
@@ -103,17 +338,147 @@ func (s *Server) listPricingRules(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listTenants(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListTenants(r.Context())
if err != nil {
s.logger.Error("list tenants failed", "error", err)
writeError(w, http.StatusInternalServerError, "list tenants failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listUsers(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListUsers(r.Context())
if err != nil {
s.logger.Error("list users failed", "error", err)
writeError(w, http.StatusInternalServerError, "list users failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listUserGroups(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListUserGroups(r.Context())
if err != nil {
s.logger.Error("list user groups failed", "error", err)
writeError(w, http.StatusInternalServerError, "list user groups failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listAPIKeys(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
items, err := s.store.ListAPIKeys(r.Context(), user)
if err != nil {
s.logger.Error("list api keys failed", "error", err)
writeError(w, http.StatusInternalServerError, "list api keys failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) listPlayableAPIKeys(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
items, err := s.store.ListPlayableAPIKeys(r.Context(), user)
if err != nil {
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
s.logger.Error("list playable api keys failed", "error", err)
writeError(w, http.StatusInternalServerError, "list playable api keys failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createAPIKey(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
var input store.CreateAPIKeyInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
created, err := s.store.CreateAPIKey(r.Context(), input, user)
if err != nil {
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
s.logger.Error("create api key failed", "error", err)
writeError(w, http.StatusInternalServerError, "create api key failed")
return
}
writeJSON(w, http.StatusCreated, created)
}
func (s *Server) disableAPIKey(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
item, err := s.store.DisableAPIKey(r.Context(), r.PathValue("apiKeyID"), user)
if err == nil {
writeJSON(w, http.StatusOK, item)
return
}
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "api key not found")
return
}
s.logger.Error("disable api key failed", "error", err)
writeError(w, http.StatusInternalServerError, "disable api key failed")
}
func (s *Server) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
err := s.store.DeleteAPIKey(r.Context(), r.PathValue("apiKeyID"), user)
if err == nil {
w.WriteHeader(http.StatusNoContent)
return
}
if errors.Is(err, store.ErrLocalUserRequired) {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "api key not found")
return
}
s.logger.Error("delete api key failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete api key failed")
}
func (s *Server) estimatePricing(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
writeJSON(w, http.StatusOK, map[string]any{
"items": []any{},
"resolver": "effective-pricing-placeholder",
"request": body,
})
model, _ := body["model"].(string)
kind, _ := body["kind"].(string)
if kind == "" {
kind = "chat.completions"
}
if model == "" {
writeError(w, http.StatusBadRequest, "model is required")
return
}
estimate, err := s.runner.Estimate(r.Context(), kind, model, body, user)
if err != nil {
if errors.Is(err, store.ErrNoModelCandidate) {
writeError(w, http.StatusNotFound, "no enabled platform model matches request")
return
}
s.logger.Error("estimate pricing failed", "error", err)
writeError(w, http.StatusInternalServerError, "estimate pricing failed")
return
}
writeJSON(w, http.StatusOK, estimate)
}
func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) {
@@ -126,7 +491,7 @@ func (s *Server) listRateLimitWindows(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createTask(kind string) http.Handler {
func (s *Server) createTask(kind string, compatible bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := auth.UserFromContext(r.Context())
if !ok {
@@ -148,6 +513,7 @@ func (s *Server) createTask(kind string) http.Handler {
task, err := s.store.CreateTask(r.Context(), store.CreateTaskInput{
Kind: kind,
Model: model,
RunMode: runModeFromRequest(body),
Request: body,
}, user)
if err != nil {
@@ -155,9 +521,44 @@ func (s *Server) createTask(kind string) http.Handler {
writeError(w, http.StatusInternalServerError, "create task failed")
return
}
if compatible {
if boolValue(body, "stream") {
flusher := prepareCompatibleStream(w)
result, runErr := s.runner.ExecuteStream(r.Context(), task, user, func(delta string) error {
writeCompatibleDelta(w, kind, model, delta)
if flusher != nil {
flusher.Flush()
}
return nil
})
if runErr != nil {
sendSSE(w, "error", map[string]any{"error": map[string]any{"message": runErr.Error(), "status": statusFromRunError(runErr)}})
if flusher != nil {
flusher.Flush()
}
return
}
writeCompatibleDone(w, kind, model, result.Output)
if flusher != nil {
flusher.Flush()
}
return
}
result, runErr := s.runner.Execute(r.Context(), task, user)
if runErr != nil {
writeError(w, statusFromRunError(runErr), runErr.Error())
return
}
writeJSON(w, http.StatusOK, result.Output)
return
}
result, runErr := s.runner.Execute(r.Context(), task, user)
if runErr != nil {
s.logger.Warn("task completed with failure", "kind", kind, "taskId", task.ID, "error", runErr)
}
writeJSON(w, http.StatusAccepted, map[string]any{
"task": task,
"task": result.Task,
"next": map[string]string{
"events": fmt.Sprintf("/api/v1/tasks/%s/events", task.ID),
"detail": fmt.Sprintf("/api/v1/tasks/%s", task.ID),
@@ -166,6 +567,22 @@ func (s *Server) createTask(kind string) http.Handler {
})
}
func statusFromRunError(err error) int {
switch {
case errors.Is(err, store.ErrNoModelCandidate):
return http.StatusNotFound
case errors.Is(err, store.ErrRateLimited):
return http.StatusTooManyRequests
default:
return http.StatusBadGateway
}
}
func boolValue(body map[string]any, key string) bool {
value, _ := body[key].(bool)
return value
}
func (s *Server) getTask(w http.ResponseWriter, r *http.Request) {
task, err := s.store.GetTask(r.Context(), r.PathValue("taskID"))
if err == nil {
@@ -195,24 +612,37 @@ func (s *Server) taskEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
sendSSE(w, "task.accepted", map[string]any{
"taskId": task.ID,
"status": task.Status,
})
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
timer := time.NewTimer(250 * time.Millisecond)
defer timer.Stop()
select {
case <-r.Context().Done():
events, err := s.store.ListTaskEvents(r.Context(), task.ID)
if err != nil {
s.logger.Error("list task events failed", "error", err)
return
case <-timer.C:
sendSSE(w, "task.placeholder", map[string]any{
"taskId": task.ID,
"message": "runtime worker is not wired yet",
}
for _, event := range events {
sendSSE(w, event.EventType, event)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
if len(events) == 0 {
sendSSE(w, "task.accepted", map[string]any{
"taskId": task.ID,
"status": task.Status,
})
}
}
func runModeFromRequest(body map[string]any) string {
if value, ok := body["runMode"].(string); ok {
return value
}
if value, ok := body["mode"].(string); ok {
return value
}
if value, ok := body["simulation"].(bool); ok && value {
return "simulation"
}
if value, ok := body["testMode"].(bool); ok && value {
return "simulation"
}
return ""
}
@@ -0,0 +1,223 @@
package httpapi
import (
"encoding/json"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) createTenant(w http.ResponseWriter, r *http.Request) {
var input store.GatewayTenantInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validTenantInput(input) {
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
return
}
item, err := s.store.CreateTenant(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
return
}
s.logger.Error("create tenant failed", "error", err)
writeError(w, http.StatusInternalServerError, "create tenant failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateTenant(w http.ResponseWriter, r *http.Request) {
var input store.GatewayTenantInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validTenantInput(input) {
writeError(w, http.StatusBadRequest, "tenantKey and name are required")
return
}
item, err := s.store.UpdateTenant(r.Context(), r.PathValue("tenantID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "tenant not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "tenant key or external tenant id already exists")
return
}
s.logger.Error("update tenant failed", "error", err)
writeError(w, http.StatusInternalServerError, "update tenant failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteTenant(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteTenant(r.Context(), r.PathValue("tenantID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "tenant not found")
return
}
s.logger.Error("delete tenant failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete tenant failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) createGatewayUser(w http.ResponseWriter, r *http.Request) {
var input store.GatewayUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validGatewayUserInput(input) {
writeError(w, http.StatusBadRequest, "username is required")
return
}
if !validOptionalPassword(input.Password) {
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
return
}
item, err := s.store.CreateGatewayUser(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
return
}
s.logger.Error("create gateway user failed", "error", err)
writeError(w, http.StatusInternalServerError, "create gateway user failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateGatewayUser(w http.ResponseWriter, r *http.Request) {
var input store.GatewayUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validGatewayUserInput(input) {
writeError(w, http.StatusBadRequest, "username is required")
return
}
if !validOptionalPassword(input.Password) {
writeError(w, http.StatusBadRequest, store.ErrWeakPassword.Error())
return
}
item, err := s.store.UpdateGatewayUser(r.Context(), r.PathValue("userID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "user not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "user key, email or external user id already exists")
return
}
s.logger.Error("update gateway user failed", "error", err)
writeError(w, http.StatusInternalServerError, "update gateway user failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteGatewayUser(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteGatewayUser(r.Context(), r.PathValue("userID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "user not found")
return
}
s.logger.Error("delete gateway user failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete gateway user failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (s *Server) createUserGroup(w http.ResponseWriter, r *http.Request) {
var input store.UserGroupInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validUserGroupInput(input) {
writeError(w, http.StatusBadRequest, "groupKey and name are required")
return
}
item, err := s.store.CreateUserGroup(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "user group key already exists")
return
}
s.logger.Error("create user group failed", "error", err)
writeError(w, http.StatusInternalServerError, "create user group failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateUserGroup(w http.ResponseWriter, r *http.Request) {
var input store.UserGroupInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validUserGroupInput(input) {
writeError(w, http.StatusBadRequest, "groupKey and name are required")
return
}
item, err := s.store.UpdateUserGroup(r.Context(), r.PathValue("groupID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "user group not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "user group key already exists")
return
}
s.logger.Error("update user group failed", "error", err)
writeError(w, http.StatusInternalServerError, "update user group failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteUserGroup(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteUserGroup(r.Context(), r.PathValue("groupID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "user group not found")
return
}
s.logger.Error("delete user group failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete user group failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validTenantInput(input store.GatewayTenantInput) bool {
return strings.TrimSpace(input.TenantKey) != "" && strings.TrimSpace(input.Name) != ""
}
func validGatewayUserInput(input store.GatewayUserInput) bool {
return strings.TrimSpace(input.Username) != ""
}
func validOptionalPassword(password string) bool {
password = strings.TrimSpace(password)
return password == "" || len(password) >= 8
}
func validUserGroupInput(input store.UserGroupInput) bool {
return strings.TrimSpace(input.GroupKey) != "" && strings.TrimSpace(input.Name) != ""
}
@@ -0,0 +1,99 @@
package httpapi
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) listPricingRuleSets(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListPricingRuleSets(r.Context())
if err != nil {
s.logger.Error("list pricing rule sets failed", "error", err)
writeError(w, http.StatusInternalServerError, "list pricing rule sets failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createPricingRuleSet(w http.ResponseWriter, r *http.Request) {
var input store.PricingRuleSetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validPricingRuleSetInput(input) {
writeError(w, http.StatusBadRequest, "ruleSetKey, name and at least one rule are required")
return
}
item, err := s.store.CreatePricingRuleSet(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "pricing rule set key already exists")
return
}
s.logger.Error("create pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "create pricing rule set failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updatePricingRuleSet(w http.ResponseWriter, r *http.Request) {
var input store.PricingRuleSetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validPricingRuleSetInput(input) {
writeError(w, http.StatusBadRequest, "ruleSetKey, name and at least one rule are required")
return
}
item, err := s.store.UpdatePricingRuleSet(r.Context(), r.PathValue("ruleSetID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "pricing rule set not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "pricing rule set key already exists")
return
}
s.logger.Error("update pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "update pricing rule set failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deletePricingRuleSet(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeletePricingRuleSet(r.Context(), r.PathValue("ruleSetID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "pricing rule set not found")
return
}
if errors.Is(err, store.ErrProtectedDefault) {
writeError(w, http.StatusForbidden, "default pricing rule set cannot be deleted")
return
}
s.logger.Error("delete pricing rule set failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete pricing rule set failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validPricingRuleSetInput(input store.PricingRuleSetInput) bool {
if strings.TrimSpace(input.RuleSetKey) == "" || strings.TrimSpace(input.Name) == "" || len(input.Rules) == 0 {
return false
}
for _, rule := range input.Rules {
if strings.TrimSpace(rule.ResourceType) == "" || strings.TrimSpace(rule.Unit) == "" {
return false
}
}
return true
}
@@ -0,0 +1,91 @@
package httpapi
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Server) listRuntimePolicySets(w http.ResponseWriter, r *http.Request) {
items, err := s.store.ListRuntimePolicySets(r.Context())
if err != nil {
s.logger.Error("list runtime policy sets failed", "error", err)
writeError(w, http.StatusInternalServerError, "list runtime policy sets failed")
return
}
writeJSON(w, http.StatusOK, map[string]any{"items": items})
}
func (s *Server) createRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
var input store.RuntimePolicySetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validRuntimePolicyInput(input) {
writeError(w, http.StatusBadRequest, "policyKey and name are required")
return
}
item, err := s.store.CreateRuntimePolicySet(r.Context(), input)
if err != nil {
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "runtime policy key already exists")
return
}
s.logger.Error("create runtime policy set failed", "error", err)
writeError(w, http.StatusInternalServerError, "create runtime policy set failed")
return
}
writeJSON(w, http.StatusCreated, item)
}
func (s *Server) updateRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
var input store.RuntimePolicySetInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "invalid json body")
return
}
if !validRuntimePolicyInput(input) {
writeError(w, http.StatusBadRequest, "policyKey and name are required")
return
}
item, err := s.store.UpdateRuntimePolicySet(r.Context(), r.PathValue("policySetID"), input)
if err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "runtime policy set not found")
return
}
if store.IsUniqueViolation(err) {
writeError(w, http.StatusConflict, "runtime policy key already exists")
return
}
s.logger.Error("update runtime policy set failed", "error", err)
writeError(w, http.StatusInternalServerError, "update runtime policy set failed")
return
}
writeJSON(w, http.StatusOK, item)
}
func (s *Server) deleteRuntimePolicySet(w http.ResponseWriter, r *http.Request) {
if err := s.store.DeleteRuntimePolicySet(r.Context(), r.PathValue("policySetID")); err != nil {
if store.IsNotFound(err) {
writeError(w, http.StatusNotFound, "runtime policy set not found")
return
}
if errors.Is(err, store.ErrProtectedDefault) {
writeError(w, http.StatusForbidden, "default runtime policy set cannot be deleted")
return
}
s.logger.Error("delete runtime policy set failed", "error", err)
writeError(w, http.StatusInternalServerError, "delete runtime policy set failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
func validRuntimePolicyInput(input store.RuntimePolicySetInput) bool {
return strings.TrimSpace(input.PolicyKey) != "" && strings.TrimSpace(input.Name) != ""
}
+101 -12
View File
@@ -7,6 +7,7 @@ import (
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/runner"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
@@ -14,6 +15,7 @@ type Server struct {
cfg config.Config
store *store.Store
auth *auth.Authenticator
runner *runner.Service
logger *slog.Logger
}
@@ -22,40 +24,117 @@ func NewServer(cfg config.Config, db *store.Store, logger *slog.Logger) http.Han
cfg: cfg,
store: db,
auth: auth.New(cfg.JWTSecret, cfg.ServerMainBaseURL, cfg.ServerMainInternalToken),
runner: runner.New(cfg, db, logger),
logger: logger,
}
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", server.health)
mux.HandleFunc("GET /readyz", server.ready)
mux.HandleFunc("GET /static/simulation/{asset}", serveSimulationAsset)
mux.Handle("POST /api/v1/auth/register", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.register)))
mux.Handle("POST /api/v1/auth/login", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.login)))
mux.Handle("GET /api/v1/me", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.me)))
mux.Handle("GET /api/v1/catalog/providers", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("GET /api/v1/catalog/base-models", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
mux.Handle("GET /api/v1/pricing/rules", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
mux.Handle("GET /api/v1/public/catalog/providers", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("GET /api/v1/public/catalog/base-models", server.auth.Require(auth.PermissionPublic, http.HandlerFunc(server.listBaseModels)))
mux.Handle("GET /api/admin/catalog/providers", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listCatalogProviders)))
mux.Handle("POST /api/admin/catalog/providers", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createCatalogProvider)))
mux.Handle("PATCH /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateCatalogProvider)))
mux.Handle("DELETE /api/admin/catalog/providers/{providerID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteCatalogProvider)))
mux.Handle("GET /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listBaseModels)))
mux.Handle("POST /api/admin/catalog/base-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createBaseModel)))
mux.Handle("POST /api/admin/catalog/base-models/reset-all", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetAllBaseModels)))
mux.Handle("PATCH /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateBaseModel)))
mux.Handle("POST /api/admin/catalog/base-models/{baseModelID}/reset", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.resetBaseModel)))
mux.Handle("DELETE /api/admin/catalog/base-models/{baseModelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteBaseModel)))
mux.Handle("GET /api/admin/tenants", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listTenants)))
mux.Handle("POST /api/admin/tenants", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createTenant)))
mux.Handle("PATCH /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateTenant)))
mux.Handle("DELETE /api/admin/tenants/{tenantID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteTenant)))
mux.Handle("GET /api/admin/users", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUsers)))
mux.Handle("POST /api/admin/users", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createGatewayUser)))
mux.Handle("PATCH /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateGatewayUser)))
mux.Handle("DELETE /api/admin/users/{userID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteGatewayUser)))
mux.Handle("GET /api/admin/user-groups", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listUserGroups)))
mux.Handle("POST /api/admin/user-groups", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createUserGroup)))
mux.Handle("PATCH /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateUserGroup)))
mux.Handle("DELETE /api/admin/user-groups/{groupID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteUserGroup)))
mux.Handle("GET /api/admin/access-rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listAccessRules)))
mux.Handle("POST /api/admin/access-rules", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createAccessRule)))
mux.Handle("POST /api/admin/access-rules/batch", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.batchAccessRules)))
mux.Handle("PATCH /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateAccessRule)))
mux.Handle("DELETE /api/admin/access-rules/{ruleID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteAccessRule)))
mux.Handle("GET /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeys)))
mux.Handle("POST /api/v1/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.createAPIKey)))
mux.Handle("GET /api/v1/api-keys/access-rules", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listAPIKeyAccessRules)))
mux.Handle("POST /api/v1/api-keys/access-rules/batch", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.batchAPIKeyAccessRules)))
mux.Handle("PATCH /api/v1/api-keys/{apiKeyID}/disable", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.disableAPIKey)))
mux.Handle("DELETE /api/v1/api-keys/{apiKeyID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.deleteAPIKey)))
mux.Handle("GET /api/playground/api-keys", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableAPIKeys)))
mux.Handle("GET /api/admin/pricing/rules", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRules)))
mux.Handle("GET /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPricingRuleSets)))
mux.Handle("POST /api/admin/pricing/rule-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPricingRuleSet)))
mux.Handle("PATCH /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePricingRuleSet)))
mux.Handle("DELETE /api/admin/pricing/rule-sets/{ruleSetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePricingRuleSet)))
mux.Handle("POST /api/v1/pricing/estimate", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.estimatePricing)))
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
mux.Handle("POST /api/v1/platforms", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.createPlatform)))
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listModels)))
mux.Handle("GET /api/v1/runtime/rate-limit-windows", server.auth.Require(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions")))
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations")))
mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations")))
mux.Handle("GET /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRuntimePolicySets)))
mux.Handle("POST /api/admin/runtime/policy-sets", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createRuntimePolicySet)))
mux.Handle("PATCH /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateRuntimePolicySet)))
mux.Handle("DELETE /api/admin/runtime/policy-sets/{policySetID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deleteRuntimePolicySet)))
mux.Handle("GET /api/admin/platforms", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listPlatforms)))
mux.Handle("POST /api/admin/platforms", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatform)))
mux.Handle("PATCH /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updatePlatform)))
mux.Handle("DELETE /api/admin/platforms/{platformID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatform)))
mux.Handle("PUT /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.replacePlatformModels)))
mux.Handle("POST /api/admin/platforms/{platformID}/models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
mux.Handle("POST /api/admin/platform-models", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.createPlatformModel)))
mux.Handle("DELETE /api/admin/platform-models/{modelID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.deletePlatformModel)))
mux.Handle("GET /api/admin/models", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listModels)))
mux.Handle("GET /api/v1/platforms", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayablePlatforms)))
mux.Handle("GET /api/v1/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
mux.Handle("GET /api/v1/playground/models", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.listPlayableModels)))
mux.Handle("GET /api/admin/runtime/rate-limit-windows", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.listRateLimitWindows)))
mux.Handle("POST /api/v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", false)))
mux.Handle("POST /api/v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", false)))
mux.Handle("POST /api/v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", false)))
mux.Handle("POST /api/v1/images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", false)))
mux.Handle("POST /api/v1/videos/generations", server.auth.Require(auth.PermissionBasic, server.createTask("videos.generations", false)))
mux.Handle("GET /api/v1/tasks/{taskID}", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.getTask)))
mux.Handle("GET /api/v1/tasks/{taskID}/events", server.auth.Require(auth.PermissionBasic, http.HandlerFunc(server.taskEvents)))
mux.Handle("POST /chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", true)))
mux.Handle("POST /v1/chat/completions", server.auth.Require(auth.PermissionBasic, server.createTask("chat.completions", true)))
mux.Handle("POST /responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
mux.Handle("POST /v1/responses", server.auth.Require(auth.PermissionBasic, server.createTask("responses", true)))
mux.Handle("POST /images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
mux.Handle("POST /v1/images/generations", server.auth.Require(auth.PermissionBasic, server.createTask("images.generations", true)))
mux.Handle("POST /images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", true)))
mux.Handle("POST /v1/images/edits", server.auth.Require(auth.PermissionBasic, server.createTask("images.edits", true)))
return server.recover(server.cors(mux))
}
func (s *Server) requireAdmin(permission auth.Permission, next http.Handler) http.Handler {
return s.auth.Require(permission, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, _ := auth.UserFromContext(r.Context())
if user != nil && strings.TrimSpace(user.APIKeyID) != "" {
writeError(w, http.StatusForbidden, "admin api does not accept api key credentials")
return
}
next.ServeHTTP(w, r)
}))
}
func (s *Server) cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" && (s.cfg.CORSAllowedOrigin == "*" || strings.EqualFold(origin, s.cfg.CORSAllowedOrigin)) {
if origin != "" && originAllowed(origin, s.cfg.CORSAllowedOrigin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Comfy-Api-Key")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
@@ -65,6 +144,16 @@ func (s *Server) cors(next http.Handler) http.Handler {
})
}
func originAllowed(origin string, allowed string) bool {
for _, item := range strings.Split(allowed, ",") {
item = strings.TrimSpace(item)
if item == "*" || strings.EqualFold(origin, item) {
return true
}
}
return false
}
func (s *Server) recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
@@ -0,0 +1,49 @@
package httpapi
import (
"bytes"
"encoding/base64"
"net/http"
"strings"
"time"
)
const simulationImageSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop stop-color="#111827"/><stop offset="0.52" stop-color="#2563eb"/><stop offset="1" stop-color="#14b8a6"/></linearGradient></defs><rect width="1024" height="1024" rx="96" fill="url(#g)"/><circle cx="784" cy="220" r="104" fill="#ffffff" opacity="0.16"/><rect x="168" y="260" width="688" height="504" rx="48" fill="#ffffff" opacity="0.15"/><path d="M232 672 392 504l120 120 96-104 184 152v48H232z" fill="#ffffff" opacity="0.72"/><circle cx="354" cy="398" r="58" fill="#ffffff" opacity="0.82"/><text x="512" y="832" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="64" font-weight="700" fill="#ffffff">EasyAI Demo</text></svg>`
const simulationImageEditSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop stop-color="#0f172a"/><stop offset="0.48" stop-color="#7c3aed"/><stop offset="1" stop-color="#f97316"/></linearGradient></defs><rect width="1024" height="1024" rx="96" fill="url(#g)"/><rect x="168" y="208" width="456" height="608" rx="44" fill="#ffffff" opacity="0.16"/><rect x="400" y="304" width="456" height="512" rx="44" fill="#ffffff" opacity="0.22"/><path d="M248 678 382 532l92 100 78-84 232 198H248z" fill="#ffffff" opacity="0.76"/><circle cx="342" cy="384" r="54" fill="#ffffff" opacity="0.84"/><text x="512" y="884" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="58" font-weight="700" fill="#ffffff">EasyAI Edit Demo</text></svg>`
const simulationVideoPosterSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 720"><defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="1"><stop stop-color="#111827"/><stop offset="0.5" stop-color="#0ea5e9"/><stop offset="1" stop-color="#22c55e"/></linearGradient></defs><rect width="1280" height="720" fill="url(#g)"/><circle cx="1000" cy="170" r="120" fill="#fff" opacity="0.16"/><path d="M548 250v220l190-110z" fill="#fff" opacity="0.9"/><text x="640" y="602" text-anchor="middle" font-family="Inter,Arial,sans-serif" font-size="64" font-weight="700" fill="#ffffff">EasyAI Video Demo</text></svg>`
const simulationVideoMP4Base64 = "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAANLbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAAZAAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAnZ0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAZAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAKAAAABaAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAGQAAAAAAABAAAAAAHubWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAyAAAAFABVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAABmW1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAVlzdGJsAAAAuXN0c2QAAAAAAAAAAQAAAKlhdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAKAAWgBIAAAASAAAAAAAAAABFUxhdmM2MS4xOS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAAL2F2Y0MBQsAL/+EAGGdCwAvaCjfkwEQAAAMABAAAAwDIPFCqgAEABGjOD8gAAAAQcGFzcAAAAAEAAAABAAAAFGJ0cnQAAAAAAAA7TAAAO0wAAAAYc3R0cwAAAAAAAAABAAAACgAAAgAAAAAUc3RzcwAAAAAAAAABAAAAAQAAABxzdHNjAAAAAAAAAAEAAAABAAAACgAAAAEAAAA8c3RzegAAAAAAAAAAAAAACgAAAp0AAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAKAAAACgAAAAoAAAAUc3RjbwAAAAAAAAABAAADewAAAGF1ZHRhAAAAWW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAG1kaXJhcHBsAAAAAAAAAAAAAAAALGlsc3QAAAAkqXRvbwAAABxkYXRhAAAAAQAAAABMYXZmNjEuNy4xMDAAAAAIZnJlZQAAAv9tZGF0AAACVAYF//9Q3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NCByMzEwOCAzMWUxOWY5IC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyMyAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTAgcmVmPTEgZGVibG9jaz0wOjA6MCBhbmFseXNlPTA6MCBtZT1kaWEgc3VibWU9MCBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0wIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MCA4eDhkY3Q9MCBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0wIHRocmVhZHM9MyBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTAgd2VpZ2h0cD0wIGtleWludD0yNTAga2V5aW50X21pbj0yNSBzY2VuZWN1dD0wIGludHJhX3JlZnJlc2g9MCByYz1jcmYgbWJ0cmVlPTAgY3JmPTIzLjAgcWNvbXA9MC42MCBxcG1pbj0wIHFwbWF4PTY5IHFwc3RlcD00IGlwX3JhdGlvPTEuNDAgYXE9MACAAAAAQWWIhDoRigACA/HAAEH6OAAIJMnJycnJycnJyddddddddddddddddddddddddddddddddddddddddddddddddddeAAAABkGaIC6B7AAAAAZBmkAygewAAAAGQZpgMoHsAAAABkGagDKB7AAAAAZBmqAygewAAAAGQZrANoHsAAAABkGa4DaB7AAAAAZBmwA2gewAAAAGQZsgNoHs"
var simulationVideoMP4 = mustDecodeSimulationAsset(simulationVideoMP4Base64)
func serveSimulationAsset(w http.ResponseWriter, r *http.Request) {
asset := strings.ToLower(strings.TrimSpace(r.PathValue("asset")))
switch asset {
case "image.svg", "image.png":
serveSimulationContent(w, r, "image.svg", "image/svg+xml; charset=utf-8", []byte(simulationImageSVG))
case "image-edit.svg", "image-edit.png":
serveSimulationContent(w, r, "image-edit.svg", "image/svg+xml; charset=utf-8", []byte(simulationImageEditSVG))
case "video-poster.svg":
serveSimulationContent(w, r, "video-poster.svg", "image/svg+xml; charset=utf-8", []byte(simulationVideoPosterSVG))
case "video.mp4":
serveSimulationContent(w, r, "video.mp4", "video/mp4", simulationVideoMP4)
default:
http.NotFound(w, r)
}
}
func serveSimulationContent(w http.ResponseWriter, r *http.Request, name string, contentType string, payload []byte) {
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Header().Set("Content-Type", contentType)
http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(payload))
}
func mustDecodeSimulationAsset(value string) []byte {
decoded, err := base64.StdEncoding.DecodeString(value)
if err != nil {
panic(err)
}
return decoded
}
+84
View File
@@ -0,0 +1,84 @@
package httpapi
import "net/http"
func prepareCompatibleStream(w http.ResponseWriter) http.Flusher {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, _ := w.(http.Flusher)
return flusher
}
func writeCompatibleDelta(w http.ResponseWriter, kind string, model string, content string) {
if kind == "responses" {
sendSSE(w, "response.output_text.delta", map[string]any{"type": "response.output_text.delta", "delta": content})
return
}
sendSSE(w, "message", map[string]any{
"id": "chatcmpl-stream",
"object": "chat.completion.chunk",
"model": model,
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": content}, "finish_reason": nil}},
})
}
func writeCompatibleDone(w http.ResponseWriter, kind string, model string, output map[string]any) {
if kind == "responses" {
sendSSE(w, "response.completed", map[string]any{"type": "response.completed", "response": output})
return
}
sendSSE(w, "message", map[string]any{
"id": firstString(output["id"], "chatcmpl-stream"),
"object": "chat.completion.chunk",
"model": model,
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}},
})
}
func writeCompatibleStream(w http.ResponseWriter, kind string, model string, output map[string]any) {
prepareCompatibleStream(w)
content := extractOutputText(output)
if content == "" {
content = "done"
}
if kind == "responses" {
sendSSE(w, "response.output_text.delta", map[string]any{"type": "response.output_text.delta", "delta": content})
sendSSE(w, "response.completed", map[string]any{"type": "response.completed", "response": output})
return
}
sendSSE(w, "message", map[string]any{
"id": output["id"],
"object": "chat.completion.chunk",
"model": model,
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{"content": content}, "finish_reason": nil}},
})
sendSSE(w, "message", map[string]any{
"id": output["id"],
"object": "chat.completion.chunk",
"model": model,
"choices": []any{map[string]any{"index": 0, "delta": map[string]any{}, "finish_reason": "stop"}},
})
}
func firstString(value any, fallback string) string {
if text, ok := value.(string); ok && text != "" {
return text
}
return fallback
}
func extractOutputText(output map[string]any) string {
if text, ok := output["output_text"].(string); ok {
return text
}
choices, _ := output["choices"].([]any)
for _, rawChoice := range choices {
choice, _ := rawChoice.(map[string]any)
message, _ := choice["message"].(map[string]any)
if content, ok := message["content"].(string); ok {
return content
}
}
return ""
}
+45
View File
@@ -0,0 +1,45 @@
package runner
import (
"math"
"strings"
)
func stringFromMap(values map[string]any, key string) string {
value, _ := values[key].(string)
return strings.TrimSpace(value)
}
func stringFromAny(value any) string {
text, _ := value.(string)
return strings.TrimSpace(text)
}
func boolFromMap(values map[string]any, key string) bool {
value, _ := values[key].(bool)
return value
}
func intFromPolicy(values map[string]any, key string) int {
switch value := values[key].(type) {
case int:
return value
case float64:
return int(math.Round(value))
default:
return 0
}
}
func floatFromAny(value any) float64 {
switch typed := value.(type) {
case int:
return float64(typed)
case int64:
return float64(typed)
case float64:
return typed
default:
return 0
}
}
+89
View File
@@ -0,0 +1,89 @@
package runner
import (
"context"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
func (s *Service) rateLimitReservations(ctx context.Context, user *auth.User, candidate store.RuntimeModelCandidate, body map[string]any) []store.RateLimitReservation {
out := make([]store.RateLimitReservation, 0)
out = append(out, reservationsFromPolicy("platform_model", candidate.PlatformModelID, effectiveRateLimitPolicy(candidate), body)...)
if group, err := s.store.ResolveUserGroupPolicy(ctx, user); err == nil && group.ID != "" {
out = append(out, reservationsFromPolicy("user_group", group.ID, group.RateLimitPolicy, body)...)
}
return out
}
func effectiveRateLimitPolicy(candidate store.RuntimeModelCandidate) map[string]any {
if hasRules(candidate.ModelRateLimitPolicy) {
return candidate.ModelRateLimitPolicy
}
if hasRules(candidate.PlatformRateLimitPolicy) {
return candidate.PlatformRateLimitPolicy
}
return nil
}
func reservationsFromPolicy(scopeType string, scopeKey string, policy map[string]any, body map[string]any) []store.RateLimitReservation {
if scopeKey == "" || !hasRules(policy) {
return nil
}
rules, _ := policy["rules"].([]any)
out := make([]store.RateLimitReservation, 0, len(rules))
estimatedTokens := estimateRequestTokens(body)
for _, rawRule := range rules {
rule, _ := rawRule.(map[string]any)
metric := strings.TrimSpace(stringFromMap(rule, "metric"))
limit := floatFromAny(rule["limit"])
amount := 1.0
if strings.HasPrefix(metric, "tpm") {
amount = float64(estimatedTokens)
}
out = append(out, store.RateLimitReservation{
ScopeType: scopeType,
ScopeKey: scopeKey,
Metric: metric,
Limit: limit,
Amount: amount,
WindowSeconds: int(floatFromAny(rule["windowSeconds"])),
LeaseTTLSeconds: int(floatFromAny(rule["leaseTtlSeconds"])),
})
}
return out
}
func hasRules(policy map[string]any) bool {
rules, _ := policy["rules"].([]any)
return len(rules) > 0
}
func estimateRequestTokens(body map[string]any) int {
text := ""
if prompt := stringFromMap(body, "prompt"); prompt != "" {
text += prompt
}
if input := stringFromMap(body, "input"); input != "" {
text += input
}
if messages, ok := body["messages"].([]any); ok {
for _, raw := range messages {
message, _ := raw.(map[string]any)
switch content := message["content"].(type) {
case string:
text += content
case []any:
for _, rawPart := range content {
part, _ := rawPart.(map[string]any)
text += stringFromMap(part, "text")
}
}
}
}
if text == "" {
return 1
}
return len([]rune(text))/4 + 1
}
+209
View File
@@ -0,0 +1,209 @@
package runner
import (
"context"
"math"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type EstimateResult struct {
Items []any `json:"items"`
Resolver string `json:"resolver"`
}
func (s *Service) Estimate(ctx context.Context, kind string, model string, body map[string]any, user *auth.User) (EstimateResult, error) {
candidates, err := s.store.ListModelCandidates(ctx, model, modelTypeFromKind(kind), user)
if err != nil {
return EstimateResult{}, err
}
candidate := candidates[0]
usage := clients.Usage{InputTokens: estimateRequestTokens(body), OutputTokens: int(floatFromAny(body["max_tokens"]))}
if usage.OutputTokens == 0 {
usage.OutputTokens = 64
}
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
response := clients.Response{Usage: usage, Result: map[string]any{"usage": map[string]any{
"prompt_tokens": usage.InputTokens,
"completion_tokens": usage.OutputTokens,
"total_tokens": usage.TotalTokens,
}}}
return EstimateResult{
Items: s.billings(ctx, user, kind, body, candidate, response, true),
Resolver: "effective-pricing-v1",
}, nil
}
func (s *Service) billings(ctx context.Context, user *auth.User, kind string, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) []any {
config := effectiveBillingConfig(candidate)
discount := effectiveDiscount(ctx, s.store, user, candidate)
if isTextGenerationKind(kind) {
inputTokens := response.Usage.InputTokens
outputTokens := response.Usage.OutputTokens
if inputTokens == 0 && outputTokens == 0 {
inputTokens = estimateRequestTokens(body)
outputTokens = 1
}
inputAmount := roundPrice(float64(inputTokens) / 1000 * resourcePrice(config, "text", "textInputPer1k", "inputTokenPrice", "basePrice") * discount)
outputAmount := roundPrice(float64(outputTokens) / 1000 * resourcePrice(config, "text", "textOutputPer1k", "outputTokenPrice", "basePrice") * discount)
return []any{
billingLine(candidate, "text_input", "1k_tokens", inputTokens, inputAmount, discount, simulated),
billingLine(candidate, "text_output", "1k_tokens", outputTokens, outputAmount, discount, simulated),
}
}
count := int(floatFromAny(body["n"]))
if count <= 0 {
count = 1
}
resource := "image"
unit := "image"
baseKey := "imageBase"
if kind == "images.edits" {
resource = "image_edit"
baseKey = "editBase"
}
if kind == "videos.generations" {
resource = "video"
unit = "video"
baseKey = "videoBase"
}
amount := float64(count) * resourcePrice(config, resource, baseKey, "basePrice") * resourceWeight(config, resource, "qualityWeights", stringFromMap(body, "quality")) * resourceWeight(config, resource, "sizeWeights", stringFromMap(body, "size")) * resourceWeight(config, resource, "resolutionWeights", firstNonEmptyString(stringFromMap(body, "resolution"), stringFromMap(body, "size"))) * discount
return []any{billingLine(candidate, resource, unit, count, roundPrice(amount), discount, simulated)}
}
func effectiveBillingConfig(candidate store.RuntimeModelCandidate) map[string]any {
base := candidate.BaseBillingConfig
if len(candidate.BillingConfig) > 0 {
base = candidate.BillingConfig
}
if len(candidate.BillingConfigOverride) > 0 {
base = mergeMap(base, candidate.BillingConfigOverride)
}
return base
}
func effectiveDiscount(ctx context.Context, db *store.Store, user *auth.User, candidate store.RuntimeModelCandidate) float64 {
discount := candidate.DefaultDiscountFactor
if candidate.DiscountFactor > 0 {
discount = candidate.DiscountFactor
}
if discount <= 0 {
discount = 1
}
if group, err := db.ResolveUserGroupPolicy(ctx, user); err == nil {
groupDiscount := floatFromAny(group.BillingDiscountPolicy["discountFactor"])
if groupDiscount > 0 {
discount *= groupDiscount
}
}
return discount
}
func billingLine(candidate store.RuntimeModelCandidate, resourceType string, unit string, quantity any, amount float64, discount float64, simulated bool) map[string]any {
return map[string]any{
"model": candidate.ModelName,
"modelAlias": candidate.ModelAlias,
"provider": candidate.Provider,
"platformId": candidate.PlatformID,
"platformModelId": candidate.PlatformModelID,
"resourceType": resourceType,
"unit": unit,
"quantity": quantity,
"amount": amount,
"currency": "resource",
"discountFactor": discount,
"simulated": simulated,
}
}
func price(config map[string]any, key string) float64 {
value := floatFromAny(config[key])
if value > 0 {
return value
}
return 0
}
func resourcePrice(config map[string]any, resource string, keys ...string) float64 {
for _, key := range keys {
if value := price(config, key); value > 0 {
return value
}
}
if resourceConfig, ok := config[resource].(map[string]any); ok {
for _, key := range keys {
if value := floatFromAny(resourceConfig[key]); value > 0 {
return value
}
}
if value := floatFromAny(resourceConfig["basePrice"]); value > 0 {
return value
}
}
if resource == "image_edit" {
return resourcePrice(config, "image", keys...)
}
return 0
}
func weighted(config map[string]any, key string, name string) float64 {
if strings.TrimSpace(name) == "" {
return 1
}
weights, _ := config[key].(map[string]any)
if value := floatFromAny(weights[name]); value > 0 {
return value
}
return 1
}
func resourceWeight(config map[string]any, resource string, key string, name string) float64 {
if value := weighted(config, key, name); value != 1 {
return value
}
if strings.TrimSpace(name) == "" {
return 1
}
resourceConfig, _ := config[resource].(map[string]any)
if len(resourceConfig) == 0 && resource == "image_edit" {
resourceConfig, _ = config["image"].(map[string]any)
}
if weights, ok := resourceConfig["dynamicWeight"].(map[string]any); ok {
if value := floatFromAny(weights[name]); value > 0 {
return value
}
}
if weights, ok := resourceConfig[key].(map[string]any); ok {
if value := floatFromAny(weights[name]); value > 0 {
return value
}
}
return 1
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
return ""
}
func roundPrice(value float64) float64 {
return math.Round(value*1000000) / 1000000
}
func mergeMap(base map[string]any, override map[string]any) map[string]any {
out := map[string]any{}
for key, value := range base {
out[key] = value
}
for key, value := range override {
out[key] = value
}
return out
}
+221
View File
@@ -0,0 +1,221 @@
package runner
import (
"strings"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type taskRecordDetails struct {
RequestID string
ResolvedModel string
Usage map[string]any
Metrics map[string]any
BillingSummary map[string]any
FinalChargeAmount float64
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
}
func buildSuccessRecord(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, billings []any, simulated bool) taskRecordDetails {
usage := usageToMap(response.Usage)
metrics := taskMetrics(task, user, body, candidate, response, simulated)
summary := summarizeBillings(billings, simulated)
finalAmount := floatFromAny(summary["totalAmount"])
return taskRecordDetails{
RequestID: response.RequestID,
ResolvedModel: candidate.ModelName,
Usage: usage,
Metrics: metrics,
BillingSummary: summary,
FinalChargeAmount: finalAmount,
ResponseStartedAt: response.ResponseStartedAt,
ResponseFinishedAt: response.ResponseFinishedAt,
ResponseDurationMS: response.ResponseDurationMS,
}
}
func taskMetrics(task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, response clients.Response, simulated bool) map[string]any {
metrics := map[string]any{
"kind": task.Kind,
"runMode": task.RunMode,
"requestedModel": task.Model,
"resolvedModel": candidate.ModelName,
"modelAlias": candidate.ModelAlias,
"providerModel": candidate.ProviderModelName,
"canonicalModel": candidate.CanonicalModelKey,
"modelType": candidate.ModelType,
"provider": candidate.Provider,
"platformId": candidate.PlatformID,
"platformName": candidate.PlatformName,
"platformModelId": candidate.PlatformModelID,
"clientId": candidate.ClientID,
"queueKey": candidate.QueueKey,
"requestId": response.RequestID,
"simulated": simulated,
}
if user != nil {
metrics["apiKeyId"] = user.APIKeyID
metrics["apiKeyName"] = user.APIKeyName
metrics["apiKeyPrefix"] = user.APIKeyPrefix
}
if response.ResponseDurationMS > 0 {
metrics["responseDurationMs"] = response.ResponseDurationMS
}
switch task.Kind {
case "chat.completions", "responses":
metrics["stream"] = boolFromMap(body, "stream")
metrics["messageCount"] = messageCount(body)
copyIfPresent(metrics, body, "temperature")
copyIfPresent(metrics, body, "max_tokens")
copyIfPresent(metrics, body, "max_output_tokens")
case "images.generations", "images.edits":
metrics["imageCount"] = requestedCount(body)
metrics["outputImageCount"] = outputDataCount(response.Result)
metrics["inputImageCount"] = imageInputCount(body)
metrics["hasMask"] = stringFromMap(body, "mask") != ""
copyIfPresent(metrics, body, "size")
copyIfPresent(metrics, body, "quality")
copyIfPresent(metrics, body, "style")
case "videos.generations":
metrics["hasReferenceImage"] = imageInputCount(body) > 0
metrics["hasReferenceVideo"] = hasAnyString(body, "video", "video_url", "videoUrl", "reference_video", "referenceVideo")
copyIfPresent(metrics, body, "duration")
copyIfPresent(metrics, body, "resolution")
copyIfPresent(metrics, body, "size")
copyIfPresent(metrics, body, "aspect_ratio")
copyIfPresent(metrics, body, "aspectRatio")
copyIfPresent(metrics, body, "fps")
}
return metrics
}
func usageToMap(usage clients.Usage) map[string]any {
out := map[string]any{}
if usage.InputTokens > 0 {
out["inputTokens"] = usage.InputTokens
out["promptTokens"] = usage.InputTokens
}
if usage.OutputTokens > 0 {
out["outputTokens"] = usage.OutputTokens
out["completionTokens"] = usage.OutputTokens
}
if usage.TotalTokens > 0 {
out["totalTokens"] = usage.TotalTokens
}
return out
}
func summarizeBillings(billings []any, simulated bool) map[string]any {
amountByCurrency := map[string]float64{}
for _, raw := range billings {
line, _ := raw.(map[string]any)
if line == nil {
continue
}
currency := strings.TrimSpace(stringFromAny(line["currency"]))
if currency == "" {
currency = "resource"
}
amountByCurrency[currency] = roundPrice(amountByCurrency[currency] + floatFromAny(line["amount"]))
}
currency := ""
totalAmount := 0.0
for key, amount := range amountByCurrency {
if currency == "" {
currency = key
} else if currency != key {
currency = "mixed"
}
totalAmount += amount
}
if currency == "" {
currency = "resource"
}
totalAmount = roundPrice(totalAmount)
return map[string]any{
"lineCount": len(billings),
"totalAmount": totalAmount,
"amountByCurrency": amountByCurrency,
"currency": currency,
"simulated": simulated,
"finalCharge": map[string]any{
"amount": totalAmount,
"currency": currency,
"simulated": simulated,
},
}
}
func failureMetrics(err error, simulated bool) (string, map[string]any, time.Time, time.Time, int64) {
meta := clients.ErrorResponseMetadata(err)
metrics := map[string]any{
"simulated": simulated,
}
if err != nil {
metrics["error"] = err.Error()
metrics["retryable"] = clients.IsRetryable(err)
}
if meta.StatusCode > 0 {
metrics["statusCode"] = meta.StatusCode
}
if meta.RequestID != "" {
metrics["requestId"] = meta.RequestID
}
if meta.ResponseDurationMS > 0 {
metrics["responseDurationMs"] = meta.ResponseDurationMS
}
return meta.RequestID, metrics, meta.ResponseStartedAt, meta.ResponseFinishedAt, meta.ResponseDurationMS
}
func messageCount(body map[string]any) int {
messages, _ := body["messages"].([]any)
return len(messages)
}
func requestedCount(body map[string]any) int {
count := int(floatFromAny(body["n"]))
if count <= 0 {
return 1
}
return count
}
func outputDataCount(result map[string]any) int {
data, _ := result["data"].([]any)
return len(data)
}
func imageInputCount(body map[string]any) int {
count := 0
for _, key := range []string{"image", "image_url", "imageUrl"} {
if stringFromMap(body, key) != "" {
count++
}
}
for _, key := range []string{"image_urls", "imageUrls", "images"} {
if values, ok := body[key].([]any); ok {
count += len(values)
}
}
return count
}
func hasAnyString(body map[string]any, keys ...string) bool {
for _, key := range keys {
if stringFromMap(body, key) != "" {
return true
}
}
return false
}
func copyIfPresent(target map[string]any, body map[string]any, key string) {
if value, ok := body[key]; ok && value != nil {
target[key] = value
}
}
+403
View File
@@ -0,0 +1,403 @@
package runner
import (
"context"
"errors"
"log/slog"
"net/http"
"strings"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/config"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type Service struct {
cfg config.Config
store *store.Store
logger *slog.Logger
clients map[string]clients.Client
}
type Result struct {
Task store.GatewayTask
Output map[string]any
}
func New(cfg config.Config, db *store.Store, logger *slog.Logger) *Service {
httpClient := &http.Client{Timeout: 120 * time.Second}
return &Service{
cfg: cfg,
store: db,
logger: logger,
clients: map[string]clients.Client{
"openai": clients.OpenAIClient{HTTPClient: httpClient},
"gemini": clients.GeminiClient{HTTPClient: httpClient},
"volces": clients.VolcesClient{HTTPClient: httpClient},
"simulation": clients.SimulationClient{},
},
}
}
func (s *Service) Execute(ctx context.Context, task store.GatewayTask, user *auth.User) (Result, error) {
return s.execute(ctx, task, user, nil)
}
func (s *Service) ExecuteStream(ctx context.Context, task store.GatewayTask, user *auth.User, onDelta clients.StreamDelta) (Result, error) {
return s.execute(ctx, task, user, onDelta)
}
func (s *Service) execute(ctx context.Context, task store.GatewayTask, user *auth.User, onDelta clients.StreamDelta) (Result, error) {
modelType := modelTypeFromKind(task.Kind)
body := normalizeRequest(task.Kind, task.Request)
if err := validateRequest(task.Kind, body); err != nil {
failed, finishErr := s.failTask(ctx, task.ID, "bad_request", err.Error(), task.RunMode == "simulation", err)
if finishErr != nil {
return Result{}, finishErr
}
return Result{Task: failed, Output: failed.Result}, err
}
candidates, err := s.store.ListModelCandidates(ctx, task.Model, modelType, user)
if err != nil {
failed, finishErr := s.failTask(ctx, task.ID, "no_model_candidate", err.Error(), task.RunMode == "simulation", err)
if finishErr != nil {
return Result{}, finishErr
}
return Result{Task: failed, Output: failed.Result}, err
}
if err := s.store.MarkTaskRunning(ctx, task.ID, modelType, body); err != nil {
return Result{}, err
}
if err := s.emit(ctx, task.ID, "task.progress", "running", "normalizing", 0.15, "request normalized", map[string]any{"modelType": modelType}, task.RunMode == "simulation"); err != nil {
return Result{}, err
}
maxAttempts := maxAttemptsForCandidates(candidates)
var lastErr error
for index, candidate := range candidates {
if index >= maxAttempts {
break
}
attemptNo := index + 1
response, err := s.runCandidate(ctx, task, user, body, candidate, attemptNo, onDelta)
if err == nil {
billings := s.billings(ctx, user, task.Kind, body, candidate, response, isSimulation(task, candidate))
record := buildSuccessRecord(task, user, body, candidate, response, billings, isSimulation(task, candidate))
finished, finishErr := s.store.FinishTaskSuccess(ctx, store.FinishTaskSuccessInput{
TaskID: task.ID,
Result: response.Result,
Billings: billings,
RequestID: record.RequestID,
ResolvedModel: record.ResolvedModel,
Usage: record.Usage,
Metrics: record.Metrics,
BillingSummary: record.BillingSummary,
FinalChargeAmount: record.FinalChargeAmount,
ResponseStartedAt: record.ResponseStartedAt,
ResponseFinishedAt: record.ResponseFinishedAt,
ResponseDurationMS: record.ResponseDurationMS,
})
if finishErr != nil {
return Result{}, finishErr
}
if err := s.emit(ctx, task.ID, "task.completed", "succeeded", "completed", 1, "task completed", map[string]any{
"result": response.Result,
"billings": billings,
"usage": record.Usage,
"metrics": record.Metrics,
"billingSummary": record.BillingSummary,
"requestId": record.RequestID,
}, isSimulation(task, candidate)); err != nil {
return Result{}, err
}
return Result{Task: finished, Output: response.Result}, nil
}
lastErr = err
retryable := clients.IsRetryable(err)
if !retryable || !retryEnabled(candidate) || attemptNo >= maxAttempts {
break
}
if err := s.emit(ctx, task.ID, "task.retrying", "running", "retry", 0.55, "retrying next client", map[string]any{"attempt": attemptNo, "error": err.Error()}, isSimulation(task, candidate)); err != nil {
return Result{}, err
}
}
code := clients.ErrorCode(lastErr)
message := "task failed"
if lastErr != nil {
message = lastErr.Error()
}
failed, err := s.failTask(ctx, task.ID, code, message, task.RunMode == "simulation", lastErr)
if err != nil {
return Result{}, err
}
return Result{Task: failed, Output: failed.Result}, lastErr
}
func (s *Service) runCandidate(ctx context.Context, task store.GatewayTask, user *auth.User, body map[string]any, candidate store.RuntimeModelCandidate, attemptNo int, onDelta clients.StreamDelta) (clients.Response, error) {
simulated := isSimulation(task, candidate)
if err := s.emit(ctx, task.ID, "task.attempt.started", "running", "submitting", 0.25, "client attempt started", map[string]any{"attempt": attemptNo, "clientId": candidate.ClientID}, simulated); err != nil {
return clients.Response{}, err
}
attemptID, err := s.store.CreateTaskAttempt(ctx, store.CreateTaskAttemptInput{
TaskID: task.ID,
AttemptNo: attemptNo,
PlatformID: candidate.PlatformID,
PlatformModelID: candidate.PlatformModelID,
ClientID: candidate.ClientID,
QueueKey: candidate.QueueKey,
Status: "running",
Simulated: simulated,
RequestSnapshot: body,
})
if err != nil {
return clients.Response{}, err
}
reservations := s.rateLimitReservations(ctx, user, candidate, body)
limitResult, err := s.store.ReserveRateLimits(ctx, task.ID, reservations)
if err != nil {
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: false,
Metrics: map[string]any{"error": err.Error(), "candidateModel": candidate.ModelName, "clientId": candidate.ClientID},
ErrorCode: "rate_limit",
ErrorMessage: err.Error(),
})
return clients.Response{}, &clients.ClientError{Code: "rate_limit", Message: err.Error(), Retryable: false}
}
defer s.store.ReleaseConcurrencyLeases(context.WithoutCancel(ctx), limitResult.LeaseIDs)
if err := s.store.RecordClientAssignment(ctx, candidate); err != nil {
return clients.Response{}, err
}
defer s.store.RecordClientRelease(context.WithoutCancel(ctx), candidate.ClientID, "")
client := s.clientFor(candidate, simulated)
callStartedAt := time.Now()
response, err := client.Run(ctx, clients.Request{
Kind: task.Kind,
ModelType: candidate.ModelType,
Model: task.Model,
Body: body,
Candidate: candidate,
Stream: boolFromMap(body, "stream"),
StreamDelta: onDelta,
})
callFinishedAt := time.Now()
if response.ResponseStartedAt.IsZero() {
response.ResponseStartedAt = callStartedAt
}
if response.ResponseFinishedAt.IsZero() {
response.ResponseFinishedAt = callFinishedAt
}
if response.ResponseDurationMS == 0 {
response.ResponseDurationMS = response.ResponseFinishedAt.Sub(response.ResponseStartedAt).Milliseconds()
if response.ResponseDurationMS < 0 {
response.ResponseDurationMS = 0
}
}
if err != nil {
retryable := clients.IsRetryable(err)
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(err, simulated)
if responseStartedAt.IsZero() {
responseStartedAt = callStartedAt
}
if responseFinishedAt.IsZero() {
responseFinishedAt = callFinishedAt
}
if responseDurationMS == 0 {
responseDurationMS = responseFinishedAt.Sub(responseStartedAt).Milliseconds()
if responseDurationMS < 0 {
responseDurationMS = 0
}
}
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: retryable,
RequestID: requestID,
Metrics: metrics,
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS,
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
})
_ = s.emit(ctx, task.ID, "task.attempt.failed", "running", "attempt_failed", 0.45, err.Error(), map[string]any{"attempt": attemptNo, "retryable": retryable, "requestId": requestID, "metrics": metrics}, simulated)
return clients.Response{}, err
}
uploadedResult, err := s.uploadGeneratedAssets(ctx, response.Result)
if err != nil {
metrics := taskMetrics(task, user, body, candidate, response, simulated)
metrics["error"] = err.Error()
metrics["retryable"] = clients.IsRetryable(err)
_ = s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "failed",
Retryable: clients.IsRetryable(err),
RequestID: response.RequestID,
Usage: usageToMap(response.Usage),
Metrics: metrics,
ResponseSnapshot: response.Result,
ResponseStartedAt: response.ResponseStartedAt,
ResponseFinishedAt: response.ResponseFinishedAt,
ResponseDurationMS: response.ResponseDurationMS,
ErrorCode: clients.ErrorCode(err),
ErrorMessage: err.Error(),
})
return clients.Response{}, err
}
response.Result = uploadedResult
for _, progress := range response.Progress {
if err := s.emit(ctx, task.ID, "task.progress", "running", progress.Phase, progress.Progress, progress.Message, progress.Payload, simulated); err != nil {
return clients.Response{}, err
}
}
if err := s.store.FinishTaskAttempt(ctx, store.FinishTaskAttemptInput{
AttemptID: attemptID,
Status: "succeeded",
RequestID: response.RequestID,
Usage: usageToMap(response.Usage),
Metrics: taskMetrics(task, user, body, candidate, response, simulated),
ResponseSnapshot: response.Result,
ResponseStartedAt: response.ResponseStartedAt,
ResponseFinishedAt: response.ResponseFinishedAt,
ResponseDurationMS: response.ResponseDurationMS,
}); err != nil {
return clients.Response{}, err
}
return response, nil
}
func (s *Service) clientFor(candidate store.RuntimeModelCandidate, simulated bool) clients.Client {
if simulated {
return s.clients["simulation"]
}
key := strings.ToLower(strings.TrimSpace(candidate.SpecType))
if key == "" {
key = strings.ToLower(strings.TrimSpace(candidate.Provider))
}
if client, ok := s.clients[key]; ok {
return client
}
return s.clients["openai"]
}
func (s *Service) failTask(ctx context.Context, taskID string, code string, message string, simulated bool, cause error) (store.GatewayTask, error) {
requestID, metrics, responseStartedAt, responseFinishedAt, responseDurationMS := failureMetrics(cause, simulated)
failed, err := s.store.FinishTaskFailure(ctx, store.FinishTaskFailureInput{
TaskID: taskID,
Code: code,
Message: message,
RequestID: requestID,
Metrics: metrics,
ResponseStartedAt: responseStartedAt,
ResponseFinishedAt: responseFinishedAt,
ResponseDurationMS: responseDurationMS,
})
if err != nil {
return store.GatewayTask{}, err
}
if eventErr := s.emit(ctx, taskID, "task.failed", "failed", "failed", 1, message, map[string]any{"code": code, "requestId": requestID, "metrics": metrics}, simulated); eventErr != nil {
return store.GatewayTask{}, eventErr
}
return failed, nil
}
func (s *Service) emit(ctx context.Context, taskID string, eventType string, status string, phase string, progress float64, message string, payload map[string]any, simulated bool) error {
event, err := s.store.AddTaskEvent(ctx, taskID, eventType, status, phase, progress, message, payload, simulated)
if err != nil {
return err
}
if s.cfg.TaskProgressCallbackEnabled {
return s.store.QueueTaskCallback(ctx, event, s.cfg.TaskProgressCallbackURL)
}
return nil
}
func modelTypeFromKind(kind string) string {
switch kind {
case "chat.completions", "responses":
return "text_generate"
case "images.generations", "images.edits":
if kind == "images.edits" {
return "image_edit"
}
return "image_generate"
case "videos.generations":
return "video_generate"
default:
return "task"
}
}
func isTextGenerationKind(kind string) bool {
return kind == "chat.completions" || kind == "responses"
}
func isSimulation(task store.GatewayTask, candidate store.RuntimeModelCandidate) bool {
if task.RunMode == "simulation" {
return true
}
return stringFromMap(candidate.Credentials, "mode") == "simulation" || boolFromMap(candidate.PlatformConfig, "testMode")
}
func retryEnabled(candidate store.RuntimeModelCandidate) bool {
if enabled, ok := candidate.ModelRetryPolicy["enabled"].(bool); ok {
return enabled
}
if enabled, ok := candidate.PlatformRetryPolicy["enabled"].(bool); ok {
return enabled
}
return true
}
func maxAttemptsForCandidates(candidates []store.RuntimeModelCandidate) int {
if len(candidates) == 0 {
return 0
}
maxAttempts := len(candidates)
for _, candidate := range candidates {
if value := intFromPolicy(candidate.ModelRetryPolicy, "maxAttempts"); value > 0 && value < maxAttempts {
maxAttempts = value
}
if value := intFromPolicy(candidate.PlatformRetryPolicy, "maxAttempts"); value > 0 && value < maxAttempts {
maxAttempts = value
}
}
if maxAttempts <= 0 {
return 1
}
return maxAttempts
}
func normalizeRequest(kind string, body map[string]any) map[string]any {
out := map[string]any{}
for key, value := range body {
out[key] = value
}
if kind == "responses" && out["messages"] == nil && out["input"] != nil {
out["messages"] = []any{map[string]any{"role": "user", "content": out["input"]}}
}
return out
}
func validateRequest(kind string, body map[string]any) error {
switch kind {
case "chat.completions":
if body["messages"] == nil {
return errors.New("messages is required")
}
case "responses":
if body["input"] == nil && body["messages"] == nil {
return errors.New("input or messages is required")
}
case "images.generations", "images.edits":
if strings.TrimSpace(stringFromMap(body, "prompt")) == "" {
return errors.New("prompt is required")
}
}
return nil
}
+104
View File
@@ -0,0 +1,104 @@
package runner
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"mime/multipart"
"net/http"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
)
func (s *Service) uploadGeneratedAssets(ctx context.Context, result map[string]any) (map[string]any, error) {
if s.cfg.ServerMainBaseURL == "" || s.cfg.ServerMainInternalToken == "" {
return result, nil
}
data, _ := result["data"].([]any)
if len(data) == 0 {
return result, nil
}
next := map[string]any{}
for key, value := range result {
next[key] = value
}
nextData := make([]any, 0, len(data))
for index, rawItem := range data {
item, _ := rawItem.(map[string]any)
if item == nil {
nextData = append(nextData, rawItem)
continue
}
b64 := stringFromMap(item, "b64_json")
if b64 == "" {
nextData = append(nextData, rawItem)
continue
}
upload, err := s.uploadBase64Image(ctx, b64, index)
if err != nil {
return nil, err
}
merged := map[string]any{}
for key, value := range item {
if key != "b64_json" {
merged[key] = value
}
}
merged["upload"] = upload
if urlValue, ok := upload["url"].(string); ok && urlValue != "" {
merged["url"] = urlValue
}
nextData = append(nextData, merged)
}
next["data"] = nextData
return next, nil
}
func (s *Service) uploadBase64Image(ctx context.Context, b64 string, index int) (map[string]any, error) {
payload, err := base64.StdEncoding.DecodeString(stripDataURLPrefix(b64))
if err != nil {
return nil, &clients.ClientError{Code: "upload_decode_failed", Message: err.Error(), Retryable: false}
}
var body bytes.Buffer
writer := multipart.NewWriter(&body)
fileWriter, err := writer.CreateFormFile("file", fmt.Sprintf("gateway-result-%d.png", index+1))
if err != nil {
return nil, err
}
if _, err := fileWriter.Write(payload); err != nil {
return nil, err
}
_ = writer.WriteField("source", "ai-gateway")
if err := writer.Close(); err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(s.cfg.ServerMainBaseURL, "/")+"/v1/files/upload", &body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
req.Header.Set("Authorization", "Bearer "+s.cfg.ServerMainInternalToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, &clients.ClientError{Code: "upload_network", Message: err.Error(), Retryable: true}
}
defer resp.Body.Close()
var decoded map[string]any
if err := json.NewDecoder(resp.Body).Decode(&decoded); err != nil {
return nil, &clients.ClientError{Code: "upload_invalid_response", Message: err.Error(), Retryable: false}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, &clients.ClientError{Code: "upload_failed", Message: "server-main upload failed", StatusCode: resp.StatusCode, Retryable: resp.StatusCode >= 500}
}
return decoded, nil
}
func stripDataURLPrefix(value string) string {
if index := strings.Index(value, ","); strings.HasPrefix(value, "data:") && index >= 0 {
return value[index+1:]
}
return value
}
+658
View File
@@ -0,0 +1,658 @@
package store
import (
"context"
"encoding/json"
"errors"
"strings"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/jackc/pgx/v5"
)
type AccessRuleInput struct {
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
ResourceType string `json:"resourceType"`
ResourceID string `json:"resourceId"`
Effect string `json:"effect"`
Priority int `json:"priority"`
MinPermissionLevel int `json:"minPermissionLevel"`
Conditions map[string]any `json:"conditions"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type AccessRuleResourceInput struct {
ResourceType string `json:"resourceType"`
ResourceID string `json:"resourceId"`
Priority int `json:"priority"`
MinPermissionLevel int `json:"minPermissionLevel"`
Conditions map[string]any `json:"conditions"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type AccessRuleBatchInput struct {
SubjectType string `json:"subjectType"`
SubjectID string `json:"subjectId"`
Effect string `json:"effect"`
UpsertResources []AccessRuleResourceInput `json:"upsertResources"`
DeleteResources []AccessRuleResourceInput `json:"deleteResources"`
}
type accessRuleResource struct {
Type string
ID string
}
func (s *Store) ListAccessRules(ctx context.Context) ([]AccessRule, error) {
rows, err := s.pool.Query(ctx, `
SELECT `+accessRuleColumns+`
FROM gateway_access_rules
ORDER BY resource_type ASC, priority ASC, subject_type ASC, created_at DESC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]AccessRule, 0)
for rows.Next() {
item, err := scanAccessRule(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) ListAPIKeyAccessRules(ctx context.Context, user *auth.User) ([]AccessRule, error) {
gatewayUserID := localGatewayUserID(user)
if gatewayUserID == "" {
return nil, ErrLocalUserRequired
}
rows, err := s.pool.Query(ctx, `
SELECT `+apiKeyAccessRuleColumns+`
FROM gateway_access_rules ar
JOIN gateway_api_keys k ON k.id = ar.subject_id
WHERE ar.subject_type = 'api_key'
AND k.gateway_user_id = $1::uuid
AND k.deleted_at IS NULL
ORDER BY ar.resource_type ASC, ar.priority ASC, ar.created_at DESC`, gatewayUserID)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]AccessRule, 0)
for rows.Next() {
item, err := scanAccessRule(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) CreateAccessRule(ctx context.Context, input AccessRuleInput) (AccessRule, error) {
input = normalizeAccessRuleInput(input)
conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanAccessRule(s.pool.QueryRow(ctx, `
INSERT INTO gateway_access_rules (
subject_type, subject_id, resource_type, resource_id, effect, priority,
min_permission_level, conditions, metadata, status
)
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
RETURNING `+accessRuleColumns,
input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
))
}
func (s *Store) UpdateAccessRule(ctx context.Context, id string, input AccessRuleInput) (AccessRule, error) {
input = normalizeAccessRuleInput(input)
conditions, _ := json.Marshal(emptyObjectIfNil(input.Conditions))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanAccessRule(s.pool.QueryRow(ctx, `
UPDATE gateway_access_rules
SET subject_type = $2,
subject_id = $3::uuid,
resource_type = $4,
resource_id = $5::uuid,
effect = $6,
priority = $7,
min_permission_level = $8,
conditions = $9::jsonb,
metadata = $10::jsonb,
status = $11,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+accessRuleColumns,
id, input.SubjectType, input.SubjectID, input.ResourceType, input.ResourceID, input.Effect,
input.Priority, input.MinPermissionLevel, string(conditions), string(metadata), input.Status,
))
}
func (s *Store) DeleteAccessRule(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `DELETE FROM gateway_access_rules WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) BatchAccessRules(ctx context.Context, input AccessRuleBatchInput) ([]AccessRule, error) {
input = normalizeAccessRuleBatchInput(input)
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
for _, resource := range dedupeAccessRuleResources(input.DeleteResources) {
resource = normalizeAccessRuleResource(resource, input.Effect)
if resource.ResourceType == "" || resource.ResourceID == "" {
continue
}
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = $1
AND subject_id = $2::uuid
AND effect = $3
AND resource_type = $4
AND resource_id = $5::uuid`,
input.SubjectType, input.SubjectID, input.Effect, resource.ResourceType, resource.ResourceID,
); err != nil {
return nil, err
}
}
for _, resource := range dedupeAccessRuleResources(input.UpsertResources) {
resource = normalizeAccessRuleResource(resource, input.Effect)
if resource.ResourceType == "" || resource.ResourceID == "" {
continue
}
oppositeEffect := "deny"
if input.Effect == "deny" {
oppositeEffect = "allow"
}
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = $1
AND subject_id = $2::uuid
AND effect = $3
AND resource_type = $4
AND resource_id = $5::uuid`,
input.SubjectType, input.SubjectID, oppositeEffect, resource.ResourceType, resource.ResourceID,
); err != nil {
return nil, err
}
conditions, _ := json.Marshal(emptyObjectIfNil(resource.Conditions))
metadata, _ := json.Marshal(emptyObjectIfNil(resource.Metadata))
if _, err := tx.Exec(ctx, `
INSERT INTO gateway_access_rules (
subject_type, subject_id, resource_type, resource_id, effect, priority,
min_permission_level, conditions, metadata, status
)
VALUES ($1, $2::uuid, $3, $4::uuid, $5, $6, $7, $8::jsonb, $9::jsonb, $10)
ON CONFLICT (subject_type, subject_id, resource_type, resource_id, effect)
DO UPDATE SET priority = EXCLUDED.priority,
min_permission_level = EXCLUDED.min_permission_level,
conditions = EXCLUDED.conditions,
metadata = EXCLUDED.metadata,
status = EXCLUDED.status,
updated_at = now()`,
input.SubjectType, input.SubjectID, resource.ResourceType, resource.ResourceID, input.Effect,
resource.Priority, resource.MinPermissionLevel, string(conditions), string(metadata), resource.Status,
); err != nil {
return nil, err
}
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return s.ListAccessRules(ctx)
}
func (s *Store) BatchAPIKeyAccessRules(ctx context.Context, input AccessRuleBatchInput, user *auth.User) ([]AccessRule, error) {
gatewayUserID := localGatewayUserID(user)
if gatewayUserID == "" {
return nil, ErrLocalUserRequired
}
input = normalizeAccessRuleBatchInput(input)
if input.SubjectType != "api_key" {
return nil, pgx.ErrNoRows
}
var exists bool
if err := s.pool.QueryRow(ctx, `
SELECT EXISTS (
SELECT 1
FROM gateway_api_keys
WHERE id = $1::uuid
AND gateway_user_id = $2::uuid
AND deleted_at IS NULL
)`, input.SubjectID, gatewayUserID).Scan(&exists); err != nil {
return nil, err
}
if !exists {
return nil, pgx.ErrNoRows
}
if err := s.ensureAPIKeyAccessRuleResourcesAllowed(ctx, user, input.UpsertResources); err != nil {
return nil, err
}
if _, err := s.BatchAccessRules(ctx, input); err != nil {
return nil, err
}
return s.ListAPIKeyAccessRules(ctx, user)
}
func (s *Store) filterCandidatesByAccessRules(ctx context.Context, user *auth.User, candidates []RuntimeModelCandidate) ([]RuntimeModelCandidate, error) {
if len(candidates) == 0 {
return candidates, nil
}
resources := candidateAccessResources(candidates)
if len(resources) == 0 {
return candidates, nil
}
rules, err := s.listActiveAccessRulesForResources(ctx, resources)
if err != nil {
return nil, err
}
if len(rules) == 0 {
return candidates, nil
}
subjects := accessRuleSubjects(user)
level := 0
if user != nil {
level = auth.PermissionLevel(user.Roles)
}
filtered := candidates[:0]
for _, candidate := range candidates {
if candidateAllowedByAccessRules(candidate, rules, subjects, level) {
filtered = append(filtered, candidate)
}
}
return filtered, nil
}
func (s *Store) ListAccessiblePlatformModels(ctx context.Context, user *auth.User) ([]PlatformModel, error) {
accessUser, err := s.resolveCurrentAccessUser(ctx, user)
if err != nil {
return nil, err
}
models, err := s.ListModels(ctx)
if err != nil {
return nil, err
}
platforms, err := s.ListPlatforms(ctx)
if err != nil {
return nil, err
}
enabledPlatforms := map[string]bool{}
for _, platform := range platforms {
if platform.Status == "enabled" {
enabledPlatforms[platform.ID] = true
}
}
enabled := make([]PlatformModel, 0, len(models))
for _, model := range models {
if model.Enabled && enabledPlatforms[model.PlatformID] {
enabled = append(enabled, model)
}
}
return s.filterPlatformModelsByAccessRules(ctx, accessUser, enabled)
}
func (s *Store) ensureAPIKeyAccessRuleResourcesAllowed(ctx context.Context, user *auth.User, resources []AccessRuleResourceInput) error {
resources = dedupeAccessRuleResources(resources)
if len(resources) == 0 {
return nil
}
allowed, err := s.accessibleAccessRuleResources(ctx, user)
if err != nil {
return err
}
for _, resource := range resources {
if !allowed[resource.ResourceType+":"+resource.ResourceID] {
return ErrAccessRuleResourceDenied
}
}
return nil
}
func (s *Store) accessibleAccessRuleResources(ctx context.Context, user *auth.User) (map[string]bool, error) {
models, err := s.ListAccessiblePlatformModels(ctx, user)
if err != nil {
return nil, err
}
allowed := map[string]bool{}
for _, model := range models {
allowed["platform:"+model.PlatformID] = true
allowed["platform_model:"+model.ID] = true
if model.BaseModelID != "" {
allowed["base_model:"+model.BaseModelID] = true
}
}
return allowed, nil
}
func (s *Store) resolveCurrentAccessUser(ctx context.Context, user *auth.User) (*auth.User, error) {
if user == nil {
return nil, nil
}
gatewayUserID := localGatewayUserID(user)
if gatewayUserID == "" {
return user, nil
}
next := *user
var userGroupID string
var err error
if strings.TrimSpace(user.APIKeyID) != "" {
err = s.pool.QueryRow(ctx, `
SELECT COALESCE(k.user_group_id::text, u.default_user_group_id::text, '')
FROM gateway_users u
JOIN gateway_api_keys k ON k.gateway_user_id = u.id
WHERE u.id = $1::uuid
AND k.id = $2::uuid
AND u.status = 'active'
AND u.deleted_at IS NULL
AND k.status = 'active'
AND k.deleted_at IS NULL`, gatewayUserID, user.APIKeyID).Scan(&userGroupID)
} else {
err = s.pool.QueryRow(ctx, `
SELECT COALESCE(default_user_group_id::text, '')
FROM gateway_users
WHERE id = $1::uuid
AND status = 'active'
AND deleted_at IS NULL`, gatewayUserID).Scan(&userGroupID)
}
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return &next, nil
}
return nil, err
}
next.UserGroupID = userGroupID
return &next, nil
}
func (s *Store) filterPlatformModelsByAccessRules(ctx context.Context, user *auth.User, models []PlatformModel) ([]PlatformModel, error) {
if len(models) == 0 {
return models, nil
}
resources := platformModelAccessResources(models)
if len(resources) == 0 {
return models, nil
}
rules, err := s.listActiveAccessRulesForResources(ctx, resources)
if err != nil {
return nil, err
}
if len(rules) == 0 {
return models, nil
}
subjects := accessRuleSubjects(user)
level := 0
if user != nil {
level = auth.PermissionLevel(user.Roles)
}
filtered := models[:0]
for _, model := range models {
if platformModelAllowedByAccessRules(model, rules, subjects, level) {
filtered = append(filtered, model)
}
}
return filtered, nil
}
func (s *Store) listActiveAccessRulesForResources(ctx context.Context, resources []accessRuleResource) ([]AccessRule, error) {
values := make([]string, 0, len(resources))
for _, resource := range resources {
if resource.Type == "" || resource.ID == "" {
continue
}
values = append(values, resource.Type+":"+resource.ID)
}
if len(values) == 0 {
return nil, nil
}
rows, err := s.pool.Query(ctx, `
SELECT `+accessRuleColumns+`
FROM gateway_access_rules
WHERE status = 'active'
AND (resource_type || ':' || resource_id::text) = ANY($1)
ORDER BY priority ASC, created_at ASC`, values)
if err != nil {
return nil, err
}
defer rows.Close()
rules := make([]AccessRule, 0)
for rows.Next() {
item, err := scanAccessRule(rows)
if err != nil {
return nil, err
}
rules = append(rules, item)
}
return rules, rows.Err()
}
func candidateAccessResources(candidates []RuntimeModelCandidate) []accessRuleResource {
seen := map[string]bool{}
out := make([]accessRuleResource, 0, len(candidates)*3)
add := func(resourceType string, resourceID string) {
key := resourceType + ":" + resourceID
if resourceID == "" || seen[key] {
return
}
seen[key] = true
out = append(out, accessRuleResource{Type: resourceType, ID: resourceID})
}
for _, candidate := range candidates {
add("platform", candidate.PlatformID)
add("platform_model", candidate.PlatformModelID)
add("base_model", candidate.BaseModelID)
}
return out
}
func platformModelAccessResources(models []PlatformModel) []accessRuleResource {
seen := map[string]bool{}
out := make([]accessRuleResource, 0, len(models)*3)
add := func(resourceType string, resourceID string) {
key := resourceType + ":" + resourceID
if resourceID == "" || seen[key] {
return
}
seen[key] = true
out = append(out, accessRuleResource{Type: resourceType, ID: resourceID})
}
for _, model := range models {
add("platform", model.PlatformID)
add("platform_model", model.ID)
add("base_model", model.BaseModelID)
}
return out
}
func candidateAllowedByAccessRules(candidate RuntimeModelCandidate, rules []AccessRule, subjects map[string]bool, permissionLevel int) bool {
resourceKeys := map[string]bool{
"platform:" + candidate.PlatformID: true,
"platform_model:" + candidate.PlatformModelID: true,
"base_model:" + candidate.BaseModelID: candidate.BaseModelID != "",
}
allowByResource := map[string]bool{}
matchedAllowByResource := map[string]bool{}
for _, rule := range rules {
resourceKey := rule.ResourceType + ":" + rule.ResourceID
if !resourceKeys[resourceKey] {
continue
}
subjectKey := rule.SubjectType + ":" + rule.SubjectID
if rule.Effect == "deny" && subjects[subjectKey] {
return false
}
if rule.Effect == "allow" {
allowByResource[resourceKey] = true
if subjects[subjectKey] && permissionLevel >= rule.MinPermissionLevel {
matchedAllowByResource[resourceKey] = true
}
}
}
for resourceKey := range allowByResource {
if !matchedAllowByResource[resourceKey] {
return false
}
}
return true
}
func platformModelAllowedByAccessRules(model PlatformModel, rules []AccessRule, subjects map[string]bool, permissionLevel int) bool {
resourceKeys := map[string]bool{
"platform:" + model.PlatformID: true,
"platform_model:" + model.ID: true,
"base_model:" + model.BaseModelID: model.BaseModelID != "",
}
allowByResource := map[string]bool{}
matchedAllowByResource := map[string]bool{}
for _, rule := range rules {
resourceKey := rule.ResourceType + ":" + rule.ResourceID
if !resourceKeys[resourceKey] {
continue
}
subjectKey := rule.SubjectType + ":" + rule.SubjectID
if rule.Effect == "deny" && subjects[subjectKey] {
return false
}
if rule.Effect == "allow" {
allowByResource[resourceKey] = true
if subjects[subjectKey] && permissionLevel >= rule.MinPermissionLevel {
matchedAllowByResource[resourceKey] = true
}
}
}
for resourceKey := range allowByResource {
if !matchedAllowByResource[resourceKey] {
return false
}
}
return true
}
func accessRuleSubjects(user *auth.User) map[string]bool {
subjects := map[string]bool{}
if user == nil {
return subjects
}
add := func(subjectType string, id string) {
id = strings.TrimSpace(id)
if id != "" {
subjects[subjectType+":"+id] = true
}
}
add("user", firstNonEmpty(user.GatewayUserID, user.ID))
add("tenant", firstNonEmpty(user.GatewayTenantID, user.TenantID))
add("api_key", user.APIKeyID)
add("user_group", user.UserGroupID)
for _, groupKey := range user.UserGroupKeys {
add("user_group", groupKey)
}
return subjects
}
const accessRuleColumns = `
id::text, subject_type, subject_id::text, resource_type, resource_id::text, effect,
priority, min_permission_level, conditions, metadata, status, created_at, updated_at`
const apiKeyAccessRuleColumns = `
ar.id::text, ar.subject_type, ar.subject_id::text, ar.resource_type, ar.resource_id::text, ar.effect,
ar.priority, ar.min_permission_level, ar.conditions, ar.metadata, ar.status, ar.created_at, ar.updated_at`
func scanAccessRule(row scanner) (AccessRule, error) {
var item AccessRule
var conditions []byte
var metadata []byte
if err := row.Scan(
&item.ID,
&item.SubjectType,
&item.SubjectID,
&item.ResourceType,
&item.ResourceID,
&item.Effect,
&item.Priority,
&item.MinPermissionLevel,
&conditions,
&metadata,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return AccessRule{}, err
}
item.Conditions = decodeObject(conditions)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizeAccessRuleInput(input AccessRuleInput) AccessRuleInput {
input.SubjectType = strings.TrimSpace(input.SubjectType)
input.SubjectID = strings.TrimSpace(input.SubjectID)
input.ResourceType = strings.TrimSpace(input.ResourceType)
input.ResourceID = strings.TrimSpace(input.ResourceID)
input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
if input.Priority == 0 {
input.Priority = 100
}
if input.MinPermissionLevel < 0 {
input.MinPermissionLevel = 0
}
return input
}
func normalizeAccessRuleBatchInput(input AccessRuleBatchInput) AccessRuleBatchInput {
input.SubjectType = strings.TrimSpace(input.SubjectType)
input.SubjectID = strings.TrimSpace(input.SubjectID)
input.Effect = firstNonEmpty(strings.TrimSpace(input.Effect), "allow")
return input
}
func normalizeAccessRuleResource(input AccessRuleResourceInput, effect string) AccessRuleResourceInput {
input.ResourceType = strings.TrimSpace(input.ResourceType)
input.ResourceID = strings.TrimSpace(input.ResourceID)
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
if input.Priority == 0 {
if effect == "deny" {
input.Priority = 10
} else {
input.Priority = 100
}
}
if input.MinPermissionLevel < 0 {
input.MinPermissionLevel = 0
}
return input
}
func dedupeAccessRuleResources(resources []AccessRuleResourceInput) []AccessRuleResourceInput {
seen := map[string]bool{}
out := make([]AccessRuleResourceInput, 0, len(resources))
for _, resource := range resources {
resource.ResourceType = strings.TrimSpace(resource.ResourceType)
resource.ResourceID = strings.TrimSpace(resource.ResourceID)
key := resource.ResourceType + ":" + resource.ResourceID
if resource.ResourceType == "" || resource.ResourceID == "" || seen[key] {
continue
}
seen[key] = true
out = append(out, resource)
}
return out
}
+471
View File
@@ -0,0 +1,471 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
)
const baseModelColumns = `
id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, COALESCE(pricing_rule_set_id::text, ''),
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override, metadata,
catalog_type, COALESCE(default_snapshot, '{}'::jsonb), COALESCE(customized_at::text, ''),
pricing_version, status, created_at, updated_at`
type BaseModelInput struct {
ProviderKey string `json:"providerKey"`
CanonicalModelKey string `json:"canonicalModelKey"`
ProviderModelName string `json:"providerModelName"`
ModelType StringList `json:"modelType"`
ModelAlias string `json:"modelAlias"`
DisplayName string `json:"displayName"`
Capabilities map[string]any `json:"capabilities"`
BaseBillingConfig map[string]any `json:"baseBillingConfig"`
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy"`
PricingRuleSetID string `json:"pricingRuleSetId"`
RuntimePolicySetID string `json:"runtimePolicySetId"`
RuntimePolicyOverride map[string]any `json:"runtimePolicyOverride"`
Metadata map[string]any `json:"metadata"`
CatalogType string `json:"catalogType"`
DefaultSnapshot map[string]any `json:"defaultSnapshot"`
PricingVersion int `json:"pricingVersion"`
Status string `json:"status"`
}
type baseModelScanner interface {
Scan(dest ...any) error
}
type StringList []string
func (list *StringList) UnmarshalJSON(data []byte) error {
var values []string
if err := json.Unmarshal(data, &values); err == nil {
*list = uniqueStringList(values)
return nil
}
var value string
if err := json.Unmarshal(data, &value); err != nil {
return err
}
*list = uniqueStringList([]string{value})
return nil
}
func (s *Store) ListBaseModels(ctx context.Context) ([]BaseModel, error) {
rows, err := s.pool.Query(ctx, `
SELECT `+baseModelColumns+`
FROM base_model_catalog
ORDER BY provider_key ASC, model_type ASC, canonical_model_key ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]BaseModel, 0)
for rows.Next() {
item, err := scanBaseModel(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) CreateBaseModel(ctx context.Context, input BaseModelInput) (BaseModel, error) {
input = normalizeBaseModelInput(input)
capabilities, _ := json.Marshal(emptyObjectIfNil(input.Capabilities))
billingConfig, _ := json.Marshal(emptyObjectIfNil(input.BaseBillingConfig))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
runtimePolicyOverride, _ := json.Marshal(emptyObjectIfNil(input.RuntimePolicyOverride))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
modelType := primaryString(input.ModelType, "text_generate")
return scanBaseModel(s.pool.QueryRow(ctx, `
INSERT INTO base_model_catalog (
provider_id, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, pricing_rule_set_id, runtime_policy_set_id, runtime_policy_override,
metadata, catalog_type, default_snapshot, pricing_version, status
)
VALUES (
(SELECT id FROM model_catalog_providers WHERE provider_key = $1 OR provider_code = $1 LIMIT 1),
$1, $2, $3, $4, $5, $6, $7, $8,
COALESCE(NULLIF($9, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
$11, $12, NULLIF($13, ''), NULLIF($14::jsonb, '{}'::jsonb), $15, $16
)
RETURNING `+baseModelColumns,
input.ProviderKey,
input.CanonicalModelKey,
input.ProviderModelName,
modelType,
input.ModelAlias,
capabilities,
billingConfig,
rateLimitPolicy,
input.PricingRuleSetID,
input.RuntimePolicySetID,
runtimePolicyOverride,
metadata,
input.CatalogType,
string(defaultSnapshot),
input.PricingVersion,
input.Status,
))
}
func (s *Store) UpdateBaseModel(ctx context.Context, id string, input BaseModelInput) (BaseModel, error) {
input = normalizeBaseModelInput(input)
capabilities, _ := json.Marshal(emptyObjectIfNil(input.Capabilities))
billingConfig, _ := json.Marshal(emptyObjectIfNil(input.BaseBillingConfig))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
runtimePolicyOverride, _ := json.Marshal(emptyObjectIfNil(input.RuntimePolicyOverride))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
defaultSnapshot, _ := json.Marshal(emptyObjectIfNil(input.DefaultSnapshot))
modelType := primaryString(input.ModelType, "text_generate")
return scanBaseModel(s.pool.QueryRow(ctx, `
UPDATE base_model_catalog
SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = $2 OR provider_code = $2 LIMIT 1),
provider_key = $2,
canonical_model_key = $3,
provider_model_name = $4,
model_type = $5,
display_name = $6,
capabilities = $7,
base_billing_config = $8,
default_rate_limit_policy = $9,
pricing_rule_set_id = COALESCE(NULLIF($10, '')::uuid, (SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1)),
runtime_policy_set_id = COALESCE(NULLIF($11, '')::uuid, (SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1)),
runtime_policy_override = $12,
metadata = $13,
catalog_type = NULLIF($14, ''),
default_snapshot = COALESCE(NULLIF($15::jsonb, '{}'::jsonb), default_snapshot),
customized_at = CASE WHEN NULLIF($14, '') = 'system' THEN now() ELSE NULL END,
pricing_version = $16,
status = $17,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+baseModelColumns,
id,
input.ProviderKey,
input.CanonicalModelKey,
input.ProviderModelName,
modelType,
input.ModelAlias,
capabilities,
billingConfig,
rateLimitPolicy,
input.PricingRuleSetID,
input.RuntimePolicySetID,
runtimePolicyOverride,
metadata,
input.CatalogType,
string(defaultSnapshot),
input.PricingVersion,
input.Status,
))
}
func (s *Store) ResetBaseModelToDefault(ctx context.Context, id string) (BaseModel, error) {
var catalogType string
var snapshotBytes []byte
if err := s.pool.QueryRow(ctx, `
SELECT catalog_type, COALESCE(default_snapshot, '{}'::jsonb)
FROM base_model_catalog
WHERE id = $1::uuid`, id).Scan(&catalogType, &snapshotBytes); err != nil {
return BaseModel{}, err
}
if catalogType != "system" {
return BaseModel{}, ErrProtectedDefault
}
snapshot := decodeObject(snapshotBytes)
if len(snapshot) == 0 {
return BaseModel{}, ErrProtectedDefault
}
return scanBaseModel(s.pool.QueryRow(ctx, `
UPDATE base_model_catalog
SET provider_id = (SELECT id FROM model_catalog_providers WHERE provider_key = COALESCE($2::text, provider_key) OR provider_code = COALESCE($2::text, provider_key) LIMIT 1),
provider_key = COALESCE($2::text, provider_key),
canonical_model_key = COALESCE($3::text, canonical_model_key),
provider_model_name = COALESCE($4::text, provider_model_name),
model_type = COALESCE($5::text, model_type),
display_name = COALESCE($6::text, display_name),
capabilities = COALESCE($7::jsonb, capabilities),
base_billing_config = COALESCE($8::jsonb, base_billing_config),
default_rate_limit_policy = COALESCE($9::jsonb, default_rate_limit_policy),
pricing_rule_set_id = COALESCE(NULLIF($10::text, '')::uuid, pricing_rule_set_id),
runtime_policy_set_id = COALESCE(NULLIF($11::text, '')::uuid, runtime_policy_set_id),
runtime_policy_override = COALESCE($12::jsonb, runtime_policy_override),
metadata = COALESCE($13::jsonb, metadata),
pricing_version = COALESCE($14::integer, pricing_version),
status = COALESCE($15::text, status),
customized_at = NULL,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+baseModelColumns,
id,
stringFromSnapshot(snapshot, "providerKey"),
stringFromSnapshot(snapshot, "canonicalModelKey"),
stringFromSnapshot(snapshot, "providerModelName"),
stringFromSnapshot(snapshot, "modelType"),
stringFromSnapshot(snapshot, "modelAlias", "displayName"),
jsonFromSnapshot(snapshot, "capabilities"),
jsonFromSnapshot(snapshot, "baseBillingConfig"),
jsonFromSnapshot(snapshot, "defaultRateLimitPolicy"),
stringFromSnapshot(snapshot, "pricingRuleSetId"),
stringFromSnapshot(snapshot, "runtimePolicySetId"),
jsonFromSnapshot(snapshot, "runtimePolicyOverride"),
jsonFromSnapshot(snapshot, "metadata"),
intFromSnapshot(snapshot, "pricingVersion"),
stringFromSnapshot(snapshot, "status"),
))
}
func (s *Store) ResetAllBaseModelsToDefault(ctx context.Context) ([]BaseModel, error) {
rows, err := s.pool.Query(ctx, `
UPDATE base_model_catalog
SET provider_id = (
SELECT id
FROM model_catalog_providers
WHERE provider_key = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key)
OR provider_code = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key)
LIMIT 1
),
provider_key = COALESCE(NULLIF(default_snapshot->>'providerKey', ''), provider_key),
canonical_model_key = COALESCE(NULLIF(default_snapshot->>'canonicalModelKey', ''), canonical_model_key),
provider_model_name = COALESCE(NULLIF(default_snapshot->>'providerModelName', ''), provider_model_name),
model_type = COALESCE(NULLIF(CASE
WHEN jsonb_typeof(default_snapshot->'modelType') = 'array' THEN default_snapshot->'modelType'->>0
ELSE default_snapshot->>'modelType'
END, ''), model_type),
display_name = COALESCE(NULLIF(COALESCE(default_snapshot->>'modelAlias', default_snapshot->>'displayName'), ''), display_name),
capabilities = COALESCE(default_snapshot->'capabilities', capabilities),
base_billing_config = COALESCE(default_snapshot->'baseBillingConfig', base_billing_config),
default_rate_limit_policy = COALESCE(default_snapshot->'defaultRateLimitPolicy', default_rate_limit_policy),
pricing_rule_set_id = COALESCE(NULLIF(default_snapshot->>'pricingRuleSetId', '')::uuid, pricing_rule_set_id),
runtime_policy_set_id = COALESCE(NULLIF(default_snapshot->>'runtimePolicySetId', '')::uuid, runtime_policy_set_id),
runtime_policy_override = COALESCE(default_snapshot->'runtimePolicyOverride', runtime_policy_override),
metadata = COALESCE(default_snapshot->'metadata', metadata),
pricing_version = COALESCE(NULLIF(default_snapshot->>'pricingVersion', '')::integer, pricing_version),
status = COALESCE(NULLIF(default_snapshot->>'status', ''), status),
customized_at = NULL,
updated_at = now()
WHERE catalog_type = 'system'
AND COALESCE(default_snapshot, '{}'::jsonb) <> '{}'::jsonb
RETURNING `+baseModelColumns)
if err != nil {
return nil, err
}
defer rows.Close()
return scanBaseModelRows(rows)
}
func (s *Store) DeleteBaseModel(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `DELETE FROM base_model_catalog WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func scanBaseModelRows(rows pgx.Rows) ([]BaseModel, error) {
items := make([]BaseModel, 0)
for rows.Next() {
item, err := scanBaseModel(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func scanBaseModel(scanner baseModelScanner) (BaseModel, error) {
var item BaseModel
var modelType string
var modelAlias string
var capabilities []byte
var billingConfig []byte
var rateLimitPolicy []byte
var runtimePolicyOverride []byte
var metadata []byte
var defaultSnapshot []byte
if err := scanner.Scan(
&item.ID,
&item.ProviderKey,
&item.CanonicalModelKey,
&item.ProviderModelName,
&modelType,
&modelAlias,
&capabilities,
&billingConfig,
&rateLimitPolicy,
&item.PricingRuleSetID,
&item.RuntimePolicySetID,
&runtimePolicyOverride,
&metadata,
&item.CatalogType,
&defaultSnapshot,
&item.CustomizedAt,
&item.PricingVersion,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return BaseModel{}, err
}
item.Capabilities = decodeObject(capabilities)
item.BaseBillingConfig = decodeObject(billingConfig)
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
item.Metadata = decodeObject(metadata)
item.DefaultSnapshot = decodeObject(defaultSnapshot)
item.ModelType = baseModelTypes(item.Capabilities, item.Metadata, modelType)
item.ModelAlias = modelAlias
item.DisplayName = modelAlias
return item, nil
}
func normalizeBaseModelInput(input BaseModelInput) BaseModelInput {
input.ProviderKey = strings.TrimSpace(input.ProviderKey)
input.CanonicalModelKey = strings.TrimSpace(input.CanonicalModelKey)
input.ProviderModelName = strings.TrimSpace(input.ProviderModelName)
input.ModelType = uniqueStringList(input.ModelType)
input.ModelAlias = strings.TrimSpace(input.ModelAlias)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.PricingRuleSetID = strings.TrimSpace(input.PricingRuleSetID)
input.RuntimePolicySetID = strings.TrimSpace(input.RuntimePolicySetID)
input.CatalogType = strings.TrimSpace(input.CatalogType)
input.Status = strings.TrimSpace(input.Status)
if input.CanonicalModelKey == "" && input.ProviderKey != "" && input.ProviderModelName != "" {
input.CanonicalModelKey = input.ProviderKey + ":" + input.ProviderModelName
}
if input.ModelAlias == "" {
input.ModelAlias = input.DisplayName
}
if input.ModelAlias == "" {
input.ModelAlias = input.ProviderModelName
}
if len(input.ModelType) == 0 {
input.ModelType = StringList{"text_generate"}
}
if input.CatalogType == "" {
input.CatalogType = "custom"
}
if input.PricingVersion <= 0 {
input.PricingVersion = 1
}
if input.Status == "" {
input.Status = "active"
}
return input
}
func stringFromSnapshot(snapshot map[string]any, keys ...string) any {
for _, key := range keys {
value, ok := snapshot[key]
if !ok {
continue
}
switch typed := value.(type) {
case string:
if strings.TrimSpace(typed) != "" {
return typed
}
case []any:
for _, item := range typed {
if text, ok := item.(string); ok && strings.TrimSpace(text) != "" {
return strings.TrimSpace(text)
}
}
case []string:
if primary := primaryString(typed, ""); primary != "" {
return primary
}
}
}
return nil
}
func intFromSnapshot(snapshot map[string]any, key string) any {
switch value := snapshot[key].(type) {
case float64:
return int(value)
case int:
return value
default:
return nil
}
}
func jsonFromSnapshot(snapshot map[string]any, key string) any {
value, ok := snapshot[key]
if !ok || value == nil {
return nil
}
raw, err := json.Marshal(value)
if err != nil {
return nil
}
return string(raw)
}
func baseModelTypes(capabilities map[string]any, metadata map[string]any, fallback string) StringList {
values := make([]string, 0)
values = append(values, stringListFromAny(capabilities["originalTypes"])...)
values = append(values, stringListFromAny(metadata["originalTypes"])...)
if fallback != "" {
values = append(values, fallback)
}
return uniqueStringList(values)
}
func stringListFromAny(value any) []string {
switch typed := value.(type) {
case []string:
return typed
case []any:
values := make([]string, 0, len(typed))
for _, item := range typed {
if text, ok := item.(string); ok {
values = append(values, text)
}
}
return values
default:
return nil
}
}
func uniqueStringList(values []string) StringList {
out := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" || seen[value] {
continue
}
seen[value] = true
out = append(out, value)
}
return out
}
func primaryString(values []string, fallback string) string {
for _, value := range values {
if value = strings.TrimSpace(value); value != "" {
return value
}
}
return fallback
}
+147
View File
@@ -0,0 +1,147 @@
package store
import (
"context"
"fmt"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
)
func (s *Store) ListModelCandidates(ctx context.Context, model string, modelType string, user *auth.User) ([]RuntimeModelCandidate, error) {
rows, err := s.pool.Query(ctx, `
SELECT p.id::text, p.platform_key, p.name, p.provider,
COALESCE(NULLIF(p.config->>'specType', ''), NULLIF(cp.provider_type, ''), NULLIF(p.config->>'sourceSpecType', ''), p.provider) AS spec_type,
COALESCE(p.base_url, ''),
p.auth_type, p.credentials, p.config, p.default_pricing_mode,
p.default_discount_factor::float8, COALESCE(p.pricing_rule_set_id::text, ''),
p.retry_policy, p.rate_limit_policy,
COALESCE(p.dynamic_priority, p.priority) AS effective_priority,
m.id::text, COALESCE(m.base_model_id::text, ''), COALESCE(b.canonical_model_key, ''),
COALESCE(b.provider_model_name, ''), m.model_name, COALESCE(m.model_alias, ''),
m.model_type, m.display_name, m.capabilities, m.capability_override,
COALESCE(b.base_billing_config, '{}'::jsonb), m.billing_config, m.billing_config_override,
m.pricing_mode, COALESCE(m.discount_factor, 0)::float8, COALESCE(m.pricing_rule_set_id::text, ''),
m.permission_config, m.retry_policy, m.rate_limit_policy, COALESCE(m.runtime_policy_set_id::text, COALESCE(b.runtime_policy_set_id::text, '')),
COALESCE(NULLIF(m.runtime_policy_override, '{}'::jsonb), b.runtime_policy_override, '{}'::jsonb)
FROM platform_models m
JOIN integration_platforms p ON p.id = m.platform_id
LEFT JOIN model_catalog_providers cp ON cp.provider_key = p.provider OR cp.provider_code = p.provider
LEFT JOIN base_model_catalog b ON b.id = m.base_model_id
LEFT JOIN runtime_client_states s
ON s.client_id = p.platform_key || ':' || m.model_type || ':' || m.model_name
WHERE p.status = 'enabled'
AND p.deleted_at IS NULL
AND m.enabled = true
AND (
m.model_type = $2
OR ($2 = 'text_generate' AND m.model_type IN ('chat', 'responses', 'text'))
OR ($2 = 'image_generate' AND m.model_type IN ('image', 'images.generations'))
OR ($2 = 'image_edit' AND m.model_type IN ('images.edits'))
OR ($2 = 'video_generate' AND m.model_type IN ('video', 'videos.generations', 'video_generate', 'text_to_video', 'image_to_video', 'omni_video', 'video_edit', 'video_reference', 'video_first_last_frame'))
)
AND (p.cooldown_until IS NULL OR p.cooldown_until <= now())
AND (
m.model_name = $1
OR m.model_alias = $1
OR b.canonical_model_key = $1
OR b.provider_model_name = $1
)
ORDER BY effective_priority ASC,
COALESCE(s.limiter_ratio, 0) ASC,
COALESCE(s.running_count, 0) ASC,
COALESCE(s.waiting_count, 0) ASC,
COALESCE(s.last_assigned_at, to_timestamp(0)) ASC,
m.created_at ASC`, model, modelType)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]RuntimeModelCandidate, 0)
for rows.Next() {
var item RuntimeModelCandidate
var credentials []byte
var platformConfig []byte
var platformRetryPolicy []byte
var platformRateLimitPolicy []byte
var capabilities []byte
var capabilityOverride []byte
var baseBilling []byte
var billing []byte
var billingOverride []byte
var permissionConfig []byte
var modelRetryPolicy []byte
var modelRateLimitPolicy []byte
var runtimePolicyOverride []byte
if err := rows.Scan(
&item.PlatformID,
&item.PlatformKey,
&item.PlatformName,
&item.Provider,
&item.SpecType,
&item.BaseURL,
&item.AuthType,
&credentials,
&platformConfig,
&item.DefaultPricingMode,
&item.DefaultDiscountFactor,
&item.PlatformPricingRuleSetID,
&platformRetryPolicy,
&platformRateLimitPolicy,
&item.PlatformPriority,
&item.PlatformModelID,
&item.BaseModelID,
&item.CanonicalModelKey,
&item.ProviderModelName,
&item.ModelName,
&item.ModelAlias,
&item.ModelType,
&item.DisplayName,
&capabilities,
&capabilityOverride,
&baseBilling,
&billing,
&billingOverride,
&item.PricingMode,
&item.DiscountFactor,
&item.ModelPricingRuleSetID,
&permissionConfig,
&modelRetryPolicy,
&modelRateLimitPolicy,
&item.RuntimePolicySetID,
&runtimePolicyOverride,
); err != nil {
return nil, err
}
item.Credentials = decodeObject(credentials)
item.PlatformConfig = decodeObject(platformConfig)
item.PlatformRetryPolicy = decodeObject(platformRetryPolicy)
item.PlatformRateLimitPolicy = decodeObject(platformRateLimitPolicy)
item.Capabilities = decodeObject(capabilities)
item.CapabilityOverride = decodeObject(capabilityOverride)
item.BaseBillingConfig = decodeObject(baseBilling)
item.BillingConfig = decodeObject(billing)
item.BillingConfigOverride = decodeObject(billingOverride)
item.PermissionConfig = decodeObject(permissionConfig)
item.ModelRetryPolicy = decodeObject(modelRetryPolicy)
item.ModelRateLimitPolicy = decodeObject(modelRateLimitPolicy)
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
item.ClientID = fmt.Sprintf("%s:%s:%s", item.PlatformKey, item.ModelType, item.ModelName)
item.QueueKey = item.ClientID
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
if len(items) == 0 {
return nil, ErrNoModelCandidate
}
items, err = s.filterCandidatesByAccessRules(ctx, user, items)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, ErrNoModelCandidate
}
return items, nil
}
@@ -0,0 +1,171 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
)
const catalogProviderColumns = `
id::text, provider_key, COALESCE(NULLIF(provider_code, ''), provider_key) AS provider_code,
display_name, provider_type, COALESCE(icon_path, '') AS icon_path,
COALESCE(default_base_url, '') AS default_base_url, COALESCE(default_auth_type, '') AS default_auth_type,
COALESCE(source, '') AS source, capability_schema, default_rate_limit_policy,
metadata, status, created_at, updated_at`
type CatalogProviderInput struct {
ProviderKey string `json:"providerKey"`
Code string `json:"code"`
DisplayName string `json:"displayName"`
ProviderType string `json:"providerType"`
IconPath string `json:"iconPath"`
DefaultBaseURL string `json:"defaultBaseUrl"`
DefaultAuthType string `json:"defaultAuthType"`
Source string `json:"source"`
CapabilitySchema map[string]any `json:"capabilitySchema"`
DefaultRateLimitPolicy map[string]any `json:"defaultRateLimitPolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type catalogProviderScanner interface {
Scan(dest ...any) error
}
func (s *Store) CreateCatalogProvider(ctx context.Context, input CatalogProviderInput) (CatalogProvider, error) {
input = normalizeCatalogProviderInput(input)
capabilitySchema, _ := json.Marshal(emptyObjectIfNil(input.CapabilitySchema))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanCatalogProvider(s.pool.QueryRow(ctx, `
INSERT INTO model_catalog_providers (
provider_key, provider_code, display_name, provider_type, icon_path, default_base_url, default_auth_type, source,
capability_schema, default_rate_limit_policy, metadata, status
)
VALUES ($1, $2, $3, $4, NULLIF($5, ''), NULLIF($6, ''), $7, $8, $9, $10, $11, $12)
RETURNING `+catalogProviderColumns,
input.ProviderKey,
input.Code,
input.DisplayName,
input.ProviderType,
input.IconPath,
input.DefaultBaseURL,
input.DefaultAuthType,
input.Source,
capabilitySchema,
rateLimitPolicy,
metadata,
input.Status,
))
}
func (s *Store) UpdateCatalogProvider(ctx context.Context, id string, input CatalogProviderInput) (CatalogProvider, error) {
input = normalizeCatalogProviderInput(input)
capabilitySchema, _ := json.Marshal(emptyObjectIfNil(input.CapabilitySchema))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.DefaultRateLimitPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanCatalogProvider(s.pool.QueryRow(ctx, `
UPDATE model_catalog_providers
SET provider_key = $2,
provider_code = $3,
display_name = $4,
provider_type = $5,
icon_path = NULLIF($6, ''),
default_base_url = NULLIF($7, ''),
default_auth_type = $8,
source = $9,
capability_schema = $10,
default_rate_limit_policy = $11,
metadata = $12,
status = $13,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+catalogProviderColumns,
id,
input.ProviderKey,
input.Code,
input.DisplayName,
input.ProviderType,
input.IconPath,
input.DefaultBaseURL,
input.DefaultAuthType,
input.Source,
capabilitySchema,
rateLimitPolicy,
metadata,
input.Status,
))
}
func (s *Store) DeleteCatalogProvider(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `DELETE FROM model_catalog_providers WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func scanCatalogProvider(scanner catalogProviderScanner) (CatalogProvider, error) {
var item CatalogProvider
var capabilitySchema []byte
var rateLimitPolicy []byte
var metadata []byte
if err := scanner.Scan(
&item.ID,
&item.ProviderKey,
&item.Code,
&item.DisplayName,
&item.ProviderType,
&item.IconPath,
&item.DefaultBaseURL,
&item.DefaultAuthType,
&item.Source,
&capabilitySchema,
&rateLimitPolicy,
&metadata,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return CatalogProvider{}, err
}
item.CapabilitySchema = decodeObject(capabilitySchema)
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizeCatalogProviderInput(input CatalogProviderInput) CatalogProviderInput {
input.ProviderKey = strings.TrimSpace(input.ProviderKey)
input.Code = strings.TrimSpace(input.Code)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.ProviderType = strings.TrimSpace(input.ProviderType)
input.IconPath = strings.TrimSpace(input.IconPath)
input.DefaultBaseURL = strings.TrimSpace(input.DefaultBaseURL)
input.DefaultAuthType = strings.TrimSpace(input.DefaultAuthType)
input.Source = strings.TrimSpace(input.Source)
input.Status = strings.TrimSpace(input.Status)
if input.Code == "" {
input.Code = input.ProviderKey
}
if input.ProviderType == "" {
input.ProviderType = "openai"
}
if input.DefaultAuthType == "" {
input.DefaultAuthType = "APIKey"
}
if input.Source == "" {
input.Source = "gateway"
}
if input.Status == "" {
input.Status = "active"
}
return input
}
+410
View File
@@ -0,0 +1,410 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
"golang.org/x/crypto/bcrypt"
)
type GatewayTenantInput struct {
TenantKey string `json:"tenantKey"`
Source string `json:"source"`
ExternalTenantID string `json:"externalTenantId"`
Name string `json:"name"`
Description string `json:"description"`
DefaultUserGroupID string `json:"defaultUserGroupId"`
PlanKey string `json:"planKey"`
BillingProfile map[string]any `json:"billingProfile"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
AuthPolicy map[string]any `json:"authPolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type GatewayUserInput struct {
UserKey string `json:"userKey"`
Source string `json:"source"`
ExternalUserID string `json:"externalUserId"`
Username string `json:"username"`
DisplayName string `json:"displayName"`
Email string `json:"email"`
Phone string `json:"phone"`
AvatarURL string `json:"avatarUrl"`
Password string `json:"password"`
GatewayTenantID string `json:"gatewayTenantId"`
TenantID string `json:"tenantId"`
TenantKey string `json:"tenantKey"`
DefaultUserGroupID string `json:"defaultUserGroupId"`
Roles []string `json:"roles"`
AuthProfile map[string]any `json:"authProfile"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type UserGroupInput struct {
GroupKey string `json:"groupKey"`
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Priority int `json:"priority"`
RechargeDiscountPolicy map[string]any `json:"rechargeDiscountPolicy"`
BillingDiscountPolicy map[string]any `json:"billingDiscountPolicy"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
QuotaPolicy map[string]any `json:"quotaPolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
func (s *Store) CreateTenant(ctx context.Context, input GatewayTenantInput) (GatewayTenant, error) {
input = normalizeTenantInput(input)
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanTenant(s.pool.QueryRow(ctx, `
INSERT INTO gateway_tenants (
tenant_key, source, external_tenant_id, name, description, default_user_group_id, plan_key,
billing_profile, rate_limit_policy, auth_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, '')::uuid, NULLIF($7, ''), $8, $9, $10, $11, $12)
RETURNING `+tenantColumns,
input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID, input.PlanKey,
billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
))
}
func (s *Store) UpdateTenant(ctx context.Context, id string, input GatewayTenantInput) (GatewayTenant, error) {
input = normalizeTenantInput(input)
billingProfile, _ := json.Marshal(emptyObjectIfNil(input.BillingProfile))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
authPolicy, _ := json.Marshal(emptyObjectIfNil(input.AuthPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanTenant(s.pool.QueryRow(ctx, `
UPDATE gateway_tenants
SET tenant_key = $2,
source = $3,
external_tenant_id = NULLIF($4, ''),
name = $5,
description = NULLIF($6, ''),
default_user_group_id = NULLIF($7, '')::uuid,
plan_key = NULLIF($8, ''),
billing_profile = $9,
rate_limit_policy = $10,
auth_policy = $11,
metadata = $12,
status = $13,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+tenantColumns,
id, input.TenantKey, input.Source, input.ExternalTenantID, input.Name, input.Description, input.DefaultUserGroupID,
input.PlanKey, billingProfile, rateLimitPolicy, authPolicy, metadata, input.Status,
))
}
func (s *Store) DeleteTenant(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `
UPDATE gateway_tenants
SET deleted_at = now(),
status = 'deleted',
tenant_key = tenant_key || ':deleted:' || left(id::text, 8),
external_tenant_id = NULL,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) CreateGatewayUser(ctx context.Context, input GatewayUserInput) (GatewayUser, error) {
input = normalizeUserInput(input)
passwordHash := ""
if strings.TrimSpace(input.Password) != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
return GatewayUser{}, err
}
passwordHash = string(hash)
}
roles, _ := json.Marshal(input.Roles)
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUser(s.pool.QueryRow(ctx, `
INSERT INTO gateway_users (
user_key, source, external_user_id, username, display_name, email, phone, avatar_url, password_hash,
gateway_tenant_id, tenant_id, tenant_key, default_user_group_id, roles, auth_profile, metadata, status
)
VALUES (
$1, $2, NULLIF($3, ''), $4, NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''), NULLIF($9, ''),
NULLIF($10, '')::uuid, NULLIF($11, ''), NULLIF($12, ''), NULLIF($13, '')::uuid, $14, $15, $16, $17
)
RETURNING `+userColumns,
input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone, input.AvatarURL, passwordHash,
input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID, roles, authProfile, metadata, input.Status,
))
}
func (s *Store) UpdateGatewayUser(ctx context.Context, id string, input GatewayUserInput) (GatewayUser, error) {
input = normalizeUserInput(input)
passwordHash := ""
if strings.TrimSpace(input.Password) != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
if err != nil {
return GatewayUser{}, err
}
passwordHash = string(hash)
}
roles, _ := json.Marshal(input.Roles)
authProfile, _ := json.Marshal(emptyObjectIfNil(input.AuthProfile))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUser(s.pool.QueryRow(ctx, `
UPDATE gateway_users
SET user_key = $2,
source = $3,
external_user_id = NULLIF($4, ''),
username = $5,
display_name = NULLIF($6, ''),
email = NULLIF($7, ''),
phone = NULLIF($8, ''),
avatar_url = NULLIF($9, ''),
password_hash = COALESCE(NULLIF($10, ''), password_hash),
gateway_tenant_id = NULLIF($11, '')::uuid,
tenant_id = NULLIF($12, ''),
tenant_key = NULLIF($13, ''),
default_user_group_id = NULLIF($14, '')::uuid,
roles = $15,
auth_profile = $16,
metadata = $17,
status = $18,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL
RETURNING `+userColumns,
id, input.UserKey, input.Source, input.ExternalUserID, input.Username, input.DisplayName, input.Email, input.Phone,
input.AvatarURL, passwordHash, input.GatewayTenantID, input.TenantID, input.TenantKey, input.DefaultUserGroupID,
roles, authProfile, metadata, input.Status,
))
}
func (s *Store) DeleteGatewayUser(ctx context.Context, id string) error {
result, err := s.pool.Exec(ctx, `
UPDATE gateway_users
SET deleted_at = now(),
status = 'deleted',
user_key = user_key || ':deleted:' || left(id::text, 8),
external_user_id = NULL,
email = NULL,
updated_at = now()
WHERE id = $1::uuid AND deleted_at IS NULL`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func (s *Store) CreateUserGroup(ctx context.Context, input UserGroupInput) (UserGroup, error) {
input = normalizeUserGroupInput(input)
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUserGroup(s.pool.QueryRow(ctx, `
INSERT INTO gateway_user_groups (
group_key, name, description, source, priority, recharge_discount_policy, billing_discount_policy,
rate_limit_policy, quota_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING `+userGroupColumns,
input.GroupKey, input.Name, input.Description, input.Source, input.Priority, rechargeDiscountPolicy, billingDiscountPolicy,
rateLimitPolicy, quotaPolicy, metadata, input.Status,
))
}
func (s *Store) UpdateUserGroup(ctx context.Context, id string, input UserGroupInput) (UserGroup, error) {
input = normalizeUserGroupInput(input)
rechargeDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.RechargeDiscountPolicy))
billingDiscountPolicy, _ := json.Marshal(emptyObjectIfNil(input.BillingDiscountPolicy))
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
quotaPolicy, _ := json.Marshal(emptyObjectIfNil(input.QuotaPolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanUserGroup(s.pool.QueryRow(ctx, `
UPDATE gateway_user_groups
SET group_key = $2,
name = $3,
description = NULLIF($4, ''),
source = $5,
priority = $6,
recharge_discount_policy = $7,
billing_discount_policy = $8,
rate_limit_policy = $9,
quota_policy = $10,
metadata = $11,
status = $12,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+userGroupColumns,
id, input.GroupKey, input.Name, input.Description, input.Source, input.Priority,
rechargeDiscountPolicy, billingDiscountPolicy, rateLimitPolicy, quotaPolicy, metadata, input.Status,
))
}
func (s *Store) DeleteUserGroup(ctx context.Context, id string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE subject_type = 'user_group' AND subject_id = $1::uuid`, id); err != nil {
return err
}
result, err := tx.Exec(ctx, `DELETE FROM gateway_user_groups WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return tx.Commit(ctx)
}
const tenantColumns = `
id::text, tenant_key, source, COALESCE(external_tenant_id, ''), name, COALESCE(description, ''),
COALESCE(default_user_group_id::text, ''), COALESCE(plan_key, ''), billing_profile, rate_limit_policy,
auth_policy, metadata, status, COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`
const userColumns = `
id::text, user_key, source, COALESCE(external_user_id, ''), username,
COALESCE(display_name, ''), COALESCE(email, ''), COALESCE(phone, ''), COALESCE(avatar_url, ''),
COALESCE(gateway_tenant_id::text, ''), COALESCE(tenant_id, ''), COALESCE(tenant_key, ''),
COALESCE(default_user_group_id::text, ''), roles, auth_profile, metadata,
status, COALESCE(last_login_at::text, ''), COALESCE(synced_at::text, ''), COALESCE(source_updated_at::text, ''),
created_at, updated_at`
const userGroupColumns = `
id::text, group_key, name, COALESCE(description, ''), source, priority,
recharge_discount_policy, billing_discount_policy, rate_limit_policy, quota_policy, metadata,
status, created_at, updated_at`
type scanner interface {
Scan(dest ...any) error
}
func scanTenant(row scanner) (GatewayTenant, error) {
var item GatewayTenant
var billingProfile []byte
var rateLimitPolicy []byte
var authPolicy []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.TenantKey, &item.Source, &item.ExternalTenantID, &item.Name, &item.Description,
&item.DefaultUserGroupID, &item.PlanKey, &billingProfile, &rateLimitPolicy, &authPolicy, &metadata,
&item.Status, &item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return GatewayTenant{}, err
}
item.BillingProfile = decodeObject(billingProfile)
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.AuthPolicy = decodeObject(authPolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func scanUser(row scanner) (GatewayUser, error) {
var item GatewayUser
var roles []byte
var authProfile []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.UserKey, &item.Source, &item.ExternalUserID, &item.Username, &item.DisplayName,
&item.Email, &item.Phone, &item.AvatarURL, &item.GatewayTenantID, &item.TenantID, &item.TenantKey,
&item.DefaultUserGroupID, &roles, &authProfile, &metadata, &item.Status, &item.LastLoginAt,
&item.SyncedAt, &item.SourceUpdatedAt, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return GatewayUser{}, err
}
item.Roles = decodeStringArray(roles)
item.AuthProfile = decodeObject(authProfile)
item.Metadata = decodeObject(metadata)
return item, nil
}
func scanUserGroup(row scanner) (UserGroup, error) {
var item UserGroup
var rechargeDiscountPolicy []byte
var billingDiscountPolicy []byte
var rateLimitPolicy []byte
var quotaPolicy []byte
var metadata []byte
if err := row.Scan(
&item.ID, &item.GroupKey, &item.Name, &item.Description, &item.Source, &item.Priority,
&rechargeDiscountPolicy, &billingDiscountPolicy, &rateLimitPolicy, &quotaPolicy, &metadata,
&item.Status, &item.CreatedAt, &item.UpdatedAt,
); err != nil {
return UserGroup{}, err
}
item.RechargeDiscountPolicy = decodeObject(rechargeDiscountPolicy)
item.BillingDiscountPolicy = decodeObject(billingDiscountPolicy)
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.QuotaPolicy = decodeObject(quotaPolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizeTenantInput(input GatewayTenantInput) GatewayTenantInput {
input.TenantKey = strings.TrimSpace(input.TenantKey)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.Name = strings.TrimSpace(input.Name)
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
return input
}
func normalizeUserInput(input GatewayUserInput) GatewayUserInput {
input.Username = strings.TrimSpace(input.Username)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.UserKey = firstNonEmpty(strings.TrimSpace(input.UserKey), input.Source+":"+input.Username)
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
input.Roles = normalizeGatewayRoles(input.Roles)
return input
}
func normalizeGatewayRoles(roles []string) []string {
for _, role := range roles {
switch strings.TrimSpace(role) {
case "manager":
return []string{"manager"}
case "admin":
return []string{"admin"}
case "operator":
return []string{"operator"}
case "creator":
return []string{"creator"}
case "user":
return []string{"user"}
}
}
return []string{"user"}
}
func normalizeUserGroupInput(input UserGroupInput) UserGroupInput {
input.GroupKey = strings.TrimSpace(input.GroupKey)
input.Name = strings.TrimSpace(input.Name)
input.Source = firstNonEmpty(strings.TrimSpace(input.Source), "gateway")
input.Status = firstNonEmpty(strings.TrimSpace(input.Status), "active")
if input.Priority == 0 {
input.Priority = 100
}
return input
}
+343
View File
@@ -0,0 +1,343 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
)
type platformModelQuerier interface {
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
type modelCatalogSnapshot struct {
ID string
ProviderKey string
CanonicalModelKey string
ProviderModelName string
ModelType string
DisplayName string
Capabilities map[string]any
BaseBillingConfig map[string]any
DefaultRateLimitPolicy map[string]any
RuntimePolicySetID string
RuntimePolicyOverride map[string]any
}
func (s *Store) CreatePlatformModel(ctx context.Context, input CreatePlatformModelInput) (PlatformModel, error) {
return s.createPlatformModel(ctx, s.pool, input)
}
func (s *Store) ReplacePlatformModels(ctx context.Context, platformID string, inputs []CreatePlatformModelInput) ([]PlatformModel, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
keptIDs := make([]string, 0, len(inputs))
for _, input := range inputs {
input.PlatformID = platformID
model, err := s.createPlatformModel(ctx, tx, input)
if err != nil {
return nil, err
}
keptIDs = append(keptIDs, model.ID)
}
if len(keptIDs) == 0 {
if _, err := tx.Exec(ctx, `
WITH deleted AS (
DELETE FROM platform_models
WHERE platform_id = $1::uuid
RETURNING id
)
DELETE FROM gateway_access_rules
WHERE resource_type = 'platform_model'
AND resource_id IN (SELECT id FROM deleted)`, platformID); err != nil {
return nil, err
}
} else if _, err := tx.Exec(ctx, `
WITH deleted AS (
DELETE FROM platform_models
WHERE platform_id = $1::uuid
AND NOT (id::text = ANY($2::text[]))
RETURNING id
)
DELETE FROM gateway_access_rules
WHERE resource_type = 'platform_model'
AND resource_id IN (SELECT id FROM deleted)`, platformID, keptIDs); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return s.ListPlatformModels(ctx, platformID)
}
func (s *Store) createPlatformModel(ctx context.Context, q platformModelQuerier, input CreatePlatformModelInput) (PlatformModel, error) {
base, err := s.lookupBaseModel(ctx, q, input.BaseModelID, input.CanonicalModelKey, input.ModelName)
if err != nil && !IsNotFound(err) {
return PlatformModel{}, err
}
if input.ModelType == "" {
input.ModelType = base.ModelType
}
if input.ModelName == "" {
input.ModelName = base.ProviderModelName
}
if input.DisplayName == "" {
input.DisplayName = firstNonEmpty(base.DisplayName, input.ModelName)
}
input.ModelAlias = normalizePlatformModelAlias(input.ModelAlias, base)
if input.PricingMode == "" {
input.PricingMode = "inherit_discount"
}
capabilities := input.Capabilities
if len(capabilities) == 0 {
capabilities = mergeObjects(base.Capabilities, input.CapabilityOverride)
}
billingConfig := input.BillingConfig
if len(billingConfig) == 0 {
billingConfig = mergeObjects(base.BaseBillingConfig, input.BillingConfigOverride)
}
rateLimitPolicy := input.RateLimitPolicy
if len(rateLimitPolicy) == 0 {
rateLimitPolicy = base.DefaultRateLimitPolicy
}
runtimePolicySetID := strings.TrimSpace(input.RuntimePolicySetID)
if runtimePolicySetID == "" {
runtimePolicySetID = base.RuntimePolicySetID
}
runtimePolicyOverride := input.RuntimePolicyOverride
if len(runtimePolicyOverride) == 0 {
runtimePolicyOverride = base.RuntimePolicyOverride
}
capabilityOverrideJSON, _ := json.Marshal(emptyObjectIfNil(input.CapabilityOverride))
capabilitiesJSON, _ := json.Marshal(emptyObjectIfNil(capabilities))
billingOverrideJSON, _ := json.Marshal(emptyObjectIfNil(input.BillingConfigOverride))
billingJSON, _ := json.Marshal(emptyObjectIfNil(billingConfig))
permissionJSON, _ := json.Marshal(emptyObjectIfNil(input.PermissionConfig))
retryJSON, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
rateLimitJSON, _ := json.Marshal(emptyObjectIfNil(rateLimitPolicy))
runtimePolicyOverrideJSON, _ := json.Marshal(emptyObjectIfNil(runtimePolicyOverride))
discount := any(nil)
if input.DiscountFactor > 0 {
discount = input.DiscountFactor
}
baseID := any(nil)
if base.ID != "" {
baseID = base.ID
}
var model PlatformModel
var capabilityOverrideBytes []byte
var capabilitiesBytes []byte
var billingOverrideBytes []byte
var billingBytes []byte
var permissionBytes []byte
var retryPolicyBytes []byte
var rateLimitPolicyBytes []byte
var runtimePolicyOverrideBytes []byte
err = q.QueryRow(ctx, `
INSERT INTO platform_models (
platform_id, base_model_id, model_name, model_alias, model_type, display_name,
capability_override, capabilities, pricing_mode, discount_factor,
pricing_rule_set_id, billing_config_override, billing_config, permission_config, retry_policy, rate_limit_policy,
runtime_policy_set_id, runtime_policy_override, enabled
)
VALUES (
$1::uuid, $2::uuid, $3, NULLIF($4, ''), $5, $6,
$7::jsonb, $8::jsonb, $9, $10::numeric,
NULLIF($11, '')::uuid, $12::jsonb, $13::jsonb, $14::jsonb, $15::jsonb, $16::jsonb,
NULLIF($17, '')::uuid, $18::jsonb, true
)
ON CONFLICT (platform_id, model_name, model_type) DO UPDATE
SET base_model_id = EXCLUDED.base_model_id,
model_alias = EXCLUDED.model_alias,
display_name = EXCLUDED.display_name,
capability_override = EXCLUDED.capability_override,
capabilities = EXCLUDED.capabilities,
pricing_mode = EXCLUDED.pricing_mode,
discount_factor = EXCLUDED.discount_factor,
pricing_rule_set_id = EXCLUDED.pricing_rule_set_id,
billing_config_override = EXCLUDED.billing_config_override,
billing_config = EXCLUDED.billing_config,
permission_config = EXCLUDED.permission_config,
retry_policy = EXCLUDED.retry_policy,
rate_limit_policy = EXCLUDED.rate_limit_policy,
runtime_policy_set_id = EXCLUDED.runtime_policy_set_id,
runtime_policy_override = EXCLUDED.runtime_policy_override,
enabled = true,
updated_at = now()
RETURNING id::text, platform_id::text, COALESCE(base_model_id::text, ''), model_name,
COALESCE(model_alias, ''), model_type, display_name, capability_override,
capabilities, pricing_mode, COALESCE(discount_factor, 0)::float8,
COALESCE(pricing_rule_set_id::text, ''), billing_config_override, billing_config,
permission_config, retry_policy, rate_limit_policy, COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override,
enabled, created_at, updated_at`,
input.PlatformID,
baseID,
input.ModelName,
input.ModelAlias,
input.ModelType,
input.DisplayName,
string(capabilityOverrideJSON),
string(capabilitiesJSON),
input.PricingMode,
discount,
input.PricingRuleSetID,
string(billingOverrideJSON),
string(billingJSON),
string(permissionJSON),
string(retryJSON),
string(rateLimitJSON),
runtimePolicySetID,
string(runtimePolicyOverrideJSON),
).Scan(
&model.ID,
&model.PlatformID,
&model.BaseModelID,
&model.ModelName,
&model.ModelAlias,
&model.ModelType,
&model.DisplayName,
&capabilityOverrideBytes,
&capabilitiesBytes,
&model.PricingMode,
&model.DiscountFactor,
&model.PricingRuleSetID,
&billingOverrideBytes,
&billingBytes,
&permissionBytes,
&retryPolicyBytes,
&rateLimitPolicyBytes,
&model.RuntimePolicySetID,
&runtimePolicyOverrideBytes,
&model.Enabled,
&model.CreatedAt,
&model.UpdatedAt,
)
if err != nil {
return PlatformModel{}, err
}
model.CapabilityOverride = decodeObject(capabilityOverrideBytes)
model.Capabilities = decodeObject(capabilitiesBytes)
model.BillingConfigOverride = decodeObject(billingOverrideBytes)
model.BillingConfig = decodeObject(billingBytes)
model.PermissionConfig = decodeObject(permissionBytes)
model.RetryPolicy = decodeObject(retryPolicyBytes)
model.RateLimitPolicy = decodeObject(rateLimitPolicyBytes)
model.RuntimePolicyOverride = decodeObject(runtimePolicyOverrideBytes)
return model, nil
}
func (s *Store) DeletePlatformModel(ctx context.Context, id string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
result, err := tx.Exec(ctx, `DELETE FROM platform_models WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
if _, err := tx.Exec(ctx, `
DELETE FROM gateway_access_rules
WHERE resource_type = 'platform_model' AND resource_id = $1::uuid`, id); err != nil {
return err
}
return tx.Commit(ctx)
}
func (s *Store) lookupBaseModel(ctx context.Context, q platformModelQuerier, id string, canonicalKey string, modelName string) (modelCatalogSnapshot, error) {
var item modelCatalogSnapshot
var capabilities []byte
var billingConfig []byte
var rateLimitPolicy []byte
var runtimePolicyOverride []byte
err := q.QueryRow(ctx, `
SELECT id::text, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy,
COALESCE(runtime_policy_set_id::text, ''), runtime_policy_override
FROM base_model_catalog
WHERE ($1 <> '' AND id = NULLIF($1, '')::uuid)
OR ($2 <> '' AND canonical_model_key = $2)
OR ($3 <> '' AND provider_model_name = $3)
ORDER BY CASE WHEN id::text = $1 THEN 0 WHEN canonical_model_key = $2 THEN 1 ELSE 2 END
LIMIT 1`, strings.TrimSpace(id), strings.TrimSpace(canonicalKey), strings.TrimSpace(modelName)).Scan(
&item.ID,
&item.ProviderKey,
&item.CanonicalModelKey,
&item.ProviderModelName,
&item.ModelType,
&item.DisplayName,
&capabilities,
&billingConfig,
&rateLimitPolicy,
&item.RuntimePolicySetID,
&runtimePolicyOverride,
)
if err != nil {
if err == pgx.ErrNoRows {
return modelCatalogSnapshot{}, err
}
return modelCatalogSnapshot{}, err
}
item.Capabilities = decodeObject(capabilities)
item.BaseBillingConfig = decodeObject(billingConfig)
item.DefaultRateLimitPolicy = decodeObject(rateLimitPolicy)
item.RuntimePolicyOverride = decodeObject(runtimePolicyOverride)
return item, nil
}
func normalizePlatformModelAlias(alias string, base modelCatalogSnapshot) string {
alias = strings.TrimSpace(alias)
if alias == "" {
alias = firstNonEmpty(base.ProviderModelName, base.DisplayName, base.CanonicalModelKey)
}
if base.ProviderKey != "" {
alias = strings.TrimPrefix(alias, base.ProviderKey+":")
}
if alias == base.CanonicalModelKey {
alias = stripAliasPrefix(alias)
}
return strings.TrimSpace(alias)
}
func stripAliasPrefix(alias string) string {
if before, after, ok := strings.Cut(alias, ":"); ok && before != "" && after != "" {
return after
}
return alias
}
func mergeObjects(base map[string]any, override map[string]any) map[string]any {
out := map[string]any{}
for key, value := range base {
out[key] = value
}
for key, value := range override {
out[key] = value
}
if len(out) == 0 {
return nil
}
return out
}
func emptyObjectIfNil(value map[string]any) map[string]any {
if value == nil {
return map[string]any{}
}
return value
}
File diff suppressed because it is too large Load Diff
+341
View File
@@ -0,0 +1,341 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
)
const pricingRuleSetColumns = `
id::text, rule_set_key, name, COALESCE(description, ''), category, currency,
status, metadata, created_at, updated_at`
const pricingRuleColumns = `
id::text, COALESCE(rule_set_id::text, ''), rule_key, display_name, scope_type,
COALESCE(scope_id::text, ''), resource_type, unit, base_price::float8, currency,
base_weight, dynamic_weight, calculator_type, dimension_schema, formula_config,
priority, status, metadata, created_at, updated_at`
type PricingRuleInput struct {
RuleKey string `json:"ruleKey"`
DisplayName string `json:"displayName"`
ResourceType string `json:"resourceType"`
Unit string `json:"unit"`
BasePrice float64 `json:"basePrice"`
Currency string `json:"currency"`
BaseWeight map[string]any `json:"baseWeight"`
DynamicWeight map[string]any `json:"dynamicWeight"`
CalculatorType string `json:"calculatorType"`
DimensionSchema map[string]any `json:"dimensionSchema"`
FormulaConfig map[string]any `json:"formulaConfig"`
Priority int `json:"priority"`
Status string `json:"status"`
Metadata map[string]any `json:"metadata"`
}
type PricingRuleSetInput struct {
RuleSetKey string `json:"ruleSetKey"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Currency string `json:"currency"`
Status string `json:"status"`
Metadata map[string]any `json:"metadata"`
Rules []PricingRuleInput `json:"rules"`
}
type pricingScanner interface {
Scan(dest ...any) error
}
func (s *Store) ListPricingRuleSets(ctx context.Context) ([]PricingRuleSet, error) {
rows, err := s.pool.Query(ctx, `SELECT `+pricingRuleSetColumns+` FROM model_pricing_rule_sets ORDER BY category ASC, name ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]PricingRuleSet, 0)
byID := map[string]int{}
for rows.Next() {
item, err := scanPricingRuleSet(rows)
if err != nil {
return nil, err
}
byID[item.ID] = len(items)
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
ruleRows, err := s.pool.Query(ctx, `
SELECT `+pricingRuleColumns+`
FROM model_pricing_rules
WHERE rule_set_id IS NOT NULL
ORDER BY rule_set_id, priority ASC, resource_type ASC, rule_key ASC`)
if err != nil {
return nil, err
}
defer ruleRows.Close()
for ruleRows.Next() {
rule, err := scanPricingRule(ruleRows)
if err != nil {
return nil, err
}
if index, ok := byID[rule.RuleSetID]; ok {
items[index].Rules = append(items[index].Rules, rule)
}
}
return items, ruleRows.Err()
}
func (s *Store) CreatePricingRuleSet(ctx context.Context, input PricingRuleSetInput) (PricingRuleSet, error) {
input = normalizePricingRuleSet(input)
tx, err := s.pool.Begin(ctx)
if err != nil {
return PricingRuleSet{}, err
}
defer tx.Rollback(ctx)
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
item, err := scanPricingRuleSet(tx.QueryRow(ctx, `
INSERT INTO model_pricing_rule_sets (rule_set_key, name, description, category, currency, status, metadata)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7)
RETURNING `+pricingRuleSetColumns,
input.RuleSetKey, input.Name, input.Description, input.Category, input.Currency, input.Status, metadata,
))
if err != nil {
return PricingRuleSet{}, err
}
if err := insertPricingRules(ctx, tx, item.ID, input.Currency, input.Rules); err != nil {
return PricingRuleSet{}, err
}
if err := tx.Commit(ctx); err != nil {
return PricingRuleSet{}, err
}
item.Rules = pricingInputsToRules(item.ID, input.Currency, input.Rules)
return item, nil
}
func (s *Store) UpdatePricingRuleSet(ctx context.Context, id string, input PricingRuleSetInput) (PricingRuleSet, error) {
input = normalizePricingRuleSet(input)
tx, err := s.pool.Begin(ctx)
if err != nil {
return PricingRuleSet{}, err
}
defer tx.Rollback(ctx)
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
item, err := scanPricingRuleSet(tx.QueryRow(ctx, `
UPDATE model_pricing_rule_sets
SET rule_set_key = $2,
name = $3,
description = NULLIF($4, ''),
category = $5,
currency = $6,
status = $7,
metadata = $8,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+pricingRuleSetColumns,
id, input.RuleSetKey, input.Name, input.Description, input.Category, input.Currency, input.Status, metadata,
))
if err != nil {
return PricingRuleSet{}, err
}
if _, err := tx.Exec(ctx, `DELETE FROM model_pricing_rules WHERE rule_set_id = $1::uuid`, id); err != nil {
return PricingRuleSet{}, err
}
if err := insertPricingRules(ctx, tx, item.ID, input.Currency, input.Rules); err != nil {
return PricingRuleSet{}, err
}
if err := tx.Commit(ctx); err != nil {
return PricingRuleSet{}, err
}
item.Rules = pricingInputsToRules(item.ID, input.Currency, input.Rules)
return item, nil
}
func (s *Store) DeletePricingRuleSet(ctx context.Context, id string) error {
var ruleSetKey string
if err := s.pool.QueryRow(ctx, `SELECT rule_set_key FROM model_pricing_rule_sets WHERE id = $1::uuid`, id).Scan(&ruleSetKey); err != nil {
return err
}
if ruleSetKey == "default-multimodal-v1" {
return ErrProtectedDefault
}
result, err := s.pool.Exec(ctx, `DELETE FROM model_pricing_rule_sets WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func insertPricingRules(ctx context.Context, tx pgx.Tx, ruleSetID string, defaultCurrency string, rules []PricingRuleInput) error {
for index, rule := range rules {
rule = normalizePricingRule(rule, index, defaultCurrency)
baseWeight, _ := json.Marshal(emptyObjectIfNil(rule.BaseWeight))
dynamicWeight, _ := json.Marshal(emptyObjectIfNil(rule.DynamicWeight))
dimensionSchema, _ := json.Marshal(emptyObjectIfNil(rule.DimensionSchema))
formulaConfig, _ := json.Marshal(emptyObjectIfNil(rule.FormulaConfig))
metadata, _ := json.Marshal(emptyObjectIfNil(rule.Metadata))
if _, err := tx.Exec(ctx, `
INSERT INTO model_pricing_rules (
rule_set_id, rule_key, display_name, scope_type, scope_id, resource_type,
unit, base_price, currency, base_weight, dynamic_weight, calculator_type,
dimension_schema, formula_config, priority, status, metadata
)
VALUES ($1::uuid, $2, $3, 'rule_set', $1::uuid, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`,
ruleSetID, rule.RuleKey, rule.DisplayName, rule.ResourceType, rule.Unit,
rule.BasePrice, rule.Currency, baseWeight, dynamicWeight, rule.CalculatorType,
dimensionSchema, formulaConfig, rule.Priority, rule.Status, metadata,
); err != nil {
return err
}
}
return nil
}
func scanPricingRuleSet(scanner pricingScanner) (PricingRuleSet, error) {
var item PricingRuleSet
var metadata []byte
if err := scanner.Scan(
&item.ID,
&item.RuleSetKey,
&item.Name,
&item.Description,
&item.Category,
&item.Currency,
&item.Status,
&metadata,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return PricingRuleSet{}, err
}
item.Metadata = decodeObject(metadata)
item.Rules = []PricingRule{}
return item, nil
}
func scanPricingRule(scanner pricingScanner) (PricingRule, error) {
var item PricingRule
var baseWeight []byte
var dynamicWeight []byte
var dimensionSchema []byte
var formulaConfig []byte
var metadata []byte
if err := scanner.Scan(
&item.ID,
&item.RuleSetID,
&item.RuleKey,
&item.DisplayName,
&item.ScopeType,
&item.ScopeID,
&item.ResourceType,
&item.Unit,
&item.BasePrice,
&item.Currency,
&baseWeight,
&dynamicWeight,
&item.CalculatorType,
&dimensionSchema,
&formulaConfig,
&item.Priority,
&item.Status,
&metadata,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return PricingRule{}, err
}
item.BaseWeight = decodeObject(baseWeight)
item.DynamicWeight = decodeObject(dynamicWeight)
item.DimensionSchema = decodeObject(dimensionSchema)
item.FormulaConfig = decodeObject(formulaConfig)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizePricingRuleSet(input PricingRuleSetInput) PricingRuleSetInput {
input.RuleSetKey = strings.TrimSpace(input.RuleSetKey)
input.Name = strings.TrimSpace(input.Name)
input.Description = strings.TrimSpace(input.Description)
input.Category = strings.TrimSpace(input.Category)
input.Currency = strings.TrimSpace(input.Currency)
input.Status = strings.TrimSpace(input.Status)
if input.Category == "" {
input.Category = "custom"
}
if input.Currency == "" {
input.Currency = "resource"
}
if input.Status == "" {
input.Status = "active"
}
return input
}
func normalizePricingRule(input PricingRuleInput, index int, defaultCurrency string) PricingRuleInput {
input.RuleKey = strings.TrimSpace(input.RuleKey)
input.DisplayName = strings.TrimSpace(input.DisplayName)
input.ResourceType = strings.TrimSpace(input.ResourceType)
input.Unit = strings.TrimSpace(input.Unit)
input.Currency = strings.TrimSpace(input.Currency)
input.CalculatorType = strings.TrimSpace(input.CalculatorType)
input.Status = strings.TrimSpace(input.Status)
if input.RuleKey == "" {
input.RuleKey = "rule_" + strings.ReplaceAll(input.ResourceType+"_"+input.Unit, " ", "_")
}
if input.DisplayName == "" {
input.DisplayName = input.ResourceType
}
if input.Unit == "" {
input.Unit = "item"
}
if input.Currency == "" {
input.Currency = defaultCurrency
}
if input.CalculatorType == "" {
input.CalculatorType = "unit_weight"
}
if input.Priority == 0 {
input.Priority = (index + 1) * 10
}
if input.Status == "" {
input.Status = "active"
}
return input
}
func pricingInputsToRules(ruleSetID string, defaultCurrency string, rules []PricingRuleInput) []PricingRule {
items := make([]PricingRule, 0, len(rules))
for index, input := range rules {
input = normalizePricingRule(input, index, defaultCurrency)
items = append(items, PricingRule{
RuleSetID: ruleSetID,
RuleKey: input.RuleKey,
DisplayName: input.DisplayName,
ScopeType: "rule_set",
ScopeID: ruleSetID,
ResourceType: input.ResourceType,
Unit: input.Unit,
BasePrice: input.BasePrice,
Currency: input.Currency,
BaseWeight: emptyObjectIfNil(input.BaseWeight),
DynamicWeight: emptyObjectIfNil(input.DynamicWeight),
CalculatorType: input.CalculatorType,
DimensionSchema: emptyObjectIfNil(input.DimensionSchema),
FormulaConfig: emptyObjectIfNil(input.FormulaConfig),
Priority: input.Priority,
Status: input.Status,
Metadata: emptyObjectIfNil(input.Metadata),
})
}
return items
}
+109
View File
@@ -0,0 +1,109 @@
package store
import (
"context"
"errors"
)
func (s *Store) ReserveRateLimits(ctx context.Context, taskID string, reservations []RateLimitReservation) (RateLimitResult, error) {
tx, err := s.pool.Begin(ctx)
if err != nil {
return RateLimitResult{}, err
}
defer tx.Rollback(ctx)
result := RateLimitResult{}
for _, reservation := range reservations {
if reservation.Limit <= 0 || reservation.Amount <= 0 {
continue
}
if reservation.Metric == "" || reservation.Amount > reservation.Limit {
return RateLimitResult{}, ErrRateLimited
}
if reservation.WindowSeconds <= 0 {
reservation.WindowSeconds = 60
}
if reservation.Metric == "concurrent" {
if reservation.LeaseTTLSeconds <= 0 {
reservation.LeaseTTLSeconds = 120
}
var active float64
if err := tx.QueryRow(ctx, `
SELECT COALESCE(SUM(lease_value), 0)::float8
FROM gateway_concurrency_leases
WHERE scope_type = $1
AND scope_key = $2
AND released_at IS NULL
AND expires_at > now()`,
reservation.ScopeType,
reservation.ScopeKey,
).Scan(&active); err != nil {
return RateLimitResult{}, err
}
if active+reservation.Amount > reservation.Limit {
return RateLimitResult{}, ErrRateLimited
}
var leaseID string
if err := tx.QueryRow(ctx, `
INSERT INTO gateway_concurrency_leases (task_id, scope_type, scope_key, lease_value, expires_at)
VALUES ($1::uuid, $2, $3, $4, now() + ($5::int * interval '1 second'))
RETURNING id::text`,
taskID,
reservation.ScopeType,
reservation.ScopeKey,
reservation.Amount,
reservation.LeaseTTLSeconds,
).Scan(&leaseID); err != nil {
return RateLimitResult{}, err
}
result.LeaseIDs = append(result.LeaseIDs, leaseID)
continue
}
tag, err := tx.Exec(ctx, `
INSERT INTO gateway_rate_limit_counters (
scope_type, scope_key, metric, window_start, limit_value, used_value, reserved_value, reset_at
)
VALUES (
$1, $2, $3, date_trunc('minute', now()), $4, $5, 0,
date_trunc('minute', now()) + ($6::int * interval '1 second')
)
ON CONFLICT (scope_type, scope_key, metric, window_start) DO UPDATE
SET limit_value = EXCLUDED.limit_value,
used_value = gateway_rate_limit_counters.used_value + EXCLUDED.used_value,
reset_at = EXCLUDED.reset_at,
updated_at = now()
WHERE gateway_rate_limit_counters.used_value + EXCLUDED.used_value <= EXCLUDED.limit_value`,
reservation.ScopeType,
reservation.ScopeKey,
reservation.Metric,
reservation.Limit,
reservation.Amount,
reservation.WindowSeconds,
)
if err != nil {
return RateLimitResult{}, err
}
if tag.RowsAffected() == 0 {
return RateLimitResult{}, ErrRateLimited
}
}
return result, tx.Commit(ctx)
}
func (s *Store) ReleaseConcurrencyLeases(ctx context.Context, leaseIDs []string) error {
if len(leaseIDs) == 0 {
return nil
}
for _, leaseID := range leaseIDs {
if leaseID == "" {
continue
}
if _, err := s.pool.Exec(ctx, `
UPDATE gateway_concurrency_leases
SET released_at = now()
WHERE id = $1::uuid AND released_at IS NULL`, leaseID); err != nil && !errors.Is(err, ErrRateLimited) {
return err
}
}
return nil
}
+150
View File
@@ -0,0 +1,150 @@
package store
import (
"context"
"encoding/json"
"strings"
"github.com/jackc/pgx/v5"
)
const runtimePolicyColumns = `
id::text, policy_key, name, COALESCE(description, ''), rate_limit_policy, retry_policy,
auto_disable_policy, degrade_policy, metadata, status, created_at, updated_at`
type RuntimePolicySetInput struct {
PolicyKey string `json:"policyKey"`
Name string `json:"name"`
Description string `json:"description"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
RetryPolicy map[string]any `json:"retryPolicy"`
AutoDisablePolicy map[string]any `json:"autoDisablePolicy"`
DegradePolicy map[string]any `json:"degradePolicy"`
Metadata map[string]any `json:"metadata"`
Status string `json:"status"`
}
type runtimePolicyScanner interface {
Scan(dest ...any) error
}
func (s *Store) ListRuntimePolicySets(ctx context.Context) ([]RuntimePolicySet, error) {
rows, err := s.pool.Query(ctx, `SELECT `+runtimePolicyColumns+` FROM model_runtime_policy_sets ORDER BY policy_key ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]RuntimePolicySet, 0)
for rows.Next() {
item, err := scanRuntimePolicySet(rows)
if err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Store) CreateRuntimePolicySet(ctx context.Context, input RuntimePolicySetInput) (RuntimePolicySet, error) {
input = normalizeRuntimePolicyInput(input)
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
retryPolicy, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
autoDisablePolicy, _ := json.Marshal(emptyObjectIfNil(input.AutoDisablePolicy))
degradePolicy, _ := json.Marshal(emptyObjectIfNil(input.DegradePolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanRuntimePolicySet(s.pool.QueryRow(ctx, `
INSERT INTO model_runtime_policy_sets (
policy_key, name, description, rate_limit_policy, retry_policy, auto_disable_policy, degrade_policy, metadata, status
)
VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7, $8, $9)
RETURNING `+runtimePolicyColumns,
input.PolicyKey, input.Name, input.Description, rateLimitPolicy, retryPolicy,
autoDisablePolicy, degradePolicy, metadata, input.Status,
))
}
func (s *Store) UpdateRuntimePolicySet(ctx context.Context, id string, input RuntimePolicySetInput) (RuntimePolicySet, error) {
input = normalizeRuntimePolicyInput(input)
rateLimitPolicy, _ := json.Marshal(emptyObjectIfNil(input.RateLimitPolicy))
retryPolicy, _ := json.Marshal(emptyObjectIfNil(input.RetryPolicy))
autoDisablePolicy, _ := json.Marshal(emptyObjectIfNil(input.AutoDisablePolicy))
degradePolicy, _ := json.Marshal(emptyObjectIfNil(input.DegradePolicy))
metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
return scanRuntimePolicySet(s.pool.QueryRow(ctx, `
UPDATE model_runtime_policy_sets
SET policy_key = $2,
name = $3,
description = NULLIF($4, ''),
rate_limit_policy = $5,
retry_policy = $6,
auto_disable_policy = $7,
degrade_policy = $8,
metadata = $9,
status = $10,
updated_at = now()
WHERE id = $1::uuid
RETURNING `+runtimePolicyColumns,
id, input.PolicyKey, input.Name, input.Description, rateLimitPolicy, retryPolicy,
autoDisablePolicy, degradePolicy, metadata, input.Status,
))
}
func (s *Store) DeleteRuntimePolicySet(ctx context.Context, id string) error {
var policyKey string
if err := s.pool.QueryRow(ctx, `SELECT policy_key FROM model_runtime_policy_sets WHERE id = $1::uuid`, id).Scan(&policyKey); err != nil {
return err
}
if policyKey == "default-runtime-v1" {
return ErrProtectedDefault
}
result, err := s.pool.Exec(ctx, `DELETE FROM model_runtime_policy_sets WHERE id = $1::uuid`, id)
if err != nil {
return err
}
if result.RowsAffected() == 0 {
return pgx.ErrNoRows
}
return nil
}
func scanRuntimePolicySet(scanner runtimePolicyScanner) (RuntimePolicySet, error) {
var item RuntimePolicySet
var rateLimitPolicy []byte
var retryPolicy []byte
var autoDisablePolicy []byte
var degradePolicy []byte
var metadata []byte
if err := scanner.Scan(
&item.ID,
&item.PolicyKey,
&item.Name,
&item.Description,
&rateLimitPolicy,
&retryPolicy,
&autoDisablePolicy,
&degradePolicy,
&metadata,
&item.Status,
&item.CreatedAt,
&item.UpdatedAt,
); err != nil {
return RuntimePolicySet{}, err
}
item.RateLimitPolicy = decodeObject(rateLimitPolicy)
item.RetryPolicy = decodeObject(retryPolicy)
item.AutoDisablePolicy = decodeObject(autoDisablePolicy)
item.DegradePolicy = decodeObject(degradePolicy)
item.Metadata = decodeObject(metadata)
return item, nil
}
func normalizeRuntimePolicyInput(input RuntimePolicySetInput) RuntimePolicySetInput {
input.PolicyKey = strings.TrimSpace(input.PolicyKey)
input.Name = strings.TrimSpace(input.Name)
input.Description = strings.TrimSpace(input.Description)
input.Status = strings.TrimSpace(input.Status)
if input.Status == "" {
input.Status = "active"
}
return input
}
+142
View File
@@ -0,0 +1,142 @@
package store
import (
"errors"
"time"
)
var (
ErrNoModelCandidate = errors.New("no enabled platform model matches request")
ErrRateLimited = errors.New("rate limit exceeded")
)
type CreatePlatformModelInput struct {
PlatformID string `json:"platformId"`
BaseModelID string `json:"baseModelId"`
CanonicalModelKey string `json:"canonicalModelKey"`
ModelName string `json:"modelName"`
ModelAlias string `json:"modelAlias"`
ModelType string `json:"modelType"`
DisplayName string `json:"displayName"`
CapabilityOverride map[string]any `json:"capabilityOverride"`
Capabilities map[string]any `json:"capabilities"`
PricingMode string `json:"pricingMode"`
DiscountFactor float64 `json:"discountFactor"`
PricingRuleSetID string `json:"pricingRuleSetId"`
BillingConfigOverride map[string]any `json:"billingConfigOverride"`
BillingConfig map[string]any `json:"billingConfig"`
PermissionConfig map[string]any `json:"permissionConfig"`
RetryPolicy map[string]any `json:"retryPolicy"`
RateLimitPolicy map[string]any `json:"rateLimitPolicy"`
RuntimePolicySetID string `json:"runtimePolicySetId"`
RuntimePolicyOverride map[string]any `json:"runtimePolicyOverride"`
Enabled bool `json:"enabled"`
}
type RuntimeModelCandidate struct {
PlatformID string
PlatformKey string
PlatformName string
Provider string
SpecType string
BaseURL string
AuthType string
Credentials map[string]any
PlatformConfig map[string]any
DefaultPricingMode string
DefaultDiscountFactor float64
PlatformRetryPolicy map[string]any
PlatformRateLimitPolicy map[string]any
PlatformPriority int
PlatformModelID string
BaseModelID string
CanonicalModelKey string
ProviderModelName string
ModelName string
ModelAlias string
ModelType string
DisplayName string
Capabilities map[string]any
CapabilityOverride map[string]any
BaseBillingConfig map[string]any
BillingConfig map[string]any
BillingConfigOverride map[string]any
PermissionConfig map[string]any
PricingMode string
DiscountFactor float64
PlatformPricingRuleSetID string
ModelPricingRuleSetID string
ModelRetryPolicy map[string]any
ModelRateLimitPolicy map[string]any
RuntimePolicySetID string
RuntimePolicyOverride map[string]any
ClientID string
QueueKey string
}
type RateLimitReservation struct {
ScopeType string
ScopeKey string
Metric string
Limit float64
Amount float64
WindowSeconds int
LeaseTTLSeconds int
}
type RateLimitResult struct {
LeaseIDs []string
}
type CreateTaskAttemptInput struct {
TaskID string
AttemptNo int
PlatformID string
PlatformModelID string
ClientID string
QueueKey string
Status string
Simulated bool
RequestSnapshot map[string]any
}
type FinishTaskAttemptInput struct {
AttemptID string
Status string
Retryable bool
RequestID string
Usage map[string]any
Metrics map[string]any
ResponseSnapshot map[string]any
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
ErrorCode string
ErrorMessage string
}
type FinishTaskSuccessInput struct {
TaskID string
Result map[string]any
Billings []any
RequestID string
ResolvedModel string
Usage map[string]any
Metrics map[string]any
BillingSummary map[string]any
FinalChargeAmount float64
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
}
type FinishTaskFailureInput struct {
TaskID string
Code string
Message string
RequestID string
Metrics map[string]any
ResponseStartedAt time.Time
ResponseFinishedAt time.Time
ResponseDurationMS int64
}
+279
View File
@@ -0,0 +1,279 @@
package store
import (
"context"
"encoding/json"
"time"
)
func (s *Store) MarkTaskRunning(ctx context.Context, taskID string, modelType string, normalizedRequest map[string]any) error {
normalizedJSON, _ := json.Marshal(emptyObjectIfNil(normalizedRequest))
_, err := s.pool.Exec(ctx, `
UPDATE gateway_tasks
SET status = 'running',
model_type = NULLIF($2, ''),
normalized_request = $3::jsonb,
locked_at = now(),
heartbeat_at = now(),
updated_at = now()
WHERE id = $1::uuid`, taskID, modelType, string(normalizedJSON))
return err
}
func (s *Store) CreateTaskAttempt(ctx context.Context, input CreateTaskAttemptInput) (string, error) {
requestJSON, _ := json.Marshal(emptyObjectIfNil(input.RequestSnapshot))
tx, err := s.pool.Begin(ctx)
if err != nil {
return "", err
}
defer tx.Rollback(ctx)
var attemptID string
err = tx.QueryRow(ctx, `
INSERT INTO gateway_task_attempts (
task_id, attempt_no, platform_id, platform_model_id, client_id, queue_key,
status, simulated, request_snapshot
)
VALUES (
$1::uuid, $2, NULLIF($3, '')::uuid, NULLIF($4, '')::uuid, NULLIF($5, ''), $6,
$7, $8, $9::jsonb
)
RETURNING id::text`,
input.TaskID,
input.AttemptNo,
input.PlatformID,
input.PlatformModelID,
input.ClientID,
input.QueueKey,
firstNonEmpty(input.Status, "running"),
input.Simulated,
string(requestJSON),
).Scan(&attemptID)
if err != nil {
return "", err
}
if _, err := tx.Exec(ctx, `
UPDATE gateway_tasks
SET attempt_count = GREATEST(attempt_count, $2), updated_at = now()
WHERE id = $1::uuid`, input.TaskID, input.AttemptNo); err != nil {
return "", err
}
return attemptID, tx.Commit(ctx)
}
func (s *Store) FinishTaskAttempt(ctx context.Context, input FinishTaskAttemptInput) error {
responseJSON, _ := json.Marshal(emptyObjectIfNil(input.ResponseSnapshot))
usageJSON, _ := json.Marshal(emptyObjectIfNil(input.Usage))
metricsJSON, _ := json.Marshal(emptyObjectIfNil(input.Metrics))
_, err := s.pool.Exec(ctx, `
UPDATE gateway_task_attempts
SET status = $2,
retryable = $3,
request_id = NULLIF($4, ''),
usage = $5::jsonb,
metrics = $6::jsonb,
response_snapshot = $7::jsonb,
response_started_at = $8::timestamptz,
response_finished_at = $9::timestamptz,
response_duration_ms = $10,
error_code = NULLIF($11, ''),
error_message = NULLIF($12, ''),
finished_at = now()
WHERE id = $1::uuid`,
input.AttemptID,
input.Status,
input.Retryable,
input.RequestID,
string(usageJSON),
string(metricsJSON),
string(responseJSON),
nullableTime(input.ResponseStartedAt),
nullableTime(input.ResponseFinishedAt),
input.ResponseDurationMS,
input.ErrorCode,
input.ErrorMessage,
)
return err
}
func (s *Store) FinishTaskSuccess(ctx context.Context, input FinishTaskSuccessInput) (GatewayTask, error) {
resultJSON, _ := json.Marshal(emptyObjectIfNil(input.Result))
billingsJSON, _ := json.Marshal(input.Billings)
usageJSON, _ := json.Marshal(emptyObjectIfNil(input.Usage))
metricsJSON, _ := json.Marshal(emptyObjectIfNil(input.Metrics))
billingSummaryJSON, _ := json.Marshal(emptyObjectIfNil(input.BillingSummary))
if _, err := s.pool.Exec(ctx, `
UPDATE gateway_tasks
SET status = 'succeeded',
result = $2::jsonb,
billings = $3::jsonb,
request_id = NULLIF($4, ''),
resolved_model = NULLIF($5, ''),
usage = $6::jsonb,
metrics = $7::jsonb,
billing_summary = $8::jsonb,
final_charge_amount = $9,
response_started_at = $10::timestamptz,
response_finished_at = $11::timestamptz,
response_duration_ms = $12,
error = NULL,
error_code = NULL,
error_message = NULL,
finished_at = now(),
updated_at = now()
WHERE id = $1::uuid`,
input.TaskID,
string(resultJSON),
string(billingsJSON),
input.RequestID,
input.ResolvedModel,
string(usageJSON),
string(metricsJSON),
string(billingSummaryJSON),
input.FinalChargeAmount,
nullableTime(input.ResponseStartedAt),
nullableTime(input.ResponseFinishedAt),
input.ResponseDurationMS,
); err != nil {
return GatewayTask{}, err
}
return s.GetTask(ctx, input.TaskID)
}
func (s *Store) FinishTaskFailure(ctx context.Context, input FinishTaskFailureInput) (GatewayTask, error) {
metricsJSON, _ := json.Marshal(emptyObjectIfNil(input.Metrics))
if _, err := s.pool.Exec(ctx, `
UPDATE gateway_tasks
SET status = 'failed',
error = NULLIF($2, ''),
error_code = NULLIF($3, ''),
error_message = NULLIF($2, ''),
request_id = NULLIF($4, ''),
metrics = $5::jsonb,
response_started_at = $6::timestamptz,
response_finished_at = $7::timestamptz,
response_duration_ms = $8,
finished_at = now(),
updated_at = now()
WHERE id = $1::uuid`,
input.TaskID,
input.Message,
input.Code,
input.RequestID,
string(metricsJSON),
nullableTime(input.ResponseStartedAt),
nullableTime(input.ResponseFinishedAt),
input.ResponseDurationMS,
); err != nil {
return GatewayTask{}, err
}
return s.GetTask(ctx, input.TaskID)
}
func nullableTime(value time.Time) any {
if value.IsZero() {
return nil
}
return value
}
func (s *Store) AddTaskEvent(ctx context.Context, taskID string, eventType string, status string, phase string, progress float64, message string, payload map[string]any, simulated bool) (TaskEvent, error) {
payloadJSON, _ := json.Marshal(emptyObjectIfNil(payload))
var event TaskEvent
var payloadBytes []byte
err := s.pool.QueryRow(ctx, `
WITH next_seq AS (
SELECT COALESCE(MAX(seq), 0) + 1 AS seq
FROM gateway_task_events
WHERE task_id = $1::uuid
)
INSERT INTO gateway_task_events (task_id, seq, event_type, status, phase, progress, message, payload, simulated)
SELECT $1::uuid, next_seq.seq, $2, NULLIF($3, ''), NULLIF($4, ''), $5, NULLIF($6, ''), $7::jsonb, $8
FROM next_seq
RETURNING id::text, task_id::text, seq, event_type, COALESCE(status, ''), COALESCE(phase, ''),
COALESCE(progress, 0)::float8, COALESCE(message, ''), payload, simulated, created_at`,
taskID,
eventType,
status,
phase,
progress,
message,
string(payloadJSON),
simulated,
).Scan(
&event.ID,
&event.TaskID,
&event.Seq,
&event.EventType,
&event.Status,
&event.Phase,
&event.Progress,
&event.Message,
&payloadBytes,
&event.Simulated,
&event.CreatedAt,
)
if err != nil {
return TaskEvent{}, err
}
event.Payload = decodeObject(payloadBytes)
return event, nil
}
func (s *Store) QueueTaskCallback(ctx context.Context, event TaskEvent, callbackURL string) error {
if callbackURL == "" {
return nil
}
payloadJSON, _ := json.Marshal(map[string]any{
"taskId": event.TaskID,
"seq": event.Seq,
"eventType": event.EventType,
"status": event.Status,
"phase": event.Phase,
"progress": event.Progress,
"message": event.Message,
"payload": event.Payload,
"simulated": event.Simulated,
"createdAt": event.CreatedAt,
})
_, err := s.pool.Exec(ctx, `
INSERT INTO gateway_task_callback_outbox (task_id, event_id, seq, callback_url, payload)
VALUES ($1::uuid, $2::uuid, $3, $4, $5::jsonb)
ON CONFLICT (task_id, seq, callback_url) DO NOTHING`,
event.TaskID,
event.ID,
event.Seq,
callbackURL,
string(payloadJSON),
)
return err
}
func (s *Store) RecordClientAssignment(ctx context.Context, candidate RuntimeModelCandidate) error {
_, err := s.pool.Exec(ctx, `
INSERT INTO runtime_client_states (
client_id, platform_id, provider, method_name, queue_key, running_count, last_assigned_at
)
VALUES ($1, $2::uuid, $3, $4, $5, 1, now())
ON CONFLICT (client_id) DO UPDATE
SET running_count = runtime_client_states.running_count + 1,
last_assigned_at = now(),
updated_at = now()`,
candidate.ClientID,
candidate.PlatformID,
candidate.Provider,
candidate.ModelType,
candidate.QueueKey,
)
return err
}
func (s *Store) RecordClientRelease(ctx context.Context, clientID string, lastError string) error {
_, err := s.pool.Exec(ctx, `
UPDATE runtime_client_states
SET running_count = GREATEST(running_count - 1, 0),
last_error = NULLIF($2, ''),
updated_at = now()
WHERE client_id = $1`, clientID, lastError)
return err
}
@@ -0,0 +1,41 @@
package store
import (
"context"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/jackc/pgx/v5"
)
type UserGroupPolicy struct {
ID string
GroupKey string
RateLimitPolicy map[string]any
BillingDiscountPolicy map[string]any
}
func (s *Store) ResolveUserGroupPolicy(ctx context.Context, user *auth.User) (UserGroupPolicy, error) {
userGroupID := ""
if user != nil {
userGroupID = user.UserGroupID
}
var item UserGroupPolicy
var rateLimit []byte
var billing []byte
err := s.pool.QueryRow(ctx, `
SELECT id::text, group_key, rate_limit_policy, billing_discount_policy
FROM gateway_user_groups
WHERE status = 'active'
AND (($1 <> '' AND id = NULLIF($1, '')::uuid) OR ($1 = '' AND group_key = 'default'))
ORDER BY CASE WHEN id::text = $1 THEN 0 ELSE 1 END, priority ASC
LIMIT 1`, userGroupID).Scan(&item.ID, &item.GroupKey, &rateLimit, &billing)
if err != nil {
if err == pgx.ErrNoRows {
return UserGroupPolicy{}, nil
}
return UserGroupPolicy{}, err
}
item.RateLimitPolicy = decodeObject(rateLimit)
item.BillingDiscountPolicy = decodeObject(billing)
return item, nil
}
+308 -1
View File
@@ -3,8 +3,13 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS model_catalog_providers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_key text NOT NULL UNIQUE,
provider_code text NOT NULL,
display_name text NOT NULL,
provider_type text NOT NULL DEFAULT 'openai_compatible',
provider_type text NOT NULL DEFAULT 'openai',
icon_path text,
default_base_url text,
default_auth_type text NOT NULL DEFAULT 'APIKey',
source text NOT NULL DEFAULT 'gateway',
capability_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
default_rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
@@ -16,6 +21,9 @@ CREATE TABLE IF NOT EXISTS model_catalog_providers (
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider_status
ON model_catalog_providers(status);
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_catalog_provider_code
ON model_catalog_providers(provider_code);
CREATE TABLE IF NOT EXISTS base_model_catalog (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id uuid REFERENCES model_catalog_providers(id) ON DELETE SET NULL,
@@ -49,8 +57,12 @@ CREATE TABLE IF NOT EXISTS integration_platforms (
auth_type text NOT NULL DEFAULT 'bearer',
credentials jsonb NOT NULL DEFAULT '{}'::jsonb,
config jsonb NOT NULL DEFAULT '{}'::jsonb,
visibility_scope text NOT NULL DEFAULT 'global',
tenant_id text,
tenant_key text,
default_pricing_mode text NOT NULL DEFAULT 'inherit_discount',
default_discount_factor numeric NOT NULL DEFAULT 1,
pricing_rule_set_id uuid,
retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
priority integer NOT NULL DEFAULT 100,
@@ -72,8 +84,27 @@ CREATE INDEX IF NOT EXISTS idx_integration_platforms_status_priority
CREATE INDEX IF NOT EXISTS idx_integration_platforms_cooldown
ON integration_platforms(cooldown_until);
CREATE INDEX IF NOT EXISTS idx_integration_platforms_tenant_scope
ON integration_platforms(visibility_scope, tenant_id, tenant_key, status);
CREATE TABLE IF NOT EXISTS model_pricing_rule_sets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_set_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
category text NOT NULL DEFAULT 'general',
currency text NOT NULL DEFAULT 'resource',
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS model_pricing_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE CASCADE,
rule_key text NOT NULL DEFAULT ('rule_' || replace(gen_random_uuid()::text, '-', '')),
display_name text NOT NULL DEFAULT '',
scope_type text NOT NULL,
scope_id uuid,
resource_type text NOT NULL,
@@ -82,6 +113,12 @@ CREATE TABLE IF NOT EXISTS model_pricing_rules (
currency text NOT NULL DEFAULT 'resource',
base_weight jsonb NOT NULL DEFAULT '{}'::jsonb,
dynamic_weight jsonb NOT NULL DEFAULT '{}'::jsonb,
calculator_type text NOT NULL DEFAULT 'unit_weight',
dimension_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
formula_config jsonb NOT NULL DEFAULT '{}'::jsonb,
priority integer NOT NULL DEFAULT 100,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
effective_from timestamptz,
effective_to timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
@@ -91,9 +128,248 @@ CREATE TABLE IF NOT EXISTS model_pricing_rules (
CREATE INDEX IF NOT EXISTS idx_model_pricing_scope
ON model_pricing_rules(scope_type, scope_id, resource_type);
CREATE INDEX IF NOT EXISTS idx_model_pricing_rule_set
ON model_pricing_rules(rule_set_id, resource_type, priority);
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_pricing_rule_set_key
ON model_pricing_rules(rule_set_id, rule_key)
WHERE rule_set_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_model_pricing_effective
ON model_pricing_rules(effective_from, effective_to);
CREATE TABLE IF NOT EXISTS gateway_user_groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
group_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
source text NOT NULL DEFAULT 'gateway',
priority integer NOT NULL DEFAULT 100,
recharge_discount_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
billing_discount_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
quota_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_gateway_user_groups_status_priority
ON gateway_user_groups(status, priority);
CREATE TABLE IF NOT EXISTS gateway_tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_key text NOT NULL UNIQUE,
source text NOT NULL DEFAULT 'gateway',
external_tenant_id text,
name text NOT NULL,
description text,
default_user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
plan_key text,
billing_profile jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
auth_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
synced_at timestamptz,
source_updated_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
UNIQUE(source, external_tenant_id)
);
CREATE INDEX IF NOT EXISTS idx_gateway_tenants_source_external
ON gateway_tenants(source, external_tenant_id)
WHERE external_tenant_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_tenants_status
ON gateway_tenants(status, created_at DESC);
CREATE TABLE IF NOT EXISTS gateway_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_key text NOT NULL UNIQUE,
source text NOT NULL DEFAULT 'gateway',
external_user_id text,
username text NOT NULL,
display_name text,
email text,
phone text,
avatar_url text,
password_hash text,
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
tenant_id text,
tenant_key text,
default_user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
roles jsonb NOT NULL DEFAULT '[]'::jsonb,
auth_profile jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
last_login_at timestamptz,
synced_at timestamptz,
source_updated_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
UNIQUE(source, external_user_id)
);
CREATE INDEX IF NOT EXISTS idx_gateway_users_source_external
ON gateway_users(source, external_user_id)
WHERE external_user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_users_status
ON gateway_users(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_users_tenant
ON gateway_users(tenant_id, tenant_key, status);
CREATE TABLE IF NOT EXISTS gateway_user_group_memberships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
group_id uuid NOT NULL REFERENCES gateway_user_groups(id) ON DELETE CASCADE,
principal_type text NOT NULL,
principal_id text NOT NULL,
source text NOT NULL DEFAULT 'gateway',
priority integer NOT NULL DEFAULT 100,
effective_from timestamptz,
effective_to timestamptz,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(group_id, principal_type, principal_id)
);
CREATE INDEX IF NOT EXISTS idx_user_group_membership_principal
ON gateway_user_group_memberships(principal_type, principal_id, status);
CREATE INDEX IF NOT EXISTS idx_user_group_membership_effective
ON gateway_user_group_memberships(effective_from, effective_to);
CREATE TABLE IF NOT EXISTS gateway_invitations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
invite_code text NOT NULL UNIQUE,
max_uses integer,
used_count integer NOT NULL DEFAULT 0,
expires_at timestamptz,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_gateway_invitations_status
ON gateway_invitations(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_invitations_expiry
ON gateway_invitations(expires_at);
CREATE TABLE IF NOT EXISTS gateway_api_keys (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE CASCADE,
tenant_id text,
tenant_key text,
user_id text,
key_prefix text NOT NULL,
key_secret text,
key_hash text NOT NULL UNIQUE,
name text NOT NULL,
scopes jsonb NOT NULL DEFAULT '[]'::jsonb,
user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
quota_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
expires_at timestamptz,
last_used_at timestamptz,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
CREATE INDEX IF NOT EXISTS idx_gateway_api_keys_owner
ON gateway_api_keys(gateway_user_id, status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_api_keys_prefix
ON gateway_api_keys(key_prefix, status);
CREATE TABLE IF NOT EXISTS gateway_wallet_accounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE CASCADE,
tenant_id text,
tenant_key text,
user_id text,
currency text NOT NULL DEFAULT 'resource',
balance numeric NOT NULL DEFAULT 0,
frozen_balance numeric NOT NULL DEFAULT 0,
total_recharged numeric NOT NULL DEFAULT 0,
total_spent numeric NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(gateway_user_id, currency)
);
CREATE INDEX IF NOT EXISTS idx_gateway_wallet_accounts_tenant
ON gateway_wallet_accounts(gateway_tenant_id, status);
CREATE TABLE IF NOT EXISTS gateway_wallet_transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
account_id uuid NOT NULL REFERENCES gateway_wallet_accounts(id) ON DELETE CASCADE,
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
direction text NOT NULL,
transaction_type text NOT NULL,
amount numeric NOT NULL,
balance_before numeric NOT NULL,
balance_after numeric NOT NULL,
idempotency_key text,
reference_type text,
reference_id text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_gateway_wallet_transactions_account
ON gateway_wallet_transactions(account_id, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_gateway_wallet_tx_idempotency
ON gateway_wallet_transactions(account_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE TABLE IF NOT EXISTS gateway_recharge_orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE CASCADE,
tenant_id text,
tenant_key text,
user_id text,
amount numeric NOT NULL,
bonus_amount numeric NOT NULL DEFAULT 0,
payable_amount numeric NOT NULL,
currency text NOT NULL DEFAULT 'resource',
channel text NOT NULL DEFAULT 'manual',
status text NOT NULL DEFAULT 'created',
external_order_id text,
idempotency_key text,
paid_at timestamptz,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_gateway_recharge_orders_user
ON gateway_recharge_orders(gateway_user_id, created_at DESC);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_gateway_recharge_order_idempotency
ON gateway_recharge_orders(gateway_user_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE TABLE IF NOT EXISTS platform_models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
platform_id uuid NOT NULL REFERENCES integration_platforms(id) ON DELETE CASCADE,
@@ -106,6 +382,7 @@ CREATE TABLE IF NOT EXISTS platform_models (
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
pricing_mode text NOT NULL DEFAULT 'inherit_discount',
discount_factor numeric,
pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL,
billing_config_override jsonb NOT NULL DEFAULT '{}'::jsonb,
billing_config jsonb NOT NULL DEFAULT '{}'::jsonb,
permission_config jsonb NOT NULL DEFAULT '{}'::jsonb,
@@ -135,8 +412,15 @@ CREATE TABLE IF NOT EXISTS gateway_tasks (
kind text NOT NULL,
run_mode text NOT NULL DEFAULT 'production',
user_id text NOT NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
user_source text NOT NULL DEFAULT 'gateway',
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
tenant_id text,
tenant_key text,
api_key_id text,
user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
user_group_key text,
user_group_policy_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
model text NOT NULL,
model_type text,
request jsonb NOT NULL DEFAULT '{}'::jsonb,
@@ -226,6 +510,29 @@ CREATE TABLE IF NOT EXISTS gateway_task_events (
CREATE INDEX IF NOT EXISTS idx_gateway_events_task_created
ON gateway_task_events(task_id, created_at);
CREATE TABLE IF NOT EXISTS gateway_task_callback_outbox (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
event_id uuid REFERENCES gateway_task_events(id) ON DELETE SET NULL,
seq bigint NOT NULL,
callback_url text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
attempts integer NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
last_error text,
delivered_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(task_id, seq, callback_url)
);
CREATE INDEX IF NOT EXISTS idx_task_callback_outbox_pending
ON gateway_task_callback_outbox(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_task_callback_outbox_task
ON gateway_task_callback_outbox(task_id, seq);
CREATE TABLE IF NOT EXISTS runtime_client_states (
client_id text PRIMARY KEY,
platform_id uuid REFERENCES integration_platforms(id) ON DELETE SET NULL,
@@ -0,0 +1,680 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS model_catalog_providers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_key text NOT NULL UNIQUE,
provider_code text NOT NULL,
display_name text NOT NULL,
provider_type text NOT NULL DEFAULT 'openai',
icon_path text,
source text NOT NULL DEFAULT 'gateway',
capability_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
default_rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS base_model_catalog (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider_id uuid REFERENCES model_catalog_providers(id) ON DELETE SET NULL,
provider_key text NOT NULL,
canonical_model_key text NOT NULL UNIQUE,
provider_model_name text NOT NULL,
model_type text NOT NULL,
display_name text NOT NULL,
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
base_billing_config jsonb NOT NULL DEFAULT '{}'::jsonb,
default_rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
pricing_version integer NOT NULL DEFAULT 1,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS integration_platforms (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
provider text NOT NULL,
platform_key text NOT NULL UNIQUE DEFAULT ('platform_' || replace(gen_random_uuid()::text, '-', '')),
name text NOT NULL,
base_url text,
auth_type text NOT NULL DEFAULT 'bearer',
credentials jsonb NOT NULL DEFAULT '{}'::jsonb,
config jsonb NOT NULL DEFAULT '{}'::jsonb,
visibility_scope text NOT NULL DEFAULT 'global',
tenant_id text,
tenant_key text,
default_pricing_mode text NOT NULL DEFAULT 'inherit_discount',
default_discount_factor numeric NOT NULL DEFAULT 1,
pricing_rule_set_id uuid,
retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
priority integer NOT NULL DEFAULT 100,
dynamic_priority integer,
status text NOT NULL DEFAULT 'enabled',
disabled_reason text,
cooldown_until timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
ALTER TABLE IF EXISTS integration_platforms
ADD COLUMN IF NOT EXISTS platform_key text,
ADD COLUMN IF NOT EXISTS visibility_scope text NOT NULL DEFAULT 'global',
ADD COLUMN IF NOT EXISTS tenant_id text,
ADD COLUMN IF NOT EXISTS tenant_key text,
ADD COLUMN IF NOT EXISTS default_pricing_mode text NOT NULL DEFAULT 'inherit_discount',
ADD COLUMN IF NOT EXISTS default_discount_factor numeric NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS pricing_rule_set_id uuid,
ADD COLUMN IF NOT EXISTS retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS dynamic_priority integer,
ADD COLUMN IF NOT EXISTS disabled_reason text,
ADD COLUMN IF NOT EXISTS cooldown_until timestamptz,
ADD COLUMN IF NOT EXISTS deleted_at timestamptz;
UPDATE integration_platforms
SET platform_key = 'platform_' || replace(id::text, '-', '')
WHERE platform_key IS NULL OR platform_key = '';
ALTER TABLE IF EXISTS integration_platforms
ALTER COLUMN platform_key SET DEFAULT ('platform_' || replace(gen_random_uuid()::text, '-', '')),
ALTER COLUMN platform_key SET NOT NULL;
CREATE TABLE IF NOT EXISTS model_pricing_rule_sets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_set_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
category text NOT NULL DEFAULT 'general',
currency text NOT NULL DEFAULT 'resource',
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS model_pricing_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE CASCADE,
rule_key text NOT NULL DEFAULT ('rule_' || replace(gen_random_uuid()::text, '-', '')),
display_name text NOT NULL DEFAULT '',
scope_type text NOT NULL,
scope_id uuid,
resource_type text NOT NULL,
unit text NOT NULL,
base_price numeric NOT NULL,
currency text NOT NULL DEFAULT 'resource',
base_weight jsonb NOT NULL DEFAULT '{}'::jsonb,
dynamic_weight jsonb NOT NULL DEFAULT '{}'::jsonb,
calculator_type text NOT NULL DEFAULT 'unit_weight',
dimension_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
formula_config jsonb NOT NULL DEFAULT '{}'::jsonb,
priority integer NOT NULL DEFAULT 100,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
effective_from timestamptz,
effective_to timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE IF EXISTS model_pricing_rules
ADD COLUMN IF NOT EXISTS rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS rule_key text NOT NULL DEFAULT ('rule_' || replace(gen_random_uuid()::text, '-', '')),
ADD COLUMN IF NOT EXISTS display_name text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS calculator_type text NOT NULL DEFAULT 'unit_weight',
ADD COLUMN IF NOT EXISTS dimension_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS formula_config jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS priority integer NOT NULL DEFAULT 100,
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'active',
ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;
CREATE TABLE IF NOT EXISTS gateway_user_groups (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
group_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
source text NOT NULL DEFAULT 'gateway',
priority integer NOT NULL DEFAULT 100,
recharge_discount_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
billing_discount_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
quota_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS gateway_tenants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_key text NOT NULL UNIQUE,
source text NOT NULL DEFAULT 'gateway',
external_tenant_id text,
name text NOT NULL,
description text,
default_user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
plan_key text,
billing_profile jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
auth_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
synced_at timestamptz,
source_updated_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
UNIQUE(source, external_tenant_id)
);
CREATE TABLE IF NOT EXISTS gateway_users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_key text NOT NULL UNIQUE,
source text NOT NULL DEFAULT 'gateway',
external_user_id text,
username text NOT NULL,
display_name text,
email text,
phone text,
avatar_url text,
password_hash text,
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
tenant_id text,
tenant_key text,
default_user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
roles jsonb NOT NULL DEFAULT '[]'::jsonb,
auth_profile jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
last_login_at timestamptz,
synced_at timestamptz,
source_updated_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
UNIQUE(source, external_user_id)
);
CREATE TABLE IF NOT EXISTS gateway_user_group_memberships (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
group_id uuid NOT NULL REFERENCES gateway_user_groups(id) ON DELETE CASCADE,
principal_type text NOT NULL,
principal_id text NOT NULL,
source text NOT NULL DEFAULT 'gateway',
priority integer NOT NULL DEFAULT 100,
effective_from timestamptz,
effective_to timestamptz,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(group_id, principal_type, principal_id)
);
DO $$
BEGIN
IF to_regclass('public.gateway_tenant_invitations') IS NOT NULL
AND to_regclass('public.gateway_invitations') IS NULL THEN
ALTER TABLE gateway_tenant_invitations RENAME TO gateway_invitations;
END IF;
END $$;
DROP INDEX IF EXISTS idx_gateway_invitations_tenant;
CREATE TABLE IF NOT EXISTS gateway_invitations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
invite_code text NOT NULL UNIQUE,
max_uses integer,
used_count integer NOT NULL DEFAULT 0,
expires_at timestamptz,
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_by uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE IF EXISTS gateway_invitations
DROP COLUMN IF EXISTS tenant_id,
DROP COLUMN IF EXISTS role,
DROP COLUMN IF EXISTS user_group_id;
CREATE TABLE IF NOT EXISTS gateway_api_keys (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE CASCADE,
tenant_id text,
tenant_key text,
user_id text,
key_prefix text NOT NULL,
key_secret text,
key_hash text NOT NULL UNIQUE,
name text NOT NULL,
scopes jsonb NOT NULL DEFAULT '[]'::jsonb,
user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
quota_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
expires_at timestamptz,
last_used_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
CREATE TABLE IF NOT EXISTS gateway_wallet_accounts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE CASCADE,
tenant_id text,
tenant_key text,
user_id text,
currency text NOT NULL DEFAULT 'resource',
balance numeric NOT NULL DEFAULT 0,
frozen_balance numeric NOT NULL DEFAULT 0,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(gateway_user_id, currency)
);
CREATE TABLE IF NOT EXISTS gateway_wallet_transactions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
wallet_account_id uuid REFERENCES gateway_wallet_accounts(id) ON DELETE SET NULL,
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
transaction_type text NOT NULL,
amount numeric NOT NULL,
balance_after numeric NOT NULL,
reference_type text,
reference_id text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS gateway_recharge_orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
tenant_id text,
tenant_key text,
user_id text,
order_no text NOT NULL UNIQUE,
amount numeric NOT NULL,
bonus_amount numeric NOT NULL DEFAULT 0,
currency text NOT NULL DEFAULT 'resource',
payment_provider text,
payment_payload jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'pending',
paid_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS platform_models (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
platform_id uuid REFERENCES integration_platforms(id) ON DELETE CASCADE,
base_model_id uuid REFERENCES base_model_catalog(id) ON DELETE SET NULL,
model_name text NOT NULL,
model_alias text,
model_type text NOT NULL,
display_name text NOT NULL DEFAULT '',
capability_override jsonb NOT NULL DEFAULT '{}'::jsonb,
capabilities jsonb NOT NULL DEFAULT '{}'::jsonb,
pricing_mode text NOT NULL DEFAULT 'inherit_discount',
discount_factor numeric,
pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL,
billing_config_override jsonb NOT NULL DEFAULT '{}'::jsonb,
billing_config jsonb NOT NULL DEFAULT '{}'::jsonb,
permission_config jsonb NOT NULL DEFAULT '{}'::jsonb,
retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
enabled boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(platform_id, model_name, model_type)
);
ALTER TABLE IF EXISTS platform_models
ADD COLUMN IF NOT EXISTS base_model_id uuid REFERENCES base_model_catalog(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS model_alias text,
ADD COLUMN IF NOT EXISTS capability_override jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS pricing_mode text NOT NULL DEFAULT 'inherit_discount',
ADD COLUMN IF NOT EXISTS discount_factor numeric,
ADD COLUMN IF NOT EXISTS pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS billing_config_override jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS permission_config jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb;
CREATE TABLE IF NOT EXISTS gateway_tasks (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
external_task_id text,
kind text NOT NULL,
run_mode text NOT NULL DEFAULT 'production',
user_id text NOT NULL,
gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
user_source text NOT NULL DEFAULT 'gateway',
gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
tenant_id text,
tenant_key text,
api_key_id text,
user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
user_group_key text,
user_group_policy_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
model text NOT NULL,
model_type text,
request jsonb NOT NULL DEFAULT '{}'::jsonb,
normalized_request jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'queued',
queue_key text NOT NULL DEFAULT 'default',
priority integer NOT NULL DEFAULT 100,
idempotency_key text,
remote_task_id text,
remote_task_payload jsonb,
simulation_profile jsonb,
simulation_seed text,
locked_by text,
locked_at timestamptz,
heartbeat_at timestamptz,
next_run_at timestamptz NOT NULL DEFAULT now(),
attempt_count integer NOT NULL DEFAULT 0,
max_attempts integer NOT NULL DEFAULT 1,
result jsonb,
billings jsonb,
error text,
error_code text,
error_message text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz
);
ALTER TABLE IF EXISTS gateway_tasks
ADD COLUMN IF NOT EXISTS external_task_id text,
ADD COLUMN IF NOT EXISTS run_mode text NOT NULL DEFAULT 'production',
ADD COLUMN IF NOT EXISTS gateway_user_id uuid REFERENCES gateway_users(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS user_source text NOT NULL DEFAULT 'gateway',
ADD COLUMN IF NOT EXISTS gateway_tenant_id uuid REFERENCES gateway_tenants(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS tenant_key text,
ADD COLUMN IF NOT EXISTS api_key_id text,
ADD COLUMN IF NOT EXISTS user_group_id uuid REFERENCES gateway_user_groups(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS user_group_key text,
ADD COLUMN IF NOT EXISTS user_group_policy_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS model_type text,
ADD COLUMN IF NOT EXISTS normalized_request jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS queue_key text NOT NULL DEFAULT 'default',
ADD COLUMN IF NOT EXISTS priority integer NOT NULL DEFAULT 100,
ADD COLUMN IF NOT EXISTS idempotency_key text,
ADD COLUMN IF NOT EXISTS remote_task_id text,
ADD COLUMN IF NOT EXISTS remote_task_payload jsonb,
ADD COLUMN IF NOT EXISTS simulation_profile jsonb,
ADD COLUMN IF NOT EXISTS simulation_seed text,
ADD COLUMN IF NOT EXISTS locked_by text,
ADD COLUMN IF NOT EXISTS locked_at timestamptz,
ADD COLUMN IF NOT EXISTS heartbeat_at timestamptz,
ADD COLUMN IF NOT EXISTS next_run_at timestamptz NOT NULL DEFAULT now(),
ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS max_attempts integer NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS error_code text,
ADD COLUMN IF NOT EXISTS error_message text,
ADD COLUMN IF NOT EXISTS finished_at timestamptz;
CREATE TABLE IF NOT EXISTS gateway_task_attempts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
attempt_no integer NOT NULL,
platform_id uuid REFERENCES integration_platforms(id) ON DELETE SET NULL,
platform_model_id uuid REFERENCES platform_models(id) ON DELETE SET NULL,
client_id text,
queue_key text NOT NULL,
status text NOT NULL,
retryable boolean NOT NULL DEFAULT false,
simulated boolean NOT NULL DEFAULT false,
remote_task_id text,
request_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
response_snapshot jsonb,
error_code text,
error_message text,
started_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
UNIQUE(task_id, attempt_no)
);
CREATE TABLE IF NOT EXISTS gateway_task_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
seq bigint NOT NULL,
event_type text NOT NULL,
status text,
phase text,
progress numeric,
message text,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
simulated boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(task_id, seq)
);
CREATE TABLE IF NOT EXISTS gateway_task_callback_outbox (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
event_id uuid REFERENCES gateway_task_events(id) ON DELETE SET NULL,
seq bigint NOT NULL,
callback_url text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
attempts integer NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
last_error text,
delivered_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(task_id, seq, callback_url)
);
CREATE TABLE IF NOT EXISTS runtime_client_states (
client_id text PRIMARY KEY,
platform_id uuid REFERENCES integration_platforms(id) ON DELETE SET NULL,
provider text NOT NULL,
method_name text NOT NULL,
queue_key text NOT NULL,
running_count integer NOT NULL DEFAULT 0,
waiting_count integer NOT NULL DEFAULT 0,
limiter_ratio numeric NOT NULL DEFAULT 0,
cooldown_until timestamptz,
last_assigned_at timestamptz,
last_error text,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS gateway_upload_assets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid REFERENCES gateway_tasks(id) ON DELETE SET NULL,
source text NOT NULL,
server_main_file_id text,
url text NOT NULL,
object_key text,
content_type text,
size bigint,
checksum text,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS gateway_retry_policies (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scope_type text NOT NULL,
scope_key text NOT NULL,
enabled boolean NOT NULL DEFAULT true,
policy jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(scope_type, scope_key)
);
CREATE TABLE IF NOT EXISTS gateway_rate_limit_policies (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scope_type text NOT NULL,
scope_key text NOT NULL,
policy jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(scope_type, scope_key)
);
CREATE TABLE IF NOT EXISTS gateway_rate_limit_counters (
scope_type text NOT NULL,
scope_key text NOT NULL,
metric text NOT NULL,
window_start timestamptz NOT NULL,
limit_value numeric NOT NULL,
used_value numeric NOT NULL DEFAULT 0,
reserved_value numeric NOT NULL DEFAULT 0,
reset_at timestamptz NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY(scope_type, scope_key, metric, window_start)
);
CREATE TABLE IF NOT EXISTS gateway_concurrency_leases (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
attempt_id uuid REFERENCES gateway_task_attempts(id) ON DELETE SET NULL,
scope_type text NOT NULL,
scope_key text NOT NULL,
lease_value numeric NOT NULL DEFAULT 1,
acquired_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL,
released_at timestamptz
);
ALTER TABLE IF EXISTS settlement_outbox
ADD COLUMN IF NOT EXISTS event_type text NOT NULL DEFAULT 'task.settlement.requested',
ADD COLUMN IF NOT EXISTS payload jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS attempts integer NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS next_attempt_at timestamptz NOT NULL DEFAULT now(),
ADD COLUMN IF NOT EXISTS updated_at timestamptz NOT NULL DEFAULT now();
CREATE TABLE IF NOT EXISTS settlement_outbox (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
task_id uuid NOT NULL REFERENCES gateway_tasks(id) ON DELETE CASCADE,
event_type text NOT NULL DEFAULT 'task.settlement.requested',
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
attempts integer NOT NULL DEFAULT 0,
next_attempt_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(task_id, event_type)
);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_integration_platforms_platform_key
ON integration_platforms(platform_key);
CREATE INDEX IF NOT EXISTS idx_model_catalog_provider_status
ON model_catalog_providers(status);
CREATE INDEX IF NOT EXISTS idx_base_model_catalog_provider
ON base_model_catalog(provider_key, model_type, status);
CREATE INDEX IF NOT EXISTS idx_base_model_catalog_capabilities
ON base_model_catalog USING gin(capabilities);
CREATE INDEX IF NOT EXISTS idx_integration_platforms_provider_status
ON integration_platforms(provider, status);
CREATE INDEX IF NOT EXISTS idx_integration_platforms_status_priority
ON integration_platforms(status, priority, dynamic_priority);
CREATE INDEX IF NOT EXISTS idx_integration_platforms_cooldown
ON integration_platforms(cooldown_until);
CREATE INDEX IF NOT EXISTS idx_integration_platforms_tenant_scope
ON integration_platforms(visibility_scope, tenant_id, tenant_key, status);
CREATE INDEX IF NOT EXISTS idx_model_pricing_scope
ON model_pricing_rules(scope_type, scope_id, resource_type);
CREATE INDEX IF NOT EXISTS idx_model_pricing_rule_set
ON model_pricing_rules(rule_set_id, resource_type, priority);
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_pricing_rule_set_key
ON model_pricing_rules(rule_set_id, rule_key)
WHERE rule_set_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_model_pricing_effective
ON model_pricing_rules(effective_from, effective_to);
CREATE INDEX IF NOT EXISTS idx_gateway_user_groups_status_priority
ON gateway_user_groups(status, priority);
CREATE INDEX IF NOT EXISTS idx_gateway_tenants_source_external
ON gateway_tenants(source, external_tenant_id)
WHERE external_tenant_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_tenants_status
ON gateway_tenants(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_users_source_external
ON gateway_users(source, external_user_id)
WHERE external_user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_users_status
ON gateway_users(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_users_tenant
ON gateway_users(tenant_id, tenant_key, status);
CREATE INDEX IF NOT EXISTS idx_user_group_membership_principal
ON gateway_user_group_memberships(principal_type, principal_id, status);
CREATE INDEX IF NOT EXISTS idx_user_group_membership_effective
ON gateway_user_group_memberships(effective_from, effective_to);
CREATE INDEX IF NOT EXISTS idx_gateway_invitations_status
ON gateway_invitations(status, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_invitations_expiry
ON gateway_invitations(expires_at);
CREATE INDEX IF NOT EXISTS idx_gateway_api_keys_prefix
ON gateway_api_keys(key_prefix, status);
CREATE INDEX IF NOT EXISTS idx_gateway_api_keys_user
ON gateway_api_keys(gateway_user_id, status);
CREATE INDEX IF NOT EXISTS idx_gateway_wallet_accounts_tenant
ON gateway_wallet_accounts(gateway_tenant_id, status);
CREATE INDEX IF NOT EXISTS idx_gateway_wallet_transactions_user
ON gateway_wallet_transactions(gateway_user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_recharge_orders_user
ON gateway_recharge_orders(gateway_user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_platform_models_base
ON platform_models(base_model_id);
CREATE INDEX IF NOT EXISTS idx_platform_models_lookup
ON platform_models(model_type, model_name, enabled);
CREATE INDEX IF NOT EXISTS idx_platform_models_alias
ON platform_models(model_alias);
CREATE INDEX IF NOT EXISTS idx_platform_models_capabilities
ON platform_models USING gin(capabilities);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_platform_models_model
ON platform_models(platform_id, model_name, model_type);
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_queue
ON gateway_tasks(status, next_run_at, priority, created_at);
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_lease
ON gateway_tasks(status, heartbeat_at);
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_user_created
ON gateway_tasks(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_external
ON gateway_tasks(external_task_id);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_gateway_tasks_idempotency
ON gateway_tasks(user_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_attempts_task
ON gateway_task_attempts(task_id);
CREATE INDEX IF NOT EXISTS idx_gateway_attempts_client
ON gateway_task_attempts(client_id, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_gateway_events_task_created
ON gateway_task_events(task_id, created_at);
CREATE INDEX IF NOT EXISTS idx_task_callback_outbox_pending
ON gateway_task_callback_outbox(status, next_attempt_at);
CREATE INDEX IF NOT EXISTS idx_task_callback_outbox_task
ON gateway_task_callback_outbox(task_id, seq);
CREATE INDEX IF NOT EXISTS idx_runtime_client_queue
ON runtime_client_states(queue_key, cooldown_until);
CREATE INDEX IF NOT EXISTS idx_runtime_client_platform
ON runtime_client_states(platform_id);
CREATE INDEX IF NOT EXISTS idx_gateway_upload_task
ON gateway_upload_assets(task_id);
CREATE INDEX IF NOT EXISTS idx_gateway_upload_file
ON gateway_upload_assets(server_main_file_id);
CREATE INDEX IF NOT EXISTS idx_concurrency_leases_active
ON gateway_concurrency_leases(scope_type, scope_key, released_at, expires_at);
CREATE INDEX IF NOT EXISTS idx_concurrency_leases_task
ON gateway_concurrency_leases(task_id);
CREATE UNIQUE INDEX IF NOT EXISTS uniq_settlement_outbox_task_event
ON settlement_outbox(task_id, event_type);
@@ -0,0 +1,244 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO gateway_user_groups (
group_key, name, description, priority,
recharge_discount_policy, billing_discount_policy, rate_limit_policy, quota_policy, metadata, status
)
VALUES
(
'default', 'Default Users', 'Built-in default group for local and synchronized users.', 100,
'{"discountFactor":1}'::jsonb,
'{"discountFactor":1}'::jsonb,
'{"rules":[{"metric":"rpm","limit":60,"windowSeconds":60},{"metric":"tpm_total","limit":120000,"windowSeconds":60},{"metric":"concurrent","limit":3,"leaseTtlSeconds":120}]}'::jsonb,
'{}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
),
(
'vip', 'VIP Users', 'Higher quota group for future recharge and concurrency policies.', 50,
'{"discountFactor":0.9}'::jsonb,
'{"discountFactor":0.95}'::jsonb,
'{"rules":[{"metric":"rpm","limit":300,"windowSeconds":60},{"metric":"tpm_total","limit":600000,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb,
'{}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
)
ON CONFLICT (group_key) DO UPDATE
SET name = EXCLUDED.name,
description = EXCLUDED.description,
priority = EXCLUDED.priority,
recharge_discount_policy = EXCLUDED.recharge_discount_policy,
billing_discount_policy = EXCLUDED.billing_discount_policy,
rate_limit_policy = EXCLUDED.rate_limit_policy,
updated_at = now();
INSERT INTO gateway_tenants (tenant_key, source, external_tenant_id, name, default_user_group_id, metadata, status)
VALUES (
'default',
'gateway',
'default',
'Default Tenant',
(SELECT id FROM gateway_user_groups WHERE group_key = 'default'),
'{"seed":"phase1"}'::jsonb,
'active'
)
ON CONFLICT (tenant_key) DO UPDATE
SET name = EXCLUDED.name,
default_user_group_id = EXCLUDED.default_user_group_id,
updated_at = now();
INSERT INTO model_catalog_providers (
provider_key, provider_code, display_name, provider_type, icon_path, source, capability_schema, default_rate_limit_policy, metadata, status
)
VALUES
(
'openai',
'openai',
'OpenAI',
'openai',
NULL,
'server-main.integration-platform',
'{"chat":true,"imageGeneration":true,"imageEdit":true,"stream":true}'::jsonb,
'{"rules":[{"metric":"rpm","limit":500,"windowSeconds":60},{"metric":"tpm_total","limit":1000000,"windowSeconds":60},{"metric":"concurrent","limit":20,"leaseTtlSeconds":120}]}'::jsonb,
'{"seed":"phase1","syncSource":"server-main.integration-platform","sourceCode":"openai"}'::jsonb,
'active'
),
(
'gemini',
'google-gemini',
'Google Gemini',
'gemini',
'https://static.51easyai.com/gemini-color.png',
'server-main.integration-platform',
'{"chat":true,"imageGeneration":true,"imageEdit":true,"stream":true}'::jsonb,
'{"rules":[{"metric":"rpm","limit":500,"windowSeconds":60},{"metric":"tpm_total","limit":1000000,"windowSeconds":60},{"metric":"concurrent","limit":20,"leaseTtlSeconds":120}]}'::jsonb,
'{"seed":"phase1","syncSource":"server-main.integration-platform","sourceCode":"google-gemini"}'::jsonb,
'active'
)
ON CONFLICT (provider_key) DO UPDATE
SET provider_code = EXCLUDED.provider_code,
display_name = EXCLUDED.display_name,
provider_type = EXCLUDED.provider_type,
icon_path = EXCLUDED.icon_path,
source = EXCLUDED.source,
capability_schema = EXCLUDED.capability_schema,
default_rate_limit_policy = EXCLUDED.default_rate_limit_policy,
metadata = model_catalog_providers.metadata || EXCLUDED.metadata,
updated_at = now();
INSERT INTO base_model_catalog (
provider_id, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, metadata, status
)
VALUES
(
(SELECT id FROM model_catalog_providers WHERE provider_key = 'openai'),
'openai',
'openai:gpt-4o-mini',
'gpt-4o-mini',
'chat',
'GPT-4o mini',
'{"stream":true,"vision":true,"inputModalities":["text","image"],"outputModalities":["text"],"maxInputTokens":128000}'::jsonb,
'{"textInputPer1k":0.15,"textOutputPer1k":0.6,"currency":"resource"}'::jsonb,
'{"rules":[{"metric":"rpm","limit":300,"windowSeconds":60},{"metric":"tpm_total","limit":500000,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
),
(
(SELECT id FROM model_catalog_providers WHERE provider_key = 'openai'),
'openai',
'openai:gpt-image-1',
'gpt-image-1',
'image',
'GPT Image 1',
'{"imageGeneration":true,"imageEdit":true,"inputModalities":["text","image","mask"],"outputModalities":["image"],"sizes":["1024x1024","1024x1536","1536x1024"],"qualities":["low","medium","high"]}'::jsonb,
'{"imageBase":5,"editBase":6,"currency":"resource","qualityWeights":{"low":0.7,"standard":1,"medium":1,"high":1.4},"sizeWeights":{"1024x1024":1,"1024x1536":1.35,"1536x1024":1.35}}'::jsonb,
'{"rules":[{"metric":"rpm","limit":60,"windowSeconds":60},{"metric":"concurrent","limit":4,"leaseTtlSeconds":180}]}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
),
(
(SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini'),
'gemini',
'gemini:gemini-2.5-flash',
'gemini-2.5-flash',
'chat',
'Gemini 2.5 Flash',
'{"stream":true,"vision":true,"inputModalities":["text","image"],"outputModalities":["text"],"maxInputTokens":1048576}'::jsonb,
'{"textInputPer1k":0.1,"textOutputPer1k":0.4,"currency":"resource"}'::jsonb,
'{"rules":[{"metric":"rpm","limit":300,"windowSeconds":60},{"metric":"tpm_total","limit":500000,"windowSeconds":60},{"metric":"concurrent","limit":10,"leaseTtlSeconds":120}]}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
),
(
(SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini'),
'gemini',
'gemini:gemini-2.5-flash-image',
'gemini-2.5-flash-image',
'image',
'Gemini 2.5 Flash Image',
'{"imageGeneration":true,"imageEdit":true,"inputModalities":["text","image","mask"],"outputModalities":["image"],"sizes":["1024x1024","1024x1536","1536x1024"],"qualities":["standard","high"]}'::jsonb,
'{"imageBase":4,"editBase":5,"currency":"resource","qualityWeights":{"standard":1,"high":1.35},"sizeWeights":{"1024x1024":1,"1024x1536":1.3,"1536x1024":1.3}}'::jsonb,
'{"rules":[{"metric":"rpm","limit":60,"windowSeconds":60},{"metric":"concurrent","limit":4,"leaseTtlSeconds":180}]}'::jsonb,
'{"seed":"phase1"}'::jsonb,
'active'
)
ON CONFLICT (canonical_model_key) DO UPDATE
SET provider_id = EXCLUDED.provider_id,
provider_key = EXCLUDED.provider_key,
provider_model_name = EXCLUDED.provider_model_name,
model_type = EXCLUDED.model_type,
display_name = EXCLUDED.display_name,
capabilities = EXCLUDED.capabilities,
base_billing_config = EXCLUDED.base_billing_config,
default_rate_limit_policy = EXCLUDED.default_rate_limit_policy,
updated_at = now();
INSERT INTO integration_platforms (
provider, platform_key, name, base_url, auth_type, credentials, config,
default_pricing_mode, default_discount_factor, retry_policy, rate_limit_policy, priority, status
)
VALUES
(
'openai', 'openai-simulation', 'OpenAI Simulation',
'https://api.openai.com/v1', 'bearer',
'{"mode":"simulation"}'::jsonb,
'{"testMode":true,"seed":"phase1"}'::jsonb,
'inherit_discount', 1,
'{"enabled":true,"maxAttempts":2,"retryOn":["rate_limit","timeout","server_error","network"]}'::jsonb,
'{"rules":[{"metric":"rpm","limit":120,"windowSeconds":60},{"metric":"tpm_total","limit":240000,"windowSeconds":60},{"metric":"concurrent","limit":6,"leaseTtlSeconds":120}]}'::jsonb,
900,
'enabled'
),
(
'gemini', 'gemini-simulation', 'Gemini Simulation',
'https://generativelanguage.googleapis.com', 'api_key',
'{"mode":"simulation"}'::jsonb,
'{"testMode":true,"seed":"phase1"}'::jsonb,
'inherit_discount', 1,
'{"enabled":true,"maxAttempts":2,"retryOn":["rate_limit","timeout","server_error","network"]}'::jsonb,
'{"rules":[{"metric":"rpm","limit":120,"windowSeconds":60},{"metric":"tpm_total","limit":240000,"windowSeconds":60},{"metric":"concurrent","limit":6,"leaseTtlSeconds":120}]}'::jsonb,
910,
'enabled'
)
ON CONFLICT (platform_key) DO UPDATE
SET name = EXCLUDED.name,
base_url = EXCLUDED.base_url,
auth_type = EXCLUDED.auth_type,
credentials = EXCLUDED.credentials,
config = EXCLUDED.config,
default_pricing_mode = EXCLUDED.default_pricing_mode,
default_discount_factor = EXCLUDED.default_discount_factor,
retry_policy = EXCLUDED.retry_policy,
rate_limit_policy = EXCLUDED.rate_limit_policy,
priority = EXCLUDED.priority,
status = EXCLUDED.status,
updated_at = now();
INSERT INTO platform_models (
platform_id, base_model_id, model_name, model_alias, model_type, display_name,
capabilities, pricing_mode, billing_config, retry_policy, rate_limit_policy, enabled
)
SELECT p.id, b.id, b.provider_model_name, b.canonical_model_key, b.model_type, b.display_name,
b.capabilities, 'inherit_discount', b.base_billing_config,
'{"enabled":true,"maxAttempts":2}'::jsonb,
b.default_rate_limit_policy,
true
FROM integration_platforms p
JOIN base_model_catalog b ON b.provider_key = p.provider
WHERE p.platform_key IN ('openai-simulation', 'gemini-simulation')
ON CONFLICT (platform_id, model_name, model_type) DO UPDATE
SET base_model_id = EXCLUDED.base_model_id,
model_alias = EXCLUDED.model_alias,
display_name = EXCLUDED.display_name,
capabilities = EXCLUDED.capabilities,
pricing_mode = EXCLUDED.pricing_mode,
billing_config = EXCLUDED.billing_config,
retry_policy = EXCLUDED.retry_policy,
rate_limit_policy = EXCLUDED.rate_limit_policy,
enabled = EXCLUDED.enabled,
updated_at = now();
INSERT INTO model_pricing_rules (scope_type, scope_id, resource_type, unit, base_price, currency, base_weight, dynamic_weight)
SELECT 'base_model', b.id, item.resource_type, item.unit, item.base_price, 'resource', item.base_weight, item.dynamic_weight
FROM base_model_catalog b
JOIN (
VALUES
('openai:gpt-4o-mini', 'text_input', '1k_tokens', 0.15::numeric, '{}'::jsonb, '{}'::jsonb),
('openai:gpt-4o-mini', 'text_output', '1k_tokens', 0.60::numeric, '{}'::jsonb, '{}'::jsonb),
('openai:gpt-image-1', 'image', 'image', 5.00::numeric, '{"mode":"generation"}'::jsonb, '{"quality":true,"size":true}'::jsonb),
('openai:gpt-image-1', 'image_edit', 'image', 6.00::numeric, '{"mode":"edit"}'::jsonb, '{"quality":true,"size":true}'::jsonb),
('gemini:gemini-2.5-flash', 'text_input', '1k_tokens', 0.10::numeric, '{}'::jsonb, '{}'::jsonb),
('gemini:gemini-2.5-flash', 'text_output', '1k_tokens', 0.40::numeric, '{}'::jsonb, '{}'::jsonb),
('gemini:gemini-2.5-flash-image', 'image', 'image', 4.00::numeric, '{"mode":"generation"}'::jsonb, '{"quality":true,"size":true}'::jsonb),
('gemini:gemini-2.5-flash-image', 'image_edit', 'image', 5.00::numeric, '{"mode":"edit"}'::jsonb, '{"quality":true,"size":true}'::jsonb)
) AS item(canonical_model_key, resource_type, unit, base_price, base_weight, dynamic_weight)
ON item.canonical_model_key = b.canonical_model_key
WHERE NOT EXISTS (
SELECT 1
FROM model_pricing_rules existing
WHERE existing.scope_type = 'base_model'
AND existing.scope_id = b.id
AND existing.resource_type = item.resource_type
AND existing.unit = item.unit
);
@@ -0,0 +1,30 @@
ALTER TABLE model_catalog_providers
ADD COLUMN IF NOT EXISTS provider_code text,
ADD COLUMN IF NOT EXISTS icon_path text,
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'gateway';
UPDATE model_catalog_providers
SET provider_code = provider_key
WHERE provider_code IS NULL OR provider_code = '';
UPDATE model_catalog_providers
SET provider_code = 'openai',
icon_path = NULL,
source = 'server-main.integration-platform',
metadata = metadata || '{"syncSource":"server-main.integration-platform","sourceCode":"openai"}'::jsonb,
updated_at = now()
WHERE provider_key = 'openai';
UPDATE model_catalog_providers
SET provider_code = 'google-gemini',
icon_path = 'https://static.51easyai.com/gemini-color.png',
source = 'server-main.integration-platform',
metadata = metadata || '{"syncSource":"server-main.integration-platform","sourceCode":"google-gemini"}'::jsonb,
updated_at = now()
WHERE provider_key = 'gemini';
ALTER TABLE model_catalog_providers
ALTER COLUMN provider_code SET NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_catalog_provider_code
ON model_catalog_providers(provider_code);
@@ -0,0 +1,64 @@
WITH source_providers(provider_key, provider_code, provider_type, display_name, icon_path) AS (
VALUES
('easyai', 'easyai', 'easyai', 'EasyAI', ''),
('runninghub', 'runninghub', 'runninghub', 'RunningHub', ''),
('LiblibAI', 'LiblibAI', 'LiblibAI', 'LiblibAI', ''),
('keling', 'keling', 'keling', '可灵AI', 'https://static.51easyai.com/kling-color.webp'),
('gemini', 'google-gemini', 'google-gemini', 'Google Gemini', 'https://static.51easyai.com/gemini-color.png'),
('openai', 'openai', 'openai', 'OpenAI', ''),
('aliyun-bailian-openai', 'aliyun-bailian-openai', 'openai', '阿里云百炼(OpenAI兼容)', ''),
('gemini-openai', 'gemini-openai', 'openai', 'Gemini OpenAI兼容', 'https://static.51easyai.com/gemini-color.png'),
('volces-openai', 'volces-openai', 'openai', '火山引擎(OpenAI兼容)', 'https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg'),
('zhipu-openai', 'zhipu-openai', 'openai', '智谱AI', 'https://static.51easyai.com/chatglm-color.webp'),
('minimax-openai', 'minimax-openai', 'openai', 'MiniMaxOpenAI兼容)', 'https://static.51easyai.com/minimax-color.png'),
('openrouter-openai', 'openrouter-openai', 'openai', 'OpenRouter', ''),
('aliyun-bailian', 'aliyun-bailian', 'aliyun-bailian', '阿里云百炼', 'https://static.51easyai.com/bailian-color.webp'),
('ollama', 'ollama', 'openai', 'Ollama', ''),
('blackforest', 'blackforest', 'blackforest', '黑森林实验室', 'https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png'),
('dify', 'dify', 'dify', 'Dify', ''),
('volces', 'volces', 'volces', '火山引擎(豆包)', 'https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg'),
('jimeng', 'jimeng', 'jimeng', '即梦AI', 'https://static.51easyai.com/jimeng-logo.png'),
('silicon-flow-openai', 'silicon-flow-openai', 'openai', '硅基流动', ''),
('tripo3d', 'tripo3d', 'tripo3d', 'Tripo3D', 'https://static.51easyai.com/tripo-logo.png'),
('tencent-hunyuan-image', 'tencent-hunyuan-image', 'tencent-hunyuan-image', '腾讯混元生图(第三方)', 'https://static.51easyai.com/hunyuan3d-logo.svg'),
('tencent-hunyuan-video', 'tencent-hunyuan-video', 'tencent-hunyuan-video', '腾讯混元视频(第三方)', 'https://static.51easyai.com/hunyuan3d-logo.svg'),
('tencent-hunyuan', 'tencent-hunyuan', 'tencent-hunyuan', '腾讯混元3D', 'https://static.51easyai.com/hunyuan3d-logo.svg'),
('suno', 'suno', 'suno', 'Suno音乐生成', 'https://static.51easyai.com/suno-logo.png'),
('minimax', 'minimax', 'minimax', 'MiniMax', 'https://static.51easyai.com/minimax-color.png'),
('midjourney', 'midjourney', 'midjourney', 'Midjourney', 'https://static.51easyai.com/midjourney.png'),
('tencent-lke', 'tencent-lke', 'tencent-lke', '腾讯云智能体开发平台', ''),
('universal', 'universal', 'universal', '自定义平台通用平台(支持自定义方式接入任意平台)', ''),
('newapi', 'newapi', 'newapi', 'NewAPI兼容平台', ''),
('vidu', 'vidu', 'vidu', 'Vidu视频生成', 'https://static.51easyai.com/vidu-color.webp'),
('n8n', 'n8n', 'n8n', 'n8n', ''),
('mock-test', 'mock-test', 'mock-test', 'Mock测试平台', '')
)
INSERT INTO model_catalog_providers (
provider_key, provider_code, provider_type, display_name, icon_path, source,
capability_schema, default_rate_limit_policy, metadata, status
)
SELECT provider_key,
provider_code,
provider_type,
display_name,
NULLIF(icon_path, ''),
'server-main.integration-platform',
'{}'::jsonb,
'{}'::jsonb,
jsonb_build_object(
'seed', 'server-main-provider-defaults',
'syncSource', 'server-main.integration-platform',
'sourceCode', provider_code,
'sourceSpecType', provider_type
),
'active'
FROM source_providers
ON CONFLICT (provider_key) DO UPDATE
SET provider_code = EXCLUDED.provider_code,
display_name = EXCLUDED.display_name,
provider_type = EXCLUDED.provider_type,
icon_path = EXCLUDED.icon_path,
source = EXCLUDED.source,
metadata = model_catalog_providers.metadata || EXCLUDED.metadata,
status = EXCLUDED.status,
updated_at = now();
@@ -0,0 +1,371 @@
-- Seeded from easyai-server-main integration-platform.data.ts.
-- Includes model types and capabilities in metadata/capabilities for gateway base catalog management.
INSERT INTO base_model_catalog (
provider_id, provider_key, canonical_model_key, provider_model_name, model_type, display_name,
capabilities, base_billing_config, default_rate_limit_policy, pricing_version, status, metadata
)
VALUES
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦V2.1文生图', '即梦V2.1文生图', 'image_generate', '即梦V2.1文生图', '{"image_generate":{"output_resolutions":["1K"],"output_max_size":4194304,"width_height_range":[256,768],"aspect_ratio_range":[0.5625,0.5625],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"即梦V2.1文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦V2.1文生图","types":["image_generate"],"alias":"即梦V2.1文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K"],"output_max_size":4194304,"width_height_range":[256,768],"aspect_ratio_range":[0.5625,0.5625],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦V3.0文生图', '即梦V3.0文生图', 'image_generate', '即梦V3.0文生图', '{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"即梦V3.0文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦V3.0文生图","types":["image_generate"],"alias":"即梦V3.0文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦V3.1文生图', '即梦V3.1文生图', 'image_generate', '即梦V3.1文生图', '{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"即梦V3.1文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦V3.1文生图","types":["image_generate"],"alias":"即梦V3.1文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦V3.0图像智能参考', '即梦V3.0图像智能参考', 'image_edit', '即梦V3.0图像智能参考', '{"image_edit":{"input_multiple_images":false,"output_resolutions":["1K","2K"],"width_height_range":[512,2016],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"originalTypes":["image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit"],"alias":"即梦V3.0图像智能参考","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦V3.0图像智能参考","types":["image_edit"],"alias":"即梦V3.0图像智能参考","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_edit":{"input_multiple_images":false,"output_resolutions":["1K","2K"],"width_height_range":[512,2016],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦V4.0图像生成及编辑', '即梦V4.0图像生成及编辑', 'image_edit', '即梦V4.0图像生成及编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"input_parameters":[{"type":"url","parameter":"image_urls"}],"output_resolutions":["1K","2K","4K"],"allow_custom_width_height_size":true,"output_max_size":16777216,"width_height_range":[1024,4096],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"image_generate":{"output_multiple_images":true,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"allow_custom_width_height_size":true,"aspect_ratio_range":[0.0625,16],"width_height_range":[1024,6198]},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit","image_generate"],"alias":"即梦V4.0图像生成及编辑","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦V4.0图像生成及编辑","types":["image_edit","image_generate"],"alias":"即梦V4.0图像生成及编辑","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"input_parameters":[{"type":"url","parameter":"image_urls"}],"output_resolutions":["1K","2K","4K"],"allow_custom_width_height_size":true,"output_max_size":16777216,"width_height_range":[1024,4096],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"image_generate":{"output_multiple_images":true,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"allow_custom_width_height_size":true,"aspect_ratio_range":[0.0625,16],"width_height_range":[1024,6198]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦文生视频V3.0', '即梦文生视频V3.0', 'video_generate', '即梦文生视频V3.0', '{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"即梦文生视频V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦文生视频V3.0","types":["video_generate"],"alias":"即梦文生视频V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦视频生成V3.0_Pro', '即梦视频生成V3.0_Pro', 'video_generate', '即梦视频生成V3.0_Pro', '{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"即梦视频生成V3.0_Pro","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦视频生成V3.0_Pro","types":["video_generate","image_to_video"],"alias":"即梦视频生成V3.0_Pro","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦图生视频V3.0', '即梦图生视频V3.0', 'image_to_video', '即梦图生视频V3.0', '{"image_to_video":{"output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦图生视频V3.0","types":["image_to_video"],"alias":"即梦图生视频V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦首尾帧视频生成V3.0', '即梦首尾帧视频生成V3.0', 'image_to_video', '即梦首尾帧视频生成V3.0', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"即梦首尾帧视频生成V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦首尾帧视频生成V3.0","types":["image_to_video"],"alias":"即梦首尾帧视频生成V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦文生视频V3.0_1080p', '即梦文生视频V3.0_1080p', 'video_generate', '即梦文生视频V3.0_1080p', '{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"即梦文生视频V3.0_1080p","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦文生视频V3.0_1080p","types":["video_generate"],"alias":"即梦文生视频V3.0_1080p","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦图生视频V3.0_1080p', '即梦图生视频V3.0_1080p', 'image_to_video', '即梦图生视频V3.0_1080p', '{"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0_1080p","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦图生视频V3.0_1080p","types":["image_to_video"],"alias":"即梦图生视频V3.0_1080p","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦图生视频V3.0_1080p_首尾帧', '即梦图生视频V3.0_1080p_首尾帧', 'image_to_video', '即梦图生视频V3.0_1080p_首尾帧', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0_1080p_首尾帧","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦图生视频V3.0_1080p_首尾帧","types":["image_to_video"],"alias":"即梦图生视频V3.0_1080p_首尾帧","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦数字人V1', '即梦数字人V1', 'digital_human_generate', '即梦数字人V1', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦数字人V1","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦数字人V1","types":["digital_human_generate"],"alias":"即梦数字人V1","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦单图音频驱动-普通模式', '即梦单图音频驱动-普通模式', 'digital_human_generate', '即梦单图音频驱动-普通模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-普通模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦单图音频驱动-普通模式","types":["digital_human_generate"],"alias":"即梦单图音频驱动-普通模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦单图音频驱动-灵动模式', '即梦单图音频驱动-灵动模式', 'digital_human_generate', '即梦单图音频驱动-灵动模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-灵动模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦单图音频驱动-灵动模式","types":["digital_human_generate"],"alias":"即梦单图音频驱动-灵动模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦视频改口型Lite模式', '即梦视频改口型Lite模式', 'digital_human_generate', '即梦视频改口型Lite模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦视频改口型Lite模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦视频改口型Lite模式","types":["digital_human_generate"],"alias":"即梦视频改口型Lite模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦单图音频驱动-大画幅灵动模式', '即梦单图音频驱动-大画幅灵动模式', 'digital_human_generate', '即梦单图音频驱动-大画幅灵动模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-大画幅灵动模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦单图音频驱动-大画幅灵动模式","types":["digital_human_generate"],"alias":"即梦单图音频驱动-大画幅灵动模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦动作模仿', '即梦动作模仿', 'digital_human_generate', '即梦动作模仿', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦动作模仿","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦动作模仿","types":["digital_human_generate"],"alias":"即梦动作模仿","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦动作模仿2.0', '即梦动作模仿2.0', 'digital_human_generate', '即梦动作模仿2.0', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦动作模仿2.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦动作模仿2.0","types":["digital_human_generate"],"alias":"即梦动作模仿2.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦数字人快速模式1.0', '即梦数字人快速模式1.0', 'digital_human_generate', '即梦数字人快速模式1.0', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦数字人快速模式1.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦数字人快速模式1.0","types":["digital_human_generate"],"alias":"即梦数字人快速模式1.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:即梦数字人快速模式1.5', '即梦数字人快速模式1.5', 'digital_human_generate', '即梦数字人快速模式1.5', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"即梦数字人快速模式1.5","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"即梦数字人快速模式1.5","types":["digital_human_generate"],"alias":"即梦数字人快速模式1.5","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-3.0图像编辑', 'doubao-3.0图像编辑', 'image_edit', 'doubao-3.0图像编辑', '{"image_edit":{"output_resolutions":["1K","2K"],"aspect_ratio_allowed":["adaptive"],"input_multiple_images":false,"output_multiple_images":false},"originalTypes":["image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit"],"alias":"doubao-3.0图像编辑","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-3.0图像编辑","types":["image_edit"],"alias":"doubao-3.0图像编辑","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"output_resolutions":["1K","2K"],"aspect_ratio_allowed":["adaptive"],"input_multiple_images":false,"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-4.0图像编辑', 'doubao-4.0图像编辑', 'image_edit', 'doubao-4.0图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"output_size_range":[921600,16777216],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[921600,16777216],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit","image_generate"],"alias":"doubao-4.0图像编辑","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-4.0图像编辑","types":["image_edit","image_generate"],"alias":"doubao-4.0图像编辑","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"output_size_range":[921600,16777216],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[921600,16777216],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-5.0图像编辑', 'doubao-5.0图像编辑', 'image_edit', 'doubao-5.0图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit","image_generate"],"alias":"doubao-5.0图像编辑","description":"支持 2K 和 3K 分辨率","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-5.0图像编辑","types":["image_edit","image_generate"],"alias":"doubao-5.0图像编辑","description":"支持 2K 和 3K 分辨率","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-5.0-lite图像编辑', 'doubao-5.0-lite图像编辑', 'image_edit', 'doubao-5.0-lite图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_edit","image_generate"],"alias":"doubao-5.0-lite图像编辑","description":"轻量版,支持 2K 和 3K 分辨率","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-5.0-lite图像编辑","types":["image_edit","image_generate"],"alias":"doubao-5.0-lite图像编辑","description":"轻量版,支持 2K 和 3K 分辨率","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-4.5图像编辑', 'doubao-4.5图像编辑', 'image_generate', 'doubao-4.5图像编辑', '{"image_generate":{"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"image_edit":{"input_size_range":["14x14","6000x6000"],"input_format_allowed":["png","jpg","jpeg","webp","gif","bmp","tiff"],"input_multiple_images":true,"input_aspect_ratio_range":[0.0625,16],"input_max_images_count":14,"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"doubao-4.5图像编辑","description":"多图稳定融合,超强编辑一致性,小字清晰,4k超高清","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-4.5图像编辑","types":["image_generate","image_edit"],"alias":"doubao-4.5图像编辑","description":"多图稳定融合,超强编辑一致性,小字清晰,4k超高清","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_generate":{"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"image_edit":{"input_size_range":["14x14","6000x6000"],"input_format_allowed":["png","jpg","jpeg","webp","gif","bmp","tiff"],"input_multiple_images":true,"input_aspect_ratio_range":[0.0625,16],"input_max_images_count":14,"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-3.0文生图', 'doubao-3.0文生图', 'image_generate', 'doubao-3.0文生图', '{"image_generate":{"output_resolutions":["2K","1K"],"output_max_size":4194304,"aspect_ratio_range":[0.42857142857142855,2.3333333333333335],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"doubao-3.0文生图","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-3.0文生图","types":["image_generate"],"alias":"doubao-3.0文生图","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_generate":{"output_resolutions":["2K","1K"],"output_max_size":4194304,"aspect_ratio_range":[0.42857142857142855,2.3333333333333335],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-1.0-lite-文生视频', 'doubao-1.0-lite-文生视频', 'video_generate', 'doubao-1.0-lite-文生视频', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"],"duration_range":[3,12]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":300,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"doubao-1.0-lite-文生视频","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-1.0-lite-文生视频","types":["video_generate"],"alias":"doubao-1.0-lite-文生视频","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":300,"max_concurrent_requests":5},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"],"duration_range":[3,12]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:doubao-1.0-lite-i2v-图生视频', 'doubao-1.0-lite-i2v-图生视频', 'image_to_video', 'doubao-1.0-lite-i2v-图生视频', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":300,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"doubao-1.0-lite-i2v-图生视频","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-1.0-lite-i2v-图生视频","types":["image_to_video"],"alias":"doubao-1.0-lite-i2v-图生视频","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":300,"max_concurrent_requests":5},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:豆包Seedance-pro', '豆包Seedance-pro', 'video_generate', '豆包Seedance-pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-pro","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"豆包Seedance-pro","types":["video_generate","image_to_video"],"alias":"豆包Seedance-pro","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:豆包Seedance-1.5-pro', '豆包Seedance-1.5-pro', 'video_generate', '豆包Seedance-1.5-pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"output_audio":true},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-1.5-pro","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"豆包Seedance-1.5-pro","types":["video_generate","image_to_video"],"alias":"豆包Seedance-1.5-pro","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"output_audio":true},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:豆包Seedance-pro-fast', '豆包Seedance-pro-fast', 'video_generate', '豆包Seedance-pro-fast', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-pro-fast","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"豆包Seedance-pro-fast","types":["video_generate","image_to_video"],"model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"alias":"豆包Seedance-pro-fast","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:豆包Seedance-2.0', '豆包Seedance-2.0', 'video_generate', '豆包Seedance-2.0', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["video_generate","image_to_video","omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"豆包Seedance-2.0","types":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:豆包Seedance-2.0-fast', '豆包Seedance-2.0-fast', 'video_generate', '豆包Seedance-2.0-fast', '{"video_generate":{"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["video_generate","image_to_video","omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0-fast","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"豆包Seedance-2.0-fast","types":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0-fast","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Midjourney_v7', 'Midjourney_v7', 'image_generate', 'Midjourney_v7', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v7","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Midjourney_v7","types":["image_generate","image_edit"],"alias":"Midjourney_v7","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Midjourney_v6', 'Midjourney_v6', 'image_generate', 'Midjourney_v6', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v6","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Midjourney_v6","types":["image_generate","image_edit"],"alias":"Midjourney_v6","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Midjourney_v6.1', 'Midjourney_v6.1', 'image_generate', 'Midjourney_v6.1', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v6.1","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Midjourney_v6.1","types":["image_generate","image_edit"],"alias":"Midjourney_v6.1","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Midjourney_Niji_v7', 'Midjourney_Niji_v7', 'image_generate', 'Midjourney_Niji_v7', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_Niji_v7","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Midjourney_Niji_v7","types":["image_generate","image_edit"],"alias":"Midjourney_Niji_v7","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Midjourney_Niji_v6', 'Midjourney_Niji_v6', 'image_generate', 'Midjourney_Niji_v6', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_Niji_v6","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Midjourney_Niji_v6","types":["image_generate","image_edit"],"alias":"Midjourney_Niji_v6","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:gemini-2.0', 'gemini-2.0', 'image_generate', 'gemini-2.0', '{"image_generate":{"output_multiple_images":false,"aspect_ratio_allowed":[],"output_resolutions":[]},"image_edit":{"output_multiple_images":false,"aspect_ratio_allowed":[],"input_multiple_images":true,"output_resolutions":[]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"gemini-2.0","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.0","types":["image_generate","image_edit"],"alias":"gemini-2.0","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":false,"aspect_ratio_allowed":[],"output_resolutions":[]},"image_edit":{"output_multiple_images":false,"aspect_ratio_allowed":[],"input_multiple_images":true,"output_resolutions":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Nano Banana', 'Nano Banana', 'image_generate', 'Nano Banana', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"output_max_images_count":4,"input_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Nano Banana","types":["image_generate","image_edit"],"alias":"Nano Banana","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"output_max_images_count":4,"input_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Nano Banana Pro 预览版', 'Nano Banana Pro 预览版', 'image_generate', 'Nano Banana Pro 预览版', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Nano Banana Pro 预览版","types":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Nano Banana 2', 'Nano Banana 2', 'image_generate', 'Nano Banana 2', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana 2","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Nano Banana 2","types":["image_generate","image_edit"],"alias":"Nano Banana 2","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini-3 Pro 预览版', 'Gemini-3 Pro 预览版', 'text_generate', 'Gemini-3 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini-3 Pro 预览版","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini-3 Flash 预览版', 'Gemini-3 Flash 预览版', 'text_generate', 'Gemini-3 Flash 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini-3 Flash 预览版","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini-3.1 Flash Lite 预览版', 'Gemini-3.1 Flash Lite 预览版', 'text_generate', 'Gemini-3.1 Flash Lite 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini-3.1 Flash Lite 预览版","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini-3.1 Pro 预览版', 'Gemini-3.1 Pro 预览版', 'text_generate', 'Gemini-3.1 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini-3.1 Pro 预览版","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini 2.5 Flash', 'Gemini 2.5 Flash', 'text_generate', 'Gemini 2.5 Flash', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Flash","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini 2.5 Flash","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Flash"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Gemini 2.5 Pro', 'Gemini 2.5 Pro', 'text_generate', 'Gemini 2.5 Pro', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Gemini 2.5 Pro","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Pro"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Doubao Seed 2.0 Pro', 'Doubao Seed 2.0 Pro', 'text_generate', 'Doubao Seed 2.0 Pro', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Pro","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Doubao Seed 2.0 Pro","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Pro","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Doubao Seed 2.0 Lite', 'Doubao Seed 2.0 Lite', 'text_generate', 'Doubao Seed 2.0 Lite', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Lite","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Doubao Seed 2.0 Lite","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Lite","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Doubao Seed 2.0 Mini', 'Doubao Seed 2.0 Mini', 'text_generate', 'Doubao Seed 2.0 Mini', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Mini","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Doubao Seed 2.0 Mini","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Mini","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Doubao Seed 2.0 Code Preview', 'Doubao Seed 2.0 Code Preview', 'text_generate', 'Doubao Seed 2.0 Code Preview', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Code Preview","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Doubao Seed 2.0 Code Preview","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Code Preview","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Doubao Seed 1.8', 'Doubao Seed 1.8', 'text_generate', 'Doubao Seed 1.8', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 1.8","description":"深度思考/文本生成/多模态理解/工具调用/结构化输出,256K 上下文;最大输入 224K,最大回答 32K,最大思维链 32K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Doubao Seed 1.8","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 1.8","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用/结构化输出,256K 上下文;最大输入 224K,最大回答 32K,最大思维链 32K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:GLM-4.7', 'GLM-4.7', 'text_generate', 'GLM-4.7', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":15000,"max_token_per_minute":1500000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"GLM-4.7","description":"深度思考/文本生成/工具调用,200K 上下文;最大输入 200K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"GLM-4.7","types":["text_generate","image_analysis","tools_call"],"alias":"GLM-4.7","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/工具调用,200K 上下文;最大输入 200K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":15000,"max_token_per_minute":1500000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:GLM-4.7-Flash', 'GLM-4.7-Flash', 'text_generate', 'GLM-4.7-Flash', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"GLM-4.7-Flash","description":"","iconPath":"https://static.51easyai.com/chatglm-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"GLM-4.7-Flash","types":["text_generate","tools_call"],"alias":"GLM-4.7-Flash","icon_path":"https://static.51easyai.com/chatglm-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:GLM-4.7-FlashX', 'GLM-4.7-FlashX', 'text_generate', 'GLM-4.7-FlashX', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"GLM-4.7-FlashX","description":"","iconPath":"https://static.51easyai.com/chatglm-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"GLM-4.7-FlashX","types":["text_generate","tools_call"],"alias":"GLM-4.7-FlashX","icon_path":"https://static.51easyai.com/chatglm-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.7', 'MiniMax M2.7', 'text_generate', 'MiniMax M2.7', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.7","description":"开启模型的自我迭代(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.7","types":["text_generate","tools_call"],"alias":"MiniMax M2.7","icon_path":"https://static.51easyai.com/minimax-color.png","description":"开启模型的自我迭代(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.7 极速版', 'MiniMax M2.7 极速版', 'text_generate', 'MiniMax M2.7 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.7 极速版","description":"M2.7 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.7 极速版","types":["text_generate","tools_call"],"alias":"MiniMax M2.7 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.7 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.5', 'MiniMax M2.5', 'text_generate', 'MiniMax M2.5', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.5","description":"顶尖性能与极致性价比,轻松驾驭复杂任务(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.5","types":["text_generate","tools_call"],"alias":"MiniMax M2.5","icon_path":"https://static.51easyai.com/minimax-color.png","description":"顶尖性能与极致性价比,轻松驾驭复杂任务(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.5 极速版', 'MiniMax M2.5 极速版', 'text_generate', 'MiniMax M2.5 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.5 极速版","description":"M2.5 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.5 极速版","types":["text_generate","tools_call"],"alias":"MiniMax M2.5 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.5 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.1', 'MiniMax M2.1', 'text_generate', 'MiniMax M2.1', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.1","description":"强大多语言编程能力,全面升级编程体验(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.1","types":["text_generate","tools_call"],"alias":"MiniMax M2.1","icon_path":"https://static.51easyai.com/minimax-color.png","description":"强大多语言编程能力,全面升级编程体验(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2.1 极速版', 'MiniMax M2.1 极速版', 'text_generate', 'MiniMax M2.1 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.1 极速版","description":"M2.1 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2.1 极速版","types":["text_generate","tools_call"],"alias":"MiniMax M2.1 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.1 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax M2', 'MiniMax M2', 'text_generate', 'MiniMax M2', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2","description":"专为高效编码与 Agent 工作流而生。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax M2","types":["text_generate","tools_call"],"alias":"MiniMax M2","icon_path":"https://static.51easyai.com/minimax-color.png","description":"专为高效编码与 Agent 工作流而生。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax Speech 2.5 HD Preview', 'MiniMax Speech 2.5 HD Preview', 'text_to_speech', 'MiniMax Speech 2.5 HD Preview', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.5 HD Preview","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax Speech 2.5 HD Preview","types":["text_to_speech"],"alias":"MiniMax Speech 2.5 HD Preview","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax Speech 2.5 Turbo Preview', 'MiniMax Speech 2.5 Turbo Preview', 'text_to_speech', 'MiniMax Speech 2.5 Turbo Preview', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.5 Turbo Preview","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax Speech 2.5 Turbo Preview","types":["text_to_speech"],"alias":"MiniMax Speech 2.5 Turbo Preview","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax Speech 2.6 HD', 'MiniMax Speech 2.6 HD', 'text_to_speech', 'MiniMax Speech 2.6 HD', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.6 HD","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax Speech 2.6 HD","types":["text_to_speech"],"alias":"MiniMax Speech 2.6 HD","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax Speech 2.6 Turbo', 'MiniMax Speech 2.6 Turbo', 'text_to_speech', 'MiniMax Speech 2.6 Turbo', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.6 Turbo","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax Speech 2.6 Turbo","types":["text_to_speech"],"alias":"MiniMax Speech 2.6 Turbo","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:海螺02', '海螺02', 'video_generate', '海螺02', '{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":{"input_first_frame":["480p","720p","1080p"],"input_first_last_frame":["720p","1080p"]},"duration_range":{"480p":[6,10],"720p":[6,10],"1080p":[6,6]},"duration_options":{"480p":[6,10],"720p":[6,10],"1080p":[6]},"input_reference_generate_single":false,"input_reference_generate_multiple":false,"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"海螺02","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"海螺02","types":["video_generate","image_to_video"],"alias":"海螺02","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":{"input_first_frame":["480p","720p","1080p"],"input_first_last_frame":["720p","1080p"]},"duration_range":{"480p":[6,10],"720p":[6,10],"1080p":[6,6]},"duration_options":{"480p":[6,10],"720p":[6,10],"1080p":[6]},"input_reference_generate_single":false,"input_reference_generate_multiple":false,"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:海螺2.3', '海螺2.3', 'video_generate', '海螺2.3', '{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"海螺2.3","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"海螺2.3","types":["video_generate","image_to_video"],"alias":"海螺2.3","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:MiniMax-Hailuo-2.3-Fast', 'MiniMax-Hailuo-2.3-Fast', 'image_to_video', 'MiniMax-Hailuo-2.3-Fast', '{"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"MiniMax-Hailuo-2.3-Fast","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-Hailuo-2.3-Fast","types":["image_to_video"],"alias":"MiniMax-Hailuo-2.3-Fast","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-2-pro', 'flux-2-pro', 'image_generate', 'flux-2-pro', '{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":8,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"flux-2-pro","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-2-pro","types":["image_generate","image_edit"],"alias":"flux-2-pro","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":8,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-2-flex', 'flux-2-flex', 'image_generate', 'flux-2-flex', '{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":10,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"flux-2-flex","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-2-flex","types":["image_generate","image_edit"],"alias":"flux-2-flex","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":10,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-kontext-pro', 'flux-kontext-pro', 'image_generate', 'flux-kontext-pro', '{"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"flux-kontext-pro","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-kontext-pro","types":["image_generate","image_edit"],"alias":"flux-kontext-pro","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-kontext-max', 'flux-kontext-max', 'image_generate', 'flux-kontext-max', '{"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"flux-kontext-max","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-kontext-max","types":["image_generate","image_edit"],"alias":"flux-kontext-max","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-pro-1.1', 'flux-pro-1.1', 'image_generate', 'flux-pro-1.1', '{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"flux-pro-1.1","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-pro-1.1","types":["image_generate"],"alias":"flux-pro-1.1","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:flux-dev', 'flux-dev', 'image_generate', 'flux-dev', '{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate"],"alias":"flux-dev","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-dev","types":["image_generate"],"alias":"flux-dev","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V1', '可灵V1', 'video_generate', '可灵V1', '{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","1:1","9:16"]},"image_to_video":{"output_resolutions":["720p"],"duration_range":{"input_first_last_frame":[5,5],"input_first_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V1","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V1","types":["video_generate","image_to_video"],"alias":"可灵V1","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","1:1","9:16"]},"image_to_video":{"output_resolutions":["720p"],"duration_range":{"input_first_last_frame":[5,5],"input_first_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V1.5', '可灵V1.5', 'image_to_video', '可灵V1.5', '{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"可灵V1.5","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V1.5","types":["image_to_video"],"alias":"可灵V1.5","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V1.6', '可灵V1.6', 'video_generate', '可灵V1.6', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"],"input_reference_generate_multiple":["720p","1080p"],"output_video_continuation":["720p","1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"input_reference_generate_multiple":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"support_video_effect_template":true,"support_video_effect_template_list":[]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V1.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V1.6","types":["video_generate","image_to_video"],"alias":"可灵V1.6","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"],"input_reference_generate_multiple":["720p","1080p"],"output_video_continuation":["720p","1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"input_reference_generate_multiple":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"support_video_effect_template":true,"support_video_effect_template_list":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V2-大师级', '可灵V2-大师级', 'video_generate', '可灵V2-大师级', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V2-大师级","types":["video_generate","image_to_video"],"alias":"可灵V2-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V2.1图生视频', '可灵V2.1图生视频', 'image_to_video', '可灵V2.1图生视频', '{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10],"input_first_last_frame":[5,10]},"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"可灵V2.1图生视频","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V2.1图生视频","types":["image_to_video"],"alias":"可灵V2.1图生视频","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10],"input_first_last_frame":[5,10]},"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V2.1-大师级', '可灵V2.1-大师级', 'video_generate', '可灵V2.1-大师级', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.1-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V2.1-大师级","types":["video_generate","image_to_video"],"alias":"可灵V2.1-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V2.5-turbo', '可灵V2.5-turbo', 'video_generate', '可灵V2.5-turbo', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.5-turbo","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V2.5-turbo","types":["video_generate","image_to_video"],"alias":"可灵V2.5-turbo","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V2.6', '可灵V2.6', 'video_generate', '可灵V2.6', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V2.6","types":["video_generate","image_to_video"],"alias":"可灵V2.6","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V3', '可灵V3', 'video_generate', '可灵V3', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"可灵V3","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V3","types":["video_generate","image_to_video"],"alias":"可灵V3","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵V3多模态', '可灵V3多模态', 'omni_video', '可灵V3多模态', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"max_images_for_last_frame":2,"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["omni_video"],"alias":"可灵V3多模态","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵V3多模态","types":["omni_video"],"alias":"可灵V3多模态","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"max_images_for_last_frame":2,"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵O1', '可灵O1', 'omni_video', '可灵O1', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"},"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["omni_video"],"alias":"可灵O1","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵O1","types":["omni_video"],"alias":"可灵O1","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"},"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵动作控制', '可灵动作控制', 'digital_human_generate', '可灵动作控制', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"可灵动作控制","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵动作控制","types":["digital_human_generate"],"alias":"可灵动作控制","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵对口型', '可灵对口型', 'digital_human_generate', '可灵对口型', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"可灵对口型","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵对口型","types":["digital_human_generate"],"alias":"可灵对口型","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:可灵数字人', '可灵数字人', 'digital_human_generate', '可灵数字人', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["digital_human_generate"],"alias":"可灵数字人","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"可灵数字人","types":["digital_human_generate"],"alias":"可灵数字人","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3-Embedding-v4', 'Qwen3-Embedding-v4', 'text_embedding', 'Qwen3-Embedding-v4', '{"text_embedding":{"dimensions":[2048,1536,1024,768,512,256,128,64]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_second":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_embedding"],"alias":"Qwen3-Embedding-v4","description":"Qwen3-Embedding 系列,默认维度 1024,最长输入 8192 tokens;支持 100+ 语种与多种编程语言。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3-Embedding-v4","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3-Embedding-v4","model_limits":{"max_request_per_second":10},"capabilities":{"text_embedding":{"dimensions":[2048,1536,1024,768,512,256,128,64]}},"description":"Qwen3-Embedding 系列,默认维度 1024,最长输入 8192 tokens;支持 100+ 语种与多种编程语言。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3-Embedding-v3', 'Qwen3-Embedding-v3', 'text_embedding', 'Qwen3-Embedding-v3', '{"text_embedding":{"dimensions":[1024,768,512,256,128,64]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_embedding"],"alias":"Qwen3-Embedding-v3","description":"Qwen3-Embedding 系列,默认维度 1024;支持中文、英文及 50+ 主流语种。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3-Embedding-v3","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3-Embedding-v3","capabilities":{"text_embedding":{"dimensions":[1024,768,512,256,128,64]}},"description":"Qwen3-Embedding 系列,默认维度 1024;支持中文、英文及 50+ 主流语种。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Text-Embedding-v2', 'Text-Embedding-v2', 'text_embedding', 'Text-Embedding-v2', '{"text_embedding":{"dimensions":[1536]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_second":25}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_embedding"],"alias":"Text-Embedding-v2","description":"固定维度 1536,最长输入 2048 tokens;支持中英西法葡印尼日韩德俄等语种。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Text-Embedding-v2","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Text-Embedding-v2","model_limits":{"max_request_per_second":25},"capabilities":{"text_embedding":{"dimensions":[1536]}},"description":"固定维度 1536,最长输入 2048 tokens;支持中英西法葡印尼日韩德俄等语种。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Text-Embedding-v1', 'Text-Embedding-v1', 'text_embedding', 'Text-Embedding-v1', '{"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_embedding"],"alias":"Text-Embedding-v1","description":"百炼 OpenAI 兼容渠道 embedding 基础模型。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Text-Embedding-v1","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Text-Embedding-v1","description":"百炼 OpenAI 兼容渠道 embedding 基础模型。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen-turbo', 'qwen-turbo', 'text_generate', 'qwen-turbo', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-turbo","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen3-235b-a22b', 'qwen3-235b-a22b', 'text_generate', 'qwen3-235b-a22b', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3-235b-a22b","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen-max', 'qwen-max', 'text_generate', 'qwen-max', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-max","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen-plus', 'qwen-plus', 'text_generate', 'qwen-plus', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-plus","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen3-max', 'qwen3-max', 'text_generate', 'qwen3-max', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3-max","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen-vl-plus', 'qwen-vl-plus', 'image_analysis', 'qwen-vl-plus', '{"originalTypes":["image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_analysis"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-vl-plus","types":["image_analysis"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:qwen-vl-max', 'qwen-vl-max', 'image_analysis', 'qwen-vl-max', '{"originalTypes":["image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_analysis"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-vl-max","types":["image_analysis"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-Plus', 'Qwen3.5-Plus', 'text_generate', 'Qwen3.5-Plus', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-Plus","description":"千问性能最强的视觉理解模型,推荐优先使用","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-Plus","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Plus","description":"千问性能最强的视觉理解模型,推荐优先使用"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-Flash', 'Qwen3.5-Flash', 'text_generate', 'Qwen3.5-Flash', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-Flash","description":"速度更快,成本更低,兼顾性能与成本的高性价比选择","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-Flash","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Flash","description":"速度更快,成本更低,兼顾性能与成本的高性价比选择"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-Omni-Plus', 'Qwen3.5-Omni-Plus', 'text_generate', 'Qwen3.5-Omni-Plus', '{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Qwen3.5-Omni-Plus","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-plus-2026-03-15 能力相同。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-Omni-Plus","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Omni-Plus","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-plus-2026-03-15 能力相同。","capabilities":{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-Omni-Flash', 'Qwen3.5-Omni-Flash', 'text_generate', 'Qwen3.5-Omni-Flash', '{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Qwen3.5-Omni-Flash","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-flash-2026-03-15 能力相同。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-Omni-Flash","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Omni-Flash","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-flash-2026-03-15 能力相同。","capabilities":{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-397B-A17B', 'Qwen3.5-397B-A17B', 'text_generate', 'Qwen3.5-397B-A17B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-397B-A17B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-397B-A17B","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-397B-A17B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-122B-A10B', 'Qwen3.5-122B-A10B', 'text_generate', 'Qwen3.5-122B-A10B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-122B-A10B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-122B-A10B","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-122B-A10B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-27B', 'Qwen3.5-27B', 'text_generate', 'Qwen3.5-27B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-27B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-27B","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-27B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.5-35B-A3B', 'Qwen3.5-35B-A3B', 'text_generate', 'Qwen3.5-35B-A3B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-35B-A3B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.5-35B-A3B","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-35B-A3B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Qwen3.6-Plus', 'Qwen3.6-Plus', 'text_generate', 'Qwen3.6-Plus', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"Qwen3.6-Plus","description":"Qwen3.6 文本模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Qwen3.6-Plus","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.6-Plus","description":"Qwen3.6 文本模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:DeepSeek-V4-Pro', 'DeepSeek-V4-Pro', 'text_generate', 'DeepSeek-V4-Pro', '{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"DeepSeek-V4-Pro","description":"DeepSeek V4 Pro 文本模型,支持工具调用,最大上下文 100 万 tokens。","iconPath":"https://static.51easyai.com/deepseek-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"DeepSeek-V4-Pro","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/deepseek-color%20%281%29.webp","alias":"DeepSeek-V4-Pro","description":"DeepSeek V4 Pro 文本模型,支持工具调用,最大上下文 100 万 tokens。","capabilities":{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:DeepSeek-V4-Flash', 'DeepSeek-V4-Flash', 'text_generate', 'DeepSeek-V4-Flash', '{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"DeepSeek-V4-Flash","description":"DeepSeek V4 Flash 文本模型,支持工具调用,最大上下文 100 万 tokens。","iconPath":"https://static.51easyai.com/deepseek-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"DeepSeek-V4-Flash","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/deepseek-color%20%281%29.webp","alias":"DeepSeek-V4-Flash","description":"DeepSeek V4 Flash 文本模型,支持工具调用,最大上下文 100 万 tokens。","capabilities":{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Wan2.5-T2V-Preview', 'Wan2.5-T2V-Preview', 'video_generate', 'Wan2.5-T2V-Preview', '{"video_generate":{"aspect_ratio_allowed":{"480p":["16:9","1:1","9:16"],"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"size_allowed":{"480p":["832x480","480x832","624x624"],"720p":["1280x720","720x1280","960x960","1088x832","832x1088"],"1080p":["1920x1080","1080x1920","1440x1440","1632x1248","1248*1632"]},"output_resolutions":["1080p","720p","480p"],"duration_range":[5,10],"input_audio":true,"output_audio":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5,"max_request_per_second":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"Wan2.5-T2V-Preview","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Wan2.5-T2V-Preview","types":["video_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.5-T2V-Preview","model_limits":{"max_concurrent_requests":5,"max_request_per_second":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":{"480p":["16:9","1:1","9:16"],"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"size_allowed":{"480p":["832x480","480x832","624x624"],"720p":["1280x720","720x1280","960x960","1088x832","832x1088"],"1080p":["1920x1080","1080x1920","1440x1440","1632x1248","1248*1632"]},"output_resolutions":["1080p","720p","480p"],"duration_range":[5,10],"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Wan2.5-I2V-Preview', 'Wan2.5-I2V-Preview', 'image_to_video', 'Wan2.5-I2V-Preview', '{"image_to_video":{"output_resolutions":["1080p","720p","480p"],"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":["adaptive"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true,"output_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Wan2.5-I2V-Preview","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Wan2.5-I2V-Preview","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.5-I2V-Preview","model_limits":{"max_concurrent_requests":5},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p","480p"],"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":["adaptive"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Wan2.6-文生视频', 'Wan2.6-文生视频', 'video_generate', 'Wan2.6-文生视频', '{"video_generate":{"aspect_ratio_allowed":{"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"output_resolutions":["1080p","720p"],"duration_range":[5,15],"duration_options":[5,10,15],"input_audio":true,"output_audio":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"Wan2.6-文生视频","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Wan2.6-文生视频","types":["video_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-文生视频","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"video_generate":{"aspect_ratio_allowed":{"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"output_resolutions":["1080p","720p"],"duration_range":[5,15],"duration_options":[5,10,15],"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Wan2.6-图生视频', 'Wan2.6-图生视频', 'image_to_video', 'Wan2.6-图生视频', '{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[5,10]},"duration_options":{"input_first_frame":[5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Wan2.6-图生视频","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Wan2.6-图生视频","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-图生视频","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[5,10]},"duration_options":{"input_first_frame":[5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Wan2.6-图生视频-Flash', 'Wan2.6-图生视频-Flash', 'image_to_video', 'Wan2.6-图生视频-Flash', '{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[2,15]},"duration_options":{"input_first_frame":[2,5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"output_audio":true,"input_audio":true,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Wan2.6-图生视频-Flash","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Wan2.6-图生视频-Flash","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-图生视频-Flash","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[2,15]},"duration_options":{"input_first_frame":[2,5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"output_audio":true,"input_audio":true,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Suno音频生成V3.0', 'Suno音频生成V3.0', 'audio_generate', 'Suno音频生成V3.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["audio_generate"],"alias":"Suno音频生成V3.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Suno音频生成V3.0","types":["audio_generate"],"alias":"Suno音频生成V3.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Suno音频生成V3.5', 'Suno音频生成V3.5', 'audio_generate', 'Suno音频生成V3.5', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["audio_generate"],"alias":"Suno音频生成V3.5","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Suno音频生成V3.5","types":["audio_generate"],"alias":"Suno音频生成V3.5","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Suno音频生成V4.0', 'Suno音频生成V4.0', 'audio_generate', 'Suno音频生成V4.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["audio_generate"],"alias":"Suno音频生成V4.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Suno音频生成V4.0","types":["audio_generate"],"alias":"Suno音频生成V4.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Suno音频生成V4.5', 'Suno音频生成V4.5', 'audio_generate', 'Suno音频生成V4.5', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["audio_generate"],"alias":"Suno音频生成V4.5","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Suno音频生成V4.5","types":["audio_generate"],"alias":"Suno音频生成V4.5","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Suno音频生成V5.0', 'Suno音频生成V5.0', 'audio_generate', 'Suno音频生成V5.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["audio_generate"],"alias":"Suno音频生成V5.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Suno音频生成V5.0","types":["audio_generate"],"alias":"Suno音频生成V5.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Sora-2', 'Sora-2', 'video_generate', 'Sora-2', '{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Sora-2","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Sora-2-Pro', 'Sora-2-Pro', 'video_generate', 'Sora-2-Pro', '{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2-Pro","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Sora-2-Pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2-Pro","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:VEO-3-Fast', 'VEO-3-Fast', 'video_generate', 'VEO-3-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"VEO-3-Fast","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:VEO-3.1-Fast', 'VEO-3.1-Fast', 'video_generate', 'VEO-3.1-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"VEO-3.1-Fast","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:VEO-3', 'VEO-3', 'video_generate', 'VEO-3', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"VEO-3","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:VEO-3.1', 'VEO-3.1', 'video_generate', 'VEO-3.1', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"VEO-3.1","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:VEO-3.1-Pro', 'VEO-3.1-Pro', 'video_generate', 'VEO-3.1-Pro', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"VEO-3.1-Pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Pro","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q2', 'Vidu-Q2', 'video_generate', 'Vidu-Q2', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10],"output_bgm":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"Vidu-Q2","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q2","types":["video_generate"],"alias":"Vidu-Q2","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10],"output_bgm":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q1', 'Vidu-Q1', 'video_generate', 'Vidu-Q1', '{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","9:16","1:1"],"duration_range":[5,5],"output_bgm":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate"],"alias":"Vidu-Q1","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q1","types":["video_generate"],"alias":"Vidu-Q1","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","9:16","1:1"],"duration_range":[5,5],"output_bgm":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q2-Pro-Fast', 'Vidu-Q2-Pro-Fast', 'image_to_video', 'Vidu-Q2-Pro-Fast', '{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_last_frame":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Pro-Fast","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q2-Pro-Fast","types":["image_to_video"],"alias":"Vidu-Q2-Pro-Fast","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_last_frame":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q2-Pro', 'Vidu-Q2-Pro', 'image_to_video', 'Vidu-Q2-Pro', '{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Pro","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q2-Pro","types":["image_to_video"],"alias":"Vidu-Q2-Pro","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q2-Turbo', 'Vidu-Q2-Turbo', 'image_to_video', 'Vidu-Q2-Turbo', '{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Turbo","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q2-Turbo","types":["image_to_video"],"alias":"Vidu-Q2-Turbo","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q3-Pro', 'Vidu-Q3-Pro', 'video_generate', 'Vidu-Q3-Pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"duration_range":[1,16],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["video_generate","image_to_video"],"alias":"Vidu-Q3-Pro","description":"高效生成优质音视频内容,让视频内容更生动、更形象、更立体","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q3-Pro","types":["video_generate","image_to_video"],"alias":"Vidu-Q3-Pro","description":"高效生成优质音视频内容,让视频内容更生动、更形象、更立体","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"duration_range":[1,16],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-Q1-Classic', 'Vidu-Q1-Classic', 'image_to_video', 'Vidu-Q1-Classic', '{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Vidu-Q1-Classic","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-Q1-Classic","types":["image_to_video"],"alias":"Vidu-Q1-Classic","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Vidu-2.0', 'Vidu-2.0', 'image_to_video', 'Vidu-2.0', '{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":{"720p":[4,8],"1080p":[4,4]},"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_to_video"],"alias":"Vidu-2.0","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Vidu-2.0","types":["image_to_video"],"alias":"Vidu-2.0","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":{"720p":[4,8],"1080p":[4,4]},"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Claude-Opus-4.6', 'Claude-Opus-4.6', 'text_generate', 'Claude-Opus-4.6', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"Claude-Opus-4.6","description":"","iconPath":"https://static.51easyai.com/claude-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Claude-Opus-4.6","types":["text_generate","tools_call"],"alias":"Claude-Opus-4.6","icon_path":"https://static.51easyai.com/claude-color%20%281%29.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:Claude-Opus-4.5', 'Claude-Opus-4.5', 'text_generate', 'Claude-Opus-4.5', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate","tools_call"],"alias":"Claude-Opus-4.5","description":"","iconPath":"https://static.51easyai.com/claude-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Claude-Opus-4.5","types":["text_generate","tools_call"],"alias":"Claude-Opus-4.5","icon_path":"https://static.51easyai.com/claude-color%20%281%29.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:gpt-image-1', 'gpt-image-1', 'image_generate', 'gpt-image-1', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-1","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-1","types":["image_generate","image_edit"],"alias":"gpt-image-1","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:gpt-image-1.5', 'gpt-image-1.5', 'image_generate', 'gpt-image-1.5', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-1.5","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-1.5","types":["image_generate","image_edit"],"alias":"gpt-image-1.5","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:gpt-image-2', 'gpt-image-2', 'image_generate', 'gpt-image-2', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-2","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-2","types":["image_generate","image_edit"],"alias":"gpt-image-2","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'easyai' OR provider_code = 'easyai' LIMIT 1), 'easyai', 'easyai:gpt-4o', 'gpt-4o', 'text_generate', 'gpt-4o', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"easyai","sourceProviderName":"EasyAI","sourceSpecType":"easyai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-4o","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v1', 'kling-v1', 'video_generate', '可灵V1', '{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","1:1","9:16"]},"image_to_video":{"output_resolutions":["720p"],"duration_range":{"input_first_last_frame":[5,5],"input_first_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V1","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v1","types":["video_generate","image_to_video"],"alias":"可灵V1","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","1:1","9:16"]},"image_to_video":{"output_resolutions":["720p"],"duration_range":{"input_first_last_frame":[5,5],"input_first_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v1-5', 'kling-v1-5', 'image_to_video', '可灵V1.5', '{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["image_to_video"],"alias":"可灵V1.5","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v1-5","types":["image_to_video"],"alias":"可灵V1.5","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v1-6', 'kling-v1-6', 'video_generate', '可灵V1.6', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"],"input_reference_generate_multiple":["720p","1080p"],"output_video_continuation":["720p","1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"input_reference_generate_multiple":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"support_video_effect_template":true,"support_video_effect_template_list":[]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V1.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v1-6","types":["video_generate","image_to_video"],"alias":"可灵V1.6","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"],"input_last_frame":["1080p"],"input_reference_generate_multiple":["720p","1080p"],"output_video_continuation":["720p","1080p"]},"duration_range":{"input_first_last_frame":[5,10],"input_first_frame":[5,10],"input_last_frame":[5,10]},"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":true,"input_reference_generate_multiple":true,"output_video_continuation":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"support_video_effect_template":true,"support_video_effect_template_list":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v2-master', 'kling-v2-master', 'video_generate', '可灵V2-大师级', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v2-master","types":["video_generate","image_to_video"],"alias":"可灵V2-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v2-1', 'kling-v2-1', 'image_to_video', '可灵V2.1图生视频', '{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10],"input_first_last_frame":[5,10]},"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["image_to_video"],"alias":"可灵V2.1图生视频","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v2-1","types":["image_to_video"],"alias":"可灵V2.1图生视频","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10],"input_first_last_frame":[5,10]},"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v2-1-master', 'kling-v2-1-master', 'video_generate', '可灵V2.1-大师级', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.1-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v2-1-master","types":["video_generate","image_to_video"],"alias":"可灵V2.1-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v2-5-turbo', 'kling-v2-5-turbo', 'video_generate', '可灵V2.5-turbo', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.5-turbo","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v2-5-turbo","types":["video_generate","image_to_video"],"alias":"可灵V2.5-turbo","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"image_to_video":{"output_resolutions":{"input_first_frame":["720p","1080p"],"input_first_last_frame":["1080p"]},"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v2-6', 'kling-v2-6', 'video_generate', '可灵V2.6', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v2-6","types":["video_generate","image_to_video"],"alias":"可灵V2.6","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v3', 'kling-v3', 'video_generate', '可灵V3', '{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["video_generate","image_to_video"],"alias":"可灵V3","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v3","types":["video_generate","image_to_video"],"alias":"可灵V3","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"output_resolutions":["720p","1080p","2160p"],"duration_range":[3,15],"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","1:1","9:16"],"output_audio":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-v3-omni', 'kling-v3-omni', 'omni_video', '可灵V3多模态', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"max_images_for_last_frame":2,"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["omni_video"],"alias":"可灵V3多模态","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v3-omni","types":["omni_video"],"alias":"可灵V3多模态","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"max_images_for_last_frame":2,"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:kling-video-o1', 'kling-video-o1', 'omni_video', '可灵O1', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"},"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["omni_video"],"alias":"可灵O1","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-video-o1","types":["omni_video"],"alias":"可灵O1","icon_path":"https://static.51easyai.com/kling-color.webp","capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"max_videos":1,"max_images":7,"max_elements":7,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"},"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:keling_motion_control', 'keling_motion_control', 'digital_human_generate', '可灵动作控制', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["digital_human_generate"],"alias":"可灵动作控制","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"keling_motion_control","types":["digital_human_generate"],"alias":"可灵动作控制","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:keling_lip_sync', 'keling_lip_sync', 'digital_human_generate', '可灵对口型', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["digital_human_generate"],"alias":"可灵对口型","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"keling_lip_sync","types":["digital_human_generate"],"alias":"可灵对口型","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'keling' OR provider_code = 'keling' LIMIT 1), 'keling', 'keling:keling_avatar_image2video', 'keling_avatar_image2video', 'digital_human_generate', '可灵数字人', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"keling","sourceProviderName":"可灵AI","sourceSpecType":"keling","originalTypes":["digital_human_generate"],"alias":"可灵数字人","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"keling_avatar_image2video","types":["digital_human_generate"],"alias":"可灵数字人","icon_path":"https://static.51easyai.com/kling-color.webp","model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-2.0-flash-exp-image-generation', 'gemini-2.0-flash-exp-image-generation', 'image_generate', 'gemini-2.0', '{"image_generate":{"output_multiple_images":false,"aspect_ratio_allowed":[],"output_resolutions":[]},"image_edit":{"output_multiple_images":false,"aspect_ratio_allowed":[],"input_multiple_images":true,"output_resolutions":[]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["image_generate","image_edit"],"alias":"gemini-2.0","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.0-flash-exp-image-generation","types":["image_generate","image_edit"],"alias":"gemini-2.0","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":false,"aspect_ratio_allowed":[],"output_resolutions":[]},"image_edit":{"output_multiple_images":false,"aspect_ratio_allowed":[],"input_multiple_images":true,"output_resolutions":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-2.5-flash-image', 'gemini-2.5-flash-image', 'image_generate', 'Nano Banana', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"output_max_images_count":4,"input_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.5-flash-image","types":["image_generate","image_edit"],"alias":"Nano Banana","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"output_max_images_count":4,"input_max_images_count":4,"output_resolutions":[],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","9:16","21:9","3:2","2:3"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3-pro-image-preview', 'gemini-3-pro-image-preview', 'image_generate', 'Nano Banana Pro 预览版', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3-pro-image-preview","types":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3.1-flash-image-preview', 'gemini-3.1-flash-image-preview', 'image_generate', 'Nano Banana 2', '{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana 2","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3.1-flash-image-preview","types":["image_generate","image_edit"],"alias":"Nano Banana 2","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_multiple_images":true,"output_max_images_count":4,"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]},"image_edit":{"output_multiple_images":true,"input_multiple_images":true,"output_resolutions":["1K","2K","4K"],"input_max_images_count":14,"aspect_ratio_allowed":["1:1","16:9","4:3","3:4","5:4","4:5","9:16","21:9","3:2","2:3","1:4","4:1","1:8","8:1"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3-pro-preview', 'gemini-3-pro-preview', 'text_generate', 'Gemini-3 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3-pro-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3-flash-preview', 'gemini-3-flash-preview', 'text_generate', 'Gemini-3 Flash 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3-flash-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3.1-flash-lite-preview', 'gemini-3.1-flash-lite-preview', 'text_generate', 'Gemini-3.1 Flash Lite 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3.1-flash-lite-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-3.1-pro-preview', 'gemini-3.1-pro-preview', 'text_generate', 'Gemini-3.1 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3.1-pro-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-2.5-flash', 'gemini-2.5-flash', 'text_generate', 'Gemini 2.5 Flash', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Flash","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.5-flash","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Flash"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini' OR provider_code = 'gemini' LIMIT 1), 'gemini', 'gemini:gemini-2.5-pro', 'gemini-2.5-pro', 'text_generate', 'Gemini 2.5 Pro', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"google-gemini","sourceProviderName":"Google Gemini","sourceSpecType":"google-gemini","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.5-pro","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Pro"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openai' OR provider_code = 'openai' LIMIT 1), 'openai', 'openai:gpt-image-1', 'gpt-image-1', 'image_generate', 'gpt-image-1', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openai","sourceProviderName":"OpenAI","sourceSpecType":"openai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-1","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-1","types":["image_generate","image_edit"],"alias":"gpt-image-1","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openai' OR provider_code = 'openai' LIMIT 1), 'openai', 'openai:gpt-image-1.5', 'gpt-image-1.5', 'image_generate', 'gpt-image-1.5', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openai","sourceProviderName":"OpenAI","sourceSpecType":"openai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-1.5","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-1.5","types":["image_generate","image_edit"],"alias":"gpt-image-1.5","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"]},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openai' OR provider_code = 'openai' LIMIT 1), 'openai', 'openai:gpt-image-2', 'gpt-image-2', 'image_generate', 'gpt-image-2', '{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openai","sourceProviderName":"OpenAI","sourceSpecType":"openai","originalTypes":["image_generate","image_edit"],"alias":"gpt-image-2","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-image-2","types":["image_generate","image_edit"],"alias":"gpt-image-2","capabilities":{"image_generate":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16},"image_edit":{"aspect_ratio_allowed":["21:9","16:9","4:3","3:2","1:1","2:3","3:4","9:16","9:21"],"aspect_ratio_range":[0.3333333333333333,3],"output_size_range":[655360,8294400],"width_height_range":[1,3840],"width_height_multiple":16,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openai' OR provider_code = 'openai' LIMIT 1), 'openai', 'openai:gpt-4o', 'gpt-4o', 'text_generate', 'gpt-4o', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openai","sourceProviderName":"OpenAI","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gpt-4o","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:text-embedding-v4', 'text-embedding-v4', 'text_embedding', 'Qwen3-Embedding-v4', '{"text_embedding":{"dimensions":[2048,1536,1024,768,512,256,128,64]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_second":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_embedding"],"alias":"Qwen3-Embedding-v4","description":"Qwen3-Embedding 系列,默认维度 1024,最长输入 8192 tokens;支持 100+ 语种与多种编程语言。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"text-embedding-v4","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3-Embedding-v4","model_limits":{"max_request_per_second":10},"capabilities":{"text_embedding":{"dimensions":[2048,1536,1024,768,512,256,128,64]}},"description":"Qwen3-Embedding 系列,默认维度 1024,最长输入 8192 tokens;支持 100+ 语种与多种编程语言。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:text-embedding-v3', 'text-embedding-v3', 'text_embedding', 'Qwen3-Embedding-v3', '{"text_embedding":{"dimensions":[1024,768,512,256,128,64]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_embedding"],"alias":"Qwen3-Embedding-v3","description":"Qwen3-Embedding 系列,默认维度 1024;支持中文、英文及 50+ 主流语种。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"text-embedding-v3","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3-Embedding-v3","capabilities":{"text_embedding":{"dimensions":[1024,768,512,256,128,64]}},"description":"Qwen3-Embedding 系列,默认维度 1024;支持中文、英文及 50+ 主流语种。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:text-embedding-v2', 'text-embedding-v2', 'text_embedding', 'Text-Embedding-v2', '{"text_embedding":{"dimensions":[1536]},"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_second":25}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_embedding"],"alias":"Text-Embedding-v2","description":"固定维度 1536,最长输入 2048 tokens;支持中英西法葡印尼日韩德俄等语种。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"text-embedding-v2","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Text-Embedding-v2","model_limits":{"max_request_per_second":25},"capabilities":{"text_embedding":{"dimensions":[1536]}},"description":"固定维度 1536,最长输入 2048 tokens;支持中英西法葡印尼日韩德俄等语种。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:text-embedding-v1', 'text-embedding-v1', 'text_embedding', 'Text-Embedding-v1', '{"originalTypes":["text_embedding"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_embedding"],"alias":"Text-Embedding-v1","description":"百炼 OpenAI 兼容渠道 embedding 基础模型。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"text-embedding-v1","types":["text_embedding"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Text-Embedding-v1","description":"百炼 OpenAI 兼容渠道 embedding 基础模型。"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-turbo', 'qwen-turbo', 'text_generate', 'qwen-turbo', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-turbo","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3-235b-a22b', 'qwen3-235b-a22b', 'text_generate', 'qwen3-235b-a22b', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3-235b-a22b","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-max', 'qwen-max', 'text_generate', 'qwen-max', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-max","types":["text_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-plus', 'qwen-plus', 'text_generate', 'qwen-plus', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-plus","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-max:2', 'qwen-max', 'text_generate', 'qwen-max', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-max","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3-max', 'qwen3-max', 'text_generate', 'qwen3-max', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3-max","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-vl-plus', 'qwen-vl-plus', 'image_analysis', 'qwen-vl-plus', '{"originalTypes":["image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["image_analysis"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-vl-plus","types":["image_analysis"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen-vl-max', 'qwen-vl-max', 'image_analysis', 'qwen-vl-max', '{"originalTypes":["image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["image_analysis"],"alias":"","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen-vl-max","types":["image_analysis"],"icon_path":"https://static.51easyai.com/qwen-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-plus', 'qwen3.5-plus', 'text_generate', 'Qwen3.5-Plus', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-Plus","description":"千问性能最强的视觉理解模型,推荐优先使用","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-plus","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Plus","description":"千问性能最强的视觉理解模型,推荐优先使用"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-flash', 'qwen3.5-flash', 'text_generate', 'Qwen3.5-Flash', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-Flash","description":"速度更快,成本更低,兼顾性能与成本的高性价比选择","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-flash","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Flash","description":"速度更快,成本更低,兼顾性能与成本的高性价比选择"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-omni-plus', 'qwen3.5-omni-plus', 'text_generate', 'Qwen3.5-Omni-Plus', '{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Qwen3.5-Omni-Plus","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-plus-2026-03-15 能力相同。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-omni-plus","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Omni-Plus","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-plus-2026-03-15 能力相同。","capabilities":{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-omni-flash', 'qwen3.5-omni-flash', 'text_generate', 'Qwen3.5-Omni-Flash', '{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Qwen3.5-Omni-Flash","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-flash-2026-03-15 能力相同。","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-omni-flash","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-Omni-Flash","description":"Qwen3.5 全模态稳定版,非思考模式,当前与 qwen3.5-omni-flash-2026-03-15 能力相同。","capabilities":{"text_generate":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"image_analysis":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"video_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"audio_understanding":{"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"tools_call":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536},"omni":{"supportTool":true,"max_context_tokens":262144,"max_input_tokens":196608,"max_output_tokens":65536}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-397b-a17b', 'qwen3.5-397b-a17b', 'text_generate', 'Qwen3.5-397B-A17B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-397B-A17B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-397b-a17b","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-397B-A17B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-122b-a10b', 'qwen3.5-122b-a10b', 'text_generate', 'Qwen3.5-122B-A10B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-122B-A10B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-122b-a10b","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-122B-A10B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-27b', 'qwen3.5-27b', 'text_generate', 'Qwen3.5-27B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-27B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-27b","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-27B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.5-35b-a3b', 'qwen3.5-35b-a3b', 'text_generate', 'Qwen3.5-35B-A3B', '{"originalTypes":["text_generate","image_analysis","video_understanding","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","tools_call"],"alias":"Qwen3.5-35B-A3B","description":"Qwen3.5 开源系列模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.5-35b-a3b","types":["text_generate","image_analysis","video_understanding","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.5-35B-A3B","description":"Qwen3.5 开源系列模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:qwen3.6-plus', 'qwen3.6-plus', 'text_generate', 'Qwen3.6-Plus', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"Qwen3.6-Plus","description":"Qwen3.6 文本模型","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"qwen3.6-plus","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Qwen3.6-Plus","description":"Qwen3.6 文本模型"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:deepseek-v4-pro', 'deepseek-v4-pro', 'text_generate', 'DeepSeek-V4-Pro', '{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"DeepSeek-V4-Pro","description":"DeepSeek V4 Pro 文本模型,支持工具调用,最大上下文 100 万 tokens。","iconPath":"https://static.51easyai.com/deepseek-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"deepseek-v4-pro","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/deepseek-color%20%281%29.webp","alias":"DeepSeek-V4-Pro","description":"DeepSeek V4 Pro 文本模型,支持工具调用,最大上下文 100 万 tokens。","capabilities":{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian-openai' OR provider_code = 'aliyun-bailian-openai' LIMIT 1), 'aliyun-bailian-openai', 'aliyun-bailian-openai:deepseek-v4-flash', 'deepseek-v4-flash', 'text_generate', 'DeepSeek-V4-Flash', '{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian-openai","sourceProviderName":"阿里云百炼(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"DeepSeek-V4-Flash","description":"DeepSeek V4 Flash 文本模型,支持工具调用,最大上下文 100 万 tokens。","iconPath":"https://static.51easyai.com/deepseek-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"deepseek-v4-flash","types":["text_generate","tools_call"],"icon_path":"https://static.51easyai.com/deepseek-color%20%281%29.webp","alias":"DeepSeek-V4-Flash","description":"DeepSeek V4 Flash 文本模型,支持工具调用,最大上下文 100 万 tokens。","capabilities":{"text_generate":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true},"tools_call":{"max_context_tokens":1000000,"supportTool":true,"supportStructuredOutput":false,"supportThinking":true,"supportThinkingModeSwitch":true,"supportWebSearch":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-3-pro-preview', 'gemini-3-pro-preview', 'text_generate', 'Gemini-3 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3-pro-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-3-flash-preview', 'gemini-3-flash-preview', 'text_generate', 'Gemini-3 Flash 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3-flash-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3 Flash 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-3.1-flash-lite-preview', 'gemini-3.1-flash-lite-preview', 'text_generate', 'Gemini-3.1 Flash Lite 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3.1-flash-lite-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Flash Lite 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:输入 $0.25/百万 tokens(文本、图片、视频)、$0.50/百万 tokens(音频),输出 $1.50/百万 tokens。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-3.1-pro-preview', 'gemini-3.1-pro-preview', 'text_generate', 'Gemini-3.1 Pro 预览版', '{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-3.1-pro-preview","types":["text_generate","image_analysis","video_understanding","audio_understanding","tools_call","omni"],"alias":"Gemini-3.1 Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","description":"全模态预览模型,知识截点 2025 年 1 月;上下文窗口 100 万输入 / 6.4 万输出 tokens;官方定价:<20 万 tokens 输入 $2/百万、输出 $12/百万,>20 万 tokens 输入 $4/百万、输出 $18/百万。","capabilities":{"text_generate":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"image_analysis":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"video_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"audio_understanding":{"supportStructuredOutput":true,"max_context_tokens":1000000,"max_output_tokens":64000},"tools_call":{"supportTool":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000},"omni":{"supportTool":true,"supportStructuredOutput":true,"supportThinking":true,"supportThinkingModeSwitch":true,"max_context_tokens":1000000,"max_output_tokens":64000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-2.5-flash', 'gemini-2.5-flash', 'text_generate', 'Gemini 2.5 Flash', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Flash","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.5-flash","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Flash"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'gemini-openai' OR provider_code = 'gemini-openai' LIMIT 1), 'gemini-openai', 'gemini-openai:gemini-2.5-pro', 'gemini-2.5-pro', 'text_generate', 'Gemini 2.5 Pro', '{"originalTypes":["text_generate","image_analysis"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"gemini-openai","sourceProviderName":"Gemini OpenAI兼容","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis"],"alias":"Gemini 2.5 Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"gemini-2.5-pro","types":["text_generate","image_analysis"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"Gemini 2.5 Pro"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:doubao-seed-2-0-pro-260215', 'doubao-seed-2-0-pro-260215', 'text_generate', 'Doubao Seed 2.0 Pro', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Pro","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seed-2-0-pro-260215","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Pro","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:doubao-seed-2-0-lite-260215', 'doubao-seed-2-0-lite-260215', 'text_generate', 'Doubao Seed 2.0 Lite', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Lite","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seed-2-0-lite-260215","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Lite","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:doubao-seed-2-0-mini-260215', 'doubao-seed-2-0-mini-260215', 'text_generate', 'Doubao Seed 2.0 Mini', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Mini","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seed-2-0-mini-260215","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Mini","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:doubao-seed-2-0-code-preview-260215', 'doubao-seed-2-0-code-preview-260215', 'text_generate', 'Doubao Seed 2.0 Code Preview', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Code Preview","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seed-2-0-code-preview-260215","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 2.0 Code Preview","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用,256K 上下文;最大输入 256K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:doubao-seed-1-8-251228', 'doubao-seed-1-8-251228', 'text_generate', 'Doubao Seed 1.8', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 1.8","description":"深度思考/文本生成/多模态理解/工具调用/结构化输出,256K 上下文;最大输入 224K,最大回答 32K,最大思维链 32K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seed-1-8-251228","types":["text_generate","image_analysis","tools_call"],"alias":"Doubao Seed 1.8","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/多模态理解/工具调用/结构化输出,256K 上下文;最大输入 224K,最大回答 32K,最大思维链 32K","model_limits":{"max_request_per_minute":30000,"max_token_per_minute":5000000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces-openai' OR provider_code = 'volces-openai' LIMIT 1), 'volces-openai', 'volces-openai:glm-4-7-251222', 'glm-4-7-251222', 'text_generate', 'GLM-4.7', '{"originalTypes":["text_generate","image_analysis","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":15000,"max_token_per_minute":1500000}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces-openai","sourceProviderName":"火山引擎(OpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","image_analysis","tools_call"],"alias":"GLM-4.7","description":"深度思考/文本生成/工具调用,200K 上下文;最大输入 200K,最大回答 128K,最大思维链 128K","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"glm-4-7-251222","types":["text_generate","image_analysis","tools_call"],"alias":"GLM-4.7","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","description":"深度思考/文本生成/工具调用,200K 上下文;最大输入 200K,最大回答 128K,最大思维链 128K","model_limits":{"max_request_per_minute":15000,"max_token_per_minute":1500000}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'zhipu-openai' OR provider_code = 'zhipu-openai' LIMIT 1), 'zhipu-openai', 'zhipu-openai:glm-4.7', 'glm-4.7', 'text_generate', 'GLM-4.7', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"zhipu-openai","sourceProviderName":"智谱AI","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"GLM-4.7","description":"","iconPath":"https://static.51easyai.com/chatglm-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"glm-4.7","types":["text_generate","tools_call"],"alias":"GLM-4.7","icon_path":"https://static.51easyai.com/chatglm-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'zhipu-openai' OR provider_code = 'zhipu-openai' LIMIT 1), 'zhipu-openai', 'zhipu-openai:glm-4.7-flash', 'glm-4.7-flash', 'text_generate', 'GLM-4.7-Flash', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"zhipu-openai","sourceProviderName":"智谱AI","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"GLM-4.7-Flash","description":"","iconPath":"https://static.51easyai.com/chatglm-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"glm-4.7-flash","types":["text_generate","tools_call"],"alias":"GLM-4.7-Flash","icon_path":"https://static.51easyai.com/chatglm-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'zhipu-openai' OR provider_code = 'zhipu-openai' LIMIT 1), 'zhipu-openai', 'zhipu-openai:glm-4.7-flashx', 'glm-4.7-flashx', 'text_generate', 'GLM-4.7-FlashX', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"zhipu-openai","sourceProviderName":"智谱AI","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"GLM-4.7-FlashX","description":"","iconPath":"https://static.51easyai.com/chatglm-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"glm-4.7-flashx","types":["text_generate","tools_call"],"alias":"GLM-4.7-FlashX","icon_path":"https://static.51easyai.com/chatglm-color.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.7', 'MiniMax-M2.7', 'text_generate', 'MiniMax M2.7', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.7","description":"开启模型的自我迭代(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.7","types":["text_generate","tools_call"],"alias":"MiniMax M2.7","icon_path":"https://static.51easyai.com/minimax-color.png","description":"开启模型的自我迭代(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.7-highspeed', 'MiniMax-M2.7-highspeed', 'text_generate', 'MiniMax M2.7 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.7 极速版","description":"M2.7 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.7-highspeed","types":["text_generate","tools_call"],"alias":"MiniMax M2.7 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.7 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.5', 'MiniMax-M2.5', 'text_generate', 'MiniMax M2.5', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.5","description":"顶尖性能与极致性价比,轻松驾驭复杂任务(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.5","types":["text_generate","tools_call"],"alias":"MiniMax M2.5","icon_path":"https://static.51easyai.com/minimax-color.png","description":"顶尖性能与极致性价比,轻松驾驭复杂任务(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.5-highspeed', 'MiniMax-M2.5-highspeed', 'text_generate', 'MiniMax M2.5 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.5 极速版","description":"M2.5 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.5-highspeed","types":["text_generate","tools_call"],"alias":"MiniMax M2.5 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.5 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.1', 'MiniMax-M2.1', 'text_generate', 'MiniMax M2.1', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.1","description":"强大多语言编程能力,全面升级编程体验(输出速度约 60 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.1","types":["text_generate","tools_call"],"alias":"MiniMax M2.1","icon_path":"https://static.51easyai.com/minimax-color.png","description":"强大多语言编程能力,全面升级编程体验(输出速度约 60 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2.1-highspeed', 'MiniMax-M2.1-highspeed', 'text_generate', 'MiniMax M2.1 极速版', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2.1 极速版","description":"M2.1 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2.1-highspeed","types":["text_generate","tools_call"],"alias":"MiniMax M2.1 极速版","icon_path":"https://static.51easyai.com/minimax-color.png","description":"M2.1 极速版:效果不变,更快、更敏捷(输出速度约 100 TPS)。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax-openai' OR provider_code = 'minimax-openai' LIMIT 1), 'minimax-openai', 'minimax-openai:MiniMax-M2', 'MiniMax-M2', 'text_generate', 'MiniMax M2', '{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true},"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax-openai","sourceProviderName":"MiniMaxOpenAI兼容)","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"MiniMax M2","description":"专为高效编码与 Agent 工作流而生。","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-M2","types":["text_generate","tools_call"],"alias":"MiniMax M2","icon_path":"https://static.51easyai.com/minimax-color.png","description":"专为高效编码与 Agent 工作流而生。","capabilities":{"text_generate":{"max_context_tokens":204800,"supportTool":true},"tools_call":{"max_context_tokens":204800,"supportTool":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openrouter-openai' OR provider_code = 'openrouter-openai' LIMIT 1), 'openrouter-openai', 'openrouter-openai:anthropic/claude-opus-4.6', 'anthropic/claude-opus-4.6', 'text_generate', 'Claude-Opus-4.6', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openrouter-openai","sourceProviderName":"OpenRouter","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"Claude-Opus-4.6","description":"","iconPath":"https://static.51easyai.com/claude-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"anthropic/claude-opus-4.6","types":["text_generate","tools_call"],"alias":"Claude-Opus-4.6","icon_path":"https://static.51easyai.com/claude-color%20%281%29.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'openrouter-openai' OR provider_code = 'openrouter-openai' LIMIT 1), 'openrouter-openai', 'openrouter-openai:anthropic/claude-opus-4.5', 'anthropic/claude-opus-4.5', 'text_generate', 'Claude-Opus-4.5', '{"originalTypes":["text_generate","tools_call"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"openrouter-openai","sourceProviderName":"OpenRouter","sourceSpecType":"openai","originalTypes":["text_generate","tools_call"],"alias":"Claude-Opus-4.5","description":"","iconPath":"https://static.51easyai.com/claude-color%20%281%29.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"anthropic/claude-opus-4.5","types":["text_generate","tools_call"],"alias":"Claude-Opus-4.5","icon_path":"https://static.51easyai.com/claude-color%20%281%29.webp"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian' OR provider_code = 'aliyun-bailian' LIMIT 1), 'aliyun-bailian', 'aliyun-bailian:wan2.5-t2v-preview', 'wan2.5-t2v-preview', 'video_generate', 'Wan2.5-T2V-Preview', '{"video_generate":{"aspect_ratio_allowed":{"480p":["16:9","1:1","9:16"],"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"size_allowed":{"480p":["832x480","480x832","624x624"],"720p":["1280x720","720x1280","960x960","1088x832","832x1088"],"1080p":["1920x1080","1080x1920","1440x1440","1632x1248","1248*1632"]},"output_resolutions":["1080p","720p","480p"],"duration_range":[5,10],"input_audio":true,"output_audio":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5,"max_request_per_second":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian","sourceProviderName":"阿里云百炼","sourceSpecType":"aliyun-bailian","originalTypes":["video_generate"],"alias":"Wan2.5-T2V-Preview","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"wan2.5-t2v-preview","types":["video_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.5-T2V-Preview","model_limits":{"max_concurrent_requests":5,"max_request_per_second":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":{"480p":["16:9","1:1","9:16"],"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"size_allowed":{"480p":["832x480","480x832","624x624"],"720p":["1280x720","720x1280","960x960","1088x832","832x1088"],"1080p":["1920x1080","1080x1920","1440x1440","1632x1248","1248*1632"]},"output_resolutions":["1080p","720p","480p"],"duration_range":[5,10],"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian' OR provider_code = 'aliyun-bailian' LIMIT 1), 'aliyun-bailian', 'aliyun-bailian:wan2.5-i2v-preview', 'wan2.5-i2v-preview', 'image_to_video', 'Wan2.5-I2V-Preview', '{"image_to_video":{"output_resolutions":["1080p","720p","480p"],"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":["adaptive"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true,"output_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian","sourceProviderName":"阿里云百炼","sourceSpecType":"aliyun-bailian","originalTypes":["image_to_video"],"alias":"Wan2.5-I2V-Preview","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"wan2.5-i2v-preview","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.5-I2V-Preview","model_limits":{"max_concurrent_requests":5},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p","480p"],"duration_range":{"input_first_frame":[5,10]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":["adaptive"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian' OR provider_code = 'aliyun-bailian' LIMIT 1), 'aliyun-bailian', 'aliyun-bailian:wan2.6-t2v', 'wan2.6-t2v', 'video_generate', 'Wan2.6-文生视频', '{"video_generate":{"aspect_ratio_allowed":{"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"output_resolutions":["1080p","720p"],"duration_range":[5,15],"duration_options":[5,10,15],"input_audio":true,"output_audio":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian","sourceProviderName":"阿里云百炼","sourceSpecType":"aliyun-bailian","originalTypes":["video_generate"],"alias":"Wan2.6-文生视频","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"wan2.6-t2v","types":["video_generate"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-文生视频","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"video_generate":{"aspect_ratio_allowed":{"720p":["16:9","1:1","9:16","4:3","3:4"],"1080p":["16:9","1:1","9:16","4:3","3:4"]},"output_resolutions":["1080p","720p"],"duration_range":[5,15],"duration_options":[5,10,15],"input_audio":true,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian' OR provider_code = 'aliyun-bailian' LIMIT 1), 'aliyun-bailian', 'aliyun-bailian:wan2.6-i2v', 'wan2.6-i2v', 'image_to_video', 'Wan2.6-图生视频', '{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[5,10]},"duration_options":{"input_first_frame":[5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian","sourceProviderName":"阿里云百炼","sourceSpecType":"aliyun-bailian","originalTypes":["image_to_video"],"alias":"Wan2.6-图生视频","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"wan2.6-i2v","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-图生视频","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[5,10]},"duration_options":{"input_first_frame":[5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'aliyun-bailian' OR provider_code = 'aliyun-bailian' LIMIT 1), 'aliyun-bailian', 'aliyun-bailian:wan2.6-i2v-flash', 'wan2.6-i2v-flash', 'image_to_video', 'Wan2.6-图生视频-Flash', '{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[2,15]},"duration_options":{"input_first_frame":[2,5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"output_audio":true,"input_audio":true,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10,"max_request_per_minute":300}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"aliyun-bailian","sourceProviderName":"阿里云百炼","sourceSpecType":"aliyun-bailian","originalTypes":["image_to_video"],"alias":"Wan2.6-图生视频-Flash","description":"","iconPath":"https://static.51easyai.com/qwen-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"wan2.6-i2v-flash","types":["image_to_video"],"icon_path":"https://static.51easyai.com/qwen-color.webp","alias":"Wan2.6-图生视频-Flash","model_limits":{"max_concurrent_requests":10,"max_request_per_minute":300},"capabilities":{"image_to_video":{"output_resolutions":["1080p","720p"],"duration_range":{"input_first_frame":[2,15]},"duration_options":{"input_first_frame":[2,5,10,15]},"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"aspect_ratio_allowed":[],"input_reference_generate_single":false,"output_audio":true,"input_audio":true,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-2-pro', 'flux-2-pro', 'image_generate', 'flux-2-pro', '{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":8,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate","image_edit"],"alias":"flux-2-pro","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-2-pro","types":["image_generate","image_edit"],"alias":"flux-2-pro","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":8,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-2-flex', 'flux-2-flex', 'image_generate', 'flux-2-flex', '{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":10,"input_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate","image_edit"],"alias":"flux-2-flex","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-2-flex","types":["image_generate","image_edit"],"alias":"flux-2-flex","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304]},"image_edit":{"output_multiple_images":false,"output_resolutions":["2K","1K"],"output_size_range":[4096,4194304],"input_max_images_count":10,"input_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-kontext-pro', 'flux-kontext-pro', 'image_generate', 'flux-kontext-pro', '{"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate","image_edit"],"alias":"flux-kontext-pro","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-kontext-pro","types":["image_generate","image_edit"],"alias":"flux-kontext-pro","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-kontext-max', 'flux-kontext-max', 'image_generate', 'flux-kontext-max', '{"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate","image_edit"],"alias":"flux-kontext-max","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-kontext-max","types":["image_generate","image_edit"],"alias":"flux-kontext-max","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-pro-1.1', 'flux-pro-1.1', 'image_generate', 'flux-pro-1.1', '{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate"],"alias":"flux-pro-1.1","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-pro-1.1","types":["image_generate"],"alias":"flux-pro-1.1","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'blackforest' OR provider_code = 'blackforest' LIMIT 1), 'blackforest', 'blackforest:flux-dev', 'flux-dev', 'image_generate', 'flux-dev', '{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"blackforest","sourceProviderName":"黑森林实验室","sourceSpecType":"blackforest","originalTypes":["image_generate"],"alias":"flux-dev","description":"","iconPath":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"flux-dev","types":["image_generate"],"alias":"flux-dev","icon_path":"https://easyai-1253343986.cos.ap-shanghai.myqcloud.com/663e19cd4fa9d8078385c7c9/upload/20250611132907371-blackforest.png","capabilities":{"image_generate":{"output_multiple_images":false,"width_height_range":[256,1440],"output_resolutions":["2K","1K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'dify' OR provider_code = 'dify' LIMIT 1), 'dify', 'dify:dify-app-[name]', 'dify-app-[name]', 'text_generate', 'dify-app-[name]', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":-1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"dify","sourceProviderName":"Dify","sourceSpecType":"dify","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"internal-compute","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"dify-app-[name]","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seededit-3-0-i2i-250628', 'doubao-seededit-3-0-i2i-250628', 'image_edit', 'doubao-3.0图像编辑', '{"image_edit":{"output_resolutions":["1K","2K"],"aspect_ratio_allowed":["adaptive"],"input_multiple_images":false,"output_multiple_images":false},"originalTypes":["image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_edit"],"alias":"doubao-3.0图像编辑","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seededit-3-0-i2i-250628","types":["image_edit"],"alias":"doubao-3.0图像编辑","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"output_resolutions":["1K","2K"],"aspect_ratio_allowed":["adaptive"],"input_multiple_images":false,"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedream-4-0-250828', 'doubao-seedream-4-0-250828', 'image_edit', 'doubao-4.0图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"output_size_range":[921600,16777216],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[921600,16777216],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_edit","image_generate"],"alias":"doubao-4.0图像编辑","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedream-4-0-250828","types":["image_edit","image_generate"],"alias":"doubao-4.0图像编辑","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"output_size_range":[921600,16777216],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[921600,16777216],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedream-5-0-260128', 'doubao-seedream-5-0-260128', 'image_edit', 'doubao-5.0图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_edit","image_generate"],"alias":"doubao-5.0图像编辑","description":"支持 2K 和 3K 分辨率","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedream-5-0-260128","types":["image_edit","image_generate"],"alias":"doubao-5.0图像编辑","description":"支持 2K 和 3K 分辨率","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedream-5-0-lite-260128', 'doubao-seedream-5-0-lite-260128', 'image_edit', 'doubao-5.0-lite图像编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_edit","image_generate"],"alias":"doubao-5.0-lite图像编辑","description":"轻量版,支持 2K 和 3K 分辨率","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedream-5-0-lite-260128","types":["image_edit","image_generate"],"alias":"doubao-5.0-lite图像编辑","description":"轻量版,支持 2K 和 3K 分辨率","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"output_resolutions":["2K","3K"],"output_max_size":10404496,"output_size_range":[3686400,10404496],"aspect_ratio_range":[0.0625,16],"output_multiple_images":true},"image_generate":{"output_resolutions":["2K","3K"],"output_max_size":10404496,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,10404496],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedream-4-5-251128', 'doubao-seedream-4-5-251128', 'image_generate', 'doubao-4.5图像编辑', '{"image_generate":{"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"image_edit":{"input_size_range":["14x14","6000x6000"],"input_format_allowed":["png","jpg","jpeg","webp","gif","bmp","tiff"],"input_multiple_images":true,"input_aspect_ratio_range":[0.0625,16],"input_max_images_count":14,"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_generate","image_edit"],"alias":"doubao-4.5图像编辑","description":"多图稳定融合,超强编辑一致性,小字清晰,4k超高清","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedream-4-5-251128","types":["image_generate","image_edit"],"alias":"doubao-4.5图像编辑","description":"多图稳定融合,超强编辑一致性,小字清晰,4k超高清","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_generate":{"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true},"image_edit":{"input_size_range":["14x14","6000x6000"],"input_format_allowed":["png","jpg","jpeg","webp","gif","bmp","tiff"],"input_multiple_images":true,"input_aspect_ratio_range":[0.0625,16],"input_max_images_count":14,"output_resolutions":["2K","4K"],"output_max_size":16777216,"aspect_ratio_range":[0.0625,16],"output_size_range":[3686400,16777216],"output_multiple_images":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedream-3-0-t2i-250415', 'doubao-seedream-3-0-t2i-250415', 'image_generate', 'doubao-3.0文生图', '{"image_generate":{"output_resolutions":["2K","1K"],"output_max_size":4194304,"aspect_ratio_range":[0.42857142857142855,2.3333333333333335],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":500}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_generate"],"alias":"doubao-3.0文生图","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedream-3-0-t2i-250415","types":["image_generate"],"alias":"doubao-3.0文生图","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"image_generate":{"output_resolutions":["2K","1K"],"output_max_size":4194304,"aspect_ratio_range":[0.42857142857142855,2.3333333333333335],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-1-0-lite-t2v-250428', 'doubao-seedance-1-0-lite-t2v-250428', 'video_generate', 'doubao-1.0-lite-文生视频', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"],"duration_range":[3,12]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":300,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate"],"alias":"doubao-1.0-lite-文生视频","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-1-0-lite-t2v-250428","types":["video_generate"],"alias":"doubao-1.0-lite-文生视频","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":300,"max_concurrent_requests":5},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"],"duration_range":[3,12]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-1-0-lite-i2v-250428', 'doubao-seedance-1-0-lite-i2v-250428', 'image_to_video', 'doubao-1.0-lite-i2v-图生视频', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":300,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["image_to_video"],"alias":"doubao-1.0-lite-i2v-图生视频","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-1-0-lite-i2v-250428","types":["image_to_video"],"alias":"doubao-1.0-lite-i2v-图生视频","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":300,"max_concurrent_requests":5},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-1-0-pro-250528', 'doubao-seedance-1-0-pro-250528', 'video_generate', '豆包Seedance-pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-pro","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-1-0-pro-250528","types":["video_generate","image_to_video"],"alias":"豆包Seedance-pro","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-1-5-pro-251215', 'doubao-seedance-1-5-pro-251215', 'video_generate', '豆包Seedance-1.5-pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"output_audio":true},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-1.5-pro","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-1-5-pro-251215","types":["video_generate","image_to_video"],"alias":"豆包Seedance-1.5-pro","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"output_audio":true},"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-1-0-pro-fast-251015', 'doubao-seedance-1-0-pro-fast-251015', 'video_generate', '豆包Seedance-pro-fast', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate","image_to_video"],"alias":"豆包Seedance-pro-fast","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-1-0-pro-fast-251015","types":["video_generate","image_to_video"],"model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"alias":"豆包Seedance-pro-fast","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12]},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[3,12],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-2-0-260128', 'doubao-seedance-2-0-260128', 'video_generate', '豆包Seedance-2.0', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["video_generate","image_to_video","omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-2-0-260128","types":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'volces' OR provider_code = 'volces' LIMIT 1), 'volces', 'volces:doubao-seedance-2-0-fast-260128', 'doubao-seedance-2-0-fast-260128', 'video_generate', '豆包Seedance-2.0-fast', '{"video_generate":{"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true},"originalTypes":["video_generate","image_to_video","omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_request_per_minute":600,"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"volces","sourceProviderName":"火山引擎(豆包)","sourceSpecType":"volces","originalTypes":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0-fast","description":"","iconPath":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"doubao-seedance-2-0-fast-260128","types":["video_generate","image_to_video","omni_video"],"alias":"豆包Seedance-2.0-fast","icon_path":"https://ecmb.bdimg.com/tam-ogel/1801637412_-164881388_88_88.jpg","model_limits":{"max_request_per_minute":600,"max_concurrent_requests":10},"capabilities":{"video_generate":{"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3},"image_to_video":{"output_resolutions":["720p","480p"],"input_first_frame":true,"input_first_last_frame":true,"input_last_frame":false,"input_reference_generate_single":true,"input_reference_generate_multiple":true,"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_range":[4,15],"input_audio":true,"support_video_effect_template":false,"output_audio":true,"output_support_return_last_frame":true,"output_video_continuation":true,"max_images":9,"max_videos":3,"max_images_for_last_frame":2},"omni_video":{"supported_modes":["element_reference","text_to_video","image_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","480p"],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9","adaptive"],"duration_options":[4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"max_videos":3,"max_images":9,"max_elements":9,"max_images_and_elements":9,"max_images_for_last_frame":2,"support_instruction_edit":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_high_aes_general_v21_L', 'jimeng_high_aes_general_v21_L', 'image_generate', '即梦V2.1文生图', '{"image_generate":{"output_resolutions":["1K"],"output_max_size":4194304,"width_height_range":[256,768],"aspect_ratio_range":[0.5625,0.5625],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":2}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_generate"],"alias":"即梦V2.1文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_high_aes_general_v21_L","types":["image_generate"],"alias":"即梦V2.1文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K"],"output_max_size":4194304,"width_height_range":[256,768],"aspect_ratio_range":[0.5625,0.5625],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_t2i_v30', 'jimeng_t2i_v30', 'image_generate', '即梦V3.0文生图', '{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":2}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_generate"],"alias":"即梦V3.0文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_t2i_v30","types":["image_generate"],"alias":"即梦V3.0文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_t2i_v31', 'jimeng_t2i_v31', 'image_generate', '即梦V3.1文生图', '{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":2}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_generate"],"alias":"即梦V3.1文生图","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_t2i_v31","types":["image_generate"],"alias":"即梦V3.1文生图","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"],"output_max_size":4194304,"width_height_range":[512,2048],"aspect_ratio_range":[0.3333333333333333,1],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_i2i_v30', 'jimeng_i2i_v30', 'image_edit', '即梦V3.0图像智能参考', '{"image_edit":{"input_multiple_images":false,"output_resolutions":["1K","2K"],"width_height_range":[512,2016],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"originalTypes":["image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":2}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_edit"],"alias":"即梦V3.0图像智能参考","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_i2i_v30","types":["image_edit"],"alias":"即梦V3.0图像智能参考","icon_path":"https://static.51easyai.com/jimeng-logo.png","capabilities":{"image_edit":{"input_multiple_images":false,"output_resolutions":["1K","2K"],"width_height_range":[512,2016],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_t2i_v40', 'jimeng_t2i_v40', 'image_edit', '即梦V4.0图像生成及编辑', '{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"input_parameters":[{"type":"url","parameter":"image_urls"}],"output_resolutions":["1K","2K","4K"],"allow_custom_width_height_size":true,"output_max_size":16777216,"width_height_range":[1024,4096],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"image_generate":{"output_multiple_images":true,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"allow_custom_width_height_size":true,"aspect_ratio_range":[0.0625,16],"width_height_range":[1024,6198]},"originalTypes":["image_edit","image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_edit","image_generate"],"alias":"即梦V4.0图像生成及编辑","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_t2i_v40","types":["image_edit","image_generate"],"alias":"即梦V4.0图像生成及编辑","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_edit":{"input_multiple_images":true,"input_max_images_count":10,"input_parameters":[{"type":"url","parameter":"image_urls"}],"output_resolutions":["1K","2K","4K"],"allow_custom_width_height_size":true,"output_max_size":16777216,"width_height_range":[1024,4096],"aspect_ratio_range":[0.0625,16],"output_multiple_images":false},"image_generate":{"output_multiple_images":true,"output_resolutions":["1K","2K","4K"],"output_max_size":16777216,"allow_custom_width_height_size":true,"aspect_ratio_range":[0.0625,16],"width_height_range":[1024,6198]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_t2v_v30', 'jimeng_t2v_v30', 'video_generate', '即梦文生视频V3.0', '{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["video_generate"],"alias":"即梦文生视频V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_t2v_v30","types":["video_generate"],"alias":"即梦文生视频V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["720p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_ti2v_v30_pro', 'jimeng_ti2v_v30_pro', 'video_generate', '即梦视频生成V3.0_Pro', '{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["video_generate","image_to_video"],"alias":"即梦视频生成V3.0_Pro","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_ti2v_v30_pro","types":["video_generate","image_to_video"],"alias":"即梦视频生成V3.0_Pro","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_i2v_first_v30', 'jimeng_i2v_first_v30', 'image_to_video', '即梦图生视频V3.0', '{"image_to_video":{"output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_i2v_first_v30","types":["image_to_video"],"alias":"即梦图生视频V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_i2v_first_tail_v30', 'jimeng_i2v_first_tail_v30', 'image_to_video', '即梦首尾帧视频生成V3.0', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_to_video"],"alias":"即梦首尾帧视频生成V3.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_i2v_first_tail_v30","types":["image_to_video"],"alias":"即梦首尾帧视频生成V3.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["720p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_t2v_v30_1080p', 'jimeng_t2v_v30_1080p', 'video_generate', '即梦文生视频V3.0_1080p', '{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["video_generate"],"alias":"即梦文生视频V3.0_1080p","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_t2v_v30_1080p","types":["video_generate"],"alias":"即梦文生视频V3.0_1080p","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"video_generate":{"output_resolutions":["1080p"],"duration_range":[5,10],"aspect_ratio_allowed":["16:9","4:3","1:1","3:4","9:16","21:9"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_i2v_first_v30_1080', 'jimeng_i2v_first_v30_1080', 'image_to_video', '即梦图生视频V3.0_1080p', '{"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0_1080p","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_i2v_first_v30_1080","types":["image_to_video"],"alias":"即梦图生视频V3.0_1080p","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_first_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_i2v_first_tail_v30_1080', 'jimeng_i2v_first_tail_v30_1080', 'image_to_video', '即梦图生视频V3.0_1080p_首尾帧', '{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["image_to_video"],"alias":"即梦图生视频V3.0_1080p_首尾帧","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_i2v_first_tail_v30_1080","types":["image_to_video"],"alias":"即梦图生视频V3.0_1080p_首尾帧","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1},"capabilities":{"image_to_video":{"input_smart_multi_frame":true,"smart_multi_frame_range":[2,5],"smart_multi_frame_mode":"stitch","output_resolutions":["1080p"],"duration_range":[5,10],"input_width_height_range":[320,4096],"input_size_limit":4928307.2,"input_first_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_digital_human_V1', 'jimeng_digital_human_V1', 'digital_human_generate', '即梦数字人V1', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦数字人V1","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_digital_human_V1","types":["digital_human_generate"],"alias":"即梦数字人V1","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:realman_avatar_picture_v2', 'realman_avatar_picture_v2', 'digital_human_generate', '即梦单图音频驱动-普通模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-普通模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"realman_avatar_picture_v2","types":["digital_human_generate"],"alias":"即梦单图音频驱动-普通模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:realman_avatar_picture_loopy', 'realman_avatar_picture_loopy', 'digital_human_generate', '即梦单图音频驱动-灵动模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-灵动模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"realman_avatar_picture_loopy","types":["digital_human_generate"],"alias":"即梦单图音频驱动-灵动模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:realman_change_lips', 'realman_change_lips', 'digital_human_generate', '即梦视频改口型Lite模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦视频改口型Lite模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"realman_change_lips","types":["digital_human_generate"],"alias":"即梦视频改口型Lite模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:realman_avatar_picture_loopyb', 'realman_avatar_picture_loopyb', 'digital_human_generate', '即梦单图音频驱动-大画幅灵动模式', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦单图音频驱动-大画幅灵动模式","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"realman_avatar_picture_loopyb","types":["digital_human_generate"],"alias":"即梦单图音频驱动-大画幅灵动模式","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_dream_actor_m1_gen_video_cv', 'jimeng_dream_actor_m1_gen_video_cv', 'digital_human_generate', '即梦动作模仿', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦动作模仿","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_dream_actor_m1_gen_video_cv","types":["digital_human_generate"],"alias":"即梦动作模仿","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_dreamactor_m20_gen_video', 'jimeng_dreamactor_m20_gen_video', 'digital_human_generate', '即梦动作模仿2.0', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦动作模仿2.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_dreamactor_m20_gen_video","types":["digital_human_generate"],"alias":"即梦动作模仿2.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_realman_avatar_picture_omni_v2', 'jimeng_realman_avatar_picture_omni_v2', 'digital_human_generate', '即梦数字人快速模式1.0', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦数字人快速模式1.0","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_realman_avatar_picture_omni_v2","types":["digital_human_generate"],"alias":"即梦数字人快速模式1.0","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'jimeng' OR provider_code = 'jimeng' LIMIT 1), 'jimeng', 'jimeng:jimeng_realman_avatar_picture_omni_v15', 'jimeng_realman_avatar_picture_omni_v15', 'digital_human_generate', '即梦数字人快速模式1.5', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"jimeng","sourceProviderName":"即梦AI","sourceSpecType":"jimeng","originalTypes":["digital_human_generate"],"alias":"即梦数字人快速模式1.5","description":"","iconPath":"https://static.51easyai.com/jimeng-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"jimeng_realman_avatar_picture_omni_v15","types":["digital_human_generate"],"alias":"即梦数字人快速模式1.5","icon_path":"https://static.51easyai.com/jimeng-logo.png","model_limits":{"max_concurrent_requests":1}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'silicon-flow-openai' OR provider_code = 'silicon-flow-openai' LIMIT 1), 'silicon-flow-openai', 'silicon-flow-openai:deepseek-ai/DeepSeek-V3', 'deepseek-ai/DeepSeek-V3', 'text_generate', 'deepseek-ai/DeepSeek-V3', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":-1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"silicon-flow-openai","sourceProviderName":"硅基流动","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"deepseek-ai/DeepSeek-V3","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'silicon-flow-openai' OR provider_code = 'silicon-flow-openai' LIMIT 1), 'silicon-flow-openai', 'silicon-flow-openai:deepseek-ai/DeepSeek-R1', 'deepseek-ai/DeepSeek-R1', 'text_generate', 'deepseek-ai/DeepSeek-R1', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":-1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"silicon-flow-openai","sourceProviderName":"硅基流动","sourceSpecType":"openai","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"deepseek-ai/DeepSeek-R1","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:Turbo-v1.0-20250506', 'Turbo-v1.0-20250506', 'text_to_model', '3D文生3D图Turbo-v1.0', '{"text_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000},"originalTypes":["text_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["text_to_model"],"alias":"3D文生3D图Turbo-v1.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Turbo-v1.0-20250506","types":["text_to_model"],"alias":"3D文生3D图Turbo-v1.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.5-20250123', 'v2.5-20250123', 'text_to_model', '3D文生3D图v2.5', '{"text_to_model":{"support_texture":true,"support_part_generation":true,"max_face_limit":1000000,"max_face_limit_quad":500000},"originalTypes":["text_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["text_to_model"],"alias":"3D文生3D图v2.5","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5-20250123","types":["text_to_model"],"alias":"3D文生3D图v2.5","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":true,"max_face_limit":1000000,"max_face_limit_quad":500000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:Turbo-v1.0-20250506:2', 'Turbo-v1.0-20250506', 'image_to_model', '3D图生3D图Turbo-v1.0', '{"image_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000,"support_image_autofix":true},"originalTypes":["image_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["image_to_model"],"alias":"3D图生3D图Turbo-v1.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Turbo-v1.0-20250506","types":["image_to_model"],"alias":"3D图生3D图Turbo-v1.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"image_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000,"support_image_autofix":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.5-20250123:2', 'v2.5-20250123', 'image_to_model', '3D图生3D图v2.5', '{"image_to_model":{"support_texture":true,"support_part_generation":true,"max_face_limit":1000000,"max_face_limit_quad":500000,"support_image_autofix":true},"originalTypes":["image_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["image_to_model"],"alias":"3D图生3D图v2.5","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5-20250123","types":["image_to_model"],"alias":"3D图生3D图v2.5","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"image_to_model":{"support_texture":true,"support_part_generation":true,"max_face_limit":1000000,"max_face_limit_quad":500000,"support_image_autofix":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.5-20250123:3', 'v2.5-20250123', 'multiview_to_model', '3D多视图生3D图v2.5', '{"multiview_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000},"originalTypes":["multiview_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["multiview_to_model"],"alias":"3D多视图生3D图v2.5","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5-20250123","types":["multiview_to_model"],"alias":"3D多视图生3D图v2.5","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"multiview_to_model":{"support_texture":true,"support_part_generation":false,"max_face_limit":1000000,"max_face_limit_quad":500000}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.5-20250123:4', 'v2.5-20250123', 'mesh_edit', '3D纹理管线v2.5', '{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D纹理管线v2.5","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5-20250123","types":["mesh_edit"],"alias":"3D纹理管线v2.5","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v1.0-20250506', 'v1.0-20250506', 'mesh_edit', '3D拆分/补全模型v1.0', '{"mesh_edit":{"supported_operations":{"segmentation":{"supported":true},"completion":{"supported":true}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D拆分/补全模型v1.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v1.0-20250506","types":["mesh_edit"],"alias":"3D拆分/补全模型v1.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"segmentation":{"supported":true},"completion":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:P-v2.0-20251225', 'P-v2.0-20251225', 'mesh_edit', '3D重拓扑模型v2.0', '{"mesh_edit":{"supported_operations":{"smart_lowpoly":{"supported":true,"smart_low_poly_face_limit":{"triangle":{"min":1000,"max":20000},"quad":{"min":500,"max":10000}}}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D重拓扑模型v2.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"P-v2.0-20251225","types":["mesh_edit"],"alias":"3D重拓扑模型v2.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"smart_lowpoly":{"supported":true,"smart_low_poly_face_limit":{"triangle":{"min":1000,"max":20000},"quad":{"min":500,"max":10000}}}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.5-20260210', 'v2.5-20260210', 'mesh_edit', '3D绑定算法v2.5', '{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D绑定算法v2.5","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5-20260210","types":["mesh_edit"],"alias":"3D绑定算法v2.5","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v2.0-20250506', 'v2.0-20250506', 'mesh_edit', '3D绑定算法v2.0', '{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D绑定算法v2.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.0-20250506","types":["mesh_edit"],"alias":"3D绑定算法v2.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v1.0-20240301', 'v1.0-20240301', 'mesh_edit', '3D绑定算法v1.0', '{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["mesh_edit"],"alias":"3D绑定算法v1.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v1.0-20240301","types":["mesh_edit"],"alias":"3D绑定算法v1.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"mesh_edit":{"supported_operations":{"check_riggable":{"supported":true},"rig":{"supported":true},"retarget_animation":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v3.0-20250812', 'v3.0-20250812', 'multiview_to_model', 'Trip3D-v3.0', '{"text_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"image_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}},"support_image_autofix":true},"multiview_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":false,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]},"convert":{"supported":true,"supported_formats":["glb","gltf","usdz","fbx","obj","stl","3mf"]},"refine":{"supported":true},"stylize":{"supported":true}}},"originalTypes":["multiview_to_model","text_to_model","image_to_model","mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["multiview_to_model","text_to_model","image_to_model","mesh_edit"],"alias":"Trip3D-v3.0","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v3.0-20250812","types":["multiview_to_model","text_to_model","image_to_model","mesh_edit"],"alias":"Trip3D-v3.0","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"text_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"image_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}},"support_image_autofix":true},"multiview_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":false,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]},"convert":{"supported":true,"supported_formats":["glb","gltf","usdz","fbx","obj","stl","3mf"]},"refine":{"supported":true},"stylize":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:v3.1-20260211', 'v3.1-20260211', 'multiview_to_model', 'Trip3D-v3.1', '{"text_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"image_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}},"support_image_autofix":true},"multiview_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":false,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]},"convert":{"supported":true,"supported_formats":["glb","gltf","usdz","fbx","obj","stl","3mf"]},"refine":{"supported":true},"stylize":{"supported":true}}},"originalTypes":["multiview_to_model","text_to_model","image_to_model","mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["multiview_to_model","text_to_model","image_to_model","mesh_edit"],"alias":"Trip3D-v3.1","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v3.1-20260211","types":["multiview_to_model","text_to_model","image_to_model","mesh_edit"],"alias":"Trip3D-v3.1","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"text_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"image_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":true,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}},"support_image_autofix":true},"multiview_to_model":{"geometry_quality_options":["standard","detailed"],"support_texture":true,"support_part_generation":false,"face_limit_by_geometry_quality":{"standard":{"triangle":1000000,"quad":500000},"detailed":{"triangle":2000000,"quad":1000000}}},"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["glb"]},"convert":{"supported":true,"supported_formats":["glb","gltf","usdz","fbx","obj","stl","3mf"]},"refine":{"supported":true},"stylize":{"supported":true}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tripo3d' OR provider_code = 'tripo3d' LIMIT 1), 'tripo3d', 'tripo3d:P1-20260311', 'P1-20260311', 'text_to_model', 'Trip3D-P1', '{"text_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000}},"image_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000},"support_image_autofix":true},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000}},"originalTypes":["text_to_model","image_to_model","multiview_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tripo3d","sourceProviderName":"Tripo3D","sourceSpecType":"tripo3d","originalTypes":["text_to_model","image_to_model","multiview_to_model"],"alias":"Trip3D-P1","description":"","iconPath":"https://static.51easyai.com/tripo-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"P1-20260311","types":["text_to_model","image_to_model","multiview_to_model"],"alias":"Trip3D-P1","icon_path":"https://static.51easyai.com/tripo-logo.png","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000}},"image_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000},"support_image_autofix":true},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_smart_low_poly":false,"face_limit_range":{"min":48,"max":20000}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-image' OR provider_code = 'tencent-hunyuan-image' LIMIT 1), 'tencent-hunyuan-image', 'tencent-hunyuan-image:Image-GI', 'Image-GI', 'image_generate', 'Nano Banana Pro 预览版', '{"image_generate":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4},"image_edit":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4,"input_multiple_images":true,"input_max_images_count":11},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-image","sourceProviderName":"腾讯混元生图(第三方)","sourceSpecType":"tencent-hunyuan-image","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Image-GI","types":["image_generate","image_edit"],"alias":"Nano Banana Pro 预览版","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4},"image_edit":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4,"input_multiple_images":true,"input_max_images_count":11}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-image' OR provider_code = 'tencent-hunyuan-image' LIMIT 1), 'tencent-hunyuan-image', 'tencent-hunyuan-image:Image-GI2', 'Image-GI2', 'image_generate', 'Nano Banana 2', '{"image_generate":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4},"image_edit":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4,"input_multiple_images":true,"input_max_images_count":11},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-image","sourceProviderName":"腾讯混元生图(第三方)","sourceSpecType":"tencent-hunyuan-image","originalTypes":["image_generate","image_edit"],"alias":"Nano Banana 2","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"Image-GI2","types":["image_generate","image_edit"],"alias":"Nano Banana 2","icon_path":"https://static.51easyai.com/gemini-color.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4},"image_edit":{"output_resolutions":["1K","2K","4K"],"aspect_ratio_allowed":["1:1","2:3","3:2","3:4","4:3","4:5","5:4","9:16","16:9","21:9"],"output_multiple_images":true,"output_max_images_count":4,"input_multiple_images":true,"input_max_images_count":11}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v1.6', 'v1.6', 'video_generate', '可灵V1.6', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":true,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V1.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v1.6","alias":"可灵V1.6","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":true,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_range":[5,10]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v2.0', 'v2.0', 'video_generate', '可灵V2-大师级', '{"video_generate":{"output_resolutions":["720p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p"],"duration_range":[5,10]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.0","alias":"可灵V2-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p"],"duration_range":[5,10]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v2.1m', 'v2.1m', 'video_generate', '可灵V2.1-大师级', '{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["1080p"],"duration_range":[5,10]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.1-大师级","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.1m","alias":"可灵V2.1-大师级","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["1080p"],"duration_range":[5,10]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v2.5', 'v2.5', 'video_generate', '可灵V2.5-turbo', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_range":[5,10]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.5-turbo","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.5","alias":"可灵V2.5-turbo","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_range":[5,10]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v2.6', 'v2.6', 'video_generate', '可灵V2.6', '{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V2.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v2.6","alias":"可灵V2.6","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[5,10],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["1080p"],"duration_range":[5,10],"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:v3.0', 'v3.0', 'video_generate', '可灵V3', '{"video_generate":{"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"可灵V3","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v3.0","alias":"可灵V3","icon_path":"https://static.51easyai.com/kling-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_range":[3,15],"output_audio":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:kling_motion_control_v2_6', 'kling_motion_control_v2_6', 'digital_human_generate', '可灵动作控制V2.6', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["digital_human_generate"],"alias":"可灵动作控制V2.6","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling_motion_control_v2_6","alias":"可灵动作控制V2.6","icon_path":"https://static.51easyai.com/kling-color.webp","types":["digital_human_generate"],"model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:kling_motion_control_v3_0', 'kling_motion_control_v3_0', 'digital_human_generate', '可灵动作控制V3.0', '{"originalTypes":["digital_human_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":10}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["digital_human_generate"],"alias":"可灵动作控制V3.0","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling_motion_control_v3_0","alias":"可灵动作控制V3.0","icon_path":"https://static.51easyai.com/kling-color.webp","types":["digital_human_generate"],"model_limits":{"max_concurrent_requests":10}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:kling-video-o1', 'kling-video-o1', 'omni_video', '可灵O1', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"input_audio":false,"max_videos":1,"max_audios":0,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["omni_video"],"alias":"可灵O1","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-video-o1","alias":"可灵O1","icon_path":"https://static.51easyai.com/kling-color.webp","types":["omni_video"],"capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit"],"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10],"output_audio":true,"input_audio":false,"max_videos":1,"max_audios":0,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4},"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:kling-v3-omni', 'kling-v3-omni', 'omni_video', '可灵V3多模态', '{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"input_audio":false,"max_videos":1,"max_audios":0,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}},"originalTypes":["omni_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["omni_video"],"alias":"可灵V3多模态","description":"","iconPath":"https://static.51easyai.com/kling-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"kling-v3-omni","alias":"可灵V3多模态","icon_path":"https://static.51easyai.com/kling-color.webp","types":["omni_video"],"capabilities":{"omni_video":{"supported_modes":["text_to_video","image_reference","element_reference","first_last_frame","video_reference","video_edit","multi_shot"],"output_resolutions":["720p","1080p","2160p"],"aspect_ratio_allowed":["16:9","1:1","9:16"],"duration_options":[3,4,5,6,7,8,9,10,11,12,13,14,15],"output_audio":true,"input_audio":false,"max_videos":1,"max_audios":0,"max_images":7,"max_elements":7,"max_images_and_elements":7,"limits_with_video":{"max_images_and_elements":4,"duration_options":[3,4,5,6,7,8,9,10]},"support_instruction_edit":true,"prompt_length_limit":{"max":2500,"count_mode":"non_ascii_weighted","label":"可灵口径"}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:viduq2', 'viduq2', 'video_generate', 'Vidu-Q2', '{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10]},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate"],"alias":"Vidu-Q2","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2","alias":"Vidu-Q2","icon_path":"https://static.51easyai.com/vidu-color.webp","types":["video_generate"],"capabilities":{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:viduq2-pro', 'viduq2-pro', 'image_to_video', 'Vidu-Q2-Pro', '{"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,10],"output_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Pro","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2-pro","alias":"Vidu-Q2-Pro","icon_path":"https://static.51easyai.com/vidu-color.webp","types":["image_to_video"],"capabilities":{"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,10],"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:viduq2-turbo', 'viduq2-turbo', 'image_to_video', 'Vidu-Q2-Turbo', '{"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,10],"output_audio":true},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Turbo","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2-turbo","alias":"Vidu-Q2-Turbo","icon_path":"https://static.51easyai.com/vidu-color.webp","types":["image_to_video"],"capabilities":{"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,10],"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:viduq3-pro', 'viduq3-pro', 'video_generate', 'Vidu-Q3-Pro', '{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,16],"output_audio":true},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"Vidu-Q3-Pro","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq3-pro","alias":"Vidu-Q3-Pro","icon_path":"https://static.51easyai.com/vidu-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,16],"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:viduq3-turbo', 'viduq3-turbo', 'video_generate', 'Vidu-Q3-Turbo', '{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,16],"output_audio":true},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"Vidu-Q3-Turbo","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq3-turbo","alias":"Vidu-Q3-Turbo","icon_path":"https://static.51easyai.com/vidu-color.webp","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["480p","720p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"input_last_frame":false,"input_first_last_frame":false,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["480p","720p","1080p"],"duration_range":[1,16],"output_audio":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:V3-Fast', 'V3-Fast', 'video_generate', 'VEO-3-Fast', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"V3-Fast","alias":"VEO-3-Fast","icon_path":"https://static.51easyai.com/gemini-color.png","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:V3', 'V3', 'video_generate', 'VEO-3', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"V3","alias":"VEO-3","icon_path":"https://static.51easyai.com/gemini-color.png","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:V3-1Fast', 'V3-1Fast', 'video_generate', 'VEO-3.1-Fast', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":true,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"V3-1Fast","alias":"VEO-3.1-Fast","icon_path":"https://static.51easyai.com/gemini-color.png","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":true,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan-video' OR provider_code = 'tencent-hunyuan-video' LIMIT 1), 'tencent-hunyuan-video', 'tencent-hunyuan-video:V3-1', 'V3-1', 'video_generate', 'VEO-3.1', '{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":true,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan-video","sourceProviderName":"腾讯混元视频(第三方)","sourceSpecType":"tencent-hunyuan-video","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"V3-1","alias":"VEO-3.1","icon_path":"https://static.51easyai.com/gemini-color.png","types":["video_generate","image_to_video"],"capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"aspect_ratio_allowed":["16:9","9:16"],"duration_options":[4,6,8]},"image_to_video":{"input_last_frame":false,"input_first_last_frame":true,"input_first_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":true,"support_video_effect_template":false,"output_resolutions":["720p","1080p"],"duration_options":[4,6,8]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-v3.0', 'hunyuan-v3.0', 'text_to_model', '混元3D-v3.0', '{"text_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"image_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true,"support_image_autofix":false},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"originalTypes":["text_to_model","image_to_model","multiview_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["text_to_model","image_to_model","multiview_to_model"],"alias":"混元3D-v3.0","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-v3.0","types":["text_to_model","image_to_model","multiview_to_model"],"alias":"混元3D-v3.0","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"image_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true,"support_image_autofix":false},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-v3.1', 'hunyuan-v3.1', 'text_to_model', '混元3D-v3.1', '{"text_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"image_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true,"support_image_autofix":false},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"originalTypes":["text_to_model","image_to_model","multiview_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["text_to_model","image_to_model","multiview_to_model"],"alias":"混元3D-v3.1","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-v3.1","types":["text_to_model","image_to_model","multiview_to_model"],"alias":"混元3D-v3.1","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true},"image_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true,"support_image_autofix":false},"multiview_to_model":{"support_texture":true,"support_part_generation":false,"face_limit_range":{"min":3000,"max":1500000},"support_quad":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-v2.5', 'hunyuan-v2.5', 'text_to_model', '混元3D-v2.5', '{"text_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false},"image_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_image_autofix":false},"originalTypes":["text_to_model","image_to_model"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["text_to_model","image_to_model"],"alias":"混元3D-v2.5","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-v2.5","types":["text_to_model","image_to_model"],"alias":"混元3D-v2.5","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"text_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false},"image_to_model":{"support_texture":true,"support_part_generation":false,"support_quad":false,"support_image_autofix":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-texture-v3.0', 'hunyuan-texture-v3.0', 'mesh_edit', '混元3D纹理-v3.0', '{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["obj","glb"],"supported_texture_prompt_modes":["text","image"]}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["mesh_edit"],"alias":"混元3D纹理-v3.0","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-texture-v3.0","types":["mesh_edit"],"alias":"混元3D纹理-v3.0","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["obj","glb"],"supported_texture_prompt_modes":["text","image"]}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-texture-v3.1', 'hunyuan-texture-v3.1', 'mesh_edit', '混元3D纹理-v3.1', '{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["obj","glb"],"supported_texture_prompt_modes":["image","multiview"]}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["mesh_edit"],"alias":"混元3D纹理-v3.1","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-texture-v3.1","types":["mesh_edit"],"alias":"混元3D纹理-v3.1","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"mesh_edit":{"supported_operations":{"texture":{"supported":true,"supported_formats":["obj","glb"],"supported_texture_prompt_modes":["image","multiview"]}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-part-v1', 'hunyuan-part-v1', 'mesh_edit', '混元3D部件拆分-v1', '{"mesh_edit":{"supported_operations":{"segmentation":{"supported":true,"supported_formats":["fbx"]}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["mesh_edit"],"alias":"混元3D部件拆分-v1","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-part-v1","types":["mesh_edit"],"alias":"混元3D部件拆分-v1","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"mesh_edit":{"supported_operations":{"segmentation":{"supported":true,"supported_formats":["fbx"]}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-hunyuan' OR provider_code = 'tencent-hunyuan' LIMIT 1), 'tencent-hunyuan', 'tencent-hunyuan:hunyuan-reduce-face-v1.5', 'hunyuan-reduce-face-v1.5', 'mesh_edit', '混元3D智能拓扑-v1.5', '{"mesh_edit":{"supported_operations":{"smart_lowpoly":{"supported":true,"supported_formats":["obj","glb"]}}},"originalTypes":["mesh_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":3,"max_request_per_second":20}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-hunyuan","sourceProviderName":"腾讯混元3D","sourceSpecType":"tencent-hunyuan","originalTypes":["mesh_edit"],"alias":"混元3D智能拓扑-v1.5","description":"","iconPath":"https://static.51easyai.com/hunyuan3d-logo.svg","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"hunyuan-reduce-face-v1.5","types":["mesh_edit"],"alias":"混元3D智能拓扑-v1.5","icon_path":"https://static.51easyai.com/hunyuan3d-logo.svg","capabilities":{"mesh_edit":{"supported_operations":{"smart_lowpoly":{"supported":true,"supported_formats":["obj","glb"]}}}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'suno' OR provider_code = 'suno' LIMIT 1), 'suno', 'suno:chirp-v3-0', 'chirp-v3-0', 'audio_generate', 'Suno音频生成V3.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"suno","sourceProviderName":"Suno音乐生成","sourceSpecType":"suno","originalTypes":["audio_generate"],"alias":"Suno音频生成V3.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"chirp-v3-0","types":["audio_generate"],"alias":"Suno音频生成V3.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'suno' OR provider_code = 'suno' LIMIT 1), 'suno', 'suno:chirp-v3-5', 'chirp-v3-5', 'audio_generate', 'Suno音频生成V3.5', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"suno","sourceProviderName":"Suno音乐生成","sourceSpecType":"suno","originalTypes":["audio_generate"],"alias":"Suno音频生成V3.5","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"chirp-v3-5","types":["audio_generate"],"alias":"Suno音频生成V3.5","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'suno' OR provider_code = 'suno' LIMIT 1), 'suno', 'suno:chirp-v4-0', 'chirp-v4-0', 'audio_generate', 'Suno音频生成V4.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"suno","sourceProviderName":"Suno音乐生成","sourceSpecType":"suno","originalTypes":["audio_generate"],"alias":"Suno音频生成V4.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"chirp-v4-0","types":["audio_generate"],"alias":"Suno音频生成V4.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'suno' OR provider_code = 'suno' LIMIT 1), 'suno', 'suno:chirp-v4-5', 'chirp-v4-5', 'audio_generate', 'Suno音频生成V4.5', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"suno","sourceProviderName":"Suno音乐生成","sourceSpecType":"suno","originalTypes":["audio_generate"],"alias":"Suno音频生成V4.5","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"chirp-v4-5","types":["audio_generate"],"alias":"Suno音频生成V4.5","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'suno' OR provider_code = 'suno' LIMIT 1), 'suno', 'suno:chirp-v5-0', 'chirp-v5-0', 'audio_generate', 'Suno音频生成V5.0', '{"originalTypes":["audio_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"suno","sourceProviderName":"Suno音乐生成","sourceSpecType":"suno","originalTypes":["audio_generate"],"alias":"Suno音频生成V5.0","description":"","iconPath":"https://static.51easyai.com/suno-logo.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"chirp-v5-0","types":["audio_generate"],"alias":"Suno音频生成V5.0","icon_path":"https://static.51easyai.com/suno-logo.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:speech-2.5-hd-preview', 'speech-2.5-hd-preview', 'text_to_speech', 'MiniMax Speech 2.5 HD Preview', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.5 HD Preview","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"speech-2.5-hd-preview","types":["text_to_speech"],"alias":"MiniMax Speech 2.5 HD Preview","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:speech-2.5-turbo-preview', 'speech-2.5-turbo-preview', 'text_to_speech', 'MiniMax Speech 2.5 Turbo Preview', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.5 Turbo Preview","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"speech-2.5-turbo-preview","types":["text_to_speech"],"alias":"MiniMax Speech 2.5 Turbo Preview","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:speech-2.6-hd', 'speech-2.6-hd', 'text_to_speech', 'MiniMax Speech 2.6 HD', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.6 HD","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"speech-2.6-hd","types":["text_to_speech"],"alias":"MiniMax Speech 2.6 HD","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:speech-2.6-turbo', 'speech-2.6-turbo', 'text_to_speech', 'MiniMax Speech 2.6 Turbo', '{"originalTypes":["text_to_speech"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["text_to_speech"],"alias":"MiniMax Speech 2.6 Turbo","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"speech-2.6-turbo","types":["text_to_speech"],"alias":"MiniMax Speech 2.6 Turbo","icon_path":"https://static.51easyai.com/minimax-color.png"}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:MiniMax-Hailuo-02', 'MiniMax-Hailuo-02', 'video_generate', '海螺02', '{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":{"input_first_frame":["480p","720p","1080p"],"input_first_last_frame":["720p","1080p"]},"duration_range":{"480p":[6,10],"720p":[6,10],"1080p":[6,6]},"duration_options":{"480p":[6,10],"720p":[6,10],"1080p":[6]},"input_reference_generate_single":false,"input_reference_generate_multiple":false,"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["video_generate","image_to_video"],"alias":"海螺02","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-Hailuo-02","types":["video_generate","image_to_video"],"alias":"海螺02","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":{"input_first_frame":["480p","720p","1080p"],"input_first_last_frame":["720p","1080p"]},"duration_range":{"480p":[6,10],"720p":[6,10],"1080p":[6,6]},"duration_options":{"480p":[6,10],"720p":[6,10],"1080p":[6]},"input_reference_generate_single":false,"input_reference_generate_multiple":false,"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":true,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:MiniMax-Hailuo-2.3', 'MiniMax-Hailuo-2.3', 'video_generate', '海螺2.3', '{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["video_generate","image_to_video"],"alias":"海螺2.3","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-Hailuo-2.3","types":["video_generate","image_to_video"],"alias":"海螺2.3","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"video_generate":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"aspect_ratio_allowed":[]},"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'minimax' OR provider_code = 'minimax' LIMIT 1), 'minimax', 'minimax:MiniMax-Hailuo-2.3-Fast', 'MiniMax-Hailuo-2.3-Fast', 'image_to_video', 'MiniMax-Hailuo-2.3-Fast', '{"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"minimax","sourceProviderName":"MiniMax","sourceSpecType":"minimax","originalTypes":["image_to_video"],"alias":"MiniMax-Hailuo-2.3-Fast","description":"","iconPath":"https://static.51easyai.com/minimax-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"MiniMax-Hailuo-2.3-Fast","types":["image_to_video"],"alias":"MiniMax-Hailuo-2.3-Fast","icon_path":"https://static.51easyai.com/minimax-color.png","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"duration_range":{"720p":[6,10],"1080p":[6,6]},"duration_options":{"720p":[6,10],"1080p":[6]},"input_first_frame":true,"input_first_last_frame":false,"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"aspect_ratio_allowed":[],"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'midjourney' OR provider_code = 'midjourney' LIMIT 1), 'midjourney', 'midjourney:v7', 'v7', 'image_generate', 'Midjourney_v7', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"midjourney","sourceProviderName":"Midjourney","sourceSpecType":"midjourney","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v7","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v7","types":["image_generate","image_edit"],"alias":"Midjourney_v7","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'midjourney' OR provider_code = 'midjourney' LIMIT 1), 'midjourney', 'midjourney:v6', 'v6', 'image_generate', 'Midjourney_v6', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"midjourney","sourceProviderName":"Midjourney","sourceSpecType":"midjourney","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v6","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v6","types":["image_generate","image_edit"],"alias":"Midjourney_v6","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'midjourney' OR provider_code = 'midjourney' LIMIT 1), 'midjourney', 'midjourney:v6.1', 'v6.1', 'image_generate', 'Midjourney_v6.1', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"midjourney","sourceProviderName":"Midjourney","sourceSpecType":"midjourney","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_v6.1","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"v6.1","types":["image_generate","image_edit"],"alias":"Midjourney_v6.1","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'midjourney' OR provider_code = 'midjourney' LIMIT 1), 'midjourney', 'midjourney:niji 7', 'niji 7', 'image_generate', 'Midjourney_Niji_v7', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"midjourney","sourceProviderName":"Midjourney","sourceSpecType":"midjourney","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_Niji_v7","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"niji 7","types":["image_generate","image_edit"],"alias":"Midjourney_Niji_v7","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'midjourney' OR provider_code = 'midjourney' LIMIT 1), 'midjourney', 'midjourney:niji 6', 'niji 6', 'image_generate', 'Midjourney_Niji_v6', '{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]},"originalTypes":["image_generate","image_edit"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"midjourney","sourceProviderName":"Midjourney","sourceSpecType":"midjourney","originalTypes":["image_generate","image_edit"],"alias":"Midjourney_Niji_v6","description":"","iconPath":"https://static.51easyai.com/midjourney.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"niji 6","types":["image_generate","image_edit"],"alias":"Midjourney_Niji_v6","icon_path":"https://static.51easyai.com/midjourney.png","capabilities":{"image_generate":{"output_resolutions":["1K","2K"]},"image_edit":{"input_multiple_images":true,"input_max_images_count":16,"output_resolutions":["1K","2K"]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'tencent-lke' OR provider_code = 'tencent-lke' LIMIT 1), 'tencent-lke', 'tencent-lke:[appid]|[appkey]', '[appid]|[appkey]', 'text_generate', '[appid]|[appkey]', '{"originalTypes":["text_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":-1}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"tencent-lke","sourceProviderName":"腾讯云智能体开发平台","sourceSpecType":"tencent-lke","originalTypes":["text_generate"],"alias":"","description":"","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"[appid]|[appkey]","types":["text_generate"]}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:sora-2', 'sora-2', 'video_generate', 'Sora-2', '{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"sora-2","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:sora-2-pro', 'sora-2-pro', 'video_generate', 'Sora-2-Pro', '{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2-Pro","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"sora-2-pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2-Pro","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:veo3-fast', 'veo3-fast', 'video_generate', 'VEO-3-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3-fast","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:veo31-fast', 'veo31-fast', 'video_generate', 'VEO-3.1-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo31-fast","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:veo3', 'veo3', 'video_generate', 'VEO-3', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:veo31', 'veo31', 'video_generate', 'VEO-3.1', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo31","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'universal' OR provider_code = 'universal' LIMIT 1), 'universal', 'universal:veo31-pro', 'veo31-pro', 'video_generate', 'VEO-3.1-Pro', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"universal","sourceProviderName":"自定义平台通用平台(支持自定义方式接入任意平台)","sourceSpecType":"universal","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo31-pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Pro","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16","1:1","4:3","3:4"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:veo3-fast', 'veo3-fast', 'video_generate', 'VEO-3-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3-fast","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:veo3.1-components', 'veo3.1-components', 'video_generate', 'VEO-3.1-Fast', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Fast","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3.1-components","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Fast","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:veo3', 'veo3', 'video_generate', 'VEO-3', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:veo3.1', 'veo3.1', 'video_generate', 'VEO-3.1', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3.1","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:veo3.1-pro', 'veo3.1-pro', 'video_generate', 'VEO-3.1-Pro', '{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"VEO-3.1-Pro","description":"","iconPath":"https://static.51easyai.com/gemini-color.png","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"veo3.1-pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/gemini-color.png","alias":"VEO-3.1-Pro","model_limits":{"max_concurrent_requests":5},"capabilities":{"video_generate":{"aspect_ratio_allowed":["16:9","9:16"],"output_resolutions":[],"duration_range":[8,8]},"image_to_video":{"output_resolutions":[],"duration_range":[8,8],"aspect_ratio_allowed":["16:9","9:16"],"input_first_last_frame":true,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_first_frame":true,"input_last_frame":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:sora-2', 'sora-2', 'video_generate', 'Sora-2', '{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"sora-2","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,15],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'newapi' OR provider_code = 'newapi' LIMIT 1), 'newapi', 'newapi:sora-2-pro', 'sora-2-pro', 'video_generate', 'Sora-2-Pro', '{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"modelLimits":{"max_concurrent_requests":15}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"newapi","sourceProviderName":"NewAPI兼容平台","sourceSpecType":"newapi","originalTypes":["video_generate","image_to_video"],"alias":"Sora-2-Pro","description":"","iconPath":"https://static.51easyai.com/sora-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"sora-2-pro","types":["video_generate","image_to_video"],"icon_path":"https://static.51easyai.com/sora-color.webp","alias":"Sora-2-Pro","model_limits":{"max_concurrent_requests":15},"capabilities":{"video_generate":{"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"]},"image_to_video":{"input_first_frame":true,"input_last_frame":false,"input_first_last_frame":false,"duration_range":[10,25],"output_resolutions":[],"aspect_ratio_allowed":["16:9","9:16"],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq2', 'viduq2', 'video_generate', 'Vidu-Q2', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10],"output_bgm":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["video_generate"],"alias":"Vidu-Q2","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2","types":["video_generate"],"alias":"Vidu-Q2","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,10],"output_bgm":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq1', 'viduq1', 'video_generate', 'Vidu-Q1', '{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","9:16","1:1"],"duration_range":[5,5],"output_bgm":true},"originalTypes":["video_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["video_generate"],"alias":"Vidu-Q1","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq1","types":["video_generate"],"alias":"Vidu-Q1","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["1080p"],"aspect_ratio_allowed":["16:9","9:16","1:1"],"duration_range":[5,5],"output_bgm":true}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq2-pro-fast', 'viduq2-pro-fast', 'image_to_video', 'Vidu-Q2-Pro-Fast', '{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_last_frame":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Pro-Fast","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2-pro-fast","types":["image_to_video"],"alias":"Vidu-Q2-Pro-Fast","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"input_last_frame":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq2-pro', 'viduq2-pro', 'image_to_video', 'Vidu-Q2-Pro', '{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Pro","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2-pro","types":["image_to_video"],"alias":"Vidu-Q2-Pro","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq2-turbo', 'viduq2-turbo', 'image_to_video', 'Vidu-Q2-Turbo', '{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-Q2-Turbo","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq2-turbo","types":["image_to_video"],"alias":"Vidu-Q2-Turbo","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[1,10],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[],"input_smart_multi_frame":true,"smart_multi_frame_range":[2,9],"smart_multi_frame_mode":"native","smart_multi_frame_duration_range":[2,7]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq3-pro', 'viduq3-pro', 'video_generate', 'Vidu-Q3-Pro', '{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"duration_range":[1,16],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["video_generate","image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["video_generate","image_to_video"],"alias":"Vidu-Q3-Pro","description":"高效生成优质音视频内容,让视频内容更生动、更形象、更立体","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq3-pro","types":["video_generate","image_to_video"],"alias":"Vidu-Q3-Pro","description":"高效生成优质音视频内容,让视频内容更生动、更形象、更立体","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"video_generate":{"output_resolutions":["720p","480p","1080p"],"aspect_ratio_allowed":["16:9","9:16","4:3","3:4","1:1"],"duration_range":[1,16],"output_audio":true},"image_to_video":{"output_resolutions":["720p","480p","1080p"],"input_first_frame":true,"input_first_last_frame":false,"duration_range":[1,16],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq1:2', 'viduq1', 'image_to_video', 'Vidu-Q1', '{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-Q1","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq1","types":["image_to_video"],"alias":"Vidu-Q1","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:viduq1-classic', 'viduq1-classic', 'image_to_video', 'Vidu-Q1-Classic', '{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-Q1-Classic","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"viduq1-classic","types":["image_to_video"],"alias":"Vidu-Q1-Classic","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":[5,5],"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'vidu' OR provider_code = 'vidu' LIMIT 1), 'vidu', 'vidu:vidu2.0', 'vidu2.0', 'image_to_video', 'Vidu-2.0', '{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":{"720p":[4,8],"1080p":[4,4]},"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]},"originalTypes":["image_to_video"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_request_per_minute":60,"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"vidu","sourceProviderName":"Vidu视频生成","sourceSpecType":"vidu","originalTypes":["image_to_video"],"alias":"Vidu-2.0","description":"","iconPath":"https://static.51easyai.com/vidu-color.webp","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"vidu2.0","types":["image_to_video"],"alias":"Vidu-2.0","icon_path":"https://static.51easyai.com/vidu-color.webp","capabilities":{"image_to_video":{"output_resolutions":["720p","1080p"],"input_first_frame":true,"input_first_last_frame":true,"duration_range":{"720p":[4,8],"1080p":[4,4]},"input_last_frame":false,"input_reference_generate_single":false,"input_reference_generate_multiple":false,"support_video_effect_template":false,"output_bgm":true,"output_audio":true,"aspect_ratio_allowed":[]}}}}'::jsonb),
((SELECT id FROM model_catalog_providers WHERE provider_key = 'mock-test' OR provider_code = 'mock-test' LIMIT 1), 'mock-test', 'mock-test:mock-test-image', 'mock-test-image', 'image_generate', 'mock-test-image', '{"image_generate":{"output_resolutions":["1K"],"output_multiple_images":false},"originalTypes":["image_generate"]}'::jsonb, '{"text":{"basePrice":0.01,"baseWeight":1},"image":{"basePrice":10,"baseWeight":1,"dynamicWeight":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4}},"audio":{"basePrice":1,"baseWeight":1},"music":{"basePrice":20,"baseWeight":1},"video":{"basePrice":100,"baseWeight":1,"dynamicWeight":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2,"audio-true":2,"audio-false":1,"reference-video-true":1.5,"reference-video-false":1,"voice-specified-true":1.2,"voice-specified-false":1}},"digital_human":{"basePrice":50,"baseWeight":1},"model":{"basePrice":20,"baseWeight":1,"dynamicWeight":{"texture-none":1,"texture-standard":2}}}'::jsonb, '{"platformLimits":{"max_concurrent_requests":5}}'::jsonb, 1, 'active', '{"source":"server-main.integration-platform","sourceProviderCode":"mock-test","sourceProviderName":"Mock测试平台","sourceSpecType":"mock-test","originalTypes":["image_generate"],"alias":"mock-test-image","description":"测试用图像生成模型,走 MockTestClient,模拟 10 秒耗时","iconPath":"","billingType":"external-api","billingMode":"","referenceModel":"","modelWeight":null,"selectable":true,"rawModel":{"name":"mock-test-image","alias":"mock-test-image","types":["image_generate"],"description":"测试用图像生成模型,走 MockTestClient,模拟 10 秒耗时","capabilities":{"image_generate":{"output_resolutions":["1K"],"output_multiple_images":false}}}}'::jsonb)
ON CONFLICT (canonical_model_key) DO UPDATE SET
provider_id = EXCLUDED.provider_id,
provider_key = EXCLUDED.provider_key,
provider_model_name = EXCLUDED.provider_model_name,
model_type = EXCLUDED.model_type,
display_name = EXCLUDED.display_name,
capabilities = EXCLUDED.capabilities,
base_billing_config = EXCLUDED.base_billing_config,
default_rate_limit_policy = EXCLUDED.default_rate_limit_policy,
pricing_version = EXCLUDED.pricing_version,
status = EXCLUDED.status,
metadata = EXCLUDED.metadata,
updated_at = now();
@@ -0,0 +1,177 @@
CREATE TABLE IF NOT EXISTS model_pricing_rule_sets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
rule_set_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
category text NOT NULL DEFAULT 'general',
currency text NOT NULL DEFAULT 'resource',
status text NOT NULL DEFAULT 'active',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
ALTER TABLE IF EXISTS model_pricing_rules
ADD COLUMN IF NOT EXISTS rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS rule_key text NOT NULL DEFAULT ('rule_' || replace(gen_random_uuid()::text, '-', '')),
ADD COLUMN IF NOT EXISTS display_name text NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS calculator_type text NOT NULL DEFAULT 'unit_weight',
ADD COLUMN IF NOT EXISTS dimension_schema jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS formula_config jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS priority integer NOT NULL DEFAULT 100,
ADD COLUMN IF NOT EXISTS status text NOT NULL DEFAULT 'active',
ADD COLUMN IF NOT EXISTS metadata jsonb NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE IF EXISTS integration_platforms
ADD COLUMN IF NOT EXISTS pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
ALTER TABLE IF EXISTS platform_models
ADD COLUMN IF NOT EXISTS pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND c.conrelid = 'integration_platforms'::regclass
AND a.attname = 'pricing_rule_set_id'
) THEN
ALTER TABLE integration_platforms
ADD CONSTRAINT fk_integration_platforms_pricing_rule_set
FOREIGN KEY (pricing_rule_set_id) REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
END IF;
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND c.conrelid = 'platform_models'::regclass
AND a.attname = 'pricing_rule_set_id'
) THEN
ALTER TABLE platform_models
ADD CONSTRAINT fk_platform_models_pricing_rule_set
FOREIGN KEY (pricing_rule_set_id) REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_model_pricing_rule_set
ON model_pricing_rules(rule_set_id, resource_type, priority);
CREATE UNIQUE INDEX IF NOT EXISTS idx_model_pricing_rule_set_key
ON model_pricing_rules(rule_set_id, rule_key)
WHERE rule_set_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_integration_platforms_pricing_rule_set
ON integration_platforms(pricing_rule_set_id);
CREATE INDEX IF NOT EXISTS idx_platform_models_pricing_rule_set
ON platform_models(pricing_rule_set_id);
INSERT INTO model_pricing_rule_sets (rule_set_key, name, description, category, currency, status, metadata)
VALUES (
'default-multimodal-v1',
'默认多模态计价规则',
'覆盖文本、图像和视频的默认计价规则,可作为平台或平台模型的基础价格模板。',
'default',
'resource',
'active',
'{"source":"gateway.default","version":1}'::jsonb
)
ON CONFLICT (rule_set_key) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
category = EXCLUDED.category,
currency = EXCLUDED.currency,
status = EXCLUDED.status,
metadata = EXCLUDED.metadata,
updated_at = now();
WITH default_set AS (
SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1'
)
INSERT INTO model_pricing_rules (
rule_set_id, rule_key, display_name, scope_type, scope_id, resource_type, unit,
base_price, currency, base_weight, dynamic_weight, calculator_type,
dimension_schema, formula_config, priority, status, metadata
)
SELECT default_set.id, item.rule_key, item.display_name, 'rule_set', default_set.id,
item.resource_type, item.unit, item.base_price, 'resource',
item.base_weight, item.dynamic_weight, item.calculator_type,
item.dimension_schema, item.formula_config, item.priority, 'active',
'{"source":"gateway.default"}'::jsonb
FROM default_set
CROSS JOIN (
VALUES
(
'text_input_tokens',
'文本输入 Token',
'text_input',
'1k_tokens',
0.01::numeric,
'{"meter":"input_tokens"}'::jsonb,
'{}'::jsonb,
'token_usage',
'{"metrics":["input_tokens"],"unitScale":1000}'::jsonb,
'{"formula":"ceil(input_tokens / 1000) * base_price"}'::jsonb,
10
),
(
'text_output_tokens',
'文本输出 Token',
'text_output',
'1k_tokens',
0.03::numeric,
'{"meter":"output_tokens"}'::jsonb,
'{}'::jsonb,
'token_usage',
'{"metrics":["output_tokens"],"unitScale":1000}'::jsonb,
'{"formula":"ceil(output_tokens / 1000) * base_price"}'::jsonb,
20
),
(
'image',
'图像',
'image',
'image',
10::numeric,
'{"meter":"count"}'::jsonb,
'{"resolutionFactors":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4},"qualityFactors":{"low":0.5,"medium":1,"high":1.5}}'::jsonb,
'unit_weight',
'{"dimensions":["count","resolution","quality"],"defaults":{"count":1,"quality":"medium","resolution":"1K"}}'::jsonb,
'{"formula":"count * base_price * resolution_factor * quality_factor"}'::jsonb,
30
),
(
'video_generation',
'视频生成',
'video',
'5s',
100::numeric,
'{"meter":"duration","unitSeconds":5}'::jsonb,
'{"resolutionWeights":{"480p":0.75,"720p":1,"1080p":1.5,"2160p":2},"audioWeights":{"true":2,"false":1},"referenceVideoWeights":{"true":1.5,"false":1},"voiceSpecifiedWeights":{"true":1.2,"false":1}}'::jsonb,
'duration_weight',
'{"dimensions":["duration_seconds","resolution","audio","reference_video","voice_specified","count"],"defaults":{"count":1,"duration_seconds":5,"resolution":"720p","audio":false,"reference_video":false,"voice_specified":false}}'::jsonb,
'{"formula":"count * ceil(duration_seconds / 5) * base_price * resolution_weight * audio_weight * reference_video_weight * voice_specified_weight"}'::jsonb,
50
)
) AS item(rule_key, display_name, resource_type, unit, base_price, base_weight, dynamic_weight, calculator_type, dimension_schema, formula_config, priority)
ON CONFLICT (rule_set_id, rule_key) WHERE rule_set_id IS NOT NULL DO UPDATE SET
display_name = EXCLUDED.display_name,
scope_type = EXCLUDED.scope_type,
scope_id = EXCLUDED.scope_id,
resource_type = EXCLUDED.resource_type,
unit = EXCLUDED.unit,
base_price = EXCLUDED.base_price,
currency = EXCLUDED.currency,
base_weight = EXCLUDED.base_weight,
dynamic_weight = EXCLUDED.dynamic_weight,
calculator_type = EXCLUDED.calculator_type,
dimension_schema = EXCLUDED.dimension_schema,
formula_config = EXCLUDED.formula_config,
priority = EXCLUDED.priority,
status = EXCLUDED.status,
metadata = EXCLUDED.metadata,
updated_at = now();
@@ -0,0 +1,26 @@
DELETE FROM model_pricing_rules
WHERE resource_type = 'image_edit'
OR rule_key = 'image_edit';
DELETE FROM model_pricing_rules old_rule
USING model_pricing_rules image_rule
WHERE old_rule.rule_set_id IS NOT DISTINCT FROM image_rule.rule_set_id
AND old_rule.rule_key = 'image_generation'
AND image_rule.rule_key = 'image';
UPDATE model_pricing_rules
SET rule_key = 'image',
display_name = '图像',
resource_type = 'image',
unit = 'image',
base_price = 10,
base_weight = '{"meter":"count"}'::jsonb,
dynamic_weight = '{"resolutionFactors":{"1K":1,"2K":1.5,"3K":1.75,"4K":2,"8K":4},"qualityFactors":{"low":0.5,"medium":1,"high":1.5}}'::jsonb,
calculator_type = 'unit_weight',
dimension_schema = '{"dimensions":["count","resolution","quality"],"defaults":{"count":1,"quality":"medium","resolution":"1K"}}'::jsonb,
formula_config = '{"formula":"count * base_price * resolution_factor * quality_factor"}'::jsonb,
priority = 30,
metadata = metadata || '{"migration":"0008_pricing_image_single_rule"}'::jsonb,
updated_at = now()
WHERE resource_type = 'image'
AND rule_key IN ('image_generation', 'image');
@@ -0,0 +1,50 @@
ALTER TABLE model_catalog_providers
ADD COLUMN IF NOT EXISTS default_base_url text,
ADD COLUMN IF NOT EXISTS default_auth_type text NOT NULL DEFAULT 'APIKey';
WITH source_defaults(provider_key, default_base_url, default_auth_type) AS (
VALUES
('easyai', 'https://51easyai.com/api/v1', 'APIKey'),
('runninghub', 'https://www.runninghub.ai', 'APIKey'),
('LiblibAI', 'https://openapi.liblibai.cloud', 'AccessKey-SecretKey'),
('keling', 'https://api-beijing.klingai.com/v1', 'AccessKey-SecretKey'),
('gemini', 'https://generativelanguage.googleapis.com/v1beta', 'APIKey'),
('openai', 'https://api.openai.com/v1', 'APIKey'),
('aliyun-bailian-openai', 'https://dashscope.aliyuncs.com/compatible-mode/v1', 'APIKey'),
('gemini-openai', 'https://generativelanguage.googleapis.com/v1beta/openai', 'APIKey'),
('volces-openai', 'https://ark.cn-beijing.volces.com/api/v3', 'APIKey'),
('zhipu-openai', 'https://open.bigmodel.cn/api/paas/v4', 'APIKey'),
('minimax-openai', 'https://api.minimaxi.com/v1', 'APIKey'),
('openrouter-openai', 'https://openrouter.ai/api/v1', 'APIKey'),
('aliyun-bailian', 'https://dashscope.aliyuncs.com/api/v1', 'APIKey'),
('ollama', 'http://<your-local-ip>:11434/v1', 'APIKey'),
('blackforest', 'https://api.bfl.ai/v1', 'APIKey'),
('dify', 'http://localhost/v1', 'APIKey'),
('volces', 'https://ark.cn-beijing.volces.com/api/v3', 'AccessKey-SecretKey'),
('jimeng', '', 'APIKey'),
('silicon-flow-openai', 'https://api.siliconflow.cn/v1', 'APIKey'),
('tripo3d', 'https://api.tripo3d.ai/v2/openapi', 'APIKey'),
('tencent-hunyuan-image', 'https://aiart.tencentcloudapi.com', 'AccessKey-SecretKey'),
('tencent-hunyuan-video', 'https://vclm.tencentcloudapi.com', 'AccessKey-SecretKey'),
('tencent-hunyuan', 'https://ai3d.tencentcloudapi.com', 'AccessKey-SecretKey'),
('suno', 'https://api.cqtai.com/api/cqt', 'APIKey'),
('minimax', 'https://api.minimaxi.com/v1', 'APIKey'),
('midjourney', 'https://api.legnext.ai/api/v1', 'APIKey'),
('tencent-lke', 'https://wss.lke.cloud.tencent.com/v1', 'AccessKey-SecretKey'),
('universal', 'https://example.com/v1/image/edits', 'APIKey'),
('newapi', 'https://api.newapi.com/v1', 'APIKey'),
('vidu', 'https://api.vidu.cn/ent/v2', 'APIKey'),
('n8n', 'http://127.0.0.1:5678', 'APIKey'),
('mock-test', 'http://mock.test', 'APIKey')
)
UPDATE model_catalog_providers p
SET default_base_url = NULLIF(source_defaults.default_base_url, ''),
default_auth_type = source_defaults.default_auth_type,
metadata = p.metadata || jsonb_build_object(
'defaultConnectionSource', 'server-main.integration-platform',
'defaultBaseUrlSynced', NULLIF(source_defaults.default_base_url, ''),
'defaultAuthTypeSynced', source_defaults.default_auth_type
),
updated_at = now()
FROM source_defaults
WHERE p.provider_key = source_defaults.provider_key;
@@ -0,0 +1,28 @@
ALTER TABLE IF EXISTS base_model_catalog
ADD COLUMN IF NOT EXISTS pricing_rule_set_id uuid REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey)
WHERE c.contype = 'f'
AND c.conrelid = 'base_model_catalog'::regclass
AND a.attname = 'pricing_rule_set_id'
) THEN
ALTER TABLE base_model_catalog
ADD CONSTRAINT fk_base_model_catalog_pricing_rule_set
FOREIGN KEY (pricing_rule_set_id) REFERENCES model_pricing_rule_sets(id) ON DELETE SET NULL;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_base_model_catalog_pricing_rule_set
ON base_model_catalog(pricing_rule_set_id);
UPDATE base_model_catalog
SET pricing_rule_set_id = (
SELECT id FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1' LIMIT 1
)
WHERE pricing_rule_set_id IS NULL
AND EXISTS (SELECT 1 FROM model_pricing_rule_sets WHERE rule_set_key = 'default-multimodal-v1');
@@ -0,0 +1,2 @@
ALTER TABLE IF EXISTS integration_platforms
ADD COLUMN IF NOT EXISTS internal_name text;
@@ -0,0 +1,51 @@
CREATE TABLE IF NOT EXISTS model_runtime_policy_sets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
policy_key text NOT NULL UNIQUE,
name text NOT NULL,
description text,
rate_limit_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
retry_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
auto_disable_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
degrade_policy jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO model_runtime_policy_sets (
policy_key, name, description, rate_limit_policy, retry_policy, auto_disable_policy, degrade_policy, metadata, status
)
VALUES (
'default-runtime-v1',
'默认运行策略',
'默认包含 TPM/RPM/并发、失败重试、自动禁用和优先级降级关键词。',
'{"rules":[{"metric":"rpm","limit":120,"windowSeconds":60},{"metric":"tpm_total","limit":240000,"windowSeconds":60},{"metric":"concurrent","limit":6,"leaseTtlSeconds":120}]}'::jsonb,
'{"enabled":true,"maxAttempts":2,"allowKeywords":["rate_limit","timeout","server_error","network","429","5xx"],"denyKeywords":["invalid_api_key","insufficient_quota","billing_not_active","permission_denied"]}'::jsonb,
'{"enabled":false,"threshold":3,"windowSeconds":300,"keywords":["invalid_api_key","account_deactivated","permission_denied","billing_not_active"]}'::jsonb,
'{"enabled":true,"cooldownSeconds":300,"keywords":["rate_limit","quota","timeout","temporarily_unavailable","overloaded"]}'::jsonb,
'{"seed":"0012_runtime_policy_sets"}'::jsonb,
'active'
)
ON CONFLICT (policy_key) DO NOTHING;
ALTER TABLE IF EXISTS base_model_catalog
ADD COLUMN IF NOT EXISTS runtime_policy_set_id uuid REFERENCES model_runtime_policy_sets(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS runtime_policy_override jsonb NOT NULL DEFAULT '{}'::jsonb;
ALTER TABLE IF EXISTS platform_models
ADD COLUMN IF NOT EXISTS runtime_policy_set_id uuid REFERENCES model_runtime_policy_sets(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS runtime_policy_override jsonb NOT NULL DEFAULT '{}'::jsonb;
CREATE INDEX IF NOT EXISTS idx_base_model_catalog_runtime_policy
ON base_model_catalog(runtime_policy_set_id);
CREATE INDEX IF NOT EXISTS idx_platform_models_runtime_policy
ON platform_models(runtime_policy_set_id);
UPDATE base_model_catalog
SET runtime_policy_set_id = (
SELECT id FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1' LIMIT 1
)
WHERE runtime_policy_set_id IS NULL
AND EXISTS (SELECT 1 FROM model_runtime_policy_sets WHERE policy_key = 'default-runtime-v1');
+25
View File
@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS gateway_access_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
subject_type text NOT NULL CHECK (subject_type IN ('user_group', 'tenant', 'user', 'api_key')),
subject_id uuid NOT NULL,
resource_type text NOT NULL CHECK (resource_type IN ('platform', 'platform_model', 'base_model')),
resource_id uuid NOT NULL,
effect text NOT NULL CHECK (effect IN ('allow', 'deny')),
priority integer NOT NULL DEFAULT 100,
min_permission_level integer NOT NULL DEFAULT 0,
conditions jsonb NOT NULL DEFAULT '{}'::jsonb,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (subject_type, subject_id, resource_type, resource_id, effect)
);
CREATE INDEX IF NOT EXISTS idx_gateway_access_rules_subject
ON gateway_access_rules(subject_type, subject_id, status);
CREATE INDEX IF NOT EXISTS idx_gateway_access_rules_resource
ON gateway_access_rules(resource_type, resource_id, status);
CREATE INDEX IF NOT EXISTS idx_gateway_access_rules_effect
ON gateway_access_rules(effect, status, priority);
@@ -0,0 +1,73 @@
ALTER TABLE IF EXISTS base_model_catalog
ADD COLUMN IF NOT EXISTS catalog_type text NOT NULL DEFAULT 'system',
ADD COLUMN IF NOT EXISTS default_snapshot jsonb,
ADD COLUMN IF NOT EXISTS customized_at timestamptz;
UPDATE base_model_catalog
SET catalog_type = CASE
WHEN metadata ->> 'source' = 'server-main.integration-platform'
OR metadata ? 'sourceProviderCode'
OR metadata ? 'rawModel'
THEN 'system'
ELSE 'custom'
END;
UPDATE base_model_catalog
SET default_snapshot = jsonb_build_object(
'providerKey', provider_key,
'canonicalModelKey', canonical_model_key,
'providerModelName', provider_model_name,
'modelType', CASE
WHEN jsonb_typeof(capabilities -> 'originalTypes') = 'array' THEN capabilities -> 'originalTypes'
ELSE jsonb_build_array(model_type)
END,
'modelAlias', display_name,
'capabilities', capabilities,
'baseBillingConfig', base_billing_config,
'defaultRateLimitPolicy', default_rate_limit_policy,
'pricingRuleSetId', COALESCE(pricing_rule_set_id::text, ''),
'runtimePolicySetId', COALESCE(runtime_policy_set_id::text, ''),
'runtimePolicyOverride', runtime_policy_override,
'metadata', metadata,
'pricingVersion', pricing_version,
'status', status
)
WHERE catalog_type = 'system'
AND COALESCE(default_snapshot, '{}'::jsonb) = '{}'::jsonb;
CREATE OR REPLACE FUNCTION fill_system_base_model_default_snapshot()
RETURNS trigger AS $$
BEGIN
IF NEW.catalog_type = 'system' AND NEW.default_snapshot IS NULL THEN
NEW.default_snapshot = jsonb_build_object(
'providerKey', NEW.provider_key,
'canonicalModelKey', NEW.canonical_model_key,
'providerModelName', NEW.provider_model_name,
'modelType', CASE
WHEN jsonb_typeof(NEW.capabilities -> 'originalTypes') = 'array' THEN NEW.capabilities -> 'originalTypes'
ELSE jsonb_build_array(NEW.model_type)
END,
'modelAlias', NEW.display_name,
'capabilities', NEW.capabilities,
'baseBillingConfig', NEW.base_billing_config,
'defaultRateLimitPolicy', NEW.default_rate_limit_policy,
'pricingRuleSetId', COALESCE(NEW.pricing_rule_set_id::text, ''),
'runtimePolicySetId', COALESCE(NEW.runtime_policy_set_id::text, ''),
'runtimePolicyOverride', NEW.runtime_policy_override,
'metadata', NEW.metadata,
'pricingVersion', NEW.pricing_version,
'status', NEW.status
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_fill_system_base_model_default_snapshot ON base_model_catalog;
CREATE TRIGGER trg_fill_system_base_model_default_snapshot
BEFORE INSERT ON base_model_catalog
FOR EACH ROW
EXECUTE FUNCTION fill_system_base_model_default_snapshot();
CREATE INDEX IF NOT EXISTS idx_base_model_catalog_type
ON base_model_catalog(catalog_type, status);
@@ -0,0 +1,41 @@
UPDATE base_model_catalog
SET default_snapshot = default_snapshot
|| jsonb_build_object(
'modelType', CASE
WHEN jsonb_typeof(default_snapshot -> 'modelType') = 'array' THEN default_snapshot -> 'modelType'
WHEN COALESCE(default_snapshot ->> 'modelType', '') <> '' THEN jsonb_build_array(default_snapshot ->> 'modelType')
WHEN jsonb_typeof(capabilities -> 'originalTypes') = 'array' THEN capabilities -> 'originalTypes'
ELSE jsonb_build_array(model_type)
END,
'modelAlias', COALESCE(default_snapshot ->> 'modelAlias', default_snapshot ->> 'displayName', display_name)
)
WHERE catalog_type = 'system'
AND COALESCE(default_snapshot, '{}'::jsonb) <> '{}'::jsonb;
CREATE OR REPLACE FUNCTION fill_system_base_model_default_snapshot()
RETURNS trigger AS $$
BEGIN
IF NEW.catalog_type = 'system' AND NEW.default_snapshot IS NULL THEN
NEW.default_snapshot = jsonb_build_object(
'providerKey', NEW.provider_key,
'canonicalModelKey', NEW.canonical_model_key,
'providerModelName', NEW.provider_model_name,
'modelType', CASE
WHEN jsonb_typeof(NEW.capabilities -> 'originalTypes') = 'array' THEN NEW.capabilities -> 'originalTypes'
ELSE jsonb_build_array(NEW.model_type)
END,
'modelAlias', NEW.display_name,
'capabilities', NEW.capabilities,
'baseBillingConfig', NEW.base_billing_config,
'defaultRateLimitPolicy', NEW.default_rate_limit_policy,
'pricingRuleSetId', COALESCE(NEW.pricing_rule_set_id::text, ''),
'runtimePolicySetId', COALESCE(NEW.runtime_policy_set_id::text, ''),
'runtimePolicyOverride', NEW.runtime_policy_override,
'metadata', NEW.metadata,
'pricingVersion', NEW.pricing_version,
'status', NEW.status
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
@@ -0,0 +1,2 @@
ALTER TABLE gateway_api_keys
ADD COLUMN IF NOT EXISTS key_secret text;
@@ -0,0 +1,72 @@
ALTER TABLE IF EXISTS gateway_tasks
ADD COLUMN IF NOT EXISTS api_key_name text,
ADD COLUMN IF NOT EXISTS api_key_prefix text,
ADD COLUMN IF NOT EXISTS requested_model text,
ADD COLUMN IF NOT EXISTS resolved_model text,
ADD COLUMN IF NOT EXISTS request_id text,
ADD COLUMN IF NOT EXISTS usage jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS metrics jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS billing_summary jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS final_charge_amount numeric NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS response_started_at timestamptz,
ADD COLUMN IF NOT EXISTS response_finished_at timestamptz,
ADD COLUMN IF NOT EXISTS response_duration_ms bigint;
ALTER TABLE IF EXISTS gateway_task_attempts
ADD COLUMN IF NOT EXISTS request_id text,
ADD COLUMN IF NOT EXISTS usage jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS metrics jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS response_started_at timestamptz,
ADD COLUMN IF NOT EXISTS response_finished_at timestamptz,
ADD COLUMN IF NOT EXISTS response_duration_ms bigint;
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_request_id
ON gateway_tasks(request_id)
WHERE request_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_gateway_tasks_api_key_created
ON gateway_tasks(api_key_id, created_at DESC)
WHERE api_key_id IS NOT NULL;
UPDATE gateway_tasks
SET requested_model = model
WHERE requested_model IS NULL;
UPDATE gateway_tasks AS t
SET api_key_name = COALESCE(t.api_key_name, k.name),
api_key_prefix = COALESCE(t.api_key_prefix, k.key_prefix)
FROM gateway_api_keys AS k
WHERE t.api_key_id = k.id::text;
WITH billing_totals AS (
SELECT
t.id,
COUNT(line.value) AS line_count,
COALESCE(SUM(
CASE
WHEN jsonb_typeof(line.value) = 'object'
AND line.value ? 'amount'
AND line.value->>'amount' ~ '^-?[0-9]+(\.[0-9]+)?$'
THEN (line.value->>'amount')::numeric
ELSE 0
END
), 0) AS total_amount
FROM gateway_tasks AS t
LEFT JOIN LATERAL jsonb_array_elements(COALESCE(t.billings, '[]'::jsonb)) AS line(value) ON true
GROUP BY t.id
)
UPDATE gateway_tasks AS t
SET final_charge_amount = billing_totals.total_amount,
billing_summary = jsonb_build_object(
'lineCount', billing_totals.line_count,
'totalAmount', billing_totals.total_amount,
'currency', 'resource',
'finalCharge', jsonb_build_object(
'amount', billing_totals.total_amount,
'currency', 'resource',
'simulated', COALESCE(t.run_mode = 'simulation', false)
)
)
FROM billing_totals
WHERE t.id = billing_totals.id
AND COALESCE(t.billing_summary, '{}'::jsonb) = '{}'::jsonb;
@@ -0,0 +1,19 @@
UPDATE model_catalog_providers
SET default_auth_type = 'APIKey',
metadata = metadata || jsonb_build_object(
'defaultConnectionSource', 'server-main.integration-platform',
'defaultAuthTypeSynced', 'APIKey'
),
updated_at = now()
WHERE provider_key = 'volces'
OR provider_code = 'volces';
UPDATE integration_platforms
SET auth_type = 'APIKey',
credentials = CASE
WHEN credentials ? 'apiKey' THEN credentials
ELSE credentials || '{"apiKey": ""}'::jsonb
END,
updated_at = now()
WHERE provider = 'volces'
AND auth_type = 'AccessKey-SecretKey';
+1 -1
View File
@@ -9,7 +9,7 @@
"executor": "nx:run-commands",
"options": {
"cwd": "apps/api",
"command": "go run ./cmd/gateway"
"command": "GO_WATCH_PRESTART='go run ./cmd/migrate' node ../../scripts/go-watch.mjs -- go run ./cmd/gateway"
}
},
"migrate": {
+21
View File
@@ -10,15 +10,36 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@assistant-ui/react": "^0.14.0",
"@assistant-ui/react-streamdown": "^0.3.0",
"@easyai-ai-gateway/contracts": "workspace:*",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@streamdown/cjk": "^1.0.3",
"@streamdown/code": "^1.1.1",
"@streamdown/math": "^1.0.2",
"@streamdown/mermaid": "^1.0.2",
"@vitejs/plugin-react": "^5.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"katex": "^0.16.45",
"lucide-react": "^1.14.0",
"react": "^19.0.0",
"react-day-picker": "^10.0.0",
"react-dom": "^19.0.0",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.5.0",
"vite": "^7.0.0"
},
"devDependencies": {
"@tailwindcss/vite": "^4.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"tailwindcss": "^4.3.0",
"typescript": "^5.8.0"
}
}
+897 -169
View File
File diff suppressed because it is too large Load Diff
+597 -7
View File
@@ -1,12 +1,33 @@
import type {
AuthResponse,
BaseModelCatalogItem,
BaseModelUpsertRequest,
CatalogProvider,
CatalogProviderUpsertRequest,
CreatedGatewayApiKey,
GatewayAccessRuleBatchRequest,
GatewayAccessRule,
GatewayAccessRuleUpsertRequest,
GatewayApiKey,
GatewayTenant,
GatewayTenantUpsertRequest,
GatewayTask,
GatewayUser,
GatewayUserUpsertRequest,
IntegrationPlatform,
ListResponse,
PlatformModel,
PlayableGatewayApiKey,
PricingRule,
PricingRuleSet,
PricingRuleSetUpsertRequest,
RateLimitWindow,
RuntimePolicySet,
RuntimePolicySetUpsertRequest,
UserGroup,
UserGroupUpsertRequest,
} from '@easyai-ai-gateway/contracts';
import type { PlatformCreateInput, PlatformModelBindingInput } from './types';
const API_BASE = import.meta.env.VITE_GATEWAY_API_BASE_URL ?? 'http://localhost:8088';
@@ -14,47 +35,616 @@ export interface HealthResponse {
ok: boolean;
service: string;
env: string;
identityMode?: string;
}
export async function getHealth(): Promise<HealthResponse> {
return request<HealthResponse>('/healthz', { auth: false });
}
export async function registerLocalAccount(input: {
username: string;
email?: string;
password: string;
displayName?: string;
invitationCode?: string;
}): Promise<AuthResponse> {
return request<AuthResponse>('/api/v1/auth/register', {
auth: false,
body: input,
method: 'POST',
});
}
export async function loginLocalAccount(input: { account: string; password: string }): Promise<AuthResponse> {
return request<AuthResponse>('/api/v1/auth/login', {
auth: false,
body: input,
method: 'POST',
});
}
export async function listPlatforms(token: string): Promise<ListResponse<IntegrationPlatform>> {
return request<ListResponse<IntegrationPlatform>>('/api/v1/platforms', { token });
return request<ListResponse<IntegrationPlatform>>('/api/admin/platforms', { token });
}
export async function listModels(token: string): Promise<ListResponse<PlatformModel>> {
return request<ListResponse<PlatformModel>>('/api/admin/models', { token });
}
export async function listPlayableModels(token: string): Promise<ListResponse<PlatformModel>> {
return request<ListResponse<PlatformModel>>('/api/v1/models', { token });
}
export async function listPublicCatalogProviders(): Promise<ListResponse<CatalogProvider>> {
return request<ListResponse<CatalogProvider>>('/api/v1/public/catalog/providers', { auth: false });
}
export async function listCatalogProviders(token: string): Promise<ListResponse<CatalogProvider>> {
return request<ListResponse<CatalogProvider>>('/api/v1/catalog/providers', { token });
return request<ListResponse<CatalogProvider>>('/api/admin/catalog/providers', { token });
}
export async function createCatalogProvider(
token: string,
input: CatalogProviderUpsertRequest,
): Promise<CatalogProvider> {
return request<CatalogProvider>('/api/admin/catalog/providers', {
body: input,
method: 'POST',
token,
});
}
export async function updateCatalogProvider(
token: string,
providerId: string,
input: CatalogProviderUpsertRequest,
): Promise<CatalogProvider> {
return request<CatalogProvider>(`/api/admin/catalog/providers/${providerId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteCatalogProvider(token: string, providerId: string): Promise<void> {
await request<void>(`/api/admin/catalog/providers/${providerId}`, {
method: 'DELETE',
token,
});
}
export async function listPublicBaseModels(): Promise<ListResponse<BaseModelCatalogItem>> {
return request<ListResponse<BaseModelCatalogItem>>('/api/v1/public/catalog/base-models', { auth: false });
}
export async function listBaseModels(token: string): Promise<ListResponse<BaseModelCatalogItem>> {
return request<ListResponse<BaseModelCatalogItem>>('/api/v1/catalog/base-models', { token });
return request<ListResponse<BaseModelCatalogItem>>('/api/admin/catalog/base-models', { token });
}
export async function createBaseModel(token: string, input: BaseModelUpsertRequest): Promise<BaseModelCatalogItem> {
return request<BaseModelCatalogItem>('/api/admin/catalog/base-models', {
body: input,
method: 'POST',
token,
});
}
export async function updateBaseModel(
token: string,
baseModelId: string,
input: BaseModelUpsertRequest,
): Promise<BaseModelCatalogItem> {
return request<BaseModelCatalogItem>(`/api/admin/catalog/base-models/${baseModelId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function resetBaseModel(token: string, baseModelId: string): Promise<BaseModelCatalogItem> {
return request<BaseModelCatalogItem>(`/api/admin/catalog/base-models/${baseModelId}/reset`, {
method: 'POST',
token,
});
}
export async function resetAllBaseModels(token: string): Promise<ListResponse<BaseModelCatalogItem>> {
return request<ListResponse<BaseModelCatalogItem>>('/api/admin/catalog/base-models/reset-all', {
method: 'POST',
token,
});
}
export async function deleteBaseModel(token: string, baseModelId: string): Promise<void> {
await request<void>(`/api/admin/catalog/base-models/${baseModelId}`, {
method: 'DELETE',
token,
});
}
export async function listPricingRules(token: string): Promise<ListResponse<PricingRule>> {
return request<ListResponse<PricingRule>>('/api/v1/pricing/rules', { token });
return request<ListResponse<PricingRule>>('/api/admin/pricing/rules', { token });
}
export async function listPricingRuleSets(token: string): Promise<ListResponse<PricingRuleSet>> {
return request<ListResponse<PricingRuleSet>>('/api/admin/pricing/rule-sets', { token });
}
export async function createPricingRuleSet(
token: string,
input: PricingRuleSetUpsertRequest,
): Promise<PricingRuleSet> {
return request<PricingRuleSet>('/api/admin/pricing/rule-sets', {
body: input,
method: 'POST',
token,
});
}
export async function updatePricingRuleSet(
token: string,
ruleSetId: string,
input: PricingRuleSetUpsertRequest,
): Promise<PricingRuleSet> {
return request<PricingRuleSet>(`/api/admin/pricing/rule-sets/${ruleSetId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deletePricingRuleSet(token: string, ruleSetId: string): Promise<void> {
await request<void>(`/api/admin/pricing/rule-sets/${ruleSetId}`, {
method: 'DELETE',
token,
});
}
export async function listRuntimePolicySets(token: string): Promise<ListResponse<RuntimePolicySet>> {
return request<ListResponse<RuntimePolicySet>>('/api/admin/runtime/policy-sets', { token });
}
export async function createRuntimePolicySet(
token: string,
input: RuntimePolicySetUpsertRequest,
): Promise<RuntimePolicySet> {
return request<RuntimePolicySet>('/api/admin/runtime/policy-sets', {
body: input,
method: 'POST',
token,
});
}
export async function updateRuntimePolicySet(
token: string,
policySetId: string,
input: RuntimePolicySetUpsertRequest,
): Promise<RuntimePolicySet> {
return request<RuntimePolicySet>(`/api/admin/runtime/policy-sets/${policySetId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteRuntimePolicySet(token: string, policySetId: string): Promise<void> {
await request<void>(`/api/admin/runtime/policy-sets/${policySetId}`, {
method: 'DELETE',
token,
});
}
export async function listTenants(token: string): Promise<ListResponse<GatewayTenant>> {
return request<ListResponse<GatewayTenant>>('/api/admin/tenants', { token });
}
export async function createTenant(token: string, input: GatewayTenantUpsertRequest): Promise<GatewayTenant> {
return request<GatewayTenant>('/api/admin/tenants', {
body: input,
method: 'POST',
token,
});
}
export async function updateTenant(token: string, tenantId: string, input: GatewayTenantUpsertRequest): Promise<GatewayTenant> {
return request<GatewayTenant>(`/api/admin/tenants/${tenantId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteTenant(token: string, tenantId: string): Promise<void> {
await request<void>(`/api/admin/tenants/${tenantId}`, {
method: 'DELETE',
token,
});
}
export async function listUsers(token: string): Promise<ListResponse<GatewayUser>> {
return request<ListResponse<GatewayUser>>('/api/admin/users', { token });
}
export async function createGatewayUser(token: string, input: GatewayUserUpsertRequest): Promise<GatewayUser> {
return request<GatewayUser>('/api/admin/users', {
body: input,
method: 'POST',
token,
});
}
export async function updateGatewayUser(token: string, userId: string, input: GatewayUserUpsertRequest): Promise<GatewayUser> {
return request<GatewayUser>(`/api/admin/users/${userId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteGatewayUser(token: string, userId: string): Promise<void> {
await request<void>(`/api/admin/users/${userId}`, {
method: 'DELETE',
token,
});
}
export async function listUserGroups(token: string): Promise<ListResponse<UserGroup>> {
return request<ListResponse<UserGroup>>('/api/admin/user-groups', { token });
}
export async function createUserGroup(token: string, input: UserGroupUpsertRequest): Promise<UserGroup> {
return request<UserGroup>('/api/admin/user-groups', {
body: input,
method: 'POST',
token,
});
}
export async function updateUserGroup(token: string, groupId: string, input: UserGroupUpsertRequest): Promise<UserGroup> {
return request<UserGroup>(`/api/admin/user-groups/${groupId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteUserGroup(token: string, groupId: string): Promise<void> {
await request<void>(`/api/admin/user-groups/${groupId}`, {
method: 'DELETE',
token,
});
}
export async function listAccessRules(token: string): Promise<ListResponse<GatewayAccessRule>> {
return request<ListResponse<GatewayAccessRule>>('/api/admin/access-rules', { token });
}
export async function listApiKeyAccessRules(token: string): Promise<ListResponse<GatewayAccessRule>> {
return request<ListResponse<GatewayAccessRule>>('/api/v1/api-keys/access-rules', { token });
}
export async function createAccessRule(token: string, input: GatewayAccessRuleUpsertRequest): Promise<GatewayAccessRule> {
return request<GatewayAccessRule>('/api/admin/access-rules', {
body: input,
method: 'POST',
token,
});
}
export async function batchAccessRules(token: string, input: GatewayAccessRuleBatchRequest): Promise<ListResponse<GatewayAccessRule>> {
return request<ListResponse<GatewayAccessRule>>('/api/admin/access-rules/batch', {
body: input,
method: 'POST',
token,
});
}
export async function batchApiKeyAccessRules(token: string, input: GatewayAccessRuleBatchRequest): Promise<ListResponse<GatewayAccessRule>> {
return request<ListResponse<GatewayAccessRule>>('/api/v1/api-keys/access-rules/batch', {
body: input,
method: 'POST',
token,
});
}
export async function updateAccessRule(
token: string,
ruleId: string,
input: GatewayAccessRuleUpsertRequest,
): Promise<GatewayAccessRule> {
return request<GatewayAccessRule>(`/api/admin/access-rules/${ruleId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deleteAccessRule(token: string, ruleId: string): Promise<void> {
await request<void>(`/api/admin/access-rules/${ruleId}`, {
method: 'DELETE',
token,
});
}
export async function listApiKeys(token: string): Promise<ListResponse<GatewayApiKey>> {
return request<ListResponse<GatewayApiKey>>('/api/v1/api-keys', { token });
}
export async function listPlayableApiKeys(token: string): Promise<ListResponse<PlayableGatewayApiKey>> {
return request<ListResponse<PlayableGatewayApiKey>>('/api/playground/api-keys', { token });
}
export async function createApiKey(
token: string,
input: { name: string; scopes?: string[]; expiresAt?: string },
): Promise<CreatedGatewayApiKey> {
return request<CreatedGatewayApiKey>('/api/v1/api-keys', {
body: input,
method: 'POST',
token,
});
}
export async function deleteApiKey(token: string, apiKeyId: string): Promise<void> {
await request<void>(`/api/v1/api-keys/${apiKeyId}`, {
method: 'DELETE',
token,
});
}
export async function createPlatform(token: string, input: PlatformCreateInput): Promise<IntegrationPlatform> {
return request<IntegrationPlatform>('/api/admin/platforms', {
body: input,
method: 'POST',
token,
});
}
export async function updatePlatform(token: string, platformId: string, input: PlatformCreateInput): Promise<IntegrationPlatform> {
return request<IntegrationPlatform>(`/api/admin/platforms/${platformId}`, {
body: input,
method: 'PATCH',
token,
});
}
export async function deletePlatform(token: string, platformId: string): Promise<void> {
await request<void>(`/api/admin/platforms/${platformId}`, {
method: 'DELETE',
token,
});
}
export async function createPlatformModel(
token: string,
platformId: string,
input: PlatformModelBindingInput,
): Promise<PlatformModel> {
return request<PlatformModel>(`/api/admin/platforms/${platformId}/models`, {
body: input,
method: 'POST',
token,
});
}
export async function replacePlatformModels(
token: string,
platformId: string,
models: PlatformModelBindingInput[],
): Promise<ListResponse<PlatformModel>> {
return request<ListResponse<PlatformModel>>(`/api/admin/platforms/${platformId}/models`, {
body: { models },
method: 'PUT',
token,
});
}
export async function deletePlatformModel(token: string, modelId: string): Promise<void> {
await request<void>(`/api/admin/platform-models/${modelId}`, {
method: 'DELETE',
token,
});
}
export async function createChatTask(
token: string,
input: { model: string; messages: Array<Record<string, unknown>>; runMode?: string; simulation?: boolean },
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/chat/completions', {
body: input,
method: 'POST',
token,
});
}
export async function streamChatCompletions(
token: string,
input: { model: string; messages: Array<Record<string, unknown>>; simulation?: boolean },
onDelta: (delta: string) => void,
): Promise<void> {
for await (const delta of streamChatCompletionText(token, input)) {
onDelta(delta);
}
}
export async function* streamChatCompletionText(
token: string,
input: { model: string; messages: Array<Record<string, unknown>>; simulation?: boolean },
signal?: AbortSignal,
): AsyncGenerator<string> {
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
body: JSON.stringify({ ...input, stream: true }),
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
method: 'POST',
signal,
});
if (!response.ok) {
const body = await response.text();
throw new Error(parseErrorMessage(body) || `Request failed: ${response.status}`);
}
if (!response.body) {
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const events = buffer.split(/\n\n/);
buffer = events.pop() ?? '';
for (const eventBlock of events) {
const delta = parseSSEBlockDelta(eventBlock);
if (delta) yield delta;
}
}
if (buffer.trim()) {
const delta = parseSSEBlockDelta(buffer);
if (delta) yield delta;
}
}
export async function createImageGenerationTask(
token: string,
input: {
model: string;
prompt: string;
aspect_ratio?: string;
count?: number;
height?: number;
n?: number;
quality?: string;
resolution?: string;
runMode?: string;
simulation?: boolean;
size?: string;
width?: number;
},
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/images/generations', {
body: input,
method: 'POST',
token,
});
}
export async function createImageEditTask(
token: string,
input: { model: string; prompt: string; image?: string; mask?: string; runMode?: string; simulation?: boolean },
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/images/edits', {
body: input,
method: 'POST',
token,
});
}
export async function createVideoGenerationTask(
token: string,
input: {
model: string;
prompt: string;
aspect_ratio?: string;
count?: number;
height?: number;
n?: number;
resolution?: string;
runMode?: string;
simulation?: boolean;
size?: string;
width?: number;
},
): Promise<{ task: GatewayTask; next: Record<string, string> }> {
return request<{ task: GatewayTask; next: Record<string, string> }>('/api/v1/videos/generations', {
body: input,
method: 'POST',
token,
});
}
export async function estimatePricing(
token: string,
input: Record<string, unknown>,
): Promise<{ items: unknown[]; resolver: string }> {
return request<{ items: unknown[]; resolver: string }>('/api/v1/pricing/estimate', {
body: input,
method: 'POST',
token,
});
}
export async function getTask(token: string, taskId: string): Promise<GatewayTask> {
return request<GatewayTask>(`/api/v1/tasks/${taskId}`, { token });
}
export function resolveApiAssetUrl(src: string) {
if (/^(https?:|data:|blob:)/i.test(src)) return src;
return `${API_BASE}${src.startsWith('/') ? src : `/${src}`}`;
}
export async function listRateLimitWindows(token: string): Promise<ListResponse<RateLimitWindow>> {
return request<ListResponse<RateLimitWindow>>('/api/v1/runtime/rate-limit-windows', { token });
return request<ListResponse<RateLimitWindow>>('/api/admin/runtime/rate-limit-windows', { token });
}
async function request<T>(path: string, options: { token?: string; auth?: boolean } = {}): Promise<T> {
async function request<T>(
path: string,
options: { token?: string; auth?: boolean; method?: string; body?: unknown } = {},
): Promise<T> {
const headers: Record<string, string> = {};
if (options.auth !== false && options.token) {
headers.Authorization = `Bearer ${options.token}`;
}
if (options.body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(`${API_BASE}${path}`, {
method: options.method ?? 'GET',
headers,
body: options.body === undefined ? undefined : JSON.stringify(options.body),
});
if (!response.ok) {
const body = await response.text();
throw new Error(body || `Request failed: ${response.status}`);
throw new Error(parseErrorMessage(body) || `Request failed: ${response.status}`);
}
if (response.status === 204) {
return undefined as T;
}
return response.json() as Promise<T>;
}
function parseErrorMessage(body: string) {
if (!body) {
return '';
}
try {
const parsed = JSON.parse(body) as { error?: { message?: string } };
return parsed.error?.message ?? body;
} catch {
return body;
}
}
function parseSSEBlockDelta(block: string) {
const data = block
.split(/\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => line.replace(/^data:\s?/, ''))
.join('\n')
.trim();
if (!data || data === '[DONE]') return '';
try {
const parsed = JSON.parse(data) as {
choices?: Array<{ delta?: { content?: string }; message?: { content?: string } }>;
delta?: string;
output_text?: string;
};
return parsed.choices?.[0]?.delta?.content ?? parsed.delta ?? parsed.output_text ?? '';
} catch {
return data;
}
}
+39
View File
@@ -0,0 +1,39 @@
import type {
BaseModelCatalogItem,
CatalogProvider,
GatewayAccessRule,
GatewayApiKey,
GatewayTask,
GatewayTenant,
GatewayUser,
IntegrationPlatform,
PlatformModel,
PricingRule,
PricingRuleSet,
RateLimitWindow,
RuntimePolicySet,
UserGroup,
} from '@easyai-ai-gateway/contracts';
export interface ConsoleData {
accessRules: GatewayAccessRule[];
apiKeys: GatewayApiKey[];
baseModels: BaseModelCatalogItem[];
models: PlatformModel[];
platforms: IntegrationPlatform[];
pricingRules: PricingRule[];
pricingRuleSets: PricingRuleSet[];
providers: CatalogProvider[];
rateLimitWindows: RateLimitWindow[];
runtimePolicySets: RuntimePolicySet[];
taskResult: GatewayTask | null;
tenants: GatewayTenant[];
userGroups: UserGroup[];
users: GatewayUser[];
}
export interface StatItem {
label: string;
value: number | string;
tone: 'blue' | 'green' | 'violet' | 'amber' | 'cyan' | 'rose' | 'slate';
}
+134
View File
@@ -0,0 +1,134 @@
import type { FormEvent } from 'react';
import { LogIn, UserPlus } from 'lucide-react';
import { Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Tabs } from './ui';
import type { AuthMode, LoadState, LoginForm, RegisterForm } from '../types';
const tabs = [
{ value: 'login', label: '账号登录' },
{ value: 'register', label: '注册账号' },
{ value: 'external', label: '外部 Token' },
] satisfies Array<{ value: AuthMode; label: string }>;
export function AuthPanel(props: {
authMode: AuthMode;
externalToken: string;
loginForm: LoginForm;
registerForm: RegisterForm;
state: LoadState;
onAuthModeChange: (value: AuthMode) => void;
onExternalTokenChange: (value: string) => void;
onLoginChange: (value: LoginForm) => void;
onRegisterChange: (value: RegisterForm) => void;
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<section className="authShell" aria-label="登录">
<Card className="authCard">
<CardHeader>
<div>
<p className="eyebrow">Gateway Identity</p>
<CardTitle> AI Gateway</CardTitle>
</div>
</CardHeader>
<CardContent className="authContent">
<Tabs value={props.authMode} tabs={tabs} onValueChange={props.onAuthModeChange} />
{props.authMode === 'login' && <LoginFormView {...props} />}
{props.authMode === 'register' && <RegisterFormView {...props} />}
{props.authMode === 'external' && <ExternalTokenForm {...props} />}
</CardContent>
</Card>
</section>
);
}
function LoginFormView(props: {
loginForm: LoginForm;
state: LoadState;
onLoginChange: (value: LoginForm) => void;
onSubmitLogin: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid" onSubmit={props.onSubmitLogin}>
<Label>
<Input
autoComplete="username"
value={props.loginForm.account}
onChange={(event) => props.onLoginChange({ ...props.loginForm, account: event.target.value })}
placeholder="用户名或邮箱"
/>
</Label>
<Label>
<Input
autoComplete="current-password"
type="password"
value={props.loginForm.password}
onChange={(event) => props.onLoginChange({ ...props.loginForm, password: event.target.value })}
placeholder="至少 8 位"
/>
</Label>
<Button type="submit" disabled={props.state === 'loading'}>
<LogIn size={15} />
{props.state === 'loading' ? '登录中' : '登录'}
</Button>
</form>
);
}
function RegisterFormView(props: {
registerForm: RegisterForm;
state: LoadState;
onRegisterChange: (value: RegisterForm) => void;
onSubmitRegister: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid two" onSubmit={props.onSubmitRegister}>
<Label>
<Input autoComplete="username" value={props.registerForm.username} onChange={(event) => props.onRegisterChange({ ...props.registerForm, username: event.target.value })} />
</Label>
<Label>
<Input autoComplete="email" type="email" value={props.registerForm.email} onChange={(event) => props.onRegisterChange({ ...props.registerForm, email: event.target.value })} />
</Label>
<Label>
<Input value={props.registerForm.displayName} onChange={(event) => props.onRegisterChange({ ...props.registerForm, displayName: event.target.value })} />
</Label>
<Label>
<Input autoComplete="new-password" type="password" value={props.registerForm.password} onChange={(event) => props.onRegisterChange({ ...props.registerForm, password: event.target.value })} />
</Label>
<Label>
<Input value={props.registerForm.invitationCode} onChange={(event) => props.onRegisterChange({ ...props.registerForm, invitationCode: event.target.value })} placeholder="可选" />
</Label>
<Button type="submit" disabled={props.state === 'loading'} className="spanTwo">
<UserPlus size={15} />
{props.state === 'loading' ? '注册中' : '注册并登录'}
</Button>
</form>
);
}
function ExternalTokenForm(props: {
externalToken: string;
state: LoadState;
onExternalTokenChange: (value: string) => void;
onSubmitExternalToken: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="formGrid" onSubmit={props.onSubmitExternalToken}>
<Label>
Access Token
<Input value={props.externalToken} onChange={(event) => props.onExternalTokenChange(event.target.value)} placeholder="粘贴 server-main access token" />
</Label>
<Button type="submit" disabled={props.state === 'loading'}>
{props.state === 'loading' ? '验证中' : '进入控制台'}
</Button>
</form>
);
}
+171
View File
@@ -0,0 +1,171 @@
import type { FormEvent } from 'react';
import type { GatewayApiKey, GatewayTask } from '@easyai-ai-gateway/contracts';
import type { LoadState, TaskForm } from '../types';
const taskKindOptions = [
['chat.completions', 'Chat'],
['images.generations', '生图'],
['images.edits', '图像编辑'],
] as const;
export function CoreFlowPanel(props: {
apiKeyForm: { name: string };
apiKeys: GatewayApiKey[];
apiKeySecret: string;
coreMessage: string;
coreState: LoadState;
platformForm: { provider: string; platformKey: string; name: string; baseUrl: string };
taskForm: TaskForm;
taskResult: GatewayTask | null;
onAPIKeyFormChange: (value: { name: string }) => void;
onPlatformFormChange: (value: { provider: string; platformKey: string; name: string; baseUrl: string }) => void;
onSubmitAPIKey: (event: FormEvent<HTMLFormElement>) => void;
onSubmitPlatform: (event: FormEvent<HTMLFormElement>) => void;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void;
}) {
return (
<section className="corePanel" aria-label="核心链路验证">
<div className="sectionHeader">
<div>
<p className="eyebrow">Smoke Flow</p>
<h2></h2>
</div>
<span>{props.coreState === 'loading' ? '运行中' : '本地闭环'}</span>
</div>
<div className="coreGrid">
<ApiKeyForm {...props} />
<PlatformForm {...props} />
<TaskSmokeForm {...props} />
</div>
{props.coreMessage && (
<p className="coreMessage" data-error={props.coreState === 'error'}>
{props.coreMessage}
</p>
)}
</section>
);
}
function ApiKeyForm(props: {
apiKeyForm: { name: string };
apiKeys: GatewayApiKey[];
apiKeySecret: string;
coreState: LoadState;
onAPIKeyFormChange: (value: { name: string }) => void;
onSubmitAPIKey: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitAPIKey}>
<h3>1. API Key</h3>
<label>
<span></span>
<input value={props.apiKeyForm.name} onChange={(event) => props.onAPIKeyFormChange({ name: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
API Key
</button>
<p className="formHint"> {props.apiKeys.length} Key</p>
{props.apiKeySecret && <code className="secretBox">{props.apiKeySecret}</code>}
</form>
);
}
function PlatformForm(props: {
coreState: LoadState;
platformForm: { provider: string; platformKey: string; name: string; baseUrl: string };
onPlatformFormChange: (value: { provider: string; platformKey: string; name: string; baseUrl: string }) => void;
onSubmitPlatform: (event: FormEvent<HTMLFormElement>) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitPlatform}>
<h3>2. </h3>
<label>
<span>Provider</span>
<input value={props.platformForm.provider} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, provider: event.target.value })} />
</label>
<label>
<span> Key</span>
<input value={props.platformForm.platformKey} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, platformKey: event.target.value })} />
</label>
<label>
<span></span>
<input value={props.platformForm.name} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, name: event.target.value })} />
</label>
<label>
<span>Base URL</span>
<input value={props.platformForm.baseUrl} onChange={(event) => props.onPlatformFormChange({ ...props.platformForm, baseUrl: event.target.value })} />
</label>
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
</form>
);
}
function TaskSmokeForm(props: {
coreState: LoadState;
taskForm: TaskForm;
taskResult: GatewayTask | null;
onSubmitTask: (event: FormEvent<HTMLFormElement>) => void;
onTaskFormChange: (value: TaskForm) => void;
}) {
return (
<form className="inlineForm" onSubmit={props.onSubmitTask}>
<h3>3. Phase 1 </h3>
<label>
<span></span>
<select value={props.taskForm.kind} onChange={(event) => props.onTaskFormChange(defaultTaskForKind(event.target.value as TaskForm['kind'], props.taskForm))}>
{taskKindOptions.map(([value, label]) => (
<option value={value} key={value}>
{label}
</option>
))}
</select>
</label>
<label>
<span></span>
<input value={props.taskForm.model} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, model: event.target.value })} />
</label>
<label>
<span>Prompt</span>
<input value={props.taskForm.prompt} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, prompt: event.target.value })} />
</label>
{props.taskForm.kind === 'images.edits' && (
<>
<label>
<span> URL</span>
<input value={props.taskForm.image ?? ''} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, image: event.target.value })} />
</label>
<label>
<span>Mask URL</span>
<input value={props.taskForm.mask ?? ''} onChange={(event) => props.onTaskFormChange({ ...props.taskForm, mask: event.target.value })} />
</label>
</>
)}
<button type="submit" disabled={props.coreState === 'loading'}>
</button>
{props.taskResult && (
<div className="resultBox">
<div>
<span className="statusPill">{props.taskResult.status}</span>
<strong>{props.taskResult.model}</strong>
</div>
<pre>{JSON.stringify(props.taskResult.result ?? {}, null, 2)}</pre>
</div>
)}
</form>
);
}
function defaultTaskForKind(kind: TaskForm['kind'], current: TaskForm): TaskForm {
if (kind === 'chat.completions') {
return { ...current, kind, model: 'gpt-4o-mini' };
}
if (kind === 'images.edits') {
return { ...current, kind, model: 'gpt-image-1', image: current.image ?? 'https://example.com/source.png', mask: current.mask ?? 'https://example.com/mask.png' };
}
return { ...current, kind, model: 'gpt-image-1' };
}
+97
View File
@@ -0,0 +1,97 @@
import type { BaseModelCatalogItem, IntegrationPlatform, PlatformModel, RateLimitWindow } from '@easyai-ai-gateway/contracts';
import { adminPages, apiDocPages, primaryModules, workspacePages } from '../navigation';
import { DataPanel } from './DataPanel';
import { ModuleList } from './ModuleList';
import { baseModelTypeText } from '../pages/admin/platform-form';
export function Dashboard(props: {
baseModels: BaseModelCatalogItem[];
models: PlatformModel[];
platforms: IntegrationPlatform[];
rateLimitWindows: RateLimitWindow[];
stats: Array<{ label: string; value: number; tone: string }>;
}) {
return (
<>
<section className="moduleBand" aria-label="一级页面">
<div className="sectionHeader">
<div>
<p className="eyebrow">Navigation</p>
<h2></h2>
</div>
<span>5 </span>
</div>
<div className="moduleGrid">
{primaryModules.map((item) => (
<article className="moduleCard" key={item.path}>
<div className="moduleCardTop">
<h3>{item.title}</h3>
<span>{item.path}</span>
</div>
<p>{item.description}</p>
<div className="moduleTags">
{item.items.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
</article>
))}
</div>
</section>
<section className="moduleBand" aria-label="工作台与文档">
<div className="sectionHeader">
<div>
<p className="eyebrow">Workspace</p>
<h2> API </h2>
</div>
<span></span>
</div>
<div className="detailGrid">
<ModuleList title="用户工作台" items={workspacePages} />
<ModuleList title="管理工作台" items={adminPages} />
<ModuleList title="API 文档" items={apiDocPages} />
</div>
</section>
<section className="metrics" aria-label="概览">
{props.stats.map((item) => (
<div className="metric" data-tone={item.tone} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</section>
<section className="split">
<DataPanel
columns={['Provider', '名称', '状态', '优先级']}
empty="暂无平台数据"
rows={props.platforms.map((item) => [item.provider, item.internalName || item.name, item.status, String(item.priority)])}
title="平台"
/>
<DataPanel
columns={['模型', '类型', '平台', '启用']}
empty="暂无模型数据"
rows={props.models.map((item) => [item.modelName, item.modelType, item.provider ?? item.platformName ?? '-', item.enabled ? '是' : '否'])}
title="模型"
/>
</section>
<section className="split secondary">
<DataPanel
columns={['Provider', '模型', '类型', '版本']}
empty="暂无基准模型"
rows={props.baseModels.map((item) => [item.providerKey, item.canonicalModelKey, baseModelTypeText(item), String(item.pricingVersion)])}
title="基准模型库"
/>
<DataPanel
columns={['Scope', '指标', '使用', '预占']}
empty="暂无限流窗口"
rows={props.rateLimitWindows.map((item) => [item.scopeKey, item.metric, `${item.usedValue}/${item.limitValue}`, String(item.reservedValue)])}
title="TPM/RPM 窗口"
/>
</section>
</>
);
}
+25
View File
@@ -0,0 +1,25 @@
export function DataPanel(props: { columns: string[]; empty: string; rows: string[][]; title: string }) {
return (
<div className="panel">
<div className="panelHeader">
<h2>{props.title}</h2>
<span>{props.rows.length}</span>
</div>
<div className="table" role="table">
<div className="row head" role="row">
{props.columns.map((column) => (
<span key={column}>{column}</span>
))}
</div>
{props.rows.map((row, index) => (
<div className="row" role="row" key={`${props.title}-${index}`}>
{row.map((cell, cellIndex) => (
<span key={`${props.title}-${index}-${cellIndex}`}>{cell}</span>
))}
</div>
))}
{!props.rows.length && <p className="empty">{props.empty}</p>}
</div>
</div>
);
}
+25
View File
@@ -0,0 +1,25 @@
import { EmptyState, Table, TableCell, TableHead, TableRow } from './ui';
export function EntityTable(props: {
columns: string[];
empty: string;
rows: Array<Array<string | number>>;
}) {
return (
<Table>
<TableRow className="shTableHeader">
{props.columns.map((column) => (
<TableHead key={column}>{column}</TableHead>
))}
</TableRow>
{props.rows.map((row, index) => (
<TableRow key={index}>
{row.map((cell, cellIndex) => (
<TableCell key={`${index}-${cellIndex}`}>{cell}</TableCell>
))}
</TableRow>
))}
{!props.rows.length && <EmptyState title={props.empty} />}
</Table>
);
}
@@ -0,0 +1,14 @@
import { AuthPanel } from './AuthPanel';
export function LoginRequiredPanel(props: Parameters<typeof AuthPanel>[0]) {
return (
<div className="loginRequiredPage">
<div className="loginRequiredCopy">
<p className="eyebrow">Identity</p>
<h1></h1>
<p>API Key server-main token</p>
</div>
<AuthPanel {...props} />
</div>
);
}
+19
View File
@@ -0,0 +1,19 @@
export function ModuleList(props: {
title: string;
items: Array<{ title: string; path: string; description: string }>;
}) {
return (
<div className="moduleList">
<h3>{props.title}</h3>
{props.items.map((item) => (
<div className="moduleRow" key={item.path}>
<div>
<strong>{item.title}</strong>
<p>{item.description}</p>
</div>
<span>{item.path}</span>
</div>
))}
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from 'react';
export function PageHeader(props: { eyebrow: string; title: string; description?: string; action?: ReactNode }) {
return (
<div className="pageHeader">
<div>
<p className="eyebrow">{props.eyebrow}</p>
<h1>{props.title}</h1>
{props.description && <p>{props.description}</p>}
</div>
{props.action && <div className="pageHeaderAction">{props.action}</div>}
</div>
);
}
+14
View File
@@ -0,0 +1,14 @@
import type { StatItem } from '../app-state';
export function StatGrid(props: { items: StatItem[] }) {
return (
<section className="statGrid" aria-label="统计">
{props.items.map((item) => (
<div className="statCard" data-tone={item.tone} key={item.label}>
<span>{item.label}</span>
<strong>{item.value}</strong>
</div>
))}
</section>
);
}
@@ -0,0 +1,83 @@
import type { ReactNode } from 'react';
import { BookOpen, Boxes, Home, RefreshCw, ShieldCheck, Sparkles, UserCircle } from 'lucide-react';
import type { HealthResponse } from '../../api';
import type { LoadState, PageKey } from '../../types';
import { Button, Badge } from '../ui';
const navItems: Array<{ key: PageKey; label: string; icon: ReactNode }> = [
{ key: 'home', label: '首页', icon: <Home size={17} /> },
{ key: 'playground', label: '在线测试', icon: <Sparkles size={17} /> },
{ key: 'models', label: '模型', icon: <Boxes size={17} /> },
{ key: 'workspace', label: '用户工作台', icon: <UserCircle size={17} /> },
{ key: 'admin', label: '管理工作台', icon: <ShieldCheck size={17} /> },
{ key: 'docs', label: 'API 文档', icon: <BookOpen size={17} /> },
];
export function AppShell(props: {
activePage: PageKey;
children: ReactNode;
health: HealthResponse | null;
isAuthenticated: boolean;
state: LoadState;
onNavigate: (page: PageKey) => void;
onLogin: () => void;
onRefresh: () => void;
onSignOut: () => void;
}) {
return (
<div className="appShell" data-page={props.activePage}>
<header className="appTopbar">
<div className="brandBlock">
<div className="brandMark">AI</div>
<div>
<strong>EasyAI Gateway</strong>
<span>Console</span>
</div>
</div>
<nav className="topNav" aria-label="主导航">
{navItems.map((item) => (
<button
type="button"
className="topNavItem"
data-active={props.activePage === item.key}
key={item.key}
onClick={() => props.onNavigate(item.key)}
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</nav>
<div className="topbarActions">
<div className="health" data-ok={props.health?.ok === true}>
<span />
{props.health?.identityMode ? `${props.health.service} · ${props.health.identityMode}` : props.health?.service ?? 'API 未连接'}
</div>
{props.isAuthenticated ? (
<>
<Button type="button" variant="outline" size="sm" onClick={props.onRefresh} disabled={props.state === 'loading'}>
<RefreshCw size={15} />
{props.state === 'loading' ? '刷新中' : '刷新'}
</Button>
<Button type="button" variant="ghost" size="sm" onClick={props.onSignOut}>
退
</Button>
</>
) : (
<Button type="button" size="sm" onClick={props.onLogin}>
</Button>
)}
</div>
</header>
<div className="workspaceShell">
<main className="contentShell" data-page={props.activePage}>
{props.state === 'error' && <Badge variant="destructive"></Badge>}
{props.children}
</main>
</div>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const badgeVariants = cva('shBadge', {
variants: {
variant: {
default: 'shBadgeDefault',
secondary: 'shBadgeSecondary',
outline: 'shBadgeOutline',
success: 'shBadgeSuccess',
warning: 'shBadgeWarning',
destructive: 'shBadgeDestructive',
},
},
defaultVariants: {
variant: 'default',
},
});
export function Badge(props: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>) {
const { className, variant, ...rest } = props;
return <div className={cn(badgeVariants({ variant, className }))} {...rest} />;
}
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const buttonVariants = cva('shButton', {
variants: {
variant: {
default: 'shButtonDefault',
secondary: 'shButtonSecondary',
outline: 'shButtonOutline',
ghost: 'shButtonGhost',
destructive: 'shButtonDestructive',
},
size: {
xs: 'shButtonXs',
sm: 'shButtonSm',
md: 'shButtonMd',
default: 'shButtonMd',
lg: 'shButtonLg',
xl: 'shButtonXl',
icon: 'shButtonIcon',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
});
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;
},
);
Button.displayName = 'Button';
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react';
import { DayPicker, type ChevronProps, type DayPickerProps } from 'react-day-picker';
import { cn } from '../../lib/utils';
export function Calendar(props: DayPickerProps) {
const { className, classNames, showOutsideDays = true, ...rest } = props;
return (
<DayPicker
className={cn('shCalendar', className)}
classNames={{
root: 'shCalendarRoot',
months: 'shCalendarMonths',
month: 'shCalendarMonth',
month_caption: 'shCalendarCaption',
caption_label: 'shCalendarCaptionLabel',
nav: 'shCalendarNav',
button_previous: 'shCalendarNavButton',
button_next: 'shCalendarNavButton',
chevron: 'shCalendarChevron',
month_grid: 'shCalendarGrid',
weekdays: 'shCalendarWeekdays',
weekday: 'shCalendarWeekday',
weeks: 'shCalendarWeeks',
week: 'shCalendarWeek',
day: 'shCalendarDay',
day_button: 'shCalendarDayButton',
outside: 'shCalendarDayOutside',
disabled: 'shCalendarDayDisabled',
selected: 'shCalendarDaySelected',
today: 'shCalendarDayToday',
hidden: 'shCalendarDayHidden',
...classNames,
}}
components={{
Chevron: CalendarChevron,
...props.components,
}}
showOutsideDays={showOutsideDays}
{...rest}
/>
);
}
function CalendarChevron(props: ChevronProps) {
const Icon = props.orientation === 'left'
? ChevronLeft
: props.orientation === 'right'
? ChevronRight
: props.orientation === 'up'
? ChevronUp
: ChevronDown;
return <Icon className={cn('shCalendarChevronIcon', props.className)} size={props.size ?? 16} />;
}
+32
View File
@@ -0,0 +1,32 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('shCard', className)} {...props} />,
);
Card.displayName = 'Card';
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('shCardHeader', className)} {...props} />,
);
CardHeader.displayName = 'CardHeader';
export const CardTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => <h3 ref={ref} className={cn('shCardTitle', className)} {...props} />,
);
CardTitle.displayName = 'CardTitle';
export const CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
({ className, ...props }, ref) => <p ref={ref} className={cn('shCardDescription', className)} {...props} />,
);
CardDescription.displayName = 'CardDescription';
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('shCardContent', className)} {...props} />,
);
CardContent.displayName = 'CardContent';
export const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => <div ref={ref} className={cn('shCardFooter', className)} {...props} />,
);
CardFooter.displayName = 'CardFooter';
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon } from 'lucide-react';
import { cn } from '../../lib/utils';
export const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer border-input data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive h-3.5 w-3.5 shrink-0 rounded-[3px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-primary-foreground transition-none">
<CheckIcon className="h-3 w-3" strokeWidth={2.5} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
@@ -0,0 +1,62 @@
import * as React from 'react';
import { AlertTriangle } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Button } from './button';
export interface ConfirmDialogProps {
cancelLabel?: string;
children?: React.ReactNode;
className?: string;
confirmLabel?: string;
confirmVariant?: 'default' | 'destructive';
description?: string;
loading?: boolean;
open: boolean;
title: string;
onCancel: () => void;
onConfirm: () => void | Promise<void>;
}
export function ConfirmDialog(props: ConfirmDialogProps) {
const { onCancel, open } = props;
React.useEffect(() => {
if (!open) return undefined;
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') onCancel();
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [onCancel, open]);
if (!open) return null;
return (
<div className="confirmDialogBackdrop" role="presentation">
<section className={cn('confirmDialog', props.className)} role="alertdialog" aria-modal="true" aria-labelledby="confirm-dialog-title">
<div className="confirmDialogIcon">
<AlertTriangle size={18} />
</div>
<div className="confirmDialogBody">
<strong id="confirm-dialog-title">{props.title}</strong>
{props.description && <p>{props.description}</p>}
{props.children}
</div>
<footer className="confirmDialogActions">
<Button type="button" variant="outline" size="sm" disabled={props.loading} onClick={props.onCancel}>
{props.cancelLabel ?? '取消'}
</Button>
<Button
type="button"
variant={props.confirmVariant ?? 'destructive'}
size="sm"
disabled={props.loading}
onClick={() => void props.onConfirm()}
>
{props.confirmLabel ?? '确认'}
</Button>
</footer>
</section>
</div>
);
}
@@ -0,0 +1,87 @@
import { useMemo, useState } from 'react';
import { format } from 'date-fns';
import { CalendarIcon, X } from 'lucide-react';
import { Button } from './button';
import { Calendar } from './calendar';
import { Input } from './input';
import { Label } from './label';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
export function DateTimePicker(props: {
disabled?: boolean;
placeholder?: string;
value: string;
onChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
const selectedDate = useMemo(() => parseLocalDateTime(props.value), [props.value]);
const timeValue = selectedDate ? formatTimeValue(selectedDate) : '';
function updateDate(nextDate: Date | undefined) {
if (!nextDate) return;
const current = selectedDate ?? new Date();
const next = new Date(nextDate);
next.setHours(current.getHours(), current.getMinutes(), 0, 0);
props.onChange(formatLocalDateTime(next));
}
function updateTime(value: string) {
const [hours, minutes] = value.split(':').map((item) => Number(item));
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return;
const next = selectedDate ? new Date(selectedDate) : new Date();
next.setHours(hours, minutes, 0, 0);
props.onChange(formatLocalDateTime(next));
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
className="dateTimePickerTrigger"
data-empty={!selectedDate}
disabled={props.disabled}
type="button"
variant="outline"
>
<CalendarIcon size={15} />
{selectedDate ? format(selectedDate, 'yyyy-MM-dd HH:mm') : <span>{props.placeholder ?? '选择日期时间'}</span>}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="dateTimePickerPopover">
<Calendar mode="single" selected={selectedDate} onSelect={updateDate} />
<div className="dateTimePickerFooter">
<Label>
<Input type="time" value={timeValue} onChange={(event) => updateTime(event.target.value)} />
</Label>
<Button type="button" variant="ghost" size="icon" title="清除有效期" disabled={!props.value} onClick={() => props.onChange('')}>
<X size={14} />
</Button>
</div>
</PopoverContent>
</Popover>
);
}
function parseLocalDateTime(value: string) {
if (!value) return undefined;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
function formatLocalDateTime(date: Date) {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
function formatTimeValue(date: Date) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function pad(value: number) {
return String(value).padStart(2, '0');
}
+53
View File
@@ -0,0 +1,53 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
import { Button } from './button';
export interface FormDialogProps {
ariaLabel?: string;
bodyClassName?: string;
children: React.ReactNode;
className?: string;
closeLabel?: string;
eyebrow?: string;
footer: React.ReactNode;
formClassName?: string;
open: boolean;
title: string;
onClose: () => void;
onSubmit: React.FormEventHandler<HTMLFormElement>;
}
export function FormDialog(props: FormDialogProps) {
const { onClose, open } = props;
React.useEffect(() => {
if (!open) return undefined;
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose();
}
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [onClose, open]);
if (!open) return null;
const closeLabel = props.closeLabel ?? '关闭';
return (
<div className="formDialogBackdrop" role="presentation">
<div className={cn('formDialog', props.className)} role="dialog" aria-modal="true" aria-label={props.ariaLabel ?? props.title}>
<header className="formDialogHeader">
<div>
{props.eyebrow && <span>{props.eyebrow}</span>}
<strong>{props.title}</strong>
</div>
<Button type="button" variant="ghost" size="sm" onClick={props.onClose}>{closeLabel}</Button>
</header>
<form className={cn('formDialogForm', props.formClassName)} onSubmit={props.onSubmit}>
<div className={cn('formDialogBody', props.bodyClassName)}>{props.children}</div>
<div className="formDialogActions">{props.footer}</div>
</form>
</div>
</div>
);
}
+17
View File
@@ -0,0 +1,17 @@
import type { ReactNode } from 'react';
import { cn } from '../../lib/utils';
export function FormItem(props: {
children: ReactNode;
className?: string;
description?: ReactNode;
label: ReactNode;
}) {
return (
<label className={cn('shFormItem', props.className)}>
<span className="shFormItemLabel">{props.label}</span>
{props.children}
{props.description ? <span className="shFormItemDescription">{props.description}</span> : null}
</label>
);
}
+18
View File
@@ -0,0 +1,18 @@
export * from './badge';
export * from './button';
export * from './calendar';
export * from './card';
export * from './checkbox';
export * from './confirm-dialog';
export * from './date-time-picker';
export * from './dialog';
export * from './form-item';
export * from './input';
export * from './label';
export * from './message';
export * from './popover';
export * from './select';
export * from './separator';
export * from './table';
export * from './tabs';
export * from './textarea';
+28
View File
@@ -0,0 +1,28 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const inputVariants = cva('shInput', {
variants: {
size: {
xs: 'shControlXs',
sm: 'shControlSm',
md: 'shControlMd',
lg: 'shControlLg',
xl: 'shControlXl',
},
},
defaultVariants: {
size: 'md',
},
});
export interface InputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>,
VariantProps<typeof inputVariants> {}
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, size, ...props }, ref) => <input ref={ref} className={cn(inputVariants({ size, className }))} {...props} />,
);
Input.displayName = 'Input';
+8
View File
@@ -0,0 +1,8 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
({ className, ...props }, ref) => <label ref={ref} className={cn('shLabel', className)} {...props} />,
);
Label.displayName = 'Label';
+47
View File
@@ -0,0 +1,47 @@
import * as React from 'react';
import { AlertCircle, CheckCircle2, Info, X } from 'lucide-react';
import { cn } from '../../lib/utils';
import { Button } from './button';
export type ScreenMessageVariant = 'info' | 'success' | 'error';
export interface ScreenMessageProps {
className?: string;
duration?: number;
message: string;
open?: boolean;
variant?: ScreenMessageVariant;
onClose?: () => void;
}
const iconMap: Record<ScreenMessageVariant, React.ReactNode> = {
error: <AlertCircle size={16} />,
info: <Info size={16} />,
success: <CheckCircle2 size={16} />,
};
export function ScreenMessage(props: ScreenMessageProps) {
const { duration = 3600, message, onClose, open = Boolean(message), variant = 'info' } = props;
React.useEffect(() => {
if (!open || !message || duration <= 0 || !onClose) return undefined;
const timer = window.setTimeout(onClose, duration);
return () => window.clearTimeout(timer);
}, [duration, message, onClose, open]);
if (!open || !message) return null;
return (
<div className="screenMessageViewport" role="presentation">
<div className={cn('screenMessage', `screenMessage-${variant}`, props.className)} role="alert">
<span className="screenMessageIcon">{iconMap[variant]}</span>
<span className="screenMessageText">{message}</span>
{onClose && (
<Button type="button" variant="ghost" size="icon" className="screenMessageClose" aria-label="关闭提示" onClick={onClose}>
<X size={14} />
</Button>
)}
</div>
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../../lib/utils';
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverAnchor = PopoverPrimitive.Anchor;
export const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ align = 'center', className, sideOffset = 6, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align={align}
className={cn('shPopoverContent', className)}
ref={ref}
sideOffset={sideOffset}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
+28
View File
@@ -0,0 +1,28 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const selectVariants = cva('shInput shSelect', {
variants: {
size: {
xs: 'shControlXs',
sm: 'shControlSm',
md: 'shControlMd',
lg: 'shControlLg',
xl: 'shControlXl',
},
},
defaultVariants: {
size: 'md',
},
});
export interface SelectProps
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'>,
VariantProps<typeof selectVariants> {}
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
({ className, size, ...props }, ref) => <select ref={ref} className={cn(selectVariants({ size, className }))} {...props} />,
);
Select.displayName = 'Select';
+7
View File
@@ -0,0 +1,7 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export function Separator(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shSeparator', className)} {...rest} />;
}
+31
View File
@@ -0,0 +1,31 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export function Table(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTable', className)} role="table" {...rest} />;
}
export function TableRow(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTableRow', className)} role="row" {...rest} />;
}
export function TableHead(props: React.HTMLAttributes<HTMLSpanElement>) {
const { className, ...rest } = props;
return <span className={cn('shTableHead', className)} role="columnheader" {...rest} />;
}
export function TableCell(props: React.HTMLAttributes<HTMLSpanElement>) {
const { className, ...rest } = props;
return <span className={cn('shTableCell', className)} role="cell" {...rest} />;
}
export function EmptyState(props: { title: string; description?: string }) {
return (
<div className="emptyState">
<strong>{props.title}</strong>
{props.description && <span>{props.description}</span>}
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export function Tabs<T extends string>(props: {
value: T;
tabs: Array<{ value: T; label: string; icon?: React.ReactNode }>;
onValueChange: (value: T) => void;
className?: string;
}) {
return (
<div className={cn('shTabs', props.className)} role="tablist">
{props.tabs.map((tab) => (
<button
type="button"
className="shTab"
data-active={props.value === tab.value}
key={tab.value}
onClick={() => props.onValueChange(tab.value)}
>
{tab.icon}
<span>{tab.label}</span>
</button>
))}
</div>
);
}
+28
View File
@@ -0,0 +1,28 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../lib/utils';
const textareaVariants = cva('shTextarea', {
variants: {
size: {
xs: 'shTextareaXs',
sm: 'shTextareaSm',
md: 'shTextareaMd',
lg: 'shTextareaLg',
xl: 'shTextareaXl',
},
},
defaultVariants: {
size: 'md',
},
});
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement>,
VariantProps<typeof textareaVariants> {}
export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, size, ...props }, ref) => <textarea ref={ref} className={cn(textareaVariants({ size, className }))} {...props} />,
);
Textarea.displayName = 'Textarea';
+136
View File
@@ -0,0 +1,136 @@
import type { Dispatch, SetStateAction } from 'react';
import type {
BaseModelCatalogItem,
BaseModelUpsertRequest,
CatalogProvider,
CatalogProviderUpsertRequest,
} from '@easyai-ai-gateway/contracts';
import {
createBaseModel,
createCatalogProvider,
deleteBaseModel,
deleteCatalogProvider,
resetAllBaseModels,
resetBaseModel,
updateBaseModel,
updateCatalogProvider,
} from '../api';
import type { LoadState } from '../types';
export function useCatalogOperations(input: {
setBaseModels: Dispatch<SetStateAction<BaseModelCatalogItem[]>>;
setCoreMessage: Dispatch<SetStateAction<string>>;
setCoreState: Dispatch<SetStateAction<LoadState>>;
setProviders: Dispatch<SetStateAction<CatalogProvider[]>>;
token: string;
}) {
async function saveProvider(payload: CatalogProviderUpsertRequest, providerId?: string) {
if (!input.token) throw new Error('请先登录后再维护模型厂商');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const provider = providerId
? await updateCatalogProvider(input.token, providerId, payload)
: await createCatalogProvider(input.token, payload);
input.setProviders((current) => [provider, ...current.filter((item) => item.id !== provider.id)]);
input.setCoreState('ready');
input.setCoreMessage(providerId ? '模型厂商已更新。' : '模型厂商已新增。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '模型厂商保存失败');
throw err;
}
}
async function removeProvider(providerId: string) {
if (!input.token) throw new Error('请先登录后再维护模型厂商');
input.setCoreState('loading');
input.setCoreMessage('');
try {
await deleteCatalogProvider(input.token, providerId);
input.setProviders((current) => current.filter((item) => item.id !== providerId));
input.setCoreState('ready');
input.setCoreMessage('模型厂商已删除。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '模型厂商删除失败');
throw err;
}
}
async function saveBaseModel(payload: BaseModelUpsertRequest, baseModelId?: string) {
if (!input.token) throw new Error('请先登录后再维护基准模型');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const model = baseModelId
? await updateBaseModel(input.token, baseModelId, payload)
: await createBaseModel(input.token, payload);
input.setBaseModels((current) => [model, ...current.filter((item) => item.id !== model.id)]);
input.setCoreState('ready');
input.setCoreMessage(baseModelId ? '基准模型已更新。' : '基准模型已新增。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '基准模型保存失败');
throw err;
}
}
async function removeBaseModel(baseModelId: string) {
if (!input.token) throw new Error('请先登录后再维护基准模型');
input.setCoreState('loading');
input.setCoreMessage('');
try {
await deleteBaseModel(input.token, baseModelId);
input.setBaseModels((current) => current.filter((item) => item.id !== baseModelId));
input.setCoreState('ready');
input.setCoreMessage('基准模型已删除。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '基准模型删除失败');
throw err;
}
}
async function resetBaseModelToDefault(baseModelId: string) {
if (!input.token) throw new Error('请先登录后再维护基准模型');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const model = await resetBaseModel(input.token, baseModelId);
input.setBaseModels((current) => current.map((item) => (item.id === model.id ? model : item)));
input.setCoreState('ready');
input.setCoreMessage('基准模型已重置为系统默认。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '基准模型重置失败');
throw err;
}
}
async function resetAllBaseModelsToDefault() {
if (!input.token) throw new Error('请先登录后再维护基准模型');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const response = await resetAllBaseModels(input.token);
const resetModels = new Map(response.items.map((item) => [item.id, item]));
input.setBaseModels((current) => current.map((item) => resetModels.get(item.id) ?? item));
input.setCoreState('ready');
input.setCoreMessage(`已重置 ${response.items.length} 个系统内置基准模型。`);
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '基准模型批量重置失败');
throw err;
}
}
return {
removeBaseModel,
removeProvider,
resetAllBaseModelsToDefault,
resetBaseModelToDefault,
saveBaseModel,
saveProvider,
};
}
@@ -0,0 +1,47 @@
import type { Dispatch, SetStateAction } from 'react';
import type { PricingRuleSet, PricingRuleSetUpsertRequest } from '@easyai-ai-gateway/contracts';
import { createPricingRuleSet, deletePricingRuleSet, updatePricingRuleSet } from '../api';
import type { LoadState } from '../types';
export function usePricingRuleSetOperations(input: {
setCoreMessage: Dispatch<SetStateAction<string>>;
setCoreState: Dispatch<SetStateAction<LoadState>>;
setPricingRuleSets: Dispatch<SetStateAction<PricingRuleSet[]>>;
token: string;
}) {
async function savePricingRuleSet(payload: PricingRuleSetUpsertRequest, ruleSetId?: string) {
if (!input.token) throw new Error('请先登录后再维护定价规则');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const item = ruleSetId
? await updatePricingRuleSet(input.token, ruleSetId, payload)
: await createPricingRuleSet(input.token, payload);
input.setPricingRuleSets((current) => [item, ...current.filter((ruleSet) => ruleSet.id !== item.id)]);
input.setCoreState('ready');
input.setCoreMessage(ruleSetId ? '定价规则已更新。' : '定价规则已新增。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '定价规则保存失败');
throw err;
}
}
async function removePricingRuleSet(ruleSetId: string) {
if (!input.token) throw new Error('请先登录后再维护定价规则');
input.setCoreState('loading');
input.setCoreMessage('');
try {
await deletePricingRuleSet(input.token, ruleSetId);
input.setPricingRuleSets((current) => current.filter((item) => item.id !== ruleSetId));
input.setCoreState('ready');
input.setCoreMessage('定价规则已删除。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '定价规则删除失败');
throw err;
}
}
return { removePricingRuleSet, savePricingRuleSet };
}
@@ -0,0 +1,47 @@
import type { Dispatch, SetStateAction } from 'react';
import type { RuntimePolicySet, RuntimePolicySetUpsertRequest } from '@easyai-ai-gateway/contracts';
import { createRuntimePolicySet, deleteRuntimePolicySet, updateRuntimePolicySet } from '../api';
import type { LoadState } from '../types';
export function useRuntimePolicySetOperations(input: {
setCoreMessage: Dispatch<SetStateAction<string>>;
setCoreState: Dispatch<SetStateAction<LoadState>>;
setRuntimePolicySets: Dispatch<SetStateAction<RuntimePolicySet[]>>;
token: string;
}) {
async function saveRuntimePolicySet(payload: RuntimePolicySetUpsertRequest, policySetId?: string) {
if (!input.token) throw new Error('请先登录后再维护运行策略');
input.setCoreState('loading');
input.setCoreMessage('');
try {
const policySet = policySetId
? await updateRuntimePolicySet(input.token, policySetId, payload)
: await createRuntimePolicySet(input.token, payload);
input.setRuntimePolicySets((current) => [policySet, ...current.filter((item) => item.id !== policySet.id)]);
input.setCoreState('ready');
input.setCoreMessage(policySetId ? '运行策略已更新。' : '运行策略已新增。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '运行策略保存失败');
throw err;
}
}
async function removeRuntimePolicySet(policySetId: string) {
if (!input.token) throw new Error('请先登录后再维护运行策略');
input.setCoreState('loading');
input.setCoreMessage('');
try {
await deleteRuntimePolicySet(input.token, policySetId);
input.setRuntimePolicySets((current) => current.filter((item) => item.id !== policySetId));
input.setCoreState('ready');
input.setCoreMessage('运行策略已删除。');
} catch (err) {
input.setCoreState('error');
input.setCoreMessage(err instanceof Error ? err.message : '运行策略删除失败');
throw err;
}
}
return { removeRuntimePolicySet, saveRuntimePolicySet };
}
+23
View File
@@ -0,0 +1,23 @@
const AUTH_TOKEN_STORAGE_KEY = 'easyai_ai_gateway_access_token';
export function readStoredAccessToken() {
if (typeof window === 'undefined') return '';
try {
return window.localStorage.getItem(AUTH_TOKEN_STORAGE_KEY) ?? '';
} catch {
return '';
}
}
export function persistAccessToken(value: string) {
if (typeof window === 'undefined') return;
try {
if (value) {
window.localStorage.setItem(AUTH_TOKEN_STORAGE_KEY, value);
} else {
window.localStorage.removeItem(AUTH_TOKEN_STORAGE_KEY);
}
} catch {
// Ignore storage failures so private browsing or quota issues do not break login.
}
}
+32
View File
@@ -0,0 +1,32 @@
import type { GatewayTask } from '@easyai-ai-gateway/contracts';
import { createChatTask, createImageEditTask, createImageGenerationTask } from '../api';
import type { TaskForm } from '../types';
export function runTask(token: string, task: TaskForm): Promise<{ task: GatewayTask; next: Record<string, string> }> {
if (task.kind === 'images.generations') {
return createImageGenerationTask(token, {
model: task.model,
prompt: task.prompt,
quality: 'medium',
runMode: 'simulation',
simulation: true,
size: '1024x1024',
});
}
if (task.kind === 'images.edits') {
return createImageEditTask(token, {
model: task.model,
prompt: task.prompt,
image: task.image,
mask: task.mask,
runMode: 'simulation',
simulation: true,
});
}
return createChatTask(token, {
model: task.model,
runMode: 'simulation',
simulation: true,
messages: [{ role: 'user', content: task.prompt }],
});
}
+6
View File
@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

Some files were not shown because too many files have changed in this diff Show More