| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- 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))
- }
- res, err := service.GetHttpClient().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")
- }
- }
|