channel-billing.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. package controller
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "github.com/gin-gonic/gin"
  7. "io"
  8. "net/http"
  9. "one-api/common"
  10. "one-api/model"
  11. "strconv"
  12. "time"
  13. )
  14. // https://github.com/songquanpeng/one-api/issues/79
  15. type OpenAISubscriptionResponse struct {
  16. Object string `json:"object"`
  17. HasPaymentMethod bool `json:"has_payment_method"`
  18. SoftLimitUSD float64 `json:"soft_limit_usd"`
  19. HardLimitUSD float64 `json:"hard_limit_usd"`
  20. SystemHardLimitUSD float64 `json:"system_hard_limit_usd"`
  21. }
  22. type OpenAIUsageDailyCost struct {
  23. Timestamp float64 `json:"timestamp"`
  24. LineItems []struct {
  25. Name string `json:"name"`
  26. Cost float64 `json:"cost"`
  27. }
  28. }
  29. type OpenAIUsageResponse struct {
  30. Object string `json:"object"`
  31. //DailyCosts []OpenAIUsageDailyCost `json:"daily_costs"`
  32. TotalUsage float64 `json:"total_usage"` // unit: 0.01 dollar
  33. }
  34. func updateChannelBalance(channel *model.Channel) (float64, error) {
  35. baseURL := common.ChannelBaseURLs[channel.Type]
  36. switch channel.Type {
  37. case common.ChannelTypeOpenAI:
  38. if channel.BaseURL != "" {
  39. baseURL = channel.BaseURL
  40. }
  41. case common.ChannelTypeAzure:
  42. return 0, errors.New("尚未实现")
  43. case common.ChannelTypeCustom:
  44. baseURL = channel.BaseURL
  45. default:
  46. return 0, errors.New("尚未实现")
  47. }
  48. url := fmt.Sprintf("%s/v1/dashboard/billing/subscription", baseURL)
  49. client := &http.Client{}
  50. req, err := http.NewRequest("GET", url, nil)
  51. if err != nil {
  52. return 0, err
  53. }
  54. auth := fmt.Sprintf("Bearer %s", channel.Key)
  55. req.Header.Add("Authorization", auth)
  56. res, err := client.Do(req)
  57. if err != nil {
  58. return 0, err
  59. }
  60. body, err := io.ReadAll(res.Body)
  61. if err != nil {
  62. return 0, err
  63. }
  64. err = res.Body.Close()
  65. if err != nil {
  66. return 0, err
  67. }
  68. subscription := OpenAISubscriptionResponse{}
  69. err = json.Unmarshal(body, &subscription)
  70. if err != nil {
  71. return 0, err
  72. }
  73. now := time.Now()
  74. startDate := fmt.Sprintf("%s-01", now.Format("2006-01"))
  75. endDate := now.Format("2006-01-02")
  76. if !subscription.HasPaymentMethod {
  77. startDate = now.AddDate(0, 0, -100).Format("2006-01-02")
  78. }
  79. url = fmt.Sprintf("%s/v1/dashboard/billing/usage?start_date=%s&end_date=%s", baseURL, startDate, endDate)
  80. req, err = http.NewRequest("GET", url, nil)
  81. if err != nil {
  82. return 0, err
  83. }
  84. req.Header.Add("Authorization", auth)
  85. res, err = client.Do(req)
  86. if err != nil {
  87. return 0, err
  88. }
  89. body, err = io.ReadAll(res.Body)
  90. if err != nil {
  91. return 0, err
  92. }
  93. err = res.Body.Close()
  94. if err != nil {
  95. return 0, err
  96. }
  97. usage := OpenAIUsageResponse{}
  98. err = json.Unmarshal(body, &usage)
  99. if err != nil {
  100. return 0, err
  101. }
  102. balance := subscription.HardLimitUSD - usage.TotalUsage/100
  103. channel.UpdateBalance(balance)
  104. return balance, nil
  105. }
  106. func UpdateChannelBalance(c *gin.Context) {
  107. id, err := strconv.Atoi(c.Param("id"))
  108. if err != nil {
  109. c.JSON(http.StatusOK, gin.H{
  110. "success": false,
  111. "message": err.Error(),
  112. })
  113. return
  114. }
  115. channel, err := model.GetChannelById(id, true)
  116. if err != nil {
  117. c.JSON(http.StatusOK, gin.H{
  118. "success": false,
  119. "message": err.Error(),
  120. })
  121. return
  122. }
  123. balance, err := updateChannelBalance(channel)
  124. if err != nil {
  125. c.JSON(http.StatusOK, gin.H{
  126. "success": false,
  127. "message": err.Error(),
  128. })
  129. return
  130. }
  131. c.JSON(http.StatusOK, gin.H{
  132. "success": true,
  133. "message": "",
  134. "balance": balance,
  135. })
  136. return
  137. }
  138. func updateAllChannelsBalance() error {
  139. channels, err := model.GetAllChannels(0, 0, true)
  140. if err != nil {
  141. return err
  142. }
  143. for _, channel := range channels {
  144. if channel.Status != common.ChannelStatusEnabled {
  145. continue
  146. }
  147. // TODO: support Azure
  148. if channel.Type != common.ChannelTypeOpenAI && channel.Type != common.ChannelTypeCustom {
  149. continue
  150. }
  151. balance, err := updateChannelBalance(channel)
  152. if err != nil {
  153. continue
  154. } else {
  155. // err is nil & balance <= 0 means quota is used up
  156. if balance <= 0 {
  157. disableChannel(channel.Id, channel.Name, "余额不足")
  158. }
  159. }
  160. }
  161. return nil
  162. }
  163. func UpdateAllChannelsBalance(c *gin.Context) {
  164. // TODO: make it async
  165. err := updateAllChannelsBalance()
  166. if err != nil {
  167. c.JSON(http.StatusOK, gin.H{
  168. "success": false,
  169. "message": err.Error(),
  170. })
  171. return
  172. }
  173. c.JSON(http.StatusOK, gin.H{
  174. "success": true,
  175. "message": "",
  176. })
  177. return
  178. }