Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b6350ce
feat: add Codex channel disclaimer (i18n, OpenAI terms)
seefs001 Feb 5, 2026
b5b6813
fix: restore log content column
seefs001 Feb 5, 2026
fca80a5
fix: Claude stream block index/type transitions
seefs001 Feb 5, 2026
7314c97
fix: Claude stream block index/type transitions
seefs001 Feb 5, 2026
2567cff
fix(oauth): enhance error handling and transaction management for OAu…
Calcium-Ion Feb 5, 2026
e8d26e5
refactor(oauth): update UpdateCustomOAuthProviderRequest to use point…
Calcium-Ion Feb 5, 2026
2504818
feat: add claude-opus-4-6
prnake Feb 6, 2026
ff71786
fix: aws claude
prnake Feb 6, 2026
c9f5de7
feat: support adaptive thinking
prnake Feb 6, 2026
729610b
fix: set temperature to 1
prnake Feb 6, 2026
8e6071f
Merge branch 'feat/claude-opus-4-6' of https://github.com/prnake/new-…
prnake Feb 6, 2026
b580b8b
fix: add paragraph breaks between reasoning summary chunks in chat2re…
seefs001 Feb 6, 2026
a1695b7
feat: gpt-5.3-codex
seefs001 Feb 6, 2026
ab5456e
🔒 fix(security): sanitize AI-generated HTML to prevent XSS in playground
t0ng7u Feb 6, 2026
aa8240e
feat: /v1/messages -> /v1/responses
seefs001 Feb 6, 2026
3af53bd
fix max_output_token
seefs001 Feb 6, 2026
6de8dea
Merge commit from fork
Calcium-Ion Feb 6, 2026
f3d6e99
Merge pull request #2863 from prnake/feat/claude-opus-4-6
Calcium-Ion Feb 6, 2026
3e1be18
fix: harden token search with pagination, rate limiting and input val…
Calcium-Ion Feb 6, 2026
053699f
Merge commit from fork
Calcium-Ion Feb 6, 2026
5ff9bc3
chore: add fmt import for improved logging in token controller
Calcium-Ion Feb 6, 2026
04dd761
fix: update LIKE pattern sanitization for token search
Calcium-Ion Feb 6, 2026
8ec16fa
feat(topup): hide subscription plans card when no plans available
QuentinHsu Feb 6, 2026
4332837
feat: Force beta=true parameter for Anthropic channel
MUTED64 Feb 6, 2026
d814d62
refactor: enhance API security with read-only token authentication an…
Calcium-Ion Feb 6, 2026
57059ac
fix: /v1/chat/completions -> /v1/responses json_schema
seefs001 Feb 6, 2026
f18aec5
Merge pull request #2876 from seefs001/fix/json_schema
Calcium-Ion Feb 6, 2026
0c0ccf5
refactor: 抽象统一计费会话 BillingSession
Calcium-Ion Feb 6, 2026
15fc77d
fix: 修复 BillingSession 多个边界问题
Calcium-Ion Feb 6, 2026
158baf0
✨ refactor(wallet): Top-up layout to embed subscription plans into th…
t0ng7u Feb 6, 2026
e57bac7
Merge pull request #2878 from QuantumNous/feat/hide-subscription-card…
Calcium-Ion Feb 6, 2026
8b8ea60
Merge pull request #2877 from QuantumNous/refactor/billing-session
Calcium-Ion Feb 6, 2026
1cc6bf1
✨ chore: Improve subscription billing fallback and UI states
t0ng7u Feb 6, 2026
10c5f5f
🛠️ fix: billing session error handling for subscription-first fallback.
t0ng7u Feb 6, 2026
82138fc
🔔 feat: Add subscription-aware quota notifications and update UI copy
t0ng7u Feb 6, 2026
60d4750
Merge pull request #2880 from QuantumNous/feat/subscription-quota-notify
Calcium-Ion Feb 7, 2026
aa31b9c
Merge pull request #2879 from QuantumNous/fix/subscription-preference…
Calcium-Ion Feb 7, 2026
c8aee5e
🔧 refactor: Update formatUserLogs function to accept start index
Calcium-Ion Feb 7, 2026
0837090
🔧 refactor: Enhance Log struct indexing for improved query performance
Calcium-Ion Feb 7, 2026
22d7fdb
codex tips
seefs001 Feb 7, 2026
78ddb85
Merge pull request #2852 from seefs001/fix/codex-tips
Calcium-Ion Feb 7, 2026
5f6f95c
Merge pull request #2874 from MUTED64/main
seefs001 Feb 7, 2026
8e1a990
Merge pull request #2857 from QuantumNous/feat/custom-oauth
Calcium-Ion Feb 7, 2026
dd5610d
Merge pull request #2854 from seefs001/fix/claude-tool-index
Calcium-Ion Feb 7, 2026
ce0113a
Merge pull request #2864 from seefs001/fix/thining-summary
Calcium-Ion Feb 7, 2026
2faa873
Merge branch 'feature/messages2responses' into upstream-main
seefs001 Feb 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion common/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ var OptionMap map[string]string
var OptionMapRWMutex sync.RWMutex

var ItemsPerPage = 10
var MaxRecentItems = 100
var MaxRecentItems = 1000

var PasswordLoginEnabled = true
var PasswordRegisterEnabled = true
Expand Down Expand Up @@ -175,6 +175,10 @@ var (

DownloadRateLimitNum = 10
DownloadRateLimitDuration int64 = 60

// Per-user search rate limit (applies after authentication, keyed by user ID)
SearchRateLimitNum = 10
SearchRateLimitDuration int64 = 60
)

var RateLimitKeyExpirationDuration = 20 * time.Minute
Expand Down
2 changes: 1 addition & 1 deletion common/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func Interface2String(inter interface{}) string {
case int:
return fmt.Sprintf("%d", inter.(int))
case float64:
return fmt.Sprintf("%f", inter.(float64))
return strconv.FormatFloat(inter.(float64), 'f', -1, 64)
case bool:
if inter.(bool) {
return "true"
Expand Down
49 changes: 30 additions & 19 deletions controller/custom_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,21 +166,21 @@ func CreateCustomOAuthProvider(c *gin.Context) {

// UpdateCustomOAuthProviderRequest is the request structure for updating a custom OAuth provider
type UpdateCustomOAuthProviderRequest struct {
Name string `json:"name"`
Slug string `json:"slug"`
Enabled bool `json:"enabled"`
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"` // Optional: if empty, keep existing
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"user_info_endpoint"`
Scopes string `json:"scopes"`
UserIdField string `json:"user_id_field"`
UsernameField string `json:"username_field"`
DisplayNameField string `json:"display_name_field"`
EmailField string `json:"email_field"`
WellKnown string `json:"well_known"`
AuthStyle int `json:"auth_style"`
Name string `json:"name"`
Slug string `json:"slug"`
Enabled *bool `json:"enabled"` // Optional: if nil, keep existing
ClientId string `json:"client_id"`
ClientSecret string `json:"client_secret"` // Optional: if empty, keep existing
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserInfoEndpoint string `json:"user_info_endpoint"`
Scopes string `json:"scopes"`
UserIdField string `json:"user_id_field"`
UsernameField string `json:"username_field"`
DisplayNameField string `json:"display_name_field"`
EmailField string `json:"email_field"`
WellKnown *string `json:"well_known"` // Optional: if nil, keep existing
AuthStyle *int `json:"auth_style"` // Optional: if nil, keep existing
}

// UpdateCustomOAuthProvider updates an existing custom OAuth provider
Expand Down Expand Up @@ -227,7 +227,9 @@ func UpdateCustomOAuthProvider(c *gin.Context) {
if req.Slug != "" {
provider.Slug = req.Slug
}
provider.Enabled = req.Enabled
if req.Enabled != nil {
provider.Enabled = *req.Enabled
}
if req.ClientId != "" {
provider.ClientId = req.ClientId
}
Expand Down Expand Up @@ -258,8 +260,12 @@ func UpdateCustomOAuthProvider(c *gin.Context) {
if req.EmailField != "" {
provider.EmailField = req.EmailField
}
provider.WellKnown = req.WellKnown
provider.AuthStyle = req.AuthStyle
if req.WellKnown != nil {
provider.WellKnown = *req.WellKnown
}
if req.AuthStyle != nil {
provider.AuthStyle = *req.AuthStyle
}

if err := model.UpdateCustomOAuthProvider(provider); err != nil {
common.ApiError(c, err)
Expand Down Expand Up @@ -296,7 +302,12 @@ func DeleteCustomOAuthProvider(c *gin.Context) {
}

// Check if there are any user bindings
count, _ := model.GetBindingCountByProviderId(id)
count, err := model.GetBindingCountByProviderId(id)
if err != nil {
common.SysError("Failed to get binding count for provider " + strconv.Itoa(id) + ": " + err.Error())
common.ApiErrorMsg(c, "检查用户绑定时发生错误,请稍后重试")
return
}
if count > 0 {
common.ApiErrorMsg(c, "该 OAuth 提供商还有用户绑定,无法删除。请先解除所有用户绑定。")
return
Expand Down
50 changes: 25 additions & 25 deletions controller/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,40 +53,32 @@ func GetUserLogs(c *gin.Context) {
return
}

// Deprecated: SearchAllLogs 已废弃,前端未使用该接口。
func SearchAllLogs(c *gin.Context) {
keyword := c.Query("keyword")
logs, err := model.SearchAllLogs(keyword)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": logs,
"success": false,
"message": "该接口已废弃",
})
return
}

// Deprecated: SearchUserLogs 已废弃,前端未使用该接口。
func SearchUserLogs(c *gin.Context) {
keyword := c.Query("keyword")
userId := c.GetInt("id")
logs, err := model.SearchUserLogs(userId, keyword)
if err != nil {
common.ApiError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": logs,
"success": false,
"message": "该接口已废弃",
})
return
}

func GetLogByKey(c *gin.Context) {
key := c.Query("key")
logs, err := model.GetLogByKey(key)
tokenId := c.GetInt("token_id")
if tokenId == 0 {
c.JSON(200, gin.H{
"success": false,
"message": "无效的令牌",
})
return
}
logs, err := model.GetLogByTokenId(tokenId)
if err != nil {
c.JSON(200, gin.H{
"success": false,
Expand All @@ -110,7 +102,11 @@ func GetLogsStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
stat := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
stat, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
if err != nil {
common.ApiError(c, err)
return
}
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, "")
c.JSON(http.StatusOK, gin.H{
"success": true,
Expand All @@ -133,7 +129,11 @@ func GetLogsSelfStat(c *gin.Context) {
modelName := c.Query("model_name")
channel, _ := strconv.Atoi(c.Query("channel"))
group := c.Query("group")
quotaNum := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
quotaNum, err := model.SumUsedQuota(logType, startTimestamp, endTimestamp, modelName, username, tokenName, channel, group)
if err != nil {
common.ApiError(c, err)
return
}
//tokenNum := model.SumUsedToken(logType, startTimestamp, endTimestamp, modelName, username, tokenName)
c.JSON(200, gin.H{
"success": true,
Expand Down
70 changes: 53 additions & 17 deletions controller/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/QuantumNous/new-api/oauth"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)

// providerParams returns map with Provider key for i18n templates
Expand Down Expand Up @@ -256,27 +257,62 @@ func findOrCreateOAuthUser(c *gin.Context, provider oauth.Provider, oauthUser *o
inviterId, _ = model.GetUserIdByAffCode(affCode.(string))
}

if err := user.Insert(inviterId); err != nil {
return nil, err
}

// For custom providers, create the binding after user is created
// Use transaction to ensure user creation and OAuth binding are atomic
if genericProvider, ok := provider.(*oauth.GenericOAuthProvider); ok {
binding := &model.UserOAuthBinding{
UserId: user.Id,
ProviderId: genericProvider.GetProviderId(),
ProviderUserId: oauthUser.ProviderUserID,
}
if err := model.CreateUserOAuthBinding(binding); err != nil {
common.SysError(fmt.Sprintf("[OAuth] Failed to create binding for user %d: %s", user.Id, err.Error()))
// Don't fail the registration, just log the error
// Custom provider: create user and binding in a transaction
err := model.DB.Transaction(func(tx *gorm.DB) error {
// Create user
if err := user.InsertWithTx(tx, inviterId); err != nil {
return err
}

// Create OAuth binding
binding := &model.UserOAuthBinding{
UserId: user.Id,
ProviderId: genericProvider.GetProviderId(),
ProviderUserId: oauthUser.ProviderUserID,
}
if err := model.CreateUserOAuthBindingWithTx(tx, binding); err != nil {
return err
}

return nil
})
if err != nil {
return nil, err
}

// Perform post-transaction tasks (logs, sidebar config, inviter rewards)
user.FinalizeOAuthUserCreation(inviterId)
} else {
// Built-in provider: set the provider user ID on the user model
provider.SetProviderUserID(user, oauthUser.ProviderUserID)
if err := user.Update(false); err != nil {
common.SysError(fmt.Sprintf("[OAuth] Failed to update provider ID for user %d: %s", user.Id, err.Error()))
// Built-in provider: create user and update provider ID in a transaction
err := model.DB.Transaction(func(tx *gorm.DB) error {
// Create user
if err := user.InsertWithTx(tx, inviterId); err != nil {
return err
}

// Set the provider user ID on the user model and update
provider.SetProviderUserID(user, oauthUser.ProviderUserID)
if err := tx.Model(user).Updates(map[string]interface{}{
"github_id": user.GitHubId,
"discord_id": user.DiscordId,
"oidc_id": user.OidcId,
"linux_do_id": user.LinuxDOId,
"wechat_id": user.WeChatId,
"telegram_id": user.TelegramId,
}).Error; err != nil {
return err
}

return nil
})
if err != nil {
return nil, err
}

// Perform post-transaction tasks
user.FinalizeOAuthUserCreation(inviterId)
}

return user, nil
Expand Down
4 changes: 2 additions & 2 deletions controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ func Relay(c *gin.Context, relayFormat types.RelayFormat) {
// Only return quota if downstream failed and quota was actually pre-consumed
if newAPIError != nil {
newAPIError = service.NormalizeViolationFeeError(newAPIError)
if relayInfo.FinalPreConsumedQuota != 0 {
service.ReturnPreConsumedQuota(c, relayInfo)
if relayInfo.Billing != nil {
relayInfo.Billing.Refund(c)
}
service.ChargeViolationFeeIfNeeded(c, relayInfo, newAPIError)
}
Expand Down
88 changes: 0 additions & 88 deletions controller/secure_verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,94 +133,6 @@ func UniversalVerify(c *gin.Context) {
})
}

// GetVerificationStatus 获取验证状态
func GetVerificationStatus(c *gin.Context) {
userId := c.GetInt("id")
if userId == 0 {
c.JSON(http.StatusUnauthorized, gin.H{
"success": false,
"message": "未登录",
})
return
}

session := sessions.Default(c)
verifiedAtRaw := session.Get(SecureVerificationSessionKey)

if verifiedAtRaw == nil {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}

verifiedAt, ok := verifiedAtRaw.(int64)
if !ok {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}

elapsed := time.Now().Unix() - verifiedAt
if elapsed >= SecureVerificationTimeout {
// 验证已过期
session.Delete(SecureVerificationSessionKey)
_ = session.Save()
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: false,
},
})
return
}

c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": VerificationStatusResponse{
Verified: true,
ExpiresAt: verifiedAt + SecureVerificationTimeout,
},
})
}

// CheckSecureVerification 检查是否已通过安全验证
// 返回 true 表示验证有效,false 表示需要重新验证
func CheckSecureVerification(c *gin.Context) bool {
session := sessions.Default(c)
verifiedAtRaw := session.Get(SecureVerificationSessionKey)

if verifiedAtRaw == nil {
return false
}

verifiedAt, ok := verifiedAtRaw.(int64)
if !ok {
return false
}

elapsed := time.Now().Unix() - verifiedAt
if elapsed >= SecureVerificationTimeout {
// 验证已过期,清除 session
session.Delete(SecureVerificationSessionKey)
_ = session.Save()
return false
}

return true
}

// PasskeyVerifyAndSetSession Passkey 验证完成后设置 session
// 这是一个辅助函数,供 PasskeyVerifyFinish 调用
func PasskeyVerifyAndSetSession(c *gin.Context) {
Expand Down
Loading