feat: 算力引擎 Go 后端核心(new-api)
- 统一 OpenAI 兼容 /v1 中继 + 渠道/额度/令牌/流水 - 各领域包:relay 模型网关、model 数据层、controller 管理 API
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
|
||||
// Import oauth package to register providers via init()
|
||||
_ "github.com/QuantumNous/new-api/oauth"
|
||||
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter := router.Group("/api")
|
||||
apiRouter.Use(middleware.RouteTag("api"))
|
||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
apiRouter.Use(middleware.BodyStorageCleanup()) // 清理请求体存储
|
||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||
anonymousRequestBodyLimit := middleware.AnonymousRequestBodyLimit()
|
||||
{
|
||||
apiRouter.GET("/setup", controller.GetSetup)
|
||||
apiRouter.POST("/setup", anonymousRequestBodyLimit, controller.PostSetup)
|
||||
apiRouter.GET("/status", controller.GetStatus)
|
||||
apiRouter.GET("/uptime/status", controller.GetUptimeKumaStatus)
|
||||
apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels)
|
||||
apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus)
|
||||
apiRouter.GET("/notice", controller.GetNotice)
|
||||
apiRouter.GET("/user-agreement", controller.GetUserAgreement)
|
||||
apiRouter.GET("/privacy-policy", controller.GetPrivacyPolicy)
|
||||
apiRouter.GET("/about", controller.GetAbout)
|
||||
//apiRouter.GET("/midjourney", controller.GetMidjourney)
|
||||
apiRouter.GET("/home_page_content", controller.GetHomePageContent)
|
||||
apiRouter.GET("/pricing", middleware.HeaderNavModuleAuth("pricing"), controller.GetPricing)
|
||||
perfMetricsRoute := apiRouter.Group("/perf-metrics")
|
||||
perfMetricsRoute.Use(middleware.HeaderNavModulePublicOrUserAuth("pricing"))
|
||||
{
|
||||
perfMetricsRoute.GET("/summary", controller.GetPerfMetricsSummary)
|
||||
perfMetricsRoute.GET("", controller.GetPerfMetrics)
|
||||
}
|
||||
apiRouter.GET("/rankings", middleware.HeaderNavModuleAuth("rankings"), controller.GetRankings)
|
||||
apiRouter.GET("/verification", middleware.EmailVerificationRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, controller.ResetPassword)
|
||||
// OAuth routes - specific routes must come before :provider wildcard
|
||||
apiRouter.POST("/oauth/state", middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.TryUserAuth(), anonymousRequestBodyLimit, controller.GenerateOAuthCode)
|
||||
apiRouter.POST("/oauth/email/bind", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.EmailBind)
|
||||
// Non-standard OAuth (WeChat, Telegram) - keep original routes
|
||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.WeChatAuth)
|
||||
apiRouter.POST("/oauth/wechat/bind", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.WeChatBind)
|
||||
apiRouter.GET("/oauth/telegram/login", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramLogin)
|
||||
apiRouter.POST("/oauth/telegram/bind/start", middleware.UserAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramBindStart)
|
||||
apiRouter.GET("/oauth/telegram/bind/:flow_token", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.TelegramBind)
|
||||
// Standard OAuth providers (GitHub, Discord, OIDC, LinuxDO) - unified route
|
||||
apiRouter.GET("/oauth/:provider", middleware.CriticalRateLimit(), middleware.DisableCache(), middleware.TryUserAuth(), controller.HandleOAuth)
|
||||
apiRouter.GET("/ratio_config", middleware.CriticalRateLimit(), controller.GetRatioConfig)
|
||||
|
||||
apiRouter.POST("/stripe/webhook", anonymousRequestBodyLimit, controller.StripeWebhook)
|
||||
apiRouter.POST("/creem/webhook", anonymousRequestBodyLimit, controller.CreemWebhook)
|
||||
apiRouter.POST("/waffo/webhook", anonymousRequestBodyLimit, controller.WaffoWebhook)
|
||||
// :env separates test vs prod URLs so the operator can register each
|
||||
// in Pancake's matching webhook slot; handler enforces env match.
|
||||
apiRouter.POST("/waffo-pancake/webhook/:env", anonymousRequestBodyLimit, controller.WaffoPancakeWebhook)
|
||||
|
||||
// Universal secure verification routes
|
||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.UniversalVerify)
|
||||
|
||||
userRoute := apiRouter.Group("/user")
|
||||
{
|
||||
userRoute.POST("/auth/refresh", middleware.SessionCookieOriginGuard(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.RefreshAuth)
|
||||
userRoute.POST("/auth/logout", middleware.SessionCookieOriginGuard(), middleware.CriticalRateLimit(), middleware.DisableCache(), controller.AuthLogout)
|
||||
userRoute.POST("/register", middleware.CriticalRateLimit(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Register)
|
||||
userRoute.POST("/login", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, middleware.TurnstileCheck(), controller.Login)
|
||||
userRoute.POST("/login/2fa", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.Verify2FALogin)
|
||||
userRoute.POST("/passkey/login/begin", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.PasskeyLoginBegin)
|
||||
userRoute.POST("/passkey/login/finish", middleware.CriticalRateLimit(), middleware.DisableCache(), anonymousRequestBodyLimit, controller.PasskeyLoginFinish)
|
||||
//userRoute.POST("/tokenlog", middleware.CriticalRateLimit(), controller.TokenLog)
|
||||
userRoute.POST("/epay/notify", anonymousRequestBodyLimit, controller.EpayNotify)
|
||||
userRoute.GET("/epay/notify", controller.EpayNotify)
|
||||
userRoute.GET("/groups", controller.GetUserGroups)
|
||||
|
||||
selfRoute := userRoute.Group("/")
|
||||
selfRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
selfRoute.GET("/sessions", middleware.DisableCache(), controller.GetLoginSessions)
|
||||
selfRoute.DELETE("/sessions/:sid", middleware.DisableCache(), controller.DeleteLoginSession)
|
||||
selfRoute.POST("/sessions/revoke-others", middleware.DisableCache(), controller.RevokeOtherLoginSessions)
|
||||
selfRoute.GET("/self/groups", controller.GetUserGroups)
|
||||
selfRoute.GET("/self", controller.GetSelf)
|
||||
selfRoute.GET("/models", controller.GetUserModels)
|
||||
selfRoute.PUT("/self", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.UpdateSelf)
|
||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||
selfRoute.GET("/token", middleware.CriticalRateLimit(), middleware.UserCriticalRateLimit("access-token"), middleware.DisableCache(), controller.GenerateAccessToken)
|
||||
selfRoute.GET("/passkey", controller.PasskeyStatus)
|
||||
selfRoute.POST("/passkey/register/begin", middleware.DisableCache(), controller.PasskeyRegisterBegin)
|
||||
selfRoute.POST("/passkey/register/finish", middleware.DisableCache(), controller.PasskeyRegisterFinish)
|
||||
selfRoute.POST("/passkey/verify/begin", middleware.DisableCache(), controller.PasskeyVerifyBegin)
|
||||
selfRoute.POST("/passkey/verify/finish", middleware.DisableCache(), controller.PasskeyVerifyFinish)
|
||||
selfRoute.DELETE("/passkey", middleware.DisableCache(), controller.PasskeyDelete)
|
||||
selfRoute.GET("/aff", controller.GetAffCode)
|
||||
selfRoute.GET("/topup/info", controller.GetTopUpInfo)
|
||||
selfRoute.GET("/topup/self", controller.GetUserTopUps)
|
||||
selfRoute.POST("/topup", middleware.CriticalRateLimit(), controller.TopUp)
|
||||
selfRoute.POST("/pay", middleware.CriticalRateLimit(), controller.RequestEpay)
|
||||
selfRoute.POST("/amount", controller.RequestAmount)
|
||||
selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay)
|
||||
selfRoute.POST("/stripe/amount", controller.RequestStripeAmount)
|
||||
selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay)
|
||||
selfRoute.POST("/waffo/amount", controller.RequestWaffoAmount)
|
||||
selfRoute.POST("/waffo/pay", middleware.CriticalRateLimit(), controller.RequestWaffoPay)
|
||||
selfRoute.POST("/waffo-pancake/amount", controller.RequestWaffoPancakeAmount)
|
||||
selfRoute.POST("/waffo-pancake/pay", middleware.CriticalRateLimit(), controller.RequestWaffoPancakePay)
|
||||
selfRoute.POST("/aff_transfer", middleware.UserCriticalRateLimit("aff-transfer"), controller.TransferAffQuota)
|
||||
selfRoute.PUT("/setting", controller.UpdateUserSetting)
|
||||
|
||||
// 2FA routes
|
||||
selfRoute.GET("/2fa/status", controller.Get2FAStatus)
|
||||
selfRoute.POST("/2fa/setup", middleware.DisableCache(), controller.Setup2FA)
|
||||
selfRoute.POST("/2fa/enable", middleware.DisableCache(), controller.Enable2FA)
|
||||
selfRoute.POST("/2fa/disable", middleware.DisableCache(), controller.Disable2FA)
|
||||
selfRoute.POST("/2fa/backup_codes", middleware.DisableCache(), controller.RegenerateBackupCodes)
|
||||
|
||||
// Check-in routes
|
||||
selfRoute.GET("/checkin", controller.GetCheckinStatus)
|
||||
selfRoute.POST("/checkin", middleware.TurnstileCheck(), controller.DoCheckin)
|
||||
|
||||
// Custom OAuth bindings
|
||||
selfRoute.GET("/oauth/bindings", controller.GetUserOAuthBindings)
|
||||
selfRoute.DELETE("/oauth/bindings/:provider_id", controller.UnbindCustomOAuth)
|
||||
}
|
||||
|
||||
adminRoute := userRoute.Group("/")
|
||||
adminRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
adminRoute.GET("/", controller.GetAllUsers)
|
||||
adminRoute.GET("/topup", controller.GetAllTopUps)
|
||||
adminRoute.POST("/topup/complete", controller.AdminCompleteTopUp)
|
||||
adminRoute.GET("/search", controller.SearchUsers)
|
||||
adminRoute.GET("/:id/oauth/bindings", controller.GetUserOAuthBindingsByAdmin)
|
||||
adminRoute.DELETE("/:id/oauth/bindings/:provider_id", controller.UnbindCustomOAuthByAdmin)
|
||||
adminRoute.DELETE("/:id/bindings/:binding_type", controller.AdminClearUserBinding)
|
||||
adminRoute.GET("/:id", controller.GetUser)
|
||||
adminRoute.POST("/", controller.CreateUser)
|
||||
adminRoute.POST("/manage", controller.ManageUser)
|
||||
adminRoute.PUT("/", controller.UpdateUser)
|
||||
adminRoute.DELETE("/:id", controller.DeleteUser)
|
||||
adminRoute.DELETE("/:id/reset_passkey", controller.AdminResetPasskey)
|
||||
|
||||
// Admin 2FA routes
|
||||
adminRoute.GET("/2fa/stats", controller.Admin2FAStats)
|
||||
adminRoute.DELETE("/:id/2fa", controller.AdminDisable2FA)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription billing (plans, purchase, admin management)
|
||||
subscriptionRoute := apiRouter.Group("/subscription")
|
||||
subscriptionRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
subscriptionRoute.GET("/plans", controller.GetSubscriptionPlans)
|
||||
subscriptionRoute.GET("/self", controller.GetSubscriptionSelf)
|
||||
subscriptionRoute.PUT("/self/preference", controller.UpdateSubscriptionPreference)
|
||||
subscriptionRoute.POST("/balance/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestBalancePay)
|
||||
subscriptionRoute.POST("/epay/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestEpay)
|
||||
subscriptionRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestStripePay)
|
||||
subscriptionRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestCreemPay)
|
||||
subscriptionRoute.POST("/waffo-pancake/pay", middleware.CriticalRateLimit(), controller.SubscriptionRequestWaffoPancakePay)
|
||||
}
|
||||
subscriptionAdminRoute := apiRouter.Group("/subscription/admin")
|
||||
subscriptionAdminRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
subscriptionAdminRoute.GET("/plans", controller.AdminListSubscriptionPlans)
|
||||
subscriptionAdminRoute.POST("/plans", controller.AdminCreateSubscriptionPlan)
|
||||
subscriptionAdminRoute.PUT("/plans/:id", controller.AdminUpdateSubscriptionPlan)
|
||||
subscriptionAdminRoute.PATCH("/plans/:id", controller.AdminUpdateSubscriptionPlanStatus)
|
||||
subscriptionAdminRoute.POST("/bind", controller.AdminBindSubscription)
|
||||
subscriptionAdminRoute.POST("/plans/:id/subscriptions/reset", controller.AdminResetPlanSubscriptions)
|
||||
|
||||
// User subscription management (admin)
|
||||
subscriptionAdminRoute.GET("/users/:id/subscriptions", controller.AdminListUserSubscriptions)
|
||||
subscriptionAdminRoute.POST("/users/:id/subscriptions", controller.AdminCreateUserSubscription)
|
||||
subscriptionAdminRoute.POST("/users/:id/subscriptions/reset", controller.AdminResetUserSubscriptionsByPlan)
|
||||
subscriptionAdminRoute.POST("/user_subscriptions/:id/invalidate", controller.AdminInvalidateUserSubscription)
|
||||
subscriptionAdminRoute.DELETE("/user_subscriptions/:id", controller.AdminDeleteUserSubscription)
|
||||
}
|
||||
|
||||
// Subscription payment callbacks (no auth)
|
||||
apiRouter.POST("/subscription/epay/notify", anonymousRequestBodyLimit, controller.SubscriptionEpayNotify)
|
||||
apiRouter.GET("/subscription/epay/notify", controller.SubscriptionEpayNotify)
|
||||
apiRouter.GET("/subscription/epay/return", controller.SubscriptionEpayReturn)
|
||||
apiRouter.POST("/subscription/epay/return", anonymousRequestBodyLimit, controller.SubscriptionEpayReturn)
|
||||
optionRoute := apiRouter.Group("/option")
|
||||
optionRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
optionRoute.GET("/", controller.GetOptions)
|
||||
optionRoute.PUT("/", controller.UpdateOption)
|
||||
optionRoute.POST("/payment_compliance", controller.ConfirmPaymentCompliance)
|
||||
optionRoute.GET("/channel_affinity_cache", controller.GetChannelAffinityCacheStats)
|
||||
optionRoute.DELETE("/channel_affinity_cache", controller.ClearChannelAffinityCache)
|
||||
optionRoute.POST("/rest_model_ratio", controller.ResetModelRatio)
|
||||
optionRoute.GET("/waffo-pancake/catalog", controller.ListWaffoPancakeCatalog)
|
||||
optionRoute.POST("/waffo-pancake/pair", controller.CreateWaffoPancakePair)
|
||||
optionRoute.POST("/waffo-pancake/save", controller.SaveWaffoPancake)
|
||||
optionRoute.POST("/waffo-pancake/subscription-product", controller.CreateWaffoPancakeSubscriptionProduct)
|
||||
optionRoute.GET("/waffo-pancake/subscription-product-options", controller.ListWaffoPancakeSubscriptionProductOptions)
|
||||
}
|
||||
|
||||
// Custom OAuth provider management (root only)
|
||||
customOAuthRoute := apiRouter.Group("/custom-oauth-provider")
|
||||
customOAuthRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
customOAuthRoute.POST("/discovery", controller.FetchCustomOAuthDiscovery)
|
||||
customOAuthRoute.GET("/", controller.GetCustomOAuthProviders)
|
||||
customOAuthRoute.GET("/:id", controller.GetCustomOAuthProvider)
|
||||
customOAuthRoute.POST("/", controller.CreateCustomOAuthProvider)
|
||||
customOAuthRoute.PUT("/:id", controller.UpdateCustomOAuthProvider)
|
||||
customOAuthRoute.DELETE("/:id", controller.DeleteCustomOAuthProvider)
|
||||
}
|
||||
performanceRoute := apiRouter.Group("/performance")
|
||||
performanceRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
performanceRoute.GET("/stats", controller.GetPerformanceStats)
|
||||
performanceRoute.DELETE("/disk_cache", controller.ClearDiskCache)
|
||||
performanceRoute.POST("/reset_stats", controller.ResetPerformanceStats)
|
||||
performanceRoute.POST("/gc", controller.ForceGC)
|
||||
performanceRoute.GET("/logs", controller.GetLogFiles)
|
||||
performanceRoute.DELETE("/logs", controller.CleanupLogFiles)
|
||||
}
|
||||
ratioSyncRoute := apiRouter.Group("/ratio_sync")
|
||||
ratioSyncRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
ratioSyncRoute.GET("/channels", controller.GetSyncableChannels)
|
||||
ratioSyncRoute.POST("/fetch", controller.FetchUpstreamRatios)
|
||||
}
|
||||
registerChannelRoutes(apiRouter)
|
||||
registerAuthzRoutes(apiRouter)
|
||||
tokenRoute := apiRouter.Group("/token")
|
||||
tokenRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
tokenRoute.GET("/", controller.GetAllTokens)
|
||||
tokenRoute.GET("/search", middleware.SearchRateLimit(), controller.SearchTokens)
|
||||
tokenRoute.GET("/auto-groups", controller.GetTokenAutoGroups)
|
||||
tokenRoute.GET("/:id", controller.GetToken)
|
||||
tokenRoute.POST("/:id/key", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetTokenKey)
|
||||
tokenRoute.POST("/", controller.AddToken)
|
||||
tokenRoute.PUT("/", controller.UpdateToken)
|
||||
tokenRoute.DELETE("/:id", controller.DeleteToken)
|
||||
tokenRoute.POST("/batch", controller.DeleteTokenBatch)
|
||||
tokenRoute.POST("/batch/keys", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetTokenKeysBatch)
|
||||
}
|
||||
|
||||
usageRoute := apiRouter.Group("/usage")
|
||||
usageRoute.Use(middleware.CORS(), middleware.CriticalRateLimit())
|
||||
{
|
||||
tokenUsageRoute := usageRoute.Group("/token")
|
||||
tokenUsageRoute.Use(middleware.TokenAuthReadOnly())
|
||||
{
|
||||
tokenUsageRoute.GET("/", controller.GetTokenUsage)
|
||||
}
|
||||
}
|
||||
|
||||
redemptionRoute := apiRouter.Group("/redemption")
|
||||
redemptionRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
redemptionRoute.GET("/", controller.GetAllRedemptions)
|
||||
redemptionRoute.GET("/search", controller.SearchRedemptions)
|
||||
redemptionRoute.GET("/:id", controller.GetRedemption)
|
||||
redemptionRoute.POST("/", controller.AddRedemption)
|
||||
redemptionRoute.PUT("/", controller.UpdateRedemption)
|
||||
redemptionRoute.DELETE("/invalid", controller.DeleteInvalidRedemption)
|
||||
redemptionRoute.DELETE("/:id", controller.DeleteRedemption)
|
||||
}
|
||||
logRoute := apiRouter.Group("/log")
|
||||
logRoute.GET("/", middleware.AdminAuth(), controller.GetAllLogs)
|
||||
logRoute.GET("/stat", middleware.AdminAuth(), controller.GetLogsStat)
|
||||
logRoute.GET("/self/stat", middleware.UserAuth(), controller.GetLogsSelfStat)
|
||||
logRoute.GET("/channel_affinity_usage_cache", middleware.AdminAuth(), controller.GetChannelAffinityUsageCacheStats)
|
||||
logRoute.GET("/search", middleware.AdminAuth(), controller.SearchAllLogs)
|
||||
logRoute.GET("/self", middleware.UserAuth(), controller.GetUserLogs)
|
||||
logRoute.GET("/self/search", middleware.UserAuth(), middleware.SearchRateLimit(), controller.SearchUserLogs)
|
||||
|
||||
systemTaskRoute := apiRouter.Group("/system-task")
|
||||
systemTaskRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
systemTaskRoute.POST("/log-cleanup", controller.CreateLogCleanupSystemTask)
|
||||
systemTaskRoute.GET("/list", controller.ListSystemTasks)
|
||||
systemTaskRoute.GET("/current", controller.GetCurrentSystemTask)
|
||||
systemTaskRoute.GET("/:task_id", controller.GetSystemTask)
|
||||
}
|
||||
systemInfoRoute := apiRouter.Group("/system-info")
|
||||
systemInfoRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
systemInfoRoute.GET("/instances", controller.ListSystemInstances)
|
||||
systemInfoRoute.DELETE("/stale-instances", controller.DeleteStaleSystemInstances)
|
||||
systemInfoRoute.DELETE("/instances/:node_name", controller.DeleteStaleSystemInstance)
|
||||
}
|
||||
|
||||
dataRoute := apiRouter.Group("/data")
|
||||
dataRoute.GET("/", middleware.AdminAuth(), controller.GetAllQuotaDates)
|
||||
dataRoute.GET("/users", middleware.AdminAuth(), controller.GetQuotaDatesByUser)
|
||||
dataRoute.GET("/self", middleware.UserAuth(), controller.GetUserQuotaDates)
|
||||
dataRoute.GET("/flow", middleware.AdminAuth(), controller.GetAllFlowQuotaDates)
|
||||
dataRoute.GET("/flow/self", middleware.UserAuth(), controller.GetUserFlowQuotaDates)
|
||||
|
||||
logRoute.Use(middleware.CORS(), middleware.CriticalRateLimit())
|
||||
{
|
||||
logRoute.GET("/token", middleware.TokenAuthReadOnly(), controller.GetLogByKey)
|
||||
}
|
||||
groupRoute := apiRouter.Group("/group")
|
||||
groupRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
groupRoute.GET("/", controller.GetGroups)
|
||||
}
|
||||
|
||||
prefillGroupRoute := apiRouter.Group("/prefill_group")
|
||||
prefillGroupRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
prefillGroupRoute.GET("/", controller.GetPrefillGroups)
|
||||
prefillGroupRoute.POST("/", controller.CreatePrefillGroup)
|
||||
prefillGroupRoute.PUT("/", controller.UpdatePrefillGroup)
|
||||
prefillGroupRoute.DELETE("/:id", controller.DeletePrefillGroup)
|
||||
}
|
||||
|
||||
mjRoute := apiRouter.Group("/mj")
|
||||
mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney)
|
||||
mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney)
|
||||
|
||||
taskRoute := apiRouter.Group("/task")
|
||||
{
|
||||
taskRoute.GET("/self", middleware.UserAuth(), controller.GetUserTask)
|
||||
taskRoute.GET("/", middleware.AdminAuth(), controller.GetAllTask)
|
||||
}
|
||||
|
||||
vendorRoute := apiRouter.Group("/vendors")
|
||||
vendorRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
vendorRoute.GET("/", controller.GetAllVendors)
|
||||
vendorRoute.GET("/search", controller.SearchVendors)
|
||||
vendorRoute.GET("/:id", controller.GetVendorMeta)
|
||||
vendorRoute.POST("/", controller.CreateVendorMeta)
|
||||
vendorRoute.PUT("/", controller.UpdateVendorMeta)
|
||||
vendorRoute.DELETE("/:id", controller.DeleteVendorMeta)
|
||||
}
|
||||
|
||||
modelsRoute := apiRouter.Group("/models")
|
||||
modelsRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
modelsRoute.GET("/sync_upstream/preview", controller.SyncUpstreamPreview)
|
||||
modelsRoute.POST("/sync_upstream", controller.SyncUpstreamModels)
|
||||
modelsRoute.GET("/missing", controller.GetMissingModels)
|
||||
modelsRoute.GET("/", controller.GetAllModelsMeta)
|
||||
modelsRoute.GET("/search", controller.SearchModelsMeta)
|
||||
modelsRoute.GET("/:id", controller.GetModelMeta)
|
||||
modelsRoute.POST("/", controller.CreateModelMeta)
|
||||
modelsRoute.PUT("/", controller.UpdateModelMeta)
|
||||
modelsRoute.DELETE("/:id", controller.DeleteModelMeta)
|
||||
}
|
||||
|
||||
// Deployments (model deployment management)
|
||||
deploymentsRoute := apiRouter.Group("/deployments")
|
||||
deploymentsRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
deploymentsRoute.GET("/settings", controller.GetModelDeploymentSettings)
|
||||
deploymentsRoute.POST("/settings/test-connection", controller.TestIoNetConnection)
|
||||
deploymentsRoute.GET("/", controller.GetAllDeployments)
|
||||
deploymentsRoute.GET("/search", controller.SearchDeployments)
|
||||
deploymentsRoute.POST("/test-connection", controller.TestIoNetConnection)
|
||||
deploymentsRoute.GET("/hardware-types", controller.GetHardwareTypes)
|
||||
deploymentsRoute.GET("/locations", controller.GetLocations)
|
||||
deploymentsRoute.GET("/available-replicas", controller.GetAvailableReplicas)
|
||||
deploymentsRoute.POST("/price-estimation", controller.GetPriceEstimation)
|
||||
deploymentsRoute.GET("/check-name", controller.CheckClusterNameAvailability)
|
||||
deploymentsRoute.POST("/", controller.CreateDeployment)
|
||||
|
||||
deploymentsRoute.GET("/:id", controller.GetDeployment)
|
||||
deploymentsRoute.GET("/:id/logs", controller.GetDeploymentLogs)
|
||||
deploymentsRoute.GET("/:id/containers", controller.ListDeploymentContainers)
|
||||
deploymentsRoute.GET("/:id/containers/:container_id", controller.GetContainerDetails)
|
||||
deploymentsRoute.PUT("/:id", controller.UpdateDeployment)
|
||||
deploymentsRoute.PUT("/:id/name", controller.UpdateDeploymentName)
|
||||
deploymentsRoute.POST("/:id/extend", controller.ExtendDeployment)
|
||||
deploymentsRoute.DELETE("/:id", controller.DeleteDeployment)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// registerAuthzRoutes mounts the authorization API under its own /authz
|
||||
// namespace. GET /authz/catalog returns the permission schema (resources,
|
||||
// actions, and role baselines) used by the client permission editor.
|
||||
func registerAuthzRoutes(apiRouter *gin.RouterGroup) {
|
||||
authzRoute := apiRouter.Group("/authz")
|
||||
authzRoute.Use(middleware.AdminAuth())
|
||||
{
|
||||
authzRoute.GET("/catalog", controller.GetPermissionCatalog)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type permissionRoute struct {
|
||||
method string
|
||||
path string
|
||||
permission authz.Permission
|
||||
handler gin.HandlerFunc
|
||||
}
|
||||
|
||||
func registerChannelRoutes(apiRouter *gin.RouterGroup) {
|
||||
channelRoute := apiRouter.Group("/channel")
|
||||
channelRoute.Use(middleware.AdminAuth())
|
||||
|
||||
channelRoute.POST("/:id/key",
|
||||
middleware.RootAuth(),
|
||||
middleware.CriticalRateLimit(),
|
||||
middleware.DisableCache(),
|
||||
middleware.SecureVerificationRequired(),
|
||||
controller.GetChannelKey,
|
||||
)
|
||||
|
||||
for _, route := range channelPermissionRoutes {
|
||||
channelRoute.Handle(route.method, route.path,
|
||||
middleware.RequirePermission(route.permission),
|
||||
route.handler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var channelPermissionRoutes = []permissionRoute{
|
||||
{method: http.MethodGet, path: "/", permission: authz.ChannelRead, handler: controller.GetAllChannels},
|
||||
{method: http.MethodGet, path: "/search", permission: authz.ChannelRead, handler: controller.SearchChannels},
|
||||
{method: http.MethodGet, path: "/models", permission: authz.ChannelRead, handler: controller.ChannelListModels},
|
||||
{method: http.MethodGet, path: "/models_enabled", permission: authz.ChannelRead, handler: controller.EnabledListModels},
|
||||
{method: http.MethodGet, path: "/ops", permission: authz.ChannelRead, handler: controller.GetChannelOps},
|
||||
{method: http.MethodGet, path: "/:id", permission: authz.ChannelRead, handler: controller.GetChannel},
|
||||
{method: http.MethodGet, path: "/test", permission: authz.ChannelOperate, handler: controller.TestAllChannels},
|
||||
{method: http.MethodGet, path: "/test/:id", permission: authz.ChannelOperate, handler: controller.TestChannel},
|
||||
{method: http.MethodGet, path: "/update_balance", permission: authz.ChannelOperate, handler: controller.UpdateAllChannelsBalance},
|
||||
{method: http.MethodGet, path: "/update_balance/:id", permission: authz.ChannelOperate, handler: controller.UpdateChannelBalance},
|
||||
{method: http.MethodPost, path: "/", permission: authz.ChannelSensitiveWrite, handler: controller.AddChannel},
|
||||
{method: http.MethodPut, path: "/", permission: authz.ChannelWrite, handler: controller.UpdateChannel},
|
||||
{method: http.MethodPost, path: "/status/batch", permission: authz.ChannelOperate, handler: controller.BatchUpdateChannelStatus},
|
||||
{method: http.MethodPost, path: "/:id/status", permission: authz.ChannelOperate, handler: controller.UpdateChannelStatus},
|
||||
{method: http.MethodDelete, path: "/disabled", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteDisabledChannel},
|
||||
{method: http.MethodPost, path: "/tag/disabled", permission: authz.ChannelOperate, handler: controller.DisableTagChannels},
|
||||
{method: http.MethodPost, path: "/tag/enabled", permission: authz.ChannelOperate, handler: controller.EnableTagChannels},
|
||||
{method: http.MethodPut, path: "/tag", permission: authz.ChannelWrite, handler: controller.EditTagChannels},
|
||||
{method: http.MethodDelete, path: "/:id", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteChannel},
|
||||
{method: http.MethodPost, path: "/batch", permission: authz.ChannelSensitiveWrite, handler: controller.DeleteChannelBatch},
|
||||
{method: http.MethodPost, path: "/fix", permission: authz.ChannelOperate, handler: controller.FixChannelsAbilities},
|
||||
{method: http.MethodGet, path: "/fetch_models/:id", permission: authz.ChannelOperate, handler: controller.FetchUpstreamModels},
|
||||
{method: http.MethodPost, path: "/fetch_models", permission: authz.ChannelSensitiveWrite, handler: controller.FetchModels},
|
||||
{method: http.MethodPost, path: "/:id/codex/refresh", permission: authz.ChannelSensitiveWrite, handler: controller.RefreshCodexChannelCredential},
|
||||
{method: http.MethodGet, path: "/:id/codex/usage", permission: authz.ChannelRead, handler: controller.GetCodexChannelUsage},
|
||||
{method: http.MethodGet, path: "/:id/codex/usage/reset-credits", permission: authz.ChannelRead, handler: controller.GetCodexChannelRateLimitResetCredits},
|
||||
{method: http.MethodPost, path: "/:id/codex/usage/reset", permission: authz.ChannelOperate, handler: controller.ResetCodexChannelUsage},
|
||||
{method: http.MethodPost, path: "/ollama/pull", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaPullModel},
|
||||
{method: http.MethodPost, path: "/ollama/pull/stream", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaPullModelStream},
|
||||
{method: http.MethodDelete, path: "/ollama/delete", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaDeleteModel},
|
||||
{method: http.MethodGet, path: "/ollama/version/:id", permission: authz.ChannelSensitiveWrite, handler: controller.OllamaVersion},
|
||||
{method: http.MethodPost, path: "/batch/tag", permission: authz.ChannelWrite, handler: controller.BatchSetChannelTag},
|
||||
{method: http.MethodGet, path: "/tag/models", permission: authz.ChannelRead, handler: controller.GetTagModels},
|
||||
{method: http.MethodPost, path: "/copy/:id", permission: authz.ChannelSensitiveWrite, handler: controller.CopyChannel},
|
||||
{method: http.MethodPost, path: "/multi_key/manage", permission: authz.ChannelOperate, handler: controller.ManageMultiKeys},
|
||||
{method: http.MethodPost, path: "/upstream_updates/apply", permission: authz.ChannelWrite, handler: controller.ApplyChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/apply_all", permission: authz.ChannelWrite, handler: controller.ApplyAllChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/detect", permission: authz.ChannelOperate, handler: controller.DetectChannelUpstreamModelUpdates},
|
||||
{method: http.MethodPost, path: "/upstream_updates/detect_all", permission: authz.ChannelOperate, handler: controller.DetectAllChannelUpstreamModelUpdates},
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/service/authz"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestChannelStatusRoutesUseOperatePermission(t *testing.T) {
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/:id/status", authz.ChannelOperate, controller.UpdateChannelStatus)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/status/batch", authz.ChannelOperate, controller.BatchUpdateChannelStatus)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/", authz.ChannelWrite, controller.UpdateChannel)
|
||||
}
|
||||
|
||||
func TestChannelDeleteRoutesUseSensitiveWritePermission(t *testing.T) {
|
||||
assertChannelRoutePermission(t, http.MethodDelete, "/:id", authz.ChannelSensitiveWrite, controller.DeleteChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/batch", authz.ChannelSensitiveWrite, controller.DeleteChannelBatch)
|
||||
assertChannelRoutePermission(t, http.MethodDelete, "/disabled", authz.ChannelSensitiveWrite, controller.DeleteDisabledChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/", authz.ChannelWrite, controller.UpdateChannel)
|
||||
assertChannelRoutePermission(t, http.MethodPut, "/tag", authz.ChannelWrite, controller.EditTagChannels)
|
||||
assertChannelRoutePermission(t, http.MethodPost, "/batch/tag", authz.ChannelWrite, controller.BatchSetChannelTag)
|
||||
}
|
||||
|
||||
func TestChannelStatusRoutesRegisterWithoutConflict(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
api := engine.Group("/api")
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
registerChannelRoutes(api)
|
||||
})
|
||||
}
|
||||
|
||||
func assertChannelRoutePermission(t *testing.T, method string, path string, permission authz.Permission, handler any) {
|
||||
t.Helper()
|
||||
for _, route := range channelPermissionRoutes {
|
||||
if route.method == method && route.path == path {
|
||||
assert.Equal(t, permission, route.permission)
|
||||
assert.Equal(t, reflect.ValueOf(handler).Pointer(), reflect.ValueOf(route.handler).Pointer())
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("route %s %s not found", method, path)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetDashboardRouter(router *gin.Engine) {
|
||||
apiRouter := router.Group("/")
|
||||
apiRouter.Use(middleware.RouteTag("old_api"))
|
||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||
apiRouter.Use(middleware.CORS())
|
||||
apiRouter.Use(middleware.TokenAuth())
|
||||
{
|
||||
apiRouter.GET("/dashboard/billing/subscription", controller.GetSubscription)
|
||||
apiRouter.GET("/v1/dashboard/billing/subscription", controller.GetSubscription)
|
||||
apiRouter.GET("/dashboard/billing/usage", controller.GetUsage)
|
||||
apiRouter.GET("/v1/dashboard/billing/usage", controller.GetUsage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetRouter(router *gin.Engine, assets WebAssets) {
|
||||
SetApiRouter(router)
|
||||
SetDashboardRouter(router)
|
||||
SetRelayRouter(router)
|
||||
SetVideoRouter(router)
|
||||
frontendBaseUrl := os.Getenv("FRONTEND_BASE_URL")
|
||||
if common.IsMasterNode && frontendBaseUrl != "" {
|
||||
frontendBaseUrl = ""
|
||||
common.SysLog("FRONTEND_BASE_URL is ignored on master node")
|
||||
}
|
||||
if frontendBaseUrl == "" {
|
||||
SetWebRouter(router, assets)
|
||||
} else {
|
||||
frontendBaseUrl = strings.TrimSuffix(frontendBaseUrl, "/")
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.Set(middleware.RouteTagKey, "web")
|
||||
c.Redirect(http.StatusMovedPermanently, fmt.Sprintf("%s%s", frontendBaseUrl, c.Request.RequestURI))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/constant"
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/QuantumNous/new-api/relay"
|
||||
"github.com/QuantumNous/new-api/relaykit/types"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetRelayRouter(router *gin.Engine) {
|
||||
router.Use(middleware.CORS())
|
||||
router.Use(middleware.DecompressRequestMiddleware())
|
||||
router.Use(middleware.BodyStorageCleanup()) // 清理请求体存储
|
||||
router.Use(middleware.StatsMiddleware())
|
||||
// https://platform.openai.com/docs/api-reference/introduction
|
||||
modelsRouter := router.Group("/v1/models")
|
||||
modelsRouter.Use(middleware.RouteTag("relay"))
|
||||
modelsRouter.Use(middleware.TokenAuth())
|
||||
{
|
||||
modelsRouter.GET("", func(c *gin.Context) {
|
||||
switch {
|
||||
case c.GetHeader("x-api-key") != "" && c.GetHeader("anthropic-version") != "":
|
||||
controller.ListModels(c, constant.ChannelTypeAnthropic)
|
||||
case c.GetHeader("x-goog-api-key") != "" || c.Query("key") != "": // 单独的适配
|
||||
controller.ListModels(c, constant.ChannelTypeGemini)
|
||||
default:
|
||||
controller.ListModels(c, constant.ChannelTypeOpenAI)
|
||||
}
|
||||
})
|
||||
|
||||
modelsRouter.GET("/:model", func(c *gin.Context) {
|
||||
switch {
|
||||
case c.GetHeader("x-api-key") != "" && c.GetHeader("anthropic-version") != "":
|
||||
controller.RetrieveModel(c, constant.ChannelTypeAnthropic)
|
||||
default:
|
||||
controller.RetrieveModel(c, constant.ChannelTypeOpenAI)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
geminiRouter := router.Group("/v1beta/models")
|
||||
geminiRouter.Use(middleware.RouteTag("relay"))
|
||||
geminiRouter.Use(middleware.TokenAuth())
|
||||
{
|
||||
geminiRouter.GET("", func(c *gin.Context) {
|
||||
controller.ListModels(c, constant.ChannelTypeGemini)
|
||||
})
|
||||
}
|
||||
|
||||
geminiCompatibleRouter := router.Group("/v1beta/openai/models")
|
||||
geminiCompatibleRouter.Use(middleware.RouteTag("relay"))
|
||||
geminiCompatibleRouter.Use(middleware.TokenAuth())
|
||||
{
|
||||
geminiCompatibleRouter.GET("", func(c *gin.Context) {
|
||||
controller.ListModels(c, constant.ChannelTypeOpenAI)
|
||||
})
|
||||
}
|
||||
|
||||
playgroundRouter := router.Group("/pg")
|
||||
playgroundRouter.Use(middleware.RouteTag("relay"))
|
||||
playgroundRouter.Use(middleware.SystemPerformanceCheck())
|
||||
playgroundRouter.Use(middleware.UserAuth(), middleware.Distribute())
|
||||
{
|
||||
playgroundRouter.POST("/chat/completions", controller.Playground)
|
||||
}
|
||||
relayV1Router := router.Group("/v1")
|
||||
relayV1Router.Use(middleware.RouteTag("relay"))
|
||||
relayV1Router.Use(middleware.SystemPerformanceCheck())
|
||||
relayV1Router.Use(middleware.TokenAuth())
|
||||
relayV1Router.Use(middleware.ModelRequestRateLimit())
|
||||
{
|
||||
// WebSocket 路由(统一到 Relay)
|
||||
wsRouter := relayV1Router.Group("")
|
||||
wsRouter.Use(middleware.Distribute())
|
||||
wsRouter.GET("/realtime", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIRealtime)
|
||||
})
|
||||
}
|
||||
{
|
||||
//http router
|
||||
httpRouter := relayV1Router.Group("")
|
||||
httpRouter.Use(middleware.Distribute())
|
||||
|
||||
// claude related routes
|
||||
httpRouter.POST("/messages", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatClaude)
|
||||
})
|
||||
|
||||
// chat related routes
|
||||
httpRouter.POST("/completions", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAI)
|
||||
})
|
||||
httpRouter.POST("/chat/completions", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAI)
|
||||
})
|
||||
|
||||
// response related routes
|
||||
httpRouter.POST("/responses", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIResponses)
|
||||
})
|
||||
httpRouter.POST("/responses/compact", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIResponsesCompaction)
|
||||
})
|
||||
|
||||
// alpha search related routes (Codex standalone web search)
|
||||
httpRouter.POST("/alpha/search", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIAlphaSearch)
|
||||
})
|
||||
|
||||
// image related routes
|
||||
httpRouter.POST("/edits", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIImage)
|
||||
})
|
||||
httpRouter.POST("/images/generations", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIImage)
|
||||
})
|
||||
httpRouter.POST("/images/edits", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIImage)
|
||||
})
|
||||
|
||||
// embedding related routes
|
||||
httpRouter.POST("/embeddings", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatEmbedding)
|
||||
})
|
||||
|
||||
// audio related routes
|
||||
httpRouter.POST("/audio/transcriptions", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIAudio)
|
||||
})
|
||||
httpRouter.POST("/audio/translations", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIAudio)
|
||||
})
|
||||
httpRouter.POST("/audio/speech", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAIAudio)
|
||||
})
|
||||
|
||||
// rerank related routes
|
||||
httpRouter.POST("/rerank", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatRerank)
|
||||
})
|
||||
|
||||
// gemini relay routes
|
||||
httpRouter.POST("/engines/:model/embeddings", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatGemini)
|
||||
})
|
||||
httpRouter.POST("/models/*path", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatGemini)
|
||||
})
|
||||
|
||||
// other relay routes
|
||||
httpRouter.POST("/moderations", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatOpenAI)
|
||||
})
|
||||
|
||||
// not implemented
|
||||
httpRouter.POST("/images/variations", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/files", controller.RelayNotImplemented)
|
||||
httpRouter.POST("/files", controller.RelayNotImplemented)
|
||||
httpRouter.DELETE("/files/:id", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/files/:id", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/files/:id/content", controller.RelayNotImplemented)
|
||||
httpRouter.POST("/fine-tunes", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/fine-tunes", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/fine-tunes/:id", controller.RelayNotImplemented)
|
||||
httpRouter.POST("/fine-tunes/:id/cancel", controller.RelayNotImplemented)
|
||||
httpRouter.GET("/fine-tunes/:id/events", controller.RelayNotImplemented)
|
||||
httpRouter.DELETE("/models/:model", controller.RelayNotImplemented)
|
||||
}
|
||||
|
||||
relayMjRouter := router.Group("/mj")
|
||||
relayMjRouter.Use(middleware.RouteTag("relay"))
|
||||
relayMjRouter.Use(middleware.SystemPerformanceCheck())
|
||||
registerMjRouterGroup(relayMjRouter)
|
||||
|
||||
relayMjModeRouter := router.Group("/:mode/mj")
|
||||
relayMjModeRouter.Use(middleware.RouteTag("relay"))
|
||||
relayMjModeRouter.Use(middleware.SystemPerformanceCheck())
|
||||
registerMjRouterGroup(relayMjModeRouter)
|
||||
//relayMjRouter.Use()
|
||||
|
||||
relaySunoRouter := router.Group("/suno")
|
||||
relaySunoRouter.Use(middleware.RouteTag("relay"))
|
||||
relaySunoRouter.Use(middleware.SystemPerformanceCheck())
|
||||
relaySunoRouter.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
relaySunoRouter.POST("/submit/:action", controller.RelayTask)
|
||||
relaySunoRouter.POST("/fetch", controller.RelayTaskFetch)
|
||||
relaySunoRouter.GET("/fetch/:id", controller.RelayTaskFetch)
|
||||
}
|
||||
|
||||
relayGeminiRouter := router.Group("/v1beta")
|
||||
relayGeminiRouter.Use(middleware.RouteTag("relay"))
|
||||
relayGeminiRouter.Use(middleware.SystemPerformanceCheck())
|
||||
relayGeminiRouter.Use(middleware.TokenAuth())
|
||||
relayGeminiRouter.Use(middleware.ModelRequestRateLimit())
|
||||
relayGeminiRouter.Use(middleware.Distribute())
|
||||
{
|
||||
// Gemini API 路径格式: /v1beta/models/{model_name}:{action}
|
||||
relayGeminiRouter.POST("/models/*path", func(c *gin.Context) {
|
||||
controller.Relay(c, types.RelayFormatGemini)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func registerMjRouterGroup(relayMjRouter *gin.RouterGroup) {
|
||||
relayMjRouter.GET("/image/:id", relay.RelayMidjourneyImage)
|
||||
relayMjRouter.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
relayMjRouter.POST("/submit/action", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/shorten", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/modal", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/imagine", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/change", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/simple-change", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/describe", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/blend", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/edits", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/video", controller.RelayMidjourney)
|
||||
//relayMjRouter.POST("/notify", controller.RelayMidjourney)
|
||||
relayMjRouter.GET("/task/:id/fetch", controller.RelayMidjourney)
|
||||
relayMjRouter.GET("/task/:id/image-seed", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/task/list-by-condition", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/insight-face/swap", controller.RelayMidjourney)
|
||||
relayMjRouter.POST("/submit/upload-discord-images", controller.RelayMidjourney)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListModelsSupportsOpenAIAndGeminiAuthentication(t *testing.T) {
|
||||
setupRelayRouterTestDB(t)
|
||||
|
||||
user := model.User{
|
||||
Username: "models-user",
|
||||
Status: common.UserStatusEnabled,
|
||||
Group: "default",
|
||||
Quota: 100,
|
||||
}
|
||||
require.NoError(t, model.DB.Create(&user).Error)
|
||||
require.NoError(t, model.DB.Create(&model.Token{
|
||||
UserId: user.Id,
|
||||
Key: "modelstestkey",
|
||||
Status: common.TokenStatusEnabled,
|
||||
ExpiredTime: -1,
|
||||
UnlimitedQuota: true,
|
||||
}).Error)
|
||||
|
||||
engine := gin.New()
|
||||
SetRelayRouter(engine)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
headerName string
|
||||
expectedObject string
|
||||
expectedField string
|
||||
}{
|
||||
{
|
||||
name: "OpenAI bearer token",
|
||||
path: "/v1/models",
|
||||
headerName: "Authorization",
|
||||
expectedObject: "list",
|
||||
expectedField: "data",
|
||||
},
|
||||
{
|
||||
name: "Gemini API key header",
|
||||
path: "/v1/models",
|
||||
headerName: "x-goog-api-key",
|
||||
expectedField: "models",
|
||||
},
|
||||
{
|
||||
name: "Gemini API key query",
|
||||
path: "/v1/models?key=modelstestkey",
|
||||
expectedField: "models",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
||||
if test.headerName != "" {
|
||||
value := "modelstestkey"
|
||||
if test.headerName == "Authorization" {
|
||||
value = "Bearer " + value
|
||||
}
|
||||
request.Header.Set(test.headerName, value)
|
||||
}
|
||||
|
||||
engine.ServeHTTP(recorder, request)
|
||||
|
||||
require.Equal(t, http.StatusOK, recorder.Code)
|
||||
var payload map[string]any
|
||||
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &payload))
|
||||
assert.Contains(t, payload, test.expectedField)
|
||||
assert.NotContains(t, payload, "error")
|
||||
if test.expectedObject != "" {
|
||||
assert.Equal(t, test.expectedObject, payload["object"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupRelayRouterTestDB(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
gin.SetMode(gin.TestMode)
|
||||
originalIsMasterNode := common.IsMasterNode
|
||||
originalRedisEnabled := common.RedisEnabled
|
||||
originalSQLitePath := common.SQLitePath
|
||||
originalMainDatabaseType := common.MainDatabaseType()
|
||||
originalLogDatabaseType := common.LogDatabaseType()
|
||||
originalSQLDSN, hadSQLDSN := os.LookupEnv("SQL_DSN")
|
||||
|
||||
common.IsMasterNode = false
|
||||
common.RedisEnabled = false
|
||||
common.SQLitePath = fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))
|
||||
common.SetDatabaseTypes(common.DatabaseTypeSQLite, common.DatabaseTypeSQLite)
|
||||
require.NoError(t, os.Setenv("SQL_DSN", "local"))
|
||||
require.NoError(t, model.InitDB())
|
||||
model.LOG_DB = model.DB
|
||||
require.NoError(t, model.DB.AutoMigrate(&model.User{}, &model.Token{}, &model.Ability{}))
|
||||
|
||||
t.Cleanup(func() {
|
||||
if sqlDB, err := model.DB.DB(); err == nil {
|
||||
_ = sqlDB.Close()
|
||||
}
|
||||
common.IsMasterNode = originalIsMasterNode
|
||||
common.RedisEnabled = originalRedisEnabled
|
||||
common.SQLitePath = originalSQLitePath
|
||||
common.SetDatabaseTypes(originalMainDatabaseType, originalLogDatabaseType)
|
||||
if hadSQLDSN {
|
||||
require.NoError(t, os.Setenv("SQL_DSN", originalSQLDSN))
|
||||
} else {
|
||||
require.NoError(t, os.Unsetenv("SQL_DSN"))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRetiredFrontendAPIRoutes(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
SetApiRouter(engine)
|
||||
|
||||
routes := make(map[string]struct{}, len(engine.Routes()))
|
||||
for _, route := range engine.Routes() {
|
||||
routes[route.Method+" "+route.Path] = struct{}{}
|
||||
}
|
||||
_, hasAsyncCleanup := routes[http.MethodPost+" /api/system-task/log-cleanup"]
|
||||
_, hasDirectDelete := routes[http.MethodDelete+" /api/log/"]
|
||||
_, hasConsoleMigration := routes[http.MethodPost+" /api/option/migrate_console_setting"]
|
||||
assert.True(t, hasAsyncCleanup)
|
||||
assert.False(t, hasDirectDelete)
|
||||
assert.False(t, hasConsoleMigration)
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetVideoRouter(router *gin.Engine) {
|
||||
// Video proxy: accepts either session auth (dashboard) or token auth (API clients)
|
||||
videoProxyRouter := router.Group("/v1")
|
||||
videoProxyRouter.Use(middleware.RouteTag("relay"))
|
||||
videoProxyRouter.Use(middleware.TokenOrUserAuth())
|
||||
{
|
||||
videoProxyRouter.GET("/videos/:task_id/content", controller.VideoProxy)
|
||||
}
|
||||
|
||||
videoV1Router := router.Group("/v1")
|
||||
videoV1Router.Use(middleware.RouteTag("relay"))
|
||||
videoV1Router.Use(middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
videoV1Router.POST("/video/generations", controller.RelayTask)
|
||||
videoV1Router.GET("/video/generations/:task_id", controller.RelayTaskFetch)
|
||||
videoV1Router.POST("/videos/:video_id/remix", controller.RelayTask)
|
||||
}
|
||||
// openai compatible API video routes
|
||||
// docs: https://platform.openai.com/docs/api-reference/videos/create
|
||||
{
|
||||
videoV1Router.POST("/videos", controller.RelayTask)
|
||||
videoV1Router.GET("/videos/:task_id", controller.RelayTaskFetch)
|
||||
}
|
||||
|
||||
klingV1Router := router.Group("/kling/v1")
|
||||
klingV1Router.Use(middleware.RouteTag("relay"))
|
||||
klingV1Router.Use(middleware.KlingRequestConvert(), middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
klingV1Router.POST("/videos/text2video", controller.RelayTask)
|
||||
klingV1Router.POST("/videos/image2video", controller.RelayTask)
|
||||
klingV1Router.GET("/videos/text2video/:task_id", controller.RelayTaskFetch)
|
||||
klingV1Router.GET("/videos/image2video/:task_id", controller.RelayTaskFetch)
|
||||
}
|
||||
|
||||
// Jimeng official API routes - direct mapping to official API format
|
||||
jimengOfficialGroup := router.Group("jimeng")
|
||||
jimengOfficialGroup.Use(middleware.RouteTag("relay"))
|
||||
jimengOfficialGroup.Use(middleware.JimengRequestConvert(), middleware.TokenAuth(), middleware.Distribute())
|
||||
{
|
||||
// Maps to: /?Action=CVSync2AsyncSubmitTask&Version=2022-08-31 and /?Action=CVSync2AsyncGetResult&Version=2022-08-31
|
||||
jimengOfficialGroup.POST("/", controller.RelayTask)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/controller"
|
||||
"github.com/QuantumNous/new-api/middleware"
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// WebAssets holds the embedded dashboard frontend assets.
|
||||
type WebAssets struct {
|
||||
BuildFS embed.FS
|
||||
IndexPage []byte
|
||||
}
|
||||
|
||||
func SetWebRouter(router *gin.Engine, assets WebAssets) {
|
||||
frontendFS := common.EmbedFolder(assets.BuildFS, "web/dist")
|
||||
|
||||
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||
router.Use(middleware.GlobalWebRateLimit())
|
||||
router.Use(middleware.Cache())
|
||||
router.Use(static.Serve("/", frontendFS))
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.Set(middleware.RouteTagKey, "web")
|
||||
if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/assets") {
|
||||
controller.RelayNotFound(c)
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Data(http.StatusOK, "text/html; charset=utf-8", assets.IndexPage)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user