Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.
 
 
 

343 строки
10 KiB

  1. package controller
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "github.com/QuantumNous/new-api/common"
  7. "github.com/QuantumNous/new-api/model"
  8. "github.com/QuantumNous/new-api/setting/ratio_setting"
  9. "github.com/gin-gonic/gin"
  10. )
  11. // GetAllChannelPricing 获取所有渠道定价(分页)
  12. func GetAllChannelPricing(c *gin.Context) {
  13. page, _ := strconv.Atoi(c.DefaultQuery("p", "1"))
  14. pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
  15. offset := (page - 1) * pageSize
  16. list, total, err := model.GetAllChannelPricing(offset, pageSize)
  17. if err != nil {
  18. common.ApiError(c, err)
  19. return
  20. }
  21. common.ApiSuccess(c, gin.H{
  22. "page": page,
  23. "page_size": pageSize,
  24. "total": total,
  25. "items": list,
  26. })
  27. }
  28. // GetChannelPricingByModel 获取指定模型的所有渠道定价
  29. func GetChannelPricingByModel(c *gin.Context) {
  30. modelName := c.Param("name")
  31. if modelName == "" {
  32. common.ApiErrorMsg(c, "model name is required")
  33. return
  34. }
  35. list, err := model.GetChannelPricingByModel(modelName)
  36. if err != nil {
  37. common.ApiError(c, err)
  38. return
  39. }
  40. common.ApiSuccess(c, list)
  41. }
  42. // CreateChannelPricingRequest 创建渠道定价请求
  43. type CreateChannelPricingRequest struct {
  44. Id int `json:"id"`
  45. ModelName string `json:"model_name" binding:"required"`
  46. ChannelId int `json:"channel_id" binding:"required"`
  47. QuotaType int `json:"quota_type"`
  48. ModelRatio float64 `json:"model_ratio"`
  49. CompletionRatio float64 `json:"completion_ratio"`
  50. ModelPrice float64 `json:"model_price"`
  51. TagIds string `json:"tag_ids"`
  52. CacheRatio float64 `json:"cache_ratio"`
  53. CacheCreationRatio float64 `json:"cache_creation_ratio"`
  54. ImageRatio float64 `json:"image_ratio"`
  55. AudioRatio float64 `json:"audio_ratio"`
  56. AudioCompletionRatio float64 `json:"audio_completion_ratio"`
  57. }
  58. // applyRequest 将请求字段应用到 ChannelPricing
  59. func applyRequestFields(cp *model.ChannelPricing, req *CreateChannelPricingRequest) {
  60. cp.ApplyFields(req.QuotaType, req.ModelRatio, req.CompletionRatio, req.ModelPrice, req.TagIds,
  61. req.CacheRatio, req.CacheCreationRatio, req.ImageRatio, req.AudioRatio, req.AudioCompletionRatio)
  62. }
  63. // CreateChannelPricing 创建或更新渠道定价
  64. func CreateChannelPricing(c *gin.Context) {
  65. var req CreateChannelPricingRequest
  66. if err := c.ShouldBindJSON(&req); err != nil {
  67. common.ApiError(c, err)
  68. return
  69. }
  70. if req.CacheRatio < 0 || req.CacheCreationRatio < 0 || req.ImageRatio < 0 || req.AudioRatio < 0 || req.AudioCompletionRatio < 0 {
  71. common.ApiErrorMsg(c, "ratio values must be >= 0")
  72. return
  73. }
  74. // 检查是否已存在
  75. existing, _ := model.GetChannelPricing(req.ModelName, req.ChannelId)
  76. if existing != nil {
  77. applyRequestFields(existing, &req)
  78. if err := existing.Update(); err != nil {
  79. common.ApiError(c, err)
  80. return
  81. }
  82. common.SysLog(fmt.Sprintf("[ChannelPricing] updated: id=%d model=%s channel=%d", existing.Id, existing.ModelName, existing.ChannelId))
  83. common.ApiSuccess(c, existing)
  84. return
  85. }
  86. // 创建
  87. cp := &model.ChannelPricing{
  88. ModelName: req.ModelName,
  89. ChannelId: req.ChannelId,
  90. }
  91. applyRequestFields(cp, &req)
  92. if err := cp.Insert(); err != nil {
  93. common.ApiError(c, err)
  94. return
  95. }
  96. common.SysLog(fmt.Sprintf("[ChannelPricing] created: model=%s channel=%d quotaType=%d modelRatio=%.4f completionRatio=%.4f modelPrice=%.4f cacheRatio=%.4f cacheCreationRatio=%.4f imageRatio=%.4f audioRatio=%.4f audioCompletionRatio=%.4f",
  97. req.ModelName, req.ChannelId, req.QuotaType, req.ModelRatio, req.CompletionRatio, req.ModelPrice,
  98. req.CacheRatio, req.CacheCreationRatio, req.ImageRatio, req.AudioRatio, req.AudioCompletionRatio))
  99. common.ApiSuccess(c, cp)
  100. }
  101. // BatchCreateChannelPricingRequest 批量创建请求
  102. type BatchCreateChannelPricingRequest struct {
  103. Items []*CreateChannelPricingRequest `json:"items" binding:"required"`
  104. }
  105. // BatchCreateChannelPricing 批量创建或更新渠道定价
  106. func BatchCreateChannelPricing(c *gin.Context) {
  107. var req BatchCreateChannelPricingRequest
  108. if err := c.ShouldBindJSON(&req); err != nil {
  109. common.ApiError(c, err)
  110. return
  111. }
  112. pricings := make([]*model.ChannelPricing, 0, len(req.Items))
  113. for _, item := range req.Items {
  114. cp := &model.ChannelPricing{
  115. ModelName: item.ModelName,
  116. ChannelId: item.ChannelId,
  117. }
  118. applyRequestFields(cp, item)
  119. pricings = append(pricings, cp)
  120. }
  121. // 使用事务逐个处理(GORM 的批量 upsert 在不同数据库表现不一致)
  122. for _, cp := range pricings {
  123. existing, _ := model.GetChannelPricing(cp.ModelName, cp.ChannelId)
  124. if existing != nil {
  125. existing.ApplyFields(cp.QuotaType, cp.ModelRatio, cp.CompletionRatio, cp.ModelPrice, cp.TagIds,
  126. cp.CacheRatio, cp.CacheCreationRatio, cp.ImageRatio, cp.AudioRatio, cp.AudioCompletionRatio)
  127. if err := existing.Update(); err != nil {
  128. common.ApiError(c, err)
  129. return
  130. }
  131. } else {
  132. if err := cp.Insert(); err != nil {
  133. common.ApiError(c, err)
  134. return
  135. }
  136. }
  137. }
  138. common.ApiSuccess(c, gin.H{"affected": len(pricings)})
  139. }
  140. // DeleteChannelPricing 删除渠道定价
  141. func DeleteChannelPricing(c *gin.Context) {
  142. idStr := c.Param("id")
  143. id, err := strconv.Atoi(idStr)
  144. if err != nil {
  145. common.ApiError(c, err)
  146. return
  147. }
  148. cp := &model.ChannelPricing{Id: id}
  149. if err := cp.Delete(); err != nil {
  150. common.ApiError(c, err)
  151. return
  152. }
  153. common.SysLog(fmt.Sprintf("[ChannelPricing] deleted: id=%d", id))
  154. common.ApiSuccess(c, nil)
  155. }
  156. // CopyGlobalPricingRequest 复制全局定价请求
  157. type CopyGlobalPricingRequest struct {
  158. Overwrite bool `json:"overwrite"` // 是否覆盖已存在的渠道定价
  159. }
  160. // CopyGlobalPricing 复制全局定价到指定渠道
  161. // 从 ratio_setting 读取全局定价信息,复制到 channel_pricing 表
  162. func CopyGlobalPricing(c *gin.Context) {
  163. channelIdStr := c.Param("channel_id")
  164. channelId, err := strconv.Atoi(channelIdStr)
  165. if err != nil {
  166. common.ApiError(c, err)
  167. return
  168. }
  169. var req CopyGlobalPricingRequest
  170. c.ShouldBindJSON(&req)
  171. // 获取该渠道支持的所有模型(使用 GetAbilitiesByChannelId)
  172. abilities, err := model.GetAbilitiesByChannelId(channelId)
  173. if err != nil {
  174. common.ApiError(c, err)
  175. return
  176. }
  177. imported := 0
  178. for _, ability := range abilities {
  179. // 检查是否已存在
  180. existing, _ := model.GetChannelPricing(ability.Model, channelId)
  181. if existing != nil && !req.Overwrite {
  182. continue
  183. }
  184. // 获取全局扩展比率
  185. globalCacheRatio, hasCacheRatio := ratio_setting.GetCacheRatio(ability.Model)
  186. if !hasCacheRatio {
  187. globalCacheRatio = 0
  188. }
  189. globalCacheCreationRatio, hasCacheCreationRatio := ratio_setting.GetCreateCacheRatio(ability.Model)
  190. if !hasCacheCreationRatio {
  191. globalCacheCreationRatio = 0
  192. }
  193. globalImageRatio, hasImageRatio := ratio_setting.GetImageRatio(ability.Model)
  194. if !hasImageRatio {
  195. globalImageRatio = 0
  196. }
  197. globalAudioRatio, hasAudioRatio := ratio_setting.GetAudioRatioV2(ability.Model)
  198. if !hasAudioRatio {
  199. globalAudioRatio = 0
  200. }
  201. globalAudioCompletionRatio, hasAudioCompRatio := ratio_setting.GetAudioCompletionRatioV2(ability.Model)
  202. if !hasAudioCompRatio {
  203. globalAudioCompletionRatio = 0
  204. }
  205. // 确定定价类型
  206. var quotaType int
  207. var ratio, completionRatio, price float64
  208. // 优先检查是否有按次计费的价格
  209. modelPrice, hasPrice := ratio_setting.GetModelPrice(ability.Model, false)
  210. if hasPrice {
  211. quotaType = model.QuotaTypeByCall
  212. price = modelPrice
  213. } else {
  214. // 使用按量计费
  215. quotaType = model.QuotaTypeByTokens
  216. modelRatio, hasRatio, _ := ratio_setting.GetModelRatio(ability.Model)
  217. if hasRatio {
  218. ratio = modelRatio
  219. }
  220. completionRatio = ratio_setting.GetCompletionRatio(ability.Model)
  221. }
  222. if existing != nil {
  223. existing.ApplyFields(quotaType, ratio, completionRatio, price, "",
  224. globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio)
  225. if err := existing.Update(); err == nil {
  226. imported++
  227. }
  228. } else {
  229. cp := &model.ChannelPricing{
  230. ModelName: ability.Model,
  231. ChannelId: channelId,
  232. }
  233. cp.ApplyFields(quotaType, ratio, completionRatio, price, "",
  234. globalCacheRatio, globalCacheCreationRatio, globalImageRatio, globalAudioRatio, globalAudioCompletionRatio)
  235. if err := cp.Insert(); err == nil {
  236. imported++
  237. }
  238. }
  239. }
  240. common.SysLog(fmt.Sprintf("[ChannelPricing] copyGlobalPricing: channel=%d imported=%d/%d", channelId, imported, len(abilities)))
  241. common.ApiSuccess(c, gin.H{
  242. "total": len(abilities),
  243. "imported": imported,
  244. })
  245. }
  246. // ChannelPricingWithTags 带标签详情的渠道定价响应
  247. type ChannelPricingWithTags struct {
  248. *model.ChannelPricing
  249. Tags []*model.PricingTag `json:"tags"`
  250. }
  251. // GetChannelPricingByModelWithChannelInfo 获取指定模型的渠道定价(带渠道信息,所有用户可访问)
  252. func GetChannelPricingByModelWithChannelInfo(c *gin.Context) {
  253. // 使用通配符路由时,参数包含前导斜杠,需要去除
  254. modelName := c.Param("name")
  255. if modelName == "" {
  256. common.ApiErrorMsg(c, "model name is required")
  257. return
  258. }
  259. // 去除前导斜杠(路由是 /channel-pricing/model/*name,name 会是 "/deepseek-ai/xxx")
  260. modelName = strings.TrimPrefix(modelName, "/")
  261. list, err := model.GetChannelPricingByModelWithChannelInfo(modelName)
  262. if err != nil {
  263. common.ApiError(c, err)
  264. return
  265. }
  266. common.ApiSuccess(c, list)
  267. }
  268. // GetChannelPricingWithTags 获取渠道定价(带标签详情)
  269. func GetChannelPricingWithTags(c *gin.Context) {
  270. page, _ := strconv.Atoi(c.DefaultQuery("p", "1"))
  271. pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10"))
  272. offset := (page - 1) * pageSize
  273. list, total, err := model.GetAllChannelPricing(offset, pageSize)
  274. if err != nil {
  275. common.ApiError(c, err)
  276. return
  277. }
  278. // 获取所有标签
  279. tags, _ := model.GetAllPricingTags()
  280. tagMap := make(map[int]*model.PricingTag)
  281. for _, tag := range tags {
  282. tagMap[tag.Id] = tag
  283. }
  284. // 为每个定价填充标签详情
  285. result := make([]*ChannelPricingWithTags, 0, len(list))
  286. for _, cp := range list {
  287. item := &ChannelPricingWithTags{
  288. ChannelPricing: cp,
  289. Tags: model.ParseTagIds(cp.TagIds, tagMap),
  290. }
  291. result = append(result, item)
  292. }
  293. common.ApiSuccess(c, gin.H{
  294. "page": page,
  295. "page_size": pageSize,
  296. "total": total,
  297. "items": result,
  298. })
  299. }