|
|
package controller |
|
|
|
|
|
import ( |
|
|
"encoding/json" |
|
|
"errors" |
|
|
"fmt" |
|
|
"io" |
|
|
"net/http" |
|
|
"strconv" |
|
|
"time" |
|
|
|
|
|
"github.com/QuantumNous/new-api/common" |
|
|
"github.com/QuantumNous/new-api/constant" |
|
|
"github.com/QuantumNous/new-api/model" |
|
|
"github.com/QuantumNous/new-api/service" |
|
|
"github.com/QuantumNous/new-api/setting/operation_setting" |
|
|
"github.com/QuantumNous/new-api/types" |
|
|
|
|
|
"github.com/shopspring/decimal" |
|
|
|
|
|
"github.com/gin-gonic/gin" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
type OpenAISubscriptionResponse struct { |
|
|
Object string `json:"object"` |
|
|
HasPaymentMethod bool `json:"has_payment_method"` |
|
|
SoftLimitUSD float64 `json:"soft_limit_usd"` |
|
|
HardLimitUSD float64 `json:"hard_limit_usd"` |
|
|
SystemHardLimitUSD float64 `json:"system_hard_limit_usd"` |
|
|
AccessUntil int64 `json:"access_until"` |
|
|
} |
|
|
|
|
|
type OpenAIUsageDailyCost struct { |
|
|
Timestamp float64 `json:"timestamp"` |
|
|
LineItems []struct { |
|
|
Name string `json:"name"` |
|
|
Cost float64 `json:"cost"` |
|
|
} |
|
|
} |
|
|
|
|
|
type OpenAICreditGrants struct { |
|
|
Object string `json:"object"` |
|
|
TotalGranted float64 `json:"total_granted"` |
|
|
TotalUsed float64 `json:"total_used"` |
|
|
TotalAvailable float64 `json:"total_available"` |
|
|
} |
|
|
|
|
|
type OpenAIUsageResponse struct { |
|
|
Object string `json:"object"` |
|
|
|
|
|
TotalUsage float64 `json:"total_usage"` |
|
|
} |
|
|
|
|
|
type OpenAISBUsageResponse struct { |
|
|
Msg string `json:"msg"` |
|
|
Data *struct { |
|
|
Credit string `json:"credit"` |
|
|
} `json:"data"` |
|
|
} |
|
|
|
|
|
type AIProxyUserOverviewResponse struct { |
|
|
Success bool `json:"success"` |
|
|
Message string `json:"message"` |
|
|
ErrorCode int `json:"error_code"` |
|
|
Data struct { |
|
|
TotalPoints float64 `json:"totalPoints"` |
|
|
} `json:"data"` |
|
|
} |
|
|
|
|
|
type API2GPTUsageResponse struct { |
|
|
Object string `json:"object"` |
|
|
TotalGranted float64 `json:"total_granted"` |
|
|
TotalUsed float64 `json:"total_used"` |
|
|
TotalRemaining float64 `json:"total_remaining"` |
|
|
} |
|
|
|
|
|
type APGC2DGPTUsageResponse struct { |
|
|
|
|
|
Object string `json:"object"` |
|
|
TotalAvailable float64 `json:"total_available"` |
|
|
TotalGranted float64 `json:"total_granted"` |
|
|
TotalUsed float64 `json:"total_used"` |
|
|
} |
|
|
|
|
|
type SiliconFlowUsageResponse struct { |
|
|
Code int `json:"code"` |
|
|
Message string `json:"message"` |
|
|
Status bool `json:"status"` |
|
|
Data struct { |
|
|
ID string `json:"id"` |
|
|
Name string `json:"name"` |
|
|
Image string `json:"image"` |
|
|
Email string `json:"email"` |
|
|
IsAdmin bool `json:"isAdmin"` |
|
|
Balance string `json:"balance"` |
|
|
Status string `json:"status"` |
|
|
Introduction string `json:"introduction"` |
|
|
Role string `json:"role"` |
|
|
ChargeBalance string `json:"chargeBalance"` |
|
|
TotalBalance string `json:"totalBalance"` |
|
|
Category string `json:"category"` |
|
|
} `json:"data"` |
|
|
} |
|
|
|
|
|
type DeepSeekUsageResponse struct { |
|
|
IsAvailable bool `json:"is_available"` |
|
|
BalanceInfos []struct { |
|
|
Currency string `json:"currency"` |
|
|
TotalBalance string `json:"total_balance"` |
|
|
GrantedBalance string `json:"granted_balance"` |
|
|
ToppedUpBalance string `json:"topped_up_balance"` |
|
|
} `json:"balance_infos"` |
|
|
} |
|
|
|
|
|
type OpenRouterCreditResponse struct { |
|
|
Data struct { |
|
|
TotalCredits float64 `json:"total_credits"` |
|
|
TotalUsage float64 `json:"total_usage"` |
|
|
} `json:"data"` |
|
|
} |
|
|
|
|
|
|
|
|
func GetAuthHeader(token string) http.Header { |
|
|
h := http.Header{} |
|
|
h.Add("Authorization", fmt.Sprintf("Bearer %s", token)) |
|
|
return h |
|
|
} |
|
|
|
|
|
|
|
|
func GetClaudeAuthHeader(token string) http.Header { |
|
|
h := http.Header{} |
|
|
h.Add("x-api-key", token) |
|
|
h.Add("anthropic-version", "2023-06-01") |
|
|
return h |
|
|
} |
|
|
|
|
|
func GetResponseBody(method, url string, channel *model.Channel, headers http.Header) ([]byte, error) { |
|
|
req, err := http.NewRequest(method, url, nil) |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
for k := range headers { |
|
|
req.Header.Add(k, headers.Get(k)) |
|
|
} |
|
|
client, err := service.NewProxyHttpClient(channel.GetSetting().Proxy) |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
res, err := client.Do(req) |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
if res.StatusCode != http.StatusOK { |
|
|
return nil, fmt.Errorf("status code: %d", res.StatusCode) |
|
|
} |
|
|
body, err := io.ReadAll(res.Body) |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
err = res.Body.Close() |
|
|
if err != nil { |
|
|
return nil, err |
|
|
} |
|
|
return body, nil |
|
|
} |
|
|
|
|
|
func updateChannelCloseAIBalance(channel *model.Channel) (float64, error) { |
|
|
url := fmt.Sprintf("%s/dashboard/billing/credit_grants", channel.GetBaseURL()) |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
|
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := OpenAICreditGrants{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(response.TotalAvailable) |
|
|
return response.TotalAvailable, nil |
|
|
} |
|
|
|
|
|
func updateChannelOpenAISBBalance(channel *model.Channel) (float64, error) { |
|
|
url := fmt.Sprintf("https://api.openai-sb.com/sb-api/user/status?api_key=%s", channel.Key) |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := OpenAISBUsageResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
if response.Data == nil { |
|
|
return 0, errors.New(response.Msg) |
|
|
} |
|
|
balance, err := strconv.ParseFloat(response.Data.Credit, 64) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(balance) |
|
|
return balance, nil |
|
|
} |
|
|
|
|
|
func updateChannelAIProxyBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://aiproxy.io/api/report/getUserOverview" |
|
|
headers := http.Header{} |
|
|
headers.Add("Api-Key", channel.Key) |
|
|
body, err := GetResponseBody("GET", url, channel, headers) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := AIProxyUserOverviewResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
if !response.Success { |
|
|
return 0, fmt.Errorf("code: %d, message: %s", response.ErrorCode, response.Message) |
|
|
} |
|
|
channel.UpdateBalance(response.Data.TotalPoints) |
|
|
return response.Data.TotalPoints, nil |
|
|
} |
|
|
|
|
|
func updateChannelAPI2GPTBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://api.api2gpt.com/dashboard/billing/credit_grants" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
|
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := API2GPTUsageResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(response.TotalRemaining) |
|
|
return response.TotalRemaining, nil |
|
|
} |
|
|
|
|
|
func updateChannelSiliconFlowBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://api.siliconflow.cn/v1/user/info" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := SiliconFlowUsageResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
if response.Code != 20000 { |
|
|
return 0, fmt.Errorf("code: %d, message: %s", response.Code, response.Message) |
|
|
} |
|
|
balance, err := strconv.ParseFloat(response.Data.TotalBalance, 64) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(balance) |
|
|
return balance, nil |
|
|
} |
|
|
|
|
|
func updateChannelDeepSeekBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://api.deepseek.com/user/balance" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := DeepSeekUsageResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
index := -1 |
|
|
for i, balanceInfo := range response.BalanceInfos { |
|
|
if balanceInfo.Currency == "CNY" { |
|
|
index = i |
|
|
break |
|
|
} |
|
|
} |
|
|
if index == -1 { |
|
|
return 0, errors.New("currency CNY not found") |
|
|
} |
|
|
balance, err := strconv.ParseFloat(response.BalanceInfos[index].TotalBalance, 64) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(balance) |
|
|
return balance, nil |
|
|
} |
|
|
|
|
|
func updateChannelAIGC2DBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://api.aigc2d.com/dashboard/billing/credit_grants" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := APGC2DGPTUsageResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
channel.UpdateBalance(response.TotalAvailable) |
|
|
return response.TotalAvailable, nil |
|
|
} |
|
|
|
|
|
func updateChannelOpenRouterBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://openrouter.ai/api/v1/credits" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
response := OpenRouterCreditResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
balance := response.Data.TotalCredits - response.Data.TotalUsage |
|
|
channel.UpdateBalance(balance) |
|
|
return balance, nil |
|
|
} |
|
|
|
|
|
func updateChannelMoonshotBalance(channel *model.Channel) (float64, error) { |
|
|
url := "https://api.moonshot.cn/v1/users/me/balance" |
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
|
|
|
type MoonshotBalanceData struct { |
|
|
AvailableBalance float64 `json:"available_balance"` |
|
|
VoucherBalance float64 `json:"voucher_balance"` |
|
|
CashBalance float64 `json:"cash_balance"` |
|
|
} |
|
|
|
|
|
type MoonshotBalanceResponse struct { |
|
|
Code int `json:"code"` |
|
|
Data MoonshotBalanceData `json:"data"` |
|
|
Scode string `json:"scode"` |
|
|
Status bool `json:"status"` |
|
|
} |
|
|
|
|
|
response := MoonshotBalanceResponse{} |
|
|
err = json.Unmarshal(body, &response) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
if !response.Status || response.Code != 0 { |
|
|
return 0, fmt.Errorf("failed to update moonshot balance, status: %v, code: %d, scode: %s", response.Status, response.Code, response.Scode) |
|
|
} |
|
|
availableBalanceCny := response.Data.AvailableBalance |
|
|
availableBalanceUsd := decimal.NewFromFloat(availableBalanceCny).Div(decimal.NewFromFloat(operation_setting.Price)).InexactFloat64() |
|
|
channel.UpdateBalance(availableBalanceUsd) |
|
|
return availableBalanceUsd, nil |
|
|
} |
|
|
|
|
|
func updateChannelBalance(channel *model.Channel) (float64, error) { |
|
|
baseURL := constant.ChannelBaseURLs[channel.Type] |
|
|
if channel.GetBaseURL() == "" { |
|
|
channel.BaseURL = &baseURL |
|
|
} |
|
|
switch channel.Type { |
|
|
case constant.ChannelTypeOpenAI: |
|
|
if channel.GetBaseURL() != "" { |
|
|
baseURL = channel.GetBaseURL() |
|
|
} |
|
|
case constant.ChannelTypeAzure: |
|
|
return 0, errors.New("尚未实现") |
|
|
case constant.ChannelTypeCustom: |
|
|
baseURL = channel.GetBaseURL() |
|
|
|
|
|
|
|
|
case constant.ChannelTypeAIProxy: |
|
|
return updateChannelAIProxyBalance(channel) |
|
|
case constant.ChannelTypeAPI2GPT: |
|
|
return updateChannelAPI2GPTBalance(channel) |
|
|
case constant.ChannelTypeAIGC2D: |
|
|
return updateChannelAIGC2DBalance(channel) |
|
|
case constant.ChannelTypeSiliconFlow: |
|
|
return updateChannelSiliconFlowBalance(channel) |
|
|
case constant.ChannelTypeDeepSeek: |
|
|
return updateChannelDeepSeekBalance(channel) |
|
|
case constant.ChannelTypeOpenRouter: |
|
|
return updateChannelOpenRouterBalance(channel) |
|
|
case constant.ChannelTypeMoonshot: |
|
|
return updateChannelMoonshotBalance(channel) |
|
|
default: |
|
|
return 0, errors.New("尚未实现") |
|
|
} |
|
|
url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL) |
|
|
|
|
|
body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
subscription := OpenAISubscriptionResponse{} |
|
|
err = json.Unmarshal(body, &subscription) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
now := time.Now() |
|
|
startDate := fmt.Sprintf("%s-01", now.Format("2006-01")) |
|
|
endDate := now.Format("2006-01-02") |
|
|
if !subscription.HasPaymentMethod { |
|
|
startDate = now.AddDate(0, 0, -100).Format("2006-01-02") |
|
|
} |
|
|
url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate) |
|
|
body, err = GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
usage := OpenAIUsageResponse{} |
|
|
err = json.Unmarshal(body, &usage) |
|
|
if err != nil { |
|
|
return 0, err |
|
|
} |
|
|
balance := subscription.HardLimitUSD - usage.TotalUsage/100 |
|
|
channel.UpdateBalance(balance) |
|
|
return balance, nil |
|
|
} |
|
|
|
|
|
func UpdateChannelBalance(c *gin.Context) { |
|
|
id, err := strconv.Atoi(c.Param("id")) |
|
|
if err != nil { |
|
|
common.ApiError(c, err) |
|
|
return |
|
|
} |
|
|
channel, err := model.CacheGetChannel(id) |
|
|
if err != nil { |
|
|
common.ApiError(c, err) |
|
|
return |
|
|
} |
|
|
if channel.ChannelInfo.IsMultiKey { |
|
|
c.JSON(http.StatusOK, gin.H{ |
|
|
"success": false, |
|
|
"message": "多密钥渠道不支持余额查询", |
|
|
}) |
|
|
return |
|
|
} |
|
|
balance, err := updateChannelBalance(channel) |
|
|
if err != nil { |
|
|
common.ApiError(c, err) |
|
|
return |
|
|
} |
|
|
c.JSON(http.StatusOK, gin.H{ |
|
|
"success": true, |
|
|
"message": "", |
|
|
"balance": balance, |
|
|
}) |
|
|
} |
|
|
|
|
|
func updateAllChannelsBalance() error { |
|
|
channels, err := model.GetAllChannels(0, 0, true, false) |
|
|
if err != nil { |
|
|
return err |
|
|
} |
|
|
for _, channel := range channels { |
|
|
if channel.Status != common.ChannelStatusEnabled { |
|
|
continue |
|
|
} |
|
|
if channel.ChannelInfo.IsMultiKey { |
|
|
continue |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
balance, err := updateChannelBalance(channel) |
|
|
if err != nil { |
|
|
continue |
|
|
} else { |
|
|
|
|
|
if balance <= 0 { |
|
|
service.DisableChannel(*types.NewChannelError(channel.Id, channel.Type, channel.Name, channel.ChannelInfo.IsMultiKey, "", channel.GetAutoBan()), "余额不足") |
|
|
} |
|
|
} |
|
|
time.Sleep(common.RequestInterval) |
|
|
} |
|
|
return nil |
|
|
} |
|
|
|
|
|
func UpdateAllChannelsBalance(c *gin.Context) { |
|
|
|
|
|
err := updateAllChannelsBalance() |
|
|
if err != nil { |
|
|
common.ApiError(c, err) |
|
|
return |
|
|
} |
|
|
c.JSON(http.StatusOK, gin.H{ |
|
|
"success": true, |
|
|
"message": "", |
|
|
}) |
|
|
return |
|
|
} |
|
|
|
|
|
func AutomaticallyUpdateChannels(frequency int) { |
|
|
for { |
|
|
time.Sleep(time.Duration(frequency) * time.Minute) |
|
|
common.SysLog("updating all channels") |
|
|
_ = updateAllChannelsBalance() |
|
|
common.SysLog("channels update done") |
|
|
} |
|
|
} |
|
|
|