This commit refactors the logging mechanism across the application by replacing direct logger calls with a centralized logging approach using the `common` package. Key changes include: - Replaced instances of `logger.SysLog` and `logger.FatalLog` with `common.SysLog` and `common.FatalLog` for consistent logging practices. - Updated resource initialization error handling to utilize the new logging structure, enhancing maintainability and readability. - Minor adjustments to improve code clarity and organization throughout various modules. This change aims to streamline logging and improve the overall architecture of the codebase.
497 lines
14 KiB
Go
497 lines
14 KiB
Go
package controller
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"one-api/common"
|
|
"one-api/constant"
|
|
"one-api/model"
|
|
"one-api/service"
|
|
"one-api/setting"
|
|
"one-api/types"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// https://github.com/songquanpeng/one-api/issues/79
|
|
|
|
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"`
|
|
//DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"`
|
|
TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar
|
|
}
|
|
|
|
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 {
|
|
//Grants interface{} `json:"grants"`
|
|
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"`
|
|
}
|
|
|
|
// GetAuthHeader get auth header
|
|
func GetAuthHeader(token string) http.Header {
|
|
h := http.Header{}
|
|
h.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
|
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(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 common.ChannelTypeOpenAISB:
|
|
// return updateChannelOpenAISBBalance(channel)
|
|
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 // skip multi-key channels
|
|
}
|
|
// TODO: support Azure
|
|
//if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom {
|
|
// continue
|
|
//}
|
|
balance, err := updateChannelBalance(channel)
|
|
if err != nil {
|
|
continue
|
|
} else {
|
|
// err is nil & balance <= 0 means quota is used up
|
|
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) {
|
|
// TODO: make it async
|
|
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")
|
|
}
|
|
}
|