billing_service.go 89 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "math"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/Wei-Shaw/sub2api/internal/config"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  14. )
  15. // APIKeyRateLimitCacheData holds rate limit usage data cached in Redis.
  16. type APIKeyRateLimitCacheData struct {
  17. Usage5h float64 `json:"usage_5h"`
  18. Usage1d float64 `json:"usage_1d"`
  19. Usage7d float64 `json:"usage_7d"`
  20. Window5h int64 `json:"window_5h"` // unix timestamp, 0 = not started
  21. Window1d int64 `json:"window_1d"`
  22. Window7d int64 `json:"window_7d"`
  23. }
  24. // UserPlatformQuotaKey 标识一个 user×platform,用于脏集出入与批量读。
  25. type UserPlatformQuotaKey struct {
  26. UserID int64
  27. Platform string
  28. }
  29. // UserPlatformQuotaCacheEntry Redis hash 反序列化结果。
  30. //
  31. // SchemaVersion 用于向后兼容:
  32. // - 0(旧 entry,无 SchemaVersion 字段)→ 视为 cache MISS,强制 refresh
  33. // - 1(当前版本)→ 包含 limits 和 window_start,可免 DB 查询
  34. //
  35. // limit 字段为 nil 表示"无限额"(DB 中对应列为 NULL)。
  36. const UserPlatformQuotaCacheSchemaV1 = int64(1)
  37. type UserPlatformQuotaCacheEntry struct {
  38. DailyUsageUSD float64
  39. WeeklyUsageUSD float64
  40. MonthlyUsageUSD float64
  41. Version int64
  42. SchemaVersion int64
  43. // 以下字段仅在 SchemaVersion >= 1 时有效
  44. DailyLimitUSD *float64
  45. WeeklyLimitUSD *float64
  46. MonthlyLimitUSD *float64
  47. DailyWindowStart *time.Time
  48. WeeklyWindowStart *time.Time
  49. MonthlyWindowStart *time.Time
  50. }
  51. // BillingCache defines cache operations for billing service
  52. type BillingCache interface {
  53. // Balance operations
  54. GetUserBalance(ctx context.Context, userID int64) (float64, error)
  55. SetUserBalance(ctx context.Context, userID int64, balance float64) error
  56. DeductUserBalance(ctx context.Context, userID int64, amount float64) error
  57. InvalidateUserBalance(ctx context.Context, userID int64) error
  58. // Subscription operations
  59. GetSubscriptionCache(ctx context.Context, userID, groupID int64) (*SubscriptionCacheData, error)
  60. SetSubscriptionCache(ctx context.Context, userID, groupID int64, data *SubscriptionCacheData) error
  61. UpdateSubscriptionUsage(ctx context.Context, userID, groupID int64, cost float64) error
  62. InvalidateSubscriptionCache(ctx context.Context, userID, groupID int64) error
  63. // API Key rate limit operations
  64. GetAPIKeyRateLimit(ctx context.Context, keyID int64) (*APIKeyRateLimitCacheData, error)
  65. SetAPIKeyRateLimit(ctx context.Context, keyID int64, data *APIKeyRateLimitCacheData) error
  66. UpdateAPIKeyRateLimitUsage(ctx context.Context, keyID int64, cost float64) error
  67. InvalidateAPIKeyRateLimit(ctx context.Context, keyID int64) error
  68. // user × platform quota 缓存
  69. GetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) (*UserPlatformQuotaCacheEntry, bool, error)
  70. SetUserPlatformQuotaCache(ctx context.Context, userID int64, platform string, entry *UserPlatformQuotaCacheEntry, ttl time.Duration) error
  71. DeleteUserPlatformQuotaCache(ctx context.Context, userID int64, platform string) error
  72. // IncrUserPlatformQuotaUsageCache 在缓存命中时累加用量;缓存未命中(key 不存在)静默返回 nil。
  73. // markDirty=true 时将该 key 的 member 写入 Redis 脏集,供 flusher 批量回写 DB。
  74. IncrUserPlatformQuotaUsageCache(ctx context.Context, userID int64, platform string, cost float64, ttl time.Duration, markDirty bool) error
  75. // 脏集读写,供 flusher 使用。
  76. PopDirtyUserPlatformQuotaKeys(ctx context.Context, n int) ([]UserPlatformQuotaKey, error)
  77. ReaddDirtyUserPlatformQuotaKeys(ctx context.Context, keys []UserPlatformQuotaKey) error
  78. BatchGetUserPlatformQuotaCache(ctx context.Context, keys []UserPlatformQuotaKey) ([]*UserPlatformQuotaCacheEntry, error)
  79. }
  80. // ModelPricing 模型价格配置(per-token价格,与LiteLLM格式一致)
  81. type ModelPricing struct {
  82. InputPricePerToken float64 // 每token输入价格 (USD)
  83. InputPricePerTokenPriority float64 // priority service tier 下每token输入价格 (USD)
  84. ImageInputPricePerToken float64 // 图片输入 token 价格 (USD),用于多模态 embedding 等图文不同价场景;为 0 时回退到 InputPricePerToken
  85. OutputPricePerToken float64 // 每token输出价格 (USD)
  86. OutputPricePerTokenPriority float64 // priority service tier 下每token输出价格 (USD)
  87. CacheCreationPricePerToken float64 // 缓存创建每token价格 (USD)
  88. CacheCreationPricePerTokenPriority float64 // priority service tier 下缓存创建每token价格 (USD)
  89. CacheCreationPriceExplicit bool // 是否由渠道/区间定价显式设定(为 true 时即使 == 0 也不回退)
  90. CacheReadPricePerToken float64 // 缓存读取每token价格 (USD)
  91. CacheReadPricePerTokenPriority float64 // priority service tier 下缓存读取每token价格 (USD)
  92. FastMultiplier *float64 // 渠道显式 Fast/priority 倍率;nil 时沿用模型目录行为
  93. FlexMultiplier *float64 // 渠道显式 Flex 倍率;nil 时沿用默认行为
  94. MaxReasoningEffortMultiplier *float64 // max 推理等级的额度/计费倍率;nil 时沿用模型默认行为
  95. CacheCreation5mPrice float64 // 5分钟缓存创建每token价格 (USD)
  96. CacheCreation1hPrice float64 // 1小时缓存创建每token价格 (USD)
  97. SupportsCacheBreakdown bool // 是否支持详细的缓存分类
  98. LongContextInputThreshold int // 超过阈值后按整次会话提升输入价格
  99. LongContextThresholdInclusive bool // 达到阈值即应用(xAI);默认保持严格大于以兼容既有模型
  100. LongContextInputMultiplier float64 // 长上下文整次会话输入倍率
  101. LongContextOutputMultiplier float64 // 长上下文整次会话输出倍率
  102. ImageOutputPricePerToken float64 // 图片输出 token 价格 (USD)
  103. ImageOutputPriceExplicit bool // 是否由渠道定价显式设定(为 true 时即使 == 0 也不回退)
  104. }
  105. func normalizeBillingServiceTier(serviceTier string) string {
  106. return strings.ToLower(strings.TrimSpace(serviceTier))
  107. }
  108. func usePriorityServiceTierPricing(serviceTier string, pricing *ModelPricing) bool {
  109. if pricing == nil {
  110. return false
  111. }
  112. tier := normalizeBillingServiceTier(serviceTier)
  113. if tier != "priority" && tier != "fast" {
  114. return false
  115. }
  116. if pricing.FastMultiplier != nil {
  117. return false
  118. }
  119. return pricing.InputPricePerTokenPriority > 0 || pricing.OutputPricePerTokenPriority > 0 ||
  120. pricing.CacheCreationPricePerTokenPriority > 0 || pricing.CacheReadPricePerTokenPriority > 0
  121. }
  122. func serviceTierCostMultiplier(serviceTier string) float64 {
  123. switch normalizeBillingServiceTier(serviceTier) {
  124. case "priority", "fast", OpenAIFastTierUltrafast:
  125. return 2.0
  126. case "flex":
  127. return 0.5
  128. default:
  129. return 1.0
  130. }
  131. }
  132. func configuredServiceTierMultiplier(serviceTier string, pricing *ModelPricing) float64 {
  133. if pricing != nil {
  134. switch normalizeBillingServiceTier(serviceTier) {
  135. case "priority", "fast":
  136. if pricing.FastMultiplier != nil {
  137. return *pricing.FastMultiplier
  138. }
  139. case "flex":
  140. if pricing.FlexMultiplier != nil {
  141. return *pricing.FlexMultiplier
  142. }
  143. }
  144. }
  145. return serviceTierCostMultiplier(serviceTier)
  146. }
  147. func pricingWithPriorityMultiplier(base *ModelPricing, multiplier float64) *ModelPricing {
  148. if base == nil {
  149. return nil
  150. }
  151. cloned := *base
  152. cloned.InputPricePerTokenPriority = cloned.InputPricePerToken * multiplier
  153. cloned.OutputPricePerTokenPriority = cloned.OutputPricePerToken * multiplier
  154. cloned.CacheCreationPricePerTokenPriority = cloned.CacheCreationPricePerToken * multiplier
  155. cloned.CacheReadPricePerTokenPriority = cloned.CacheReadPricePerToken * multiplier
  156. return &cloned
  157. }
  158. // UsageTokens 使用的token数量
  159. type UsageTokens struct {
  160. InputTokens int
  161. ImageInputTokens int
  162. OutputTokens int
  163. CacheCreationTokens int
  164. CacheReadTokens int
  165. CacheCreation5mTokens int
  166. CacheCreation1hTokens int
  167. ImageOutputTokens int
  168. }
  169. // CostBreakdown 费用明细
  170. type CostBreakdown struct {
  171. InputCost float64 // 文本输入费用(不含图片输入,图片输入单独记入 ImageInputCost)
  172. ImageInputCost float64 // 图片输入 token 费用(如 gpt-image-2 图片编辑)
  173. OutputCost float64
  174. ImageOutputCost float64
  175. CacheCreationCost float64
  176. CacheReadCost float64
  177. TotalCost float64
  178. ActualCost float64 // 应用倍率后的实际费用
  179. BillingMode string // 计费模式("token"/"per_request"/"image"),由 CalculateCostUnified 填充
  180. LongContextBillingApplied bool
  181. }
  182. func applyCostBreakdownMultiplier(cost *CostBreakdown, multiplier float64) {
  183. if cost == nil || multiplier == 1 {
  184. return
  185. }
  186. cost.InputCost *= multiplier
  187. cost.ImageInputCost *= multiplier
  188. cost.OutputCost *= multiplier
  189. cost.ImageOutputCost *= multiplier
  190. cost.CacheCreationCost *= multiplier
  191. cost.CacheReadCost *= multiplier
  192. cost.TotalCost *= multiplier
  193. cost.ActualCost *= multiplier
  194. }
  195. const claudeFable51MaxReasoningEffortMultiplier = 3.0
  196. func isClaudeFable51Model(model string) bool {
  197. model = strings.ToLower(strings.TrimSpace(model))
  198. for _, marker := range []string{"fable-5-1", "fable-5.1", "fable5.1", "fable51"} {
  199. if at := strings.Index(model, marker); at >= 0 {
  200. after := at + len(marker)
  201. if after == len(model) || model[after] < '0' || model[after] > '9' {
  202. return true
  203. }
  204. }
  205. }
  206. return false
  207. }
  208. func defaultMaxReasoningEffortMultiplier(model string) *float64 {
  209. if !isClaudeFable51Model(model) {
  210. return nil
  211. }
  212. multiplier := claudeFable51MaxReasoningEffortMultiplier
  213. return &multiplier
  214. }
  215. func maxReasoningEffortBillingMultiplier(model, effort string, pricing *ModelPricing) float64 {
  216. if NormalizeMaxReasoningEffort(effort) != "max" {
  217. return 1
  218. }
  219. if pricing != nil && pricing.MaxReasoningEffortMultiplier != nil && *pricing.MaxReasoningEffortMultiplier > 0 {
  220. return *pricing.MaxReasoningEffortMultiplier
  221. }
  222. if multiplier := defaultMaxReasoningEffortMultiplier(model); multiplier != nil {
  223. return *multiplier
  224. }
  225. return 1
  226. }
  227. func resolvedChannelTimeMultiplier(resolved *ResolvedPricing, at time.Time) float64 {
  228. if resolved == nil || resolved.Source != PricingSourceChannel || resolved.channelPricing == nil {
  229. return 1
  230. }
  231. return resolved.channelPricing.TimePricing.MultiplierAt(at)
  232. }
  233. // ErrModelPricingUnavailable indicates that none of the configured pricing
  234. // sources can price the requested model.
  235. var ErrModelPricingUnavailable = errors.New("pricing not found")
  236. // ---- DeepSeek 官方低谷价($/token,2026-08-23 起生效)----
  237. // Source: https://api-docs.deepseek.com/quick_start/pricing
  238. // 高峰价 = 2× 低谷价;高峰时段 01:00–04:00 与 06:00–10:00 UTC(仅工作日),
  239. // 北京时间周六/周日全天低谷。时段判定见 deepseekPeakMultiplierAt。
  240. const (
  241. deepseekFlashOffPeakInputPrice = 2.2e-7 // $0.22 per MTok (cache miss)
  242. deepseekFlashOffPeakOutputPrice = 6.6e-7 // $0.66 per MTok
  243. deepseekFlashOffPeakCacheRead = 7e-9 // $0.007 per MTok (cache hit)
  244. deepseekProOffPeakInputPrice = 6.6e-7 // $0.66 per MTok (cache miss)
  245. deepseekProOffPeakOutputPrice = 1.98e-6 // $1.98 per MTok
  246. deepseekProOffPeakCacheRead = 2.2e-8 // $0.022 per MTok (cache hit)
  247. )
  248. // isDeepSeekModel 判断模型名是否为 DeepSeek 模型(大小写不敏感)。
  249. // 任意 deepseek- 前缀均视为 DeepSeek 模型:官方模型(v4-flash / v4-pro /
  250. // v4-flash-vision-exp)按各自价卡计价,其余 deepseek-*(含已停服的
  251. // deepseek-chat / deepseek-reasoner 与未知型号)统一按 flash 价兜底,
  252. // 避免计费中断;新名字由 fallback warn 日志(每模型每进程一条)暴露,
  253. // 运营者据此更新价卡。
  254. func isDeepSeekModel(model string) bool {
  255. return strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek-")
  256. }
  257. // deepseekPeakMultiplierAt 返回指定时刻的 DeepSeek 官方峰谷定价因子。
  258. // 官方口径(2026-08-23 起生效):高峰价 = 2× 低谷价;高峰时段为
  259. // 01:00–04:00 与 06:00–10:00 UTC(半开区间),仅工作日;
  260. // 周末(北京时间周六/周日)全天低谷。北京时间用固定 +8 偏移(无夏令时)。
  261. func deepseekPeakMultiplierAt(now time.Time) float64 {
  262. beijing := now.In(time.FixedZone("Asia/Shanghai", 8*3600))
  263. switch beijing.Weekday() {
  264. case time.Saturday, time.Sunday:
  265. return 1.0
  266. }
  267. switch h := now.UTC().Hour(); {
  268. case h >= 1 && h < 4, h >= 6 && h < 10:
  269. return 2.0
  270. }
  271. return 1.0
  272. }
  273. // BillingService 计费服务
  274. type BillingService struct {
  275. cfg *config.Config
  276. pricingService *PricingService
  277. fallbackPrices map[string]*ModelPricing // 硬编码回退价格
  278. // fallbackWarnSeen 记录已打过 fallback 警告日志的(已小写化)模型名,
  279. // 让 "[Billing] Using fallback pricing" 每个模型每进程最多打一条,
  280. // 避免热路径上每请求刷屏(issue #3394)。零值即可用,无需在构造函数初始化。
  281. fallbackWarnSeen sync.Map
  282. }
  283. // NewBillingService 创建计费服务实例
  284. func NewBillingService(cfg *config.Config, pricingService *PricingService) *BillingService {
  285. s := &BillingService{
  286. cfg: cfg,
  287. pricingService: pricingService,
  288. fallbackPrices: make(map[string]*ModelPricing),
  289. }
  290. // 初始化硬编码回退价格(当动态价格不可用时使用)
  291. s.initFallbackPricing()
  292. return s
  293. }
  294. // initFallbackPricing 初始化硬编码回退价格(当动态价格不可用时使用)
  295. // 价格单位:USD per token(与LiteLLM格式一致)
  296. func (s *BillingService) initFallbackPricing() {
  297. // Claude 4.5 Opus
  298. s.fallbackPrices["claude-opus-4.5"] = &ModelPricing{
  299. InputPricePerToken: 5e-6, // $5 per MTok
  300. OutputPricePerToken: 25e-6, // $25 per MTok
  301. CacheCreationPricePerToken: 6.25e-6, // $6.25 per MTok
  302. CacheReadPricePerToken: 0.5e-6, // $0.50 per MTok
  303. SupportsCacheBreakdown: false,
  304. }
  305. // Claude 4 Sonnet
  306. s.fallbackPrices["claude-sonnet-4"] = &ModelPricing{
  307. InputPricePerToken: 3e-6, // $3 per MTok
  308. OutputPricePerToken: 15e-6, // $15 per MTok
  309. CacheCreationPricePerToken: 3.75e-6, // $3.75 per MTok
  310. CacheReadPricePerToken: 0.3e-6, // $0.30 per MTok
  311. SupportsCacheBreakdown: false,
  312. }
  313. // Claude 3.5 Sonnet
  314. s.fallbackPrices["claude-3-5-sonnet"] = &ModelPricing{
  315. InputPricePerToken: 3e-6, // $3 per MTok
  316. OutputPricePerToken: 15e-6, // $15 per MTok
  317. CacheCreationPricePerToken: 3.75e-6, // $3.75 per MTok
  318. CacheReadPricePerToken: 0.3e-6, // $0.30 per MTok
  319. SupportsCacheBreakdown: false,
  320. }
  321. // Claude 3.5 Haiku
  322. s.fallbackPrices["claude-3-5-haiku"] = &ModelPricing{
  323. InputPricePerToken: 1e-6, // $1 per MTok
  324. OutputPricePerToken: 5e-6, // $5 per MTok
  325. CacheCreationPricePerToken: 1.25e-6, // $1.25 per MTok
  326. CacheReadPricePerToken: 0.1e-6, // $0.10 per MTok
  327. SupportsCacheBreakdown: false,
  328. }
  329. // Claude 3 Opus
  330. s.fallbackPrices["claude-3-opus"] = &ModelPricing{
  331. InputPricePerToken: 15e-6, // $15 per MTok
  332. OutputPricePerToken: 75e-6, // $75 per MTok
  333. CacheCreationPricePerToken: 18.75e-6, // $18.75 per MTok
  334. CacheReadPricePerToken: 1.5e-6, // $1.50 per MTok
  335. SupportsCacheBreakdown: false,
  336. }
  337. // Claude 3 Haiku
  338. s.fallbackPrices["claude-3-haiku"] = &ModelPricing{
  339. InputPricePerToken: 0.25e-6, // $0.25 per MTok
  340. OutputPricePerToken: 1.25e-6, // $1.25 per MTok
  341. CacheCreationPricePerToken: 0.3e-6, // $0.30 per MTok
  342. CacheReadPricePerToken: 0.03e-6, // $0.03 per MTok
  343. SupportsCacheBreakdown: false,
  344. }
  345. // Claude 4.6 Opus (与4.5同价)
  346. s.fallbackPrices["claude-opus-4.6"] = s.fallbackPrices["claude-opus-4.5"]
  347. // Claude 4.7 Opus (暂与4.6同价,待官方定价更新)
  348. s.fallbackPrices["claude-opus-4.7"] = s.fallbackPrices["claude-opus-4.6"]
  349. // Claude 4.8 Opus / Claude Opus 5(标准 $5/$25,Fast $10/$50 per MTok)。
  350. // 缺少这两条时 getFallbackPricing 会掉到 claude-3-opus($15/$75),造成 3 倍超收。
  351. s.fallbackPrices["claude-opus-4.8"] = pricingWithPriorityMultiplier(s.fallbackPrices["claude-opus-4.7"], 2)
  352. s.fallbackPrices["claude-opus-5"] = pricingWithPriorityMultiplier(s.fallbackPrices["claude-opus-4.8"], 2)
  353. // Claude Fable 5.x uses the same input/output and cache-write prices, while
  354. // Fable 5.1 reduces cache reads from $1 to $0.25 per MTok.
  355. s.fallbackPrices["claude-fable-5"] = &ModelPricing{
  356. InputPricePerToken: 10e-6,
  357. OutputPricePerToken: 50e-6,
  358. CacheCreationPricePerToken: 12.5e-6,
  359. CacheCreation5mPrice: 12.5e-6,
  360. CacheCreation1hPrice: 20e-6,
  361. CacheReadPricePerToken: 1e-6,
  362. SupportsCacheBreakdown: true,
  363. }
  364. s.fallbackPrices["claude-fable-5-1"] = &ModelPricing{
  365. InputPricePerToken: 10e-6,
  366. OutputPricePerToken: 50e-6,
  367. CacheCreationPricePerToken: 12.5e-6,
  368. CacheCreation5mPrice: 12.5e-6,
  369. CacheCreation1hPrice: 20e-6,
  370. CacheReadPricePerToken: 0.25e-6,
  371. SupportsCacheBreakdown: true,
  372. }
  373. // Gemini 3.1 Pro
  374. s.fallbackPrices["gemini-3.1-pro"] = &ModelPricing{
  375. InputPricePerToken: 2e-6, // $2 per MTok
  376. OutputPricePerToken: 12e-6, // $12 per MTok
  377. CacheCreationPricePerToken: 2e-6, // $2 per MTok
  378. CacheReadPricePerToken: 0.2e-6, // $0.20 per MTok
  379. SupportsCacheBreakdown: false,
  380. }
  381. // Gemini 3.6 Flash (Google AI pricing: $1.50 input / $7.50 output /
  382. // $0.15 cached input per MTok). Antigravity's -high/-low/-medium/-tiered
  383. // aliases are matched below so unavailable remote pricing never records
  384. // token-bearing requests at $0.
  385. s.fallbackPrices["gemini-3.6-flash"] = &ModelPricing{
  386. InputPricePerToken: 1.5e-6,
  387. OutputPricePerToken: 7.5e-6,
  388. CacheReadPricePerToken: 0.15e-6,
  389. SupportsCacheBreakdown: false,
  390. }
  391. // OpenAI GPT-5.4(业务指定价格)
  392. s.fallbackPrices["gpt-5.4"] = &ModelPricing{
  393. InputPricePerToken: 2.5e-6, // $2.5 per MTok
  394. InputPricePerTokenPriority: 5e-6, // $5 per MTok
  395. OutputPricePerToken: 15e-6, // $15 per MTok
  396. OutputPricePerTokenPriority: 30e-6, // $30 per MTok
  397. CacheCreationPricePerToken: 2.5e-6, // $2.5 per MTok
  398. CacheReadPricePerToken: 0.25e-6, // $0.25 per MTok
  399. CacheReadPricePerTokenPriority: 0.5e-6, // $0.5 per MTok
  400. SupportsCacheBreakdown: false,
  401. }
  402. // OpenAI GPT-5.5 官方价格;Fast 为标准价 2.5 倍。
  403. // Source: https://platform.openai.com/docs/pricing
  404. s.fallbackPrices["gpt-5.5"] = pricingWithPriorityMultiplier(&ModelPricing{
  405. InputPricePerToken: 5e-6,
  406. OutputPricePerToken: 30e-6,
  407. // 官方未列独立 cache-write 价;内部出现 cache creation token 时按输入价兜底。
  408. CacheCreationPricePerToken: 5e-6,
  409. CacheReadPricePerToken: 0.5e-6,
  410. SupportsCacheBreakdown: false,
  411. }, 2.5)
  412. // GPT-5.5 Pro 当前不提供 Fast;保留标准、Flex 和长上下文 fallback 价格。
  413. s.fallbackPrices["gpt-5.5-pro"] = &ModelPricing{
  414. InputPricePerToken: 30e-6,
  415. OutputPricePerToken: 180e-6,
  416. // 官方未列独立 cached-input/cache-write 价;内部出现对应 token 时按输入价兜底。
  417. CacheCreationPricePerToken: 30e-6,
  418. CacheReadPricePerToken: 30e-6,
  419. SupportsCacheBreakdown: false,
  420. }
  421. s.fallbackPrices["gpt-6-astra"] = &ModelPricing{
  422. InputPricePerToken: 10e-6,
  423. InputPricePerTokenPriority: 20e-6,
  424. OutputPricePerToken: 50e-6,
  425. OutputPricePerTokenPriority: 100e-6,
  426. CacheCreationPricePerToken: 12.5e-6,
  427. CacheCreationPricePerTokenPriority: 25e-6,
  428. CacheReadPricePerToken: 1e-6,
  429. CacheReadPricePerTokenPriority: 2e-6,
  430. LongContextInputThreshold: 272_000,
  431. LongContextInputMultiplier: 2,
  432. LongContextOutputMultiplier: 1.5,
  433. }
  434. // OpenAI GPT-5.6 官方价格(USD/token)。缓存写入为输入价的 1.25 倍。
  435. s.fallbackPrices["gpt-5.6-sol"] = &ModelPricing{
  436. InputPricePerToken: 5e-6,
  437. InputPricePerTokenPriority: 10e-6,
  438. OutputPricePerToken: 30e-6,
  439. OutputPricePerTokenPriority: 60e-6,
  440. CacheCreationPricePerToken: 6.25e-6,
  441. CacheCreationPricePerTokenPriority: 12.5e-6,
  442. CacheReadPricePerToken: 0.5e-6,
  443. CacheReadPricePerTokenPriority: 1e-6,
  444. }
  445. s.fallbackPrices["gpt-5.6-terra"] = &ModelPricing{
  446. InputPricePerToken: 2e-6,
  447. InputPricePerTokenPriority: 4e-6,
  448. OutputPricePerToken: 12e-6,
  449. OutputPricePerTokenPriority: 24e-6,
  450. CacheCreationPricePerToken: 2.5e-6,
  451. CacheCreationPricePerTokenPriority: 5e-6,
  452. CacheReadPricePerToken: 0.2e-6,
  453. CacheReadPricePerTokenPriority: 0.4e-6,
  454. }
  455. s.fallbackPrices["gpt-5.6-luna"] = &ModelPricing{
  456. InputPricePerToken: 0.2e-6,
  457. InputPricePerTokenPriority: 0.4e-6,
  458. OutputPricePerToken: 1.2e-6,
  459. OutputPricePerTokenPriority: 2.4e-6,
  460. CacheCreationPricePerToken: 0.25e-6,
  461. CacheCreationPricePerTokenPriority: 0.5e-6,
  462. CacheReadPricePerToken: 0.02e-6,
  463. CacheReadPricePerTokenPriority: 0.04e-6,
  464. }
  465. s.fallbackPrices["gpt-5.4-mini"] = &ModelPricing{
  466. InputPricePerToken: 7.5e-7,
  467. OutputPricePerToken: 4.5e-6,
  468. CacheReadPricePerToken: 7.5e-8,
  469. SupportsCacheBreakdown: false,
  470. }
  471. s.fallbackPrices["gpt-5.4-nano"] = &ModelPricing{
  472. InputPricePerToken: 2e-7,
  473. OutputPricePerToken: 1.25e-6,
  474. CacheReadPricePerToken: 2e-8,
  475. SupportsCacheBreakdown: false,
  476. }
  477. // OpenAI GPT-5.2(本地兜底)
  478. s.fallbackPrices["gpt-5.2"] = &ModelPricing{
  479. InputPricePerToken: 1.75e-6,
  480. InputPricePerTokenPriority: 3.5e-6,
  481. OutputPricePerToken: 14e-6,
  482. OutputPricePerTokenPriority: 28e-6,
  483. CacheCreationPricePerToken: 1.75e-6,
  484. CacheReadPricePerToken: 0.175e-6,
  485. CacheReadPricePerTokenPriority: 0.35e-6,
  486. SupportsCacheBreakdown: false,
  487. }
  488. // Codex 族兜底统一按 GPT-5.3 Codex 价格计费
  489. s.fallbackPrices["gpt-5.3-codex"] = &ModelPricing{
  490. InputPricePerToken: 1.5e-6, // $1.5 per MTok
  491. InputPricePerTokenPriority: 3e-6, // $3 per MTok
  492. OutputPricePerToken: 12e-6, // $12 per MTok
  493. OutputPricePerTokenPriority: 24e-6, // $24 per MTok
  494. CacheCreationPricePerToken: 1.5e-6, // $1.5 per MTok
  495. CacheReadPricePerToken: 0.15e-6,
  496. CacheReadPricePerTokenPriority: 0.3e-6,
  497. SupportsCacheBreakdown: false,
  498. }
  499. // ============================================================
  500. // 国产 LLM 兜底定价(数据源:各家官方定价页/USD 口径)
  501. // 顺序:DeepSeek → 智谱 GLM → 月之暗面 Kimi → MiniMax
  502. // 覆盖逻辑见同文件 getFallbackPricing()
  503. // ============================================================
  504. // ---- DeepSeek 系列 ----
  505. // Source: https://api-docs.deepseek.com/quick_start/pricing
  506. // 官方口径(2026-08-23 起生效):现行模型为 deepseek-v4-flash /
  507. // deepseek-v4-pro / deepseek-v4-flash-vision-exp;deepseek-chat /
  508. // deepseek-reasoner 已停止服务,其余 deepseek-*(含未知型号)统一按
  509. // flash 价兜底(见 getFallbackPricing),避免计费中断。
  510. // 以下均为官方低谷价;高峰价 = 2× 低谷价(高峰时段 01:00–04:00
  511. // 与 06:00–10:00 UTC,仅工作日;北京时间周六/周日全天低谷),见 deepseekPeakMultiplierAt。
  512. s.fallbackPrices["deepseek-v4-pro"] = &ModelPricing{
  513. InputPricePerToken: deepseekProOffPeakInputPrice, // $0.66 per MTok (cache miss, off-peak)
  514. OutputPricePerToken: deepseekProOffPeakOutputPrice, // $1.98 per MTok
  515. CacheReadPricePerToken: deepseekProOffPeakCacheRead, // $0.022 per MTok (cache hit)
  516. SupportsCacheBreakdown: false,
  517. }
  518. s.fallbackPrices["deepseek-v4-flash"] = &ModelPricing{
  519. InputPricePerToken: deepseekFlashOffPeakInputPrice, // $0.22 per MTok (cache miss, off-peak)
  520. OutputPricePerToken: deepseekFlashOffPeakOutputPrice, // $0.66 per MTok
  521. CacheReadPricePerToken: deepseekFlashOffPeakCacheRead, // $0.007 per MTok (cache hit)
  522. SupportsCacheBreakdown: false,
  523. }
  524. s.fallbackPrices["deepseek-v4-flash-vision-exp"] = &ModelPricing{
  525. InputPricePerToken: deepseekFlashOffPeakInputPrice,
  526. OutputPricePerToken: deepseekFlashOffPeakOutputPrice,
  527. CacheReadPricePerToken: deepseekFlashOffPeakCacheRead,
  528. SupportsCacheBreakdown: false,
  529. }
  530. // ---- 智谱 GLM(Z.AI)----
  531. // Source: https://docs.z.ai/guides/overview/pricing (USD per 1M tokens)
  532. // 注意:CacheReadPricePerToken 即"缓存命中"价格,CacheCreationPricePerToken 留空(智谱未公开写入价,按 0 处理)。
  533. // GLM-4.6 与 GLM-4.5 在 z.ai 国际版上定价一致;GLM-4.5 国内按 ¥0.8/¥2,汇率换算后约 $0.112/$0.28,与国际版 $0.6/$2.2 不同,本分支采用国际版 USD 口径与现有 Claude/GPT 一致。
  534. // GLM-5.3 / GLM-5.2 与 GLM-5.1 在 z.ai 上同价。
  535. // GLM-5.3-Flash 列表价 $0.15/$0.50(2026-09-09 前五折促销,此处按列表价,与其它模型口径一致)。
  536. s.fallbackPrices["glm-5.3-flash"] = &ModelPricing{
  537. InputPricePerToken: 0.15e-6, // $0.15 per MTok
  538. OutputPricePerToken: 0.5e-6, // $0.50 per MTok
  539. CacheReadPricePerToken: 0.03e-6,
  540. SupportsCacheBreakdown: false,
  541. }
  542. s.fallbackPrices["glm-5.3"] = &ModelPricing{
  543. InputPricePerToken: 1.4e-6, // $1.40 per MTok
  544. OutputPricePerToken: 4.4e-6, // $4.40 per MTok
  545. CacheReadPricePerToken: 0.26e-6,
  546. SupportsCacheBreakdown: false,
  547. }
  548. s.fallbackPrices["glm-5.2"] = &ModelPricing{
  549. InputPricePerToken: 1.4e-6, // $1.40 per MTok
  550. OutputPricePerToken: 4.4e-6, // $4.40 per MTok
  551. CacheReadPricePerToken: 0.26e-6,
  552. SupportsCacheBreakdown: false,
  553. }
  554. s.fallbackPrices["glm-5.1"] = &ModelPricing{
  555. InputPricePerToken: 1.4e-6, // $1.40 per MTok
  556. OutputPricePerToken: 4.4e-6, // $4.40 per MTok
  557. CacheReadPricePerToken: 0.26e-6,
  558. SupportsCacheBreakdown: false,
  559. }
  560. s.fallbackPrices["glm-5"] = &ModelPricing{
  561. InputPricePerToken: 1e-6, // $1.00 per MTok
  562. OutputPricePerToken: 3.2e-6,
  563. CacheReadPricePerToken: 0.2e-6,
  564. SupportsCacheBreakdown: false,
  565. }
  566. s.fallbackPrices["glm-5-turbo"] = &ModelPricing{
  567. InputPricePerToken: 1.2e-6,
  568. OutputPricePerToken: 4e-6,
  569. CacheReadPricePerToken: 0.24e-6,
  570. SupportsCacheBreakdown: false,
  571. }
  572. s.fallbackPrices["glm-4.7"] = &ModelPricing{
  573. InputPricePerToken: 0.6e-6, // $0.60 per MTok
  574. OutputPricePerToken: 2.2e-6,
  575. CacheReadPricePerToken: 0.11e-6,
  576. SupportsCacheBreakdown: false,
  577. }
  578. s.fallbackPrices["glm-4.7-flashx"] = &ModelPricing{
  579. InputPricePerToken: 0.07e-6, // $0.07 per MTok
  580. OutputPricePerToken: 0.4e-6,
  581. CacheReadPricePerToken: 0.01e-6,
  582. SupportsCacheBreakdown: false,
  583. }
  584. s.fallbackPrices["glm-4.6"] = &ModelPricing{
  585. InputPricePerToken: 0.6e-6, // $0.60 per MTok
  586. OutputPricePerToken: 2.2e-6,
  587. CacheReadPricePerToken: 0.11e-6,
  588. SupportsCacheBreakdown: false,
  589. }
  590. s.fallbackPrices["glm-4.5"] = &ModelPricing{
  591. InputPricePerToken: 0.6e-6, // $0.60 per MTok
  592. OutputPricePerToken: 2.2e-6,
  593. CacheReadPricePerToken: 0.11e-6,
  594. SupportsCacheBreakdown: false,
  595. }
  596. s.fallbackPrices["glm-4.5-x"] = &ModelPricing{
  597. InputPricePerToken: 2.2e-6, // $2.20 per MTok
  598. OutputPricePerToken: 8.9e-6,
  599. CacheReadPricePerToken: 0.45e-6,
  600. SupportsCacheBreakdown: false,
  601. }
  602. s.fallbackPrices["glm-4.5-air"] = &ModelPricing{
  603. InputPricePerToken: 0.2e-6, // $0.20 per MTok
  604. OutputPricePerToken: 1.1e-6,
  605. CacheReadPricePerToken: 0.03e-6,
  606. SupportsCacheBreakdown: false,
  607. }
  608. s.fallbackPrices["glm-4.5-airx"] = &ModelPricing{
  609. InputPricePerToken: 1.1e-6,
  610. OutputPricePerToken: 4.5e-6,
  611. CacheReadPricePerToken: 0.22e-6,
  612. SupportsCacheBreakdown: false,
  613. }
  614. s.fallbackPrices["glm-4-32b-0414-128k"] = &ModelPricing{
  615. InputPricePerToken: 0.1e-6, // $0.10 per MTok
  616. OutputPricePerToken: 0.1e-6,
  617. SupportsCacheBreakdown: false,
  618. }
  619. // GLM-4.5-Flash / GLM-4.7-Flash 在 z.ai 上为 Free,保留 zero-cost entry 防止未知 alias 误计费。
  620. s.fallbackPrices["glm-4.5-flash"] = &ModelPricing{
  621. InputPricePerToken: 0,
  622. OutputPricePerToken: 0,
  623. SupportsCacheBreakdown: false,
  624. }
  625. s.fallbackPrices["glm-4.7-flash"] = &ModelPricing{
  626. InputPricePerToken: 0,
  627. OutputPricePerToken: 0,
  628. SupportsCacheBreakdown: false,
  629. }
  630. // ---- 月之暗面 Kimi(K 系列)----
  631. // Source: https://platform.moonshot.cn/docs/pricing/overview (元/百万 tokens 口径)
  632. // 交叉验证:https://www.tmtpost.com/7961404.html (USD 口径)
  633. // Moonshot V1 (¥2/¥5/¥10 多 tier) 公开页未直接标注 USD 价,本分支不覆盖,避免误计价。
  634. // K2-0905 / K2-0711 官方页面未保留定价,不覆盖。
  635. // Kimi K3 国际站 USD 价目:https://platform.kimi.ai/docs/pricing/chat-k3.md
  636. // Kimi Code bare aliases(k3 / k3-256k)官方无按 token 价目;复用 API Platform
  637. // kimi-k3 档位作代理计费 fallback(同 kimi-for-coding 对 K2.6 的处理口径)。
  638. s.fallbackPrices["kimi-k3"] = &ModelPricing{
  639. InputPricePerToken: 3e-6, // $3.00 per MTok (cache miss)
  640. OutputPricePerToken: 15e-6, // $15.00 per MTok
  641. CacheReadPricePerToken: 0.30e-6, // $0.30 per MTok (cache hit)
  642. SupportsCacheBreakdown: false,
  643. }
  644. s.fallbackPrices["kimi-k2.6"] = &ModelPricing{
  645. InputPricePerToken: 0.95e-6, // $0.95 per MTok (cache miss)
  646. OutputPricePerToken: 4e-6, // $4.00 per MTok
  647. CacheReadPricePerToken: 0.15e-6, // $0.15 per MTok (cache hit, ¥1.10)
  648. SupportsCacheBreakdown: false,
  649. }
  650. // kimi-for-coding 走 Kimi Coding endpoint,按当前 K2.6 coding 档位兜底计费。
  651. s.fallbackPrices["kimi-for-coding"] = &ModelPricing{
  652. InputPricePerToken: 0.95e-6,
  653. OutputPricePerToken: 4e-6,
  654. CacheReadPricePerToken: 0.15e-6,
  655. SupportsCacheBreakdown: false,
  656. }
  657. s.fallbackPrices["kimi-k2.5"] = &ModelPricing{
  658. InputPricePerToken: 0.60e-6, // $0.60 per MTok
  659. OutputPricePerToken: 3e-6, // $3.00 per MTok
  660. CacheReadPricePerToken: 0.098e-6,
  661. SupportsCacheBreakdown: false,
  662. }
  663. s.fallbackPrices["kimi-k2-thinking"] = &ModelPricing{
  664. InputPricePerToken: 0.56e-6, // ¥4/百万 ≈ $0.56
  665. OutputPricePerToken: 2.24e-6, // ¥16/百万
  666. CacheReadPricePerToken: 0.14e-6, // ¥1/百万
  667. SupportsCacheBreakdown: false,
  668. }
  669. s.fallbackPrices["kimi-k2"] = &ModelPricing{
  670. InputPricePerToken: 0.56e-6, // ¥4/百万
  671. OutputPricePerToken: 2.24e-6, // ¥16/百万
  672. CacheReadPricePerToken: 0.14e-6, // ¥1/百万
  673. SupportsCacheBreakdown: false,
  674. }
  675. // ---- MiniMax M 系列 ----
  676. // Source: https://platform.minimax.io/docs/guides/pricing-paygo
  677. // 注意:MiniMax M3 在 >512K context 时价格翻倍,本兜底采用 ≤512K 标准 tier(保守口径,对用户有利)。
  678. // 如需支持长上下文 multiplier,可后续参考 GPT-5.4 模式扩展 LongContextXxx 字段。
  679. s.fallbackPrices["minimax-m3"] = &ModelPricing{
  680. InputPricePerToken: 0.60e-6, // $0.60 per MTok (≤512K standard tier, 含 50% 永久折扣前原价 $1.20)
  681. OutputPricePerToken: 2.40e-6,
  682. CacheReadPricePerToken: 0.12e-6,
  683. SupportsCacheBreakdown: false,
  684. }
  685. s.fallbackPrices["minimax-m2.7"] = &ModelPricing{
  686. InputPricePerToken: 0.30e-6, // $0.30 per MTok
  687. OutputPricePerToken: 1.20e-6,
  688. CacheReadPricePerToken: 0.06e-6,
  689. SupportsCacheBreakdown: false,
  690. }
  691. s.fallbackPrices["minimax-m2.7-highspeed"] = &ModelPricing{
  692. InputPricePerToken: 0.60e-6,
  693. OutputPricePerToken: 2.40e-6,
  694. CacheReadPricePerToken: 0.06e-6,
  695. SupportsCacheBreakdown: false,
  696. }
  697. s.fallbackPrices["minimax-m2.5"] = &ModelPricing{
  698. InputPricePerToken: 0.30e-6,
  699. OutputPricePerToken: 1.20e-6,
  700. CacheReadPricePerToken: 0.03e-6,
  701. SupportsCacheBreakdown: false,
  702. }
  703. s.fallbackPrices["minimax-m2.1"] = &ModelPricing{
  704. InputPricePerToken: 0.30e-6,
  705. OutputPricePerToken: 1.20e-6,
  706. CacheReadPricePerToken: 0.03e-6,
  707. SupportsCacheBreakdown: false,
  708. }
  709. s.fallbackPrices["minimax-m2"] = &ModelPricing{
  710. InputPricePerToken: 0.30e-6,
  711. OutputPricePerToken: 1.20e-6,
  712. CacheReadPricePerToken: 0.03e-6,
  713. SupportsCacheBreakdown: false,
  714. }
  715. // ---- 火山方舟 豆包 Embedding(多模态向量化)----
  716. // doubao-embedding-vision 图文向量化:上游 usage 回传 prompt_tokens_details.{text_tokens,image_tokens},
  717. // 按量付费官方价 文本 ¥0.7/MTok、图片 ¥1.8/MTok;汇率口径 ÷7.14(与本表其他国产模型一致,¥1≈$0.14)。
  718. // embedding 无 output,OutputPricePerToken 置 0。
  719. s.fallbackPrices["doubao-embedding-vision"] = &ModelPricing{
  720. InputPricePerToken: 0.098e-6, // ¥0.7/MTok ≈ $0.098(文本输入)
  721. ImageInputPricePerToken: 0.252e-6, // ¥1.8/MTok ≈ $0.252(图片输入)
  722. OutputPricePerToken: 0,
  723. SupportsCacheBreakdown: false,
  724. }
  725. // xAI Grok 4.5: $2 input / $0.30 cached input / $6 output below 200k;
  726. // long-context rates are $4 / $0.60 / $12 (>=200k prompt tokens).
  727. s.fallbackPrices["grok-4.5"] = &ModelPricing{
  728. InputPricePerToken: 2e-6,
  729. OutputPricePerToken: 6e-6,
  730. CacheReadPricePerToken: 0.3e-6,
  731. SupportsCacheBreakdown: false,
  732. LongContextInputThreshold: 200000,
  733. LongContextThresholdInclusive: true,
  734. LongContextInputMultiplier: 2,
  735. LongContextOutputMultiplier: 2,
  736. }
  737. // xAI Grok 4.6: $2 input / $0.50 cached input / $6 output below 200k;
  738. // long-context rates are $4 / $1 / $12 (>=200k prompt tokens).
  739. s.fallbackPrices["grok-4.6"] = &ModelPricing{
  740. InputPricePerToken: 2e-6,
  741. OutputPricePerToken: 6e-6,
  742. CacheReadPricePerToken: 0.5e-6,
  743. SupportsCacheBreakdown: false,
  744. LongContextInputThreshold: 200000,
  745. LongContextThresholdInclusive: true,
  746. LongContextInputMultiplier: 2,
  747. LongContextOutputMultiplier: 2,
  748. }
  749. // xAI Grok 4.3: $1.25 input / $0.20 cached / $2.50 output below 200k;
  750. // long-context rates are $2.50 / $0.40 / $5.
  751. s.fallbackPrices["grok-4.3"] = &ModelPricing{
  752. InputPricePerToken: 1.25e-6,
  753. OutputPricePerToken: 2.5e-6,
  754. CacheReadPricePerToken: 0.2e-6,
  755. SupportsCacheBreakdown: false,
  756. LongContextInputThreshold: 200000,
  757. LongContextThresholdInclusive: true,
  758. LongContextInputMultiplier: 2,
  759. LongContextOutputMultiplier: 2,
  760. }
  761. // Grok 4.20 variants share the official $1.25 / $0.20 / $2.50 card
  762. // (and $2.50 / $0.40 / $5 long-context rates) with Grok 4.3.
  763. s.fallbackPrices["grok-4.20"] = &ModelPricing{
  764. InputPricePerToken: 1.25e-6,
  765. OutputPricePerToken: 2.5e-6,
  766. CacheReadPricePerToken: 0.2e-6,
  767. SupportsCacheBreakdown: false,
  768. LongContextInputThreshold: 200000,
  769. LongContextThresholdInclusive: true,
  770. LongContextInputMultiplier: 2,
  771. LongContextOutputMultiplier: 2,
  772. }
  773. // Keep legacy Grok 3 Mini requests on their own historical xAI price card;
  774. // otherwise the generic Grok fallback bills them as Grok 4.5.
  775. s.fallbackPrices["grok-3-mini"] = &ModelPricing{
  776. InputPricePerToken: 0.30e-6,
  777. OutputPricePerToken: 0.50e-6,
  778. CacheReadPricePerToken: 0.075e-6,
  779. SupportsCacheBreakdown: false,
  780. }
  781. s.fallbackPrices["grok-3-mini-fast"] = &ModelPricing{
  782. InputPricePerToken: 0.60e-6,
  783. OutputPricePerToken: 4e-6,
  784. CacheReadPricePerToken: 0.15e-6,
  785. SupportsCacheBreakdown: false,
  786. }
  787. // xAI Grok Build 0.1 (official docs: $1 input / $0.20 cached input /
  788. // $2 output per MTok). Composer is available only through Grok Build and
  789. // has no standalone public API rate card, so its aliases use this coding
  790. // model rate instead of silently billing at zero.
  791. s.fallbackPrices["grok-build-0.1"] = &ModelPricing{
  792. InputPricePerToken: 1e-6,
  793. OutputPricePerToken: 2e-6,
  794. CacheReadPricePerToken: 0.2e-6,
  795. SupportsCacheBreakdown: false,
  796. LongContextInputThreshold: 200000,
  797. LongContextThresholdInclusive: true,
  798. LongContextInputMultiplier: 2,
  799. LongContextOutputMultiplier: 2,
  800. }
  801. }
  802. // getFallbackPricing 根据模型系列获取回退价格
  803. func (s *BillingService) getFallbackPricing(model string) *ModelPricing {
  804. modelLower := strings.ToLower(model)
  805. // 按模型系列匹配
  806. if isClaudeFable51Model(modelLower) {
  807. return s.fallbackPrices["claude-fable-5-1"]
  808. }
  809. if strings.Contains(modelLower, "fable-5") || strings.Contains(modelLower, "fable5") {
  810. return s.fallbackPrices["claude-fable-5"]
  811. }
  812. if strings.Contains(modelLower, "opus") {
  813. // "opus-5" 必须先判:不能用裸 "5" 匹配,否则 claude-opus-4-5 会被误判。
  814. if strings.Contains(modelLower, "opus-5") || strings.Contains(modelLower, "opus5") {
  815. return s.fallbackPrices["claude-opus-5"]
  816. }
  817. if strings.Contains(modelLower, "4.8") || strings.Contains(modelLower, "4-8") {
  818. return s.fallbackPrices["claude-opus-4.8"]
  819. }
  820. if strings.Contains(modelLower, "4.7") || strings.Contains(modelLower, "4-7") {
  821. return s.fallbackPrices["claude-opus-4.7"]
  822. }
  823. if strings.Contains(modelLower, "4.6") || strings.Contains(modelLower, "4-6") {
  824. return s.fallbackPrices["claude-opus-4.6"]
  825. }
  826. if strings.Contains(modelLower, "4.5") || strings.Contains(modelLower, "4-5") {
  827. return s.fallbackPrices["claude-opus-4.5"]
  828. }
  829. return s.fallbackPrices["claude-3-opus"]
  830. }
  831. if strings.Contains(modelLower, "sonnet") {
  832. if strings.Contains(modelLower, "4") && !strings.Contains(modelLower, "3") {
  833. return s.fallbackPrices["claude-sonnet-4"]
  834. }
  835. return s.fallbackPrices["claude-3-5-sonnet"]
  836. }
  837. if strings.Contains(modelLower, "haiku") {
  838. if strings.Contains(modelLower, "3-5") || strings.Contains(modelLower, "3.5") {
  839. return s.fallbackPrices["claude-3-5-haiku"]
  840. }
  841. return s.fallbackPrices["claude-3-haiku"]
  842. }
  843. // Claude 未知型号统一回退到 Sonnet,避免计费中断。
  844. if strings.Contains(modelLower, "claude") {
  845. return s.fallbackPrices["claude-sonnet-4"]
  846. }
  847. if strings.Contains(modelLower, "gemini-3.1-pro") || strings.Contains(modelLower, "gemini-3-1-pro") {
  848. return s.fallbackPrices["gemini-3.1-pro"]
  849. }
  850. if strings.Contains(modelLower, "gemini-3.6-flash") || strings.Contains(modelLower, "gemini-3-6-flash") {
  851. return s.fallbackPrices["gemini-3.6-flash"]
  852. }
  853. // DeepSeek 系列:官方模型 V4 Pro/Flash(含 vision-exp)按各自价卡;
  854. // 其余 deepseek-*(含已停服的 deepseek-chat / deepseek-reasoner 与未知型号)
  855. // 统一按 flash 价兜底,避免计费中断。新名字由 fallback warn 日志
  856. // (每模型每进程一条)暴露,运营者据此更新价卡。
  857. // "deepseek-v4-flash-vision-exp" 含 "deepseek-v4-flash" 子串,显式分支置于 flash 之前,语义清晰。
  858. if strings.Contains(modelLower, "deepseek-v4-flash-vision-exp") {
  859. return s.fallbackPrices["deepseek-v4-flash-vision-exp"]
  860. }
  861. if strings.Contains(modelLower, "deepseek-v4-flash") {
  862. return s.fallbackPrices["deepseek-v4-flash"]
  863. }
  864. if strings.Contains(modelLower, "deepseek-v4-pro") {
  865. return s.fallbackPrices["deepseek-v4-pro"]
  866. }
  867. if strings.HasPrefix(modelLower, "deepseek-") {
  868. return s.fallbackPrices["deepseek-v4-flash"]
  869. }
  870. // ---- 国产 LLM 兜底匹配 ----
  871. // 匹配策略:长 key 优先(具体模型 → 系列 / 厂商),未知型号不回退以避免误计价。
  872. // 与 DeepSeek 一样采用"白名单"语义:未在本表命中的国产模型 alias 一律不返回兜底价。
  873. // 智谱 GLM(z.ai 公开 SKU:glm-5.3 / glm-5.3-flash / glm-5.2 / glm-5.1 / glm-5 / glm-5-turbo / glm-4.7 / glm-4.6 / glm-4.5 等)
  874. // 匹配顺序:先判别最高 tier,再依次降级。
  875. // 注意:带小数点的型号必须排在裸 "glm-5" 之前,否则会被 strings.Contains 抢走;
  876. // glm-5.3-flash 必须排在 glm-5.3 之前(前者包含后者子串)。
  877. if strings.Contains(modelLower, "glm-5.3-flash") || strings.Contains(modelLower, "glm-5.3flash") {
  878. return s.fallbackPrices["glm-5.3-flash"]
  879. }
  880. if strings.Contains(modelLower, "glm-5.3") {
  881. return s.fallbackPrices["glm-5.3"]
  882. }
  883. if strings.Contains(modelLower, "glm-5.2") {
  884. return s.fallbackPrices["glm-5.2"]
  885. }
  886. if strings.Contains(modelLower, "glm-5.1") {
  887. return s.fallbackPrices["glm-5.1"]
  888. }
  889. if strings.Contains(modelLower, "glm-5-turbo") || strings.Contains(modelLower, "glm-5turbo") {
  890. return s.fallbackPrices["glm-5-turbo"]
  891. }
  892. if strings.Contains(modelLower, "glm-5") {
  893. return s.fallbackPrices["glm-5"]
  894. }
  895. if strings.Contains(modelLower, "glm-4.7-flashx") {
  896. return s.fallbackPrices["glm-4.7-flashx"]
  897. }
  898. if strings.Contains(modelLower, "glm-4.7-flash") {
  899. return s.fallbackPrices["glm-4.7-flash"]
  900. }
  901. if strings.Contains(modelLower, "glm-4.7") {
  902. return s.fallbackPrices["glm-4.7"]
  903. }
  904. if strings.Contains(modelLower, "glm-4.6") {
  905. return s.fallbackPrices["glm-4.6"]
  906. }
  907. if strings.Contains(modelLower, "glm-4.5-flash") {
  908. return s.fallbackPrices["glm-4.5-flash"]
  909. }
  910. if strings.Contains(modelLower, "glm-4.5-x") || strings.Contains(modelLower, "glm-4.5x") {
  911. return s.fallbackPrices["glm-4.5-x"]
  912. }
  913. if strings.Contains(modelLower, "glm-4.5-airx") || strings.Contains(modelLower, "glm-4.5airx") {
  914. return s.fallbackPrices["glm-4.5-airx"]
  915. }
  916. if strings.Contains(modelLower, "glm-4.5-air") || strings.Contains(modelLower, "glm-4.5air") {
  917. return s.fallbackPrices["glm-4.5-air"]
  918. }
  919. if strings.Contains(modelLower, "glm-4.5") {
  920. return s.fallbackPrices["glm-4.5"]
  921. }
  922. if strings.Contains(modelLower, "glm-4-32b") {
  923. return s.fallbackPrices["glm-4-32b-0414-128k"]
  924. }
  925. // 月之暗面 Kimi(kimi-k3 / k3 / k3-256k / kimi-k2.6 / kimi-for-coding / kimi-k2.5 / kimi-k2-thinking / kimi-k2)
  926. // K2-0905 / K2-0711 官方未保留定价,不进入 fallback。
  927. // K3 规则置于 K2 前:API Platform 仅官方 kimi-k3(及 / 路径后缀);
  928. // Code bare aliases 仅精确 k3 / k3-256k 或 /k3|/k3-256k 后缀,避免 kimi-k30 等未知型号误命中。
  929. // 注意:kimi-k3[1m] 是 Claude Code 上下文选择语法,不是 Kimi API 模型 ID,不进入 fallback。
  930. if strings.Contains(modelLower, "kimi-for-coding") {
  931. return s.fallbackPrices["kimi-for-coding"]
  932. }
  933. if modelLower == "kimi-k3" || strings.HasSuffix(modelLower, "/kimi-k3") ||
  934. modelLower == "k3" || modelLower == "k3-256k" ||
  935. strings.HasSuffix(modelLower, "/k3") || strings.HasSuffix(modelLower, "/k3-256k") {
  936. return s.fallbackPrices["kimi-k3"]
  937. }
  938. if strings.Contains(modelLower, "kimi-k2.6") || strings.Contains(modelLower, "kimi-k2-6") {
  939. return s.fallbackPrices["kimi-k2.6"]
  940. }
  941. if strings.Contains(modelLower, "kimi-k2.5") || strings.Contains(modelLower, "kimi-k2-5") {
  942. return s.fallbackPrices["kimi-k2.5"]
  943. }
  944. if strings.Contains(modelLower, "kimi-k2-thinking") || strings.Contains(modelLower, "kimi-k2-thinking-") {
  945. return s.fallbackPrices["kimi-k2-thinking"]
  946. }
  947. if strings.Contains(modelLower, "kimi-k2") || strings.Contains(modelLower, "kimi/k2") {
  948. return s.fallbackPrices["kimi-k2"]
  949. }
  950. // MiniMax M 系列(M3 / M2.7 / M2.5 / M2.1 / M2;含 highspeed 变体)
  951. if strings.Contains(modelLower, "minimax-m3") {
  952. return s.fallbackPrices["minimax-m3"]
  953. }
  954. if strings.Contains(modelLower, "minimax-m2.7-highspeed") || strings.Contains(modelLower, "minimax-m2-7-highspeed") {
  955. return s.fallbackPrices["minimax-m2.7-highspeed"]
  956. }
  957. if strings.Contains(modelLower, "minimax-m2.7") || strings.Contains(modelLower, "minimax-m2-7") {
  958. return s.fallbackPrices["minimax-m2.7"]
  959. }
  960. if strings.Contains(modelLower, "minimax-m2.5") || strings.Contains(modelLower, "minimax-m2-5") {
  961. return s.fallbackPrices["minimax-m2.5"]
  962. }
  963. if strings.Contains(modelLower, "minimax-m2.1") || strings.Contains(modelLower, "minimax-m2-1") {
  964. return s.fallbackPrices["minimax-m2.1"]
  965. }
  966. if strings.Contains(modelLower, "minimax-m2") || strings.Contains(modelLower, "minimax-m-2") {
  967. return s.fallbackPrices["minimax-m2"]
  968. }
  969. // 火山方舟 豆包 Embedding(多模态向量化)。
  970. // most-specific-first:放在未来任何 doubao-embedding / doubao 宽匹配之前。
  971. // 覆盖带版本后缀的别名(如 doubao-embedding-vision-251215)。
  972. if strings.Contains(modelLower, "doubao-embedding-vision") {
  973. return s.fallbackPrices["doubao-embedding-vision"]
  974. }
  975. // OpenAI(GPT-5 / Codex 族):仅匹配已知型号,避免未知 OpenAI 型号误计价。
  976. if normalized := normalizeKnownOpenAICodexModel(modelLower); normalized != "" {
  977. switch normalized {
  978. case "gpt-6-astra":
  979. return s.fallbackPrices["gpt-6-astra"]
  980. case "gpt-5.6-sol":
  981. return s.fallbackPrices["gpt-5.6-sol"]
  982. case "gpt-5.6-terra":
  983. return s.fallbackPrices["gpt-5.6-terra"]
  984. case "gpt-5.6-luna":
  985. return s.fallbackPrices["gpt-5.6-luna"]
  986. case "gpt-5.5-pro":
  987. return s.fallbackPrices["gpt-5.5-pro"]
  988. case "gpt-5.5":
  989. return s.fallbackPrices["gpt-5.5"]
  990. case "gpt-5.4-mini":
  991. return s.fallbackPrices["gpt-5.4-mini"]
  992. case "gpt-5.4-nano":
  993. return s.fallbackPrices["gpt-5.4-nano"]
  994. case "gpt-5.4":
  995. return s.fallbackPrices["gpt-5.4"]
  996. case "gpt-5.2":
  997. return s.fallbackPrices["gpt-5.2"]
  998. case "gpt-5.3-codex", "gpt-5.3-codex-spark":
  999. return s.fallbackPrices["gpt-5.3-codex"]
  1000. }
  1001. }
  1002. switch modelLower {
  1003. case "grok", "grok-latest", "grok-4.6", "grok-4.6-latest":
  1004. return s.fallbackPrices["grok-4.6"]
  1005. case "grok-4.5", "grok-4.5-latest":
  1006. return s.fallbackPrices["grok-4.5"]
  1007. case "grok-3-mini":
  1008. return s.fallbackPrices["grok-3-mini"]
  1009. case "grok-3-mini-fast":
  1010. return s.fallbackPrices["grok-3-mini-fast"]
  1011. case "grok-4.3":
  1012. return s.fallbackPrices["grok-4.3"]
  1013. case "grok-4.20-0309-reasoning",
  1014. "grok-4.20-0309-non-reasoning",
  1015. "grok-4.20-multi-agent-0309",
  1016. "grok-4.20-reasoning",
  1017. "grok-4.20-non-reasoning":
  1018. return s.fallbackPrices["grok-4.20"]
  1019. case "grok-build", "grok-build-latest", "grok-build-0.1", "grok-composer", "grok-composer-2.5-fast", "composer-2.5":
  1020. return s.fallbackPrices["grok-build-0.1"]
  1021. }
  1022. // Unknown Grok text IDs (grok-5, dated snapshots, provider-prefixed) inherit
  1023. // the current default text card so a new model cannot ship unbilled.
  1024. if pricing := s.grokUnknownTextFamilyFallback(modelLower); pricing != nil {
  1025. return pricing
  1026. }
  1027. return nil
  1028. }
  1029. func (s *BillingService) grokUnknownTextFamilyFallback(model string) *ModelPricing {
  1030. if s == nil || !isGrokUnknownTextFamilyModel(model) {
  1031. return nil
  1032. }
  1033. return s.fallbackPrices["grok-4.6"]
  1034. }
  1035. func isGrokUnknownTextFamilyModel(model string) bool {
  1036. native := strings.ToLower(strings.TrimSpace(xai.StripGrokProviderPrefix(model)))
  1037. if isGrokMediaFamilyModel(native) {
  1038. return false
  1039. }
  1040. switch {
  1041. case native == "grok", native == "grok-latest":
  1042. return true
  1043. case strings.HasPrefix(native, "grok-build"),
  1044. strings.HasPrefix(native, "grok-composer"),
  1045. strings.HasPrefix(native, "composer-"):
  1046. return true
  1047. case len(native) > 5 && strings.HasPrefix(native, "grok-"):
  1048. rest := native[len("grok-"):]
  1049. return rest[0] >= '0' && rest[0] <= '9'
  1050. default:
  1051. return false
  1052. }
  1053. }
  1054. // isGrokMediaFamilyModel matches ids that are billed per image/video/audio unit
  1055. // rather than per token, so version-numbered media ids (grok-2-image-1212,
  1056. // grok-5-video) cannot slip into the unknown-text fallback and pick up a token
  1057. // card. "vision" is deliberately absent: multimodal chat models are token billed.
  1058. func isGrokMediaFamilyModel(native string) bool {
  1059. for _, marker := range []string{"imagine", "image", "video", "audio", "speech", "tts", "transcribe", "realtime"} {
  1060. if strings.Contains(native, marker) {
  1061. return true
  1062. }
  1063. }
  1064. return false
  1065. }
  1066. // HasIdentifiedTokenPricing 判断模型能否在价格表中被"确定性识别"出 token 价格。
  1067. //
  1068. // 与 GetModelPricing 的关键区别:本函数拒绝按子串猜系列的兜底。GetModelPricing 会
  1069. // 让任意含 "haiku"/"opus"/"claude" 的名字(哪怕是不存在的型号)落到 getFallbackPricing
  1070. // 的系列兜底价上,因此凡是模型名来自外部、且"能查到价"会直接影响计费金额的场景
  1071. // (如按上游响应自报模型计费),都必须用本函数而不是 GetModelPricing 做准入判断。
  1072. func (s *BillingService) HasIdentifiedTokenPricing(model string) bool {
  1073. if s == nil {
  1074. return false
  1075. }
  1076. model = strings.ToLower(strings.TrimSpace(model))
  1077. if model == "" {
  1078. return false
  1079. }
  1080. if s.pricingService != nil {
  1081. // 仅有图片价的条目不能用于 token 计费,口径与 GetModelPricing 保持一致。
  1082. if pricing := s.pricingService.GetIdentifiedModelPricing(model); pricing != nil && !pricing.TokenPricingAbsent {
  1083. return true
  1084. }
  1085. }
  1086. pricing, ok := s.fallbackPrices[model]
  1087. return ok && pricing != nil
  1088. }
  1089. // GetModelPricing 获取模型价格配置
  1090. func (s *BillingService) GetModelPricing(model string) (*ModelPricing, error) {
  1091. // 标准化模型名称(转小写)
  1092. model = strings.ToLower(model)
  1093. // 1. 优先从动态价格服务获取
  1094. if s.pricingService != nil {
  1095. litellmPricing := s.pricingService.GetModelPricing(model)
  1096. // 仅有图片价、无 token 价的条目(如 LiteLLM 的 imagen 类模型)不能用于
  1097. // token 计费:直接返回会把 token 流量按 $0 计费。跳过后走 fallback,
  1098. // 无 fallback 则 fail-closed(ErrModelPricingUnavailable)。
  1099. // 图片计费路径(getDefaultImagePrice / getImageUnitPrice)直接读
  1100. // PricingService,不受影响。
  1101. if litellmPricing != nil && litellmPricing.TokenPricingAbsent {
  1102. litellmPricing = nil
  1103. }
  1104. if litellmPricing != nil {
  1105. // 启用 5m/1h 分类计费的条件:
  1106. // 1. 存在 1h 价格
  1107. // 2. 1h 价格 > 5m 价格(防止 LiteLLM 数据错误导致少收费)
  1108. price5m := litellmPricing.CacheCreationInputTokenCost
  1109. price1h := litellmPricing.CacheCreationInputTokenCostAbove1hr
  1110. enableBreakdown := price1h > 0 && price1h > price5m
  1111. return s.applyModelSpecificPricingPolicy(model, &ModelPricing{
  1112. InputPricePerToken: litellmPricing.InputCostPerToken,
  1113. InputPricePerTokenPriority: litellmPricing.InputCostPerTokenPriority,
  1114. OutputPricePerToken: litellmPricing.OutputCostPerToken,
  1115. OutputPricePerTokenPriority: litellmPricing.OutputCostPerTokenPriority,
  1116. CacheCreationPricePerToken: litellmPricing.CacheCreationInputTokenCost,
  1117. CacheCreationPricePerTokenPriority: litellmPricing.CacheCreationInputTokenCostPriority,
  1118. CacheReadPricePerToken: litellmPricing.CacheReadInputTokenCost,
  1119. CacheReadPricePerTokenPriority: litellmPricing.CacheReadInputTokenCostPriority,
  1120. CacheCreation5mPrice: price5m,
  1121. CacheCreation1hPrice: price1h,
  1122. SupportsCacheBreakdown: enableBreakdown,
  1123. LongContextInputThreshold: litellmPricing.LongContextInputTokenThreshold,
  1124. // xAI 的长上下文阈值语义为"达到即进高档"(LiteLLM 同口径),其余提供商为严格大于。
  1125. LongContextThresholdInclusive: strings.EqualFold(litellmPricing.LiteLLMProvider, "xai"),
  1126. LongContextInputMultiplier: litellmPricing.LongContextInputCostMultiplier,
  1127. LongContextOutputMultiplier: litellmPricing.LongContextOutputCostMultiplier,
  1128. ImageInputPricePerToken: litellmPricing.InputCostPerImageToken,
  1129. ImageOutputPricePerToken: litellmPricing.OutputCostPerImageToken,
  1130. }), nil
  1131. }
  1132. }
  1133. // 2. 使用硬编码回退价格
  1134. fallback := s.getFallbackPricing(model)
  1135. if fallback != nil {
  1136. // 按模型名去重:每个模型每进程最多打一条 warn,避免热路径每请求刷屏(issue #3394)。
  1137. // model 在函数入口已 ToLower,故 GLM-5.2 / glm-5.2 视为同一条目。
  1138. if _, seen := s.fallbackWarnSeen.LoadOrStore(model, struct{}{}); !seen {
  1139. log.Printf("[Billing] Using fallback pricing for model: %s", model)
  1140. }
  1141. return s.applyModelSpecificPricingPolicy(model, fallback), nil
  1142. }
  1143. return nil, fmt.Errorf("%w for model: %s", ErrModelPricingUnavailable, model)
  1144. }
  1145. // GetModelPricingWithChannel 获取模型定价,渠道配置的价格覆盖默认值
  1146. // 渠道存在时,未配置的图片输出价格归零(不回退到 LiteLLM)
  1147. func (s *BillingService) GetModelPricingWithChannel(model string, channelPricing *ChannelModelPricing) (*ModelPricing, error) {
  1148. pricing, err := s.GetModelPricing(model)
  1149. if err != nil {
  1150. return nil, err
  1151. }
  1152. if channelPricing == nil {
  1153. return pricing, nil
  1154. }
  1155. // 防止修改 fallbackPrices 中的共享指针
  1156. cloned := *pricing
  1157. pricing = &cloned
  1158. applyChannelTokenPriceOverrides(pricing, channelPricing)
  1159. pricing.FastMultiplier = channelPricing.FastMultiplier
  1160. pricing.FlexMultiplier = channelPricing.FlexMultiplier
  1161. if channelPricing.MaxReasoningEffortMultiplier != nil {
  1162. pricing.MaxReasoningEffortMultiplier = channelPricing.MaxReasoningEffortMultiplier
  1163. }
  1164. if channelPricing.ImageOutputPrice != nil {
  1165. pricing.ImageOutputPricePerToken = *channelPricing.ImageOutputPrice
  1166. } else {
  1167. pricing.ImageOutputPricePerToken = 0
  1168. }
  1169. pricing.ImageOutputPriceExplicit = true
  1170. applyChannelImageInputPrice(channelPricing, pricing)
  1171. return pricing, nil
  1172. }
  1173. // channelTierOverridePrice applies a Standard-tier override while preserving
  1174. // an explicit model-catalog Fast/Priority ratio. If the catalog has no tier
  1175. // price, generic service-tier defaults remain responsible for the fallback.
  1176. func channelTierOverridePrice(baseStandard, baseTier, channelStandard float64) float64 {
  1177. if baseStandard > 0 && baseTier > 0 {
  1178. return channelStandard * (baseTier / baseStandard)
  1179. }
  1180. return 0
  1181. }
  1182. func applyChannelTokenPriceOverrides(pricing *ModelPricing, channelPricing *ChannelModelPricing) {
  1183. if pricing == nil || channelPricing == nil {
  1184. return
  1185. }
  1186. if channelPricing.InputPrice != nil {
  1187. priority := channelTierOverridePrice(pricing.InputPricePerToken, pricing.InputPricePerTokenPriority, *channelPricing.InputPrice)
  1188. pricing.InputPricePerToken = *channelPricing.InputPrice
  1189. pricing.InputPricePerTokenPriority = priority
  1190. }
  1191. if channelPricing.OutputPrice != nil {
  1192. priority := channelTierOverridePrice(pricing.OutputPricePerToken, pricing.OutputPricePerTokenPriority, *channelPricing.OutputPrice)
  1193. pricing.OutputPricePerToken = *channelPricing.OutputPrice
  1194. pricing.OutputPricePerTokenPriority = priority
  1195. }
  1196. if channelPricing.CacheWritePrice != nil {
  1197. priority := channelTierOverridePrice(pricing.CacheCreationPricePerToken, pricing.CacheCreationPricePerTokenPriority, *channelPricing.CacheWritePrice)
  1198. pricing.CacheCreationPricePerToken = *channelPricing.CacheWritePrice
  1199. pricing.CacheCreationPricePerTokenPriority = priority
  1200. pricing.CacheCreationPriceExplicit = true
  1201. pricing.CacheCreation5mPrice = *channelPricing.CacheWritePrice
  1202. if channelPricing.CacheWrite1hPrice == nil {
  1203. // Preserve the pre-split behavior for existing configurations: a lone
  1204. // cache_write_price continues to override both TTL tiers.
  1205. pricing.CacheCreation1hPrice = *channelPricing.CacheWritePrice
  1206. }
  1207. }
  1208. if channelPricing.CacheWrite1hPrice != nil {
  1209. pricing.CacheCreation1hPrice = *channelPricing.CacheWrite1hPrice
  1210. pricing.SupportsCacheBreakdown = true
  1211. }
  1212. if channelPricing.CacheReadPrice != nil {
  1213. priority := channelTierOverridePrice(pricing.CacheReadPricePerToken, pricing.CacheReadPricePerTokenPriority, *channelPricing.CacheReadPrice)
  1214. pricing.CacheReadPricePerToken = *channelPricing.CacheReadPrice
  1215. pricing.CacheReadPricePerTokenPriority = priority
  1216. }
  1217. }
  1218. // --- 统一计费入口 ---
  1219. // CostInput 统一计费输入
  1220. type CostInput struct {
  1221. Ctx context.Context
  1222. Model string
  1223. GroupID *int64 // 用于渠道定价查找
  1224. Group *Group
  1225. Tokens UsageTokens
  1226. RequestCount int // 按次计费时使用
  1227. UsageUnits float64 // 音频等连续计量单位(分钟/小时/百万字符)
  1228. SizeTier string // 按次/图片模式的层级标签("1K","2K","4K","HD" 等)
  1229. RateMultiplier float64
  1230. PricingAt time.Time // 渠道分时定价使用的计费时刻
  1231. ServiceTier string // "priority","flex","" 等
  1232. ReasoningEffort string // 最终转发的推理等级;max 可触发模型/渠道倍率
  1233. Resolver *ModelPricingResolver // 定价解析器
  1234. Resolved *ResolvedPricing // 可选:预解析的定价结果(避免重复 Resolve 调用)
  1235. LongContextBillingEnabled *bool
  1236. }
  1237. // CalculateCostUnified 统一计费入口,支持三种计费模式。
  1238. // 使用 ModelPricingResolver 解析定价,然后根据 BillingMode 分发计算。
  1239. func (s *BillingService) CalculateCostUnified(input CostInput) (*CostBreakdown, error) {
  1240. if input.Resolver == nil {
  1241. // 无 Resolver,回退到旧路径
  1242. applyLongContextBilling := true
  1243. if input.LongContextBillingEnabled != nil {
  1244. applyLongContextBilling = *input.LongContextBillingEnabled
  1245. }
  1246. breakdown, err := s.calculateCostInternalWithPolicy(
  1247. input.Model,
  1248. input.Tokens,
  1249. input.RateMultiplier,
  1250. input.ServiceTier,
  1251. nil,
  1252. applyLongContextBilling,
  1253. )
  1254. if err == nil {
  1255. applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(input.Model, input.ReasoningEffort, nil))
  1256. }
  1257. return breakdown, err
  1258. }
  1259. // 优先使用预解析结果,避免重复 Resolve 调用
  1260. resolved := input.Resolved
  1261. if resolved == nil {
  1262. resolved = input.Resolver.Resolve(input.Ctx, PricingInput{
  1263. Model: input.Model,
  1264. GroupID: input.GroupID,
  1265. Group: input.Group,
  1266. })
  1267. }
  1268. // 保存时强制 > 0;若仍有负数泄漏(缓存/迁移残留),按 0 处理避免按 1x 误扣。
  1269. if input.RateMultiplier < 0 {
  1270. input.RateMultiplier = 0
  1271. }
  1272. var breakdown *CostBreakdown
  1273. var err error
  1274. switch resolved.Mode {
  1275. case BillingModePerRequest, BillingModeImage, BillingModeVideo:
  1276. breakdown, err = s.calculatePerRequestCost(resolved, input)
  1277. default: // BillingModeToken
  1278. breakdown, err = s.calculateTokenCost(resolved, input)
  1279. }
  1280. if err == nil && breakdown != nil {
  1281. breakdown.BillingMode = string(resolved.Mode)
  1282. if breakdown.BillingMode == "" {
  1283. breakdown.BillingMode = string(BillingModeToken)
  1284. }
  1285. }
  1286. return breakdown, err
  1287. }
  1288. // calculateTokenCost 按 token 区间计费
  1289. func (s *BillingService) calculateTokenCost(resolved *ResolvedPricing, input CostInput) (*CostBreakdown, error) {
  1290. totalContext := input.Tokens.InputTokens + input.Tokens.CacheCreationTokens + input.Tokens.CacheReadTokens
  1291. // 分组开关是统一入口;账号 API 开关保留为额外开启能力,但 false 不否决分组配置。
  1292. contextTierPricingEnabled := resolved.longContextPricingEnabled
  1293. if input.LongContextBillingEnabled != nil && *input.LongContextBillingEnabled {
  1294. contextTierPricingEnabled = true
  1295. }
  1296. pricingContext := totalContext
  1297. if !contextTierPricingEnabled {
  1298. // 渠道可能显式配置了第一档,也可能只配置高上下文档。用 1 token
  1299. // 选择最低档;未命中时自然回退到渠道基础价。
  1300. pricingContext = 1
  1301. }
  1302. pricing := input.Resolver.GetIntervalPricing(resolved, pricingContext)
  1303. if pricing == nil {
  1304. return nil, fmt.Errorf("no pricing available for model: %s: %w", input.Model, ErrModelPricingUnavailable)
  1305. }
  1306. // 默认价卡(Source=LiteLLM)应用 DeepSeek 官方价强制覆盖(幂等,GetModelPricing
  1307. // 内部已强制过);分组/渠道自定义定价保留运营者配置,不强制覆盖官方价。
  1308. pricing = s.applyModelSpecificPricingPolicyEx(input.Model, pricing, resolved.Source == PricingSourceLiteLLM)
  1309. // DeepSeek 模型默认价卡按官方峰谷口径调整:高峰时段(01:00–04:00 与
  1310. // 06:00–10:00 UTC,仅工作日;北京时间周末全天低谷)按 2× 低谷价计费。
  1311. // 仅作用于默认价卡(Source=LiteLLM,无分组/渠道自定义定价)——分组/渠道
  1312. // 自定义定价保持运营者语义,不叠加。PricingAt 为零值时回退当前时刻。
  1313. // 先克隆再乘,避免污染共享 fallbackPrices 指针。
  1314. if resolved.Source == PricingSourceLiteLLM && isDeepSeekModel(input.Model) {
  1315. pricingAt := input.PricingAt
  1316. if pricingAt.IsZero() {
  1317. pricingAt = timezone.Now()
  1318. }
  1319. if mult := deepseekPeakMultiplierAt(pricingAt); mult > 1 {
  1320. cloned := *pricing
  1321. cloned.InputPricePerToken *= mult
  1322. cloned.OutputPricePerToken *= mult
  1323. cloned.CacheReadPricePerToken *= mult
  1324. pricing = &cloned
  1325. }
  1326. }
  1327. // 官方长上下文阶梯仅在无区间定价时应用(区间定价已包含上下文分层)。
  1328. applyLongCtx := len(resolved.Intervals) == 0 && contextTierPricingEnabled
  1329. breakdown := s.computeTokenBreakdown(pricing, input.Tokens, input.RateMultiplier, input.ServiceTier, applyLongCtx)
  1330. applyCostBreakdownMultiplier(breakdown, resolvedChannelTimeMultiplier(resolved, input.PricingAt))
  1331. applyCostBreakdownMultiplier(breakdown, maxReasoningEffortBillingMultiplier(input.Model, input.ReasoningEffort, pricing))
  1332. return breakdown, nil
  1333. }
  1334. // computeTokenBreakdown 是 token 计费的核心逻辑,由 calculateTokenCost 和 calculateCostInternal 共用。
  1335. // applyLongCtx 控制是否检查长上下文定价(区间定价已自含上下文分层,不需要额外应用)。
  1336. func (s *BillingService) computeTokenBreakdown(
  1337. pricing *ModelPricing, tokens UsageTokens,
  1338. rateMultiplier float64, serviceTier string,
  1339. applyLongCtx bool,
  1340. ) *CostBreakdown {
  1341. // 保存时强制 > 0;若仍有负数泄漏,按 0 处理避免按 1x 误扣。
  1342. if rateMultiplier < 0 {
  1343. rateMultiplier = 0
  1344. }
  1345. inputPrice := pricing.InputPricePerToken
  1346. outputPrice := pricing.OutputPricePerToken
  1347. cacheReadPrice := pricing.CacheReadPricePerToken
  1348. cacheCreationPrice := pricing.CacheCreationPricePerToken
  1349. cacheCreationMultiplier := 1.0
  1350. tierMultiplier := 1.0
  1351. if usePriorityServiceTierPricing(serviceTier, pricing) {
  1352. if pricing.InputPricePerTokenPriority > 0 {
  1353. inputPrice = pricing.InputPricePerTokenPriority
  1354. }
  1355. if pricing.OutputPricePerTokenPriority > 0 {
  1356. outputPrice = pricing.OutputPricePerTokenPriority
  1357. }
  1358. if pricing.CacheReadPricePerTokenPriority > 0 {
  1359. cacheReadPrice = pricing.CacheReadPricePerTokenPriority
  1360. }
  1361. if pricing.CacheCreationPricePerTokenPriority > 0 {
  1362. cacheCreationPrice = pricing.CacheCreationPricePerTokenPriority
  1363. }
  1364. } else {
  1365. tierMultiplier = configuredServiceTierMultiplier(serviceTier, pricing)
  1366. }
  1367. longContextPricingEligible := applyLongCtx && s.shouldApplySessionLongContextPricing(tokens, pricing)
  1368. var baselineCost *CostBreakdown
  1369. if longContextPricingEligible {
  1370. baselineCost = s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, false)
  1371. // 倍率 ≤0 表示该项未配置(目录/覆写条目可能只写了 input 或 output 一侧),
  1372. // 按 1 计而不是乘 0:乘 0 会把超阈值请求的对应分项算成免费。
  1373. longCtxInputMultiplier := longContextMultiplierOrOne(pricing.LongContextInputMultiplier)
  1374. inputPrice *= longCtxInputMultiplier
  1375. outputPrice *= longContextMultiplierOrOne(pricing.LongContextOutputMultiplier)
  1376. // 缓存读取本质上是输入侧的复用,应与 input 一同应用长上下文倍率;
  1377. // 否则 cache hit 越多,少计的费用越多(见 #2293)。
  1378. cacheReadPrice *= longCtxInputMultiplier
  1379. // 缓存创建(cache_write)也是输入侧操作,三档价格(标准 / 5m / 1h)
  1380. // 都通过 computeCacheCreationCost 直接读取 pricing.*,不会经过这里
  1381. // 的倍率修改,因此显式向下传一个倍率,避免长上下文场景下被漏乘。
  1382. cacheCreationMultiplier = longCtxInputMultiplier
  1383. }
  1384. bd := &CostBreakdown{}
  1385. // 分离图片输入 token 与文本输入 token(多模态 embedding、图片编辑等图文不同价场景)。
  1386. // InputCost 仅计文本输入,图片输入费用单独记入 ImageInputCost,便于对账;总额不变。
  1387. // ImageInputTokens 为 0 时(绝大多数 chat/vision 流量)走原始单价路径,行为不变。
  1388. if tokens.ImageInputTokens > 0 {
  1389. imageInputTokens := tokens.ImageInputTokens
  1390. textInputTokens := tokens.InputTokens - imageInputTokens
  1391. if textInputTokens < 0 {
  1392. textInputTokens = 0
  1393. imageInputTokens = tokens.InputTokens
  1394. }
  1395. imageInputPrice := pricing.ImageInputPricePerToken
  1396. if imageInputPrice == 0 {
  1397. // 未配置图片输入档时回退到文本 input 价(已含 priority / 长上下文调整)
  1398. imageInputPrice = inputPrice
  1399. }
  1400. bd.InputCost = float64(textInputTokens) * inputPrice
  1401. bd.ImageInputCost = float64(imageInputTokens) * imageInputPrice
  1402. } else {
  1403. bd.InputCost = float64(tokens.InputTokens) * inputPrice
  1404. }
  1405. // 分离图片输出 token 与文本输出 token
  1406. textOutputTokens := tokens.OutputTokens - tokens.ImageOutputTokens
  1407. if textOutputTokens < 0 {
  1408. textOutputTokens = 0
  1409. }
  1410. bd.OutputCost = float64(textOutputTokens) * outputPrice
  1411. // 图片输出 token 费用(独立费率)
  1412. if tokens.ImageOutputTokens > 0 {
  1413. imgPrice := pricing.ImageOutputPricePerToken
  1414. if imgPrice == 0 && !pricing.ImageOutputPriceExplicit {
  1415. imgPrice = outputPrice
  1416. }
  1417. bd.ImageOutputCost = float64(tokens.ImageOutputTokens) * imgPrice
  1418. }
  1419. // 缓存创建费用
  1420. bd.CacheCreationCost = s.computeCacheCreationCost(pricing, tokens, cacheCreationPrice, cacheCreationMultiplier)
  1421. bd.CacheReadCost = float64(tokens.CacheReadTokens) * cacheReadPrice
  1422. if tierMultiplier != 1.0 {
  1423. bd.InputCost *= tierMultiplier
  1424. bd.ImageInputCost *= tierMultiplier
  1425. bd.OutputCost *= tierMultiplier
  1426. bd.ImageOutputCost *= tierMultiplier
  1427. bd.CacheCreationCost *= tierMultiplier
  1428. bd.CacheReadCost *= tierMultiplier
  1429. }
  1430. bd.TotalCost = bd.InputCost + bd.ImageInputCost + bd.OutputCost + bd.ImageOutputCost +
  1431. bd.CacheCreationCost + bd.CacheReadCost
  1432. bd.ActualCost = bd.TotalCost * rateMultiplier
  1433. bd.LongContextBillingApplied = baselineCost != nil && bd.ActualCost > baselineCost.ActualCost
  1434. return bd
  1435. }
  1436. // computeCacheCreationCost 计算缓存创建费用(支持 5m/1h 分类或标准计费)。
  1437. // multiplier 用于长上下文等场景下的整体价格缩放(普通调用传 1.0 即可)。
  1438. func (s *BillingService) computeCacheCreationCost(pricing *ModelPricing, tokens UsageTokens, price, multiplier float64) float64 {
  1439. if pricing.SupportsCacheBreakdown && (pricing.CacheCreation5mPrice > 0 || pricing.CacheCreation1hPrice > 0) {
  1440. cacheCreation5mTokens, cacheCreation1hTokens := normalizeCacheCreationBreakdown(tokens)
  1441. if cacheCreation5mTokens == 0 && cacheCreation1hTokens == 0 && tokens.CacheCreationTokens > 0 {
  1442. // API 未返回 ephemeral 明细,回退到全部按 5m 单价计费
  1443. return float64(tokens.CacheCreationTokens) * pricing.CacheCreation5mPrice * multiplier
  1444. }
  1445. return float64(cacheCreation5mTokens)*pricing.CacheCreation5mPrice*multiplier +
  1446. float64(cacheCreation1hTokens)*pricing.CacheCreation1hPrice*multiplier
  1447. }
  1448. return float64(tokens.CacheCreationTokens) * price * multiplier
  1449. }
  1450. // normalizeCacheCreationBreakdown caps contradictory 5m/1h details at an explicitly
  1451. // positive aggregate while retaining their reported ratio as closely as integer tokens allow.
  1452. func normalizeCacheCreationBreakdown(tokens UsageTokens) (int, int) {
  1453. cacheCreation5mTokens := tokens.CacheCreation5mTokens
  1454. cacheCreation1hTokens := tokens.CacheCreation1hTokens
  1455. aggregate := tokens.CacheCreationTokens
  1456. if cacheCreation5mTokens < 0 {
  1457. cacheCreation5mTokens = 0
  1458. }
  1459. if cacheCreation1hTokens < 0 {
  1460. cacheCreation1hTokens = 0
  1461. }
  1462. if aggregate <= 0 || (cacheCreation5mTokens <= aggregate && cacheCreation1hTokens <= aggregate-cacheCreation5mTokens) {
  1463. return cacheCreation5mTokens, cacheCreation1hTokens
  1464. }
  1465. detailTotal := float64(cacheCreation5mTokens) + float64(cacheCreation1hTokens)
  1466. normalized5mTokens := math.Round(float64(aggregate) * float64(cacheCreation5mTokens) / detailTotal)
  1467. if normalized5mTokens >= float64(aggregate) {
  1468. cacheCreation5mTokens = aggregate
  1469. } else {
  1470. cacheCreation5mTokens = int(normalized5mTokens)
  1471. }
  1472. return cacheCreation5mTokens, aggregate - cacheCreation5mTokens
  1473. }
  1474. // calculatePerRequestCost 按次/图片计费
  1475. func (s *BillingService) calculatePerRequestCost(resolved *ResolvedPricing, input CostInput) (*CostBreakdown, error) {
  1476. units := input.UsageUnits
  1477. if units <= 0 {
  1478. count := input.RequestCount
  1479. if count <= 0 {
  1480. count = 1
  1481. }
  1482. units = float64(count)
  1483. }
  1484. var unitPrice float64
  1485. if input.SizeTier != "" {
  1486. unitPrice = input.Resolver.GetRequestTierPrice(resolved, input.SizeTier)
  1487. }
  1488. if unitPrice == 0 {
  1489. totalContext := input.Tokens.InputTokens + input.Tokens.CacheCreationTokens + input.Tokens.CacheReadTokens
  1490. unitPrice = input.Resolver.GetRequestTierPriceByContext(resolved, totalContext)
  1491. }
  1492. // 回退到默认按次价格
  1493. if unitPrice == 0 {
  1494. unitPrice = resolved.DefaultPerRequestPrice
  1495. }
  1496. totalCost := unitPrice * units
  1497. actualCost := totalCost * input.RateMultiplier
  1498. return &CostBreakdown{
  1499. TotalCost: totalCost,
  1500. ActualCost: actualCost,
  1501. }, nil
  1502. }
  1503. // CalculateCost 计算使用费用
  1504. func (s *BillingService) CalculateCost(model string, tokens UsageTokens, rateMultiplier float64) (*CostBreakdown, error) {
  1505. return s.calculateCostInternal(model, tokens, rateMultiplier, "", nil)
  1506. }
  1507. func (s *BillingService) CalculateCostWithServiceTier(model string, tokens UsageTokens, rateMultiplier float64, serviceTier string) (*CostBreakdown, error) {
  1508. return s.calculateCostInternal(model, tokens, rateMultiplier, serviceTier, nil)
  1509. }
  1510. func (s *BillingService) calculateCostWithServiceTierPolicy(
  1511. model string,
  1512. tokens UsageTokens,
  1513. rateMultiplier float64,
  1514. serviceTier string,
  1515. longContextBillingEnabled bool,
  1516. ) (*CostBreakdown, error) {
  1517. return s.calculateCostInternalWithPolicy(model, tokens, rateMultiplier, serviceTier, nil, longContextBillingEnabled)
  1518. }
  1519. func (s *BillingService) calculateCostInternal(model string, tokens UsageTokens, rateMultiplier float64, serviceTier string, channelPricing *ChannelModelPricing) (*CostBreakdown, error) {
  1520. return s.calculateCostInternalWithPolicy(model, tokens, rateMultiplier, serviceTier, channelPricing, true)
  1521. }
  1522. func (s *BillingService) calculateCostInternalWithPolicy(
  1523. model string,
  1524. tokens UsageTokens,
  1525. rateMultiplier float64,
  1526. serviceTier string,
  1527. channelPricing *ChannelModelPricing,
  1528. longContextBillingEnabled bool,
  1529. ) (*CostBreakdown, error) {
  1530. var pricing *ModelPricing
  1531. var err error
  1532. if channelPricing != nil {
  1533. pricing, err = s.GetModelPricingWithChannel(model, channelPricing)
  1534. } else {
  1535. pricing, err = s.GetModelPricing(model)
  1536. }
  1537. if err != nil {
  1538. return nil, err
  1539. }
  1540. return s.computeTokenBreakdown(pricing, tokens, rateMultiplier, serviceTier, longContextBillingEnabled), nil
  1541. }
  1542. // applyModelSpecificPricingPolicy 对目录数据做模型特定修正:DeepSeek 官方价
  1543. // 强制覆盖;GPT-5.6 缺 cache_write 价时按官方规则补 1.25 倍输入价;Fast/priority
  1544. // 档按业务倍率改写(本地/远程目录的 priority 价可能沿用官方旧口径)。长上下文
  1545. // 阶梯不在此处补齐:一律由目录数据(above_XXXk 折算或显式 long_context_* 字段)
  1546. // 驱动。默认强制 DeepSeek 官方价——该路径仅被默认价卡(GetModelPricing 内部)
  1547. // 调用;分组/渠道自定义定价路径用带参数的 applyModelSpecificPricingPolicyEx
  1548. // 关闭强制,保留运营者配置。
  1549. func (s *BillingService) applyModelSpecificPricingPolicy(model string, pricing *ModelPricing) *ModelPricing {
  1550. return s.applyModelSpecificPricingPolicyEx(model, pricing, true)
  1551. }
  1552. // applyModelSpecificPricingPolicyEx 与 applyModelSpecificPricingPolicy 相同,
  1553. // 但由调用方控制是否强制 DeepSeek 官方价(forceDeepSeekRates)。
  1554. // calculateTokenCost 对分组/渠道自定义定价(Source 非 LiteLLM)传 false:
  1555. // 强制覆盖会把运营者配置的售价盖回官方价,违反自定义定价语义。
  1556. func (s *BillingService) applyModelSpecificPricingPolicyEx(model string, pricing *ModelPricing, forceDeepSeekRates bool) *ModelPricing {
  1557. if pricing == nil {
  1558. return nil
  1559. }
  1560. // DeepSeek 模型:无论 JSON/远端价格表给什么价,一律强制官方低谷价
  1561. // (2026-08-23 起生效)。这是覆盖远端旧价的关键——远端仓库不可改,生产会先
  1562. // 拉到旧价,必须在此兜底修正;克隆后再覆盖,避免污染共享 fallbackPrices 指针。
  1563. // 档位判定:含 "deepseek-v4-pro" 的版本化名称(如 deepseek-v4-pro-0813)归 pro 档,
  1564. // 其余 deepseek-*(含已停服的 chat/reasoner 与未知型号)统一归 flash 档。
  1565. // 高峰时段倍率不在本函数处理,由 calculateTokenCost 按 deepseekPeakMultiplierAt
  1566. // 对默认价卡另行叠加(分组/渠道自定义定价不叠加)。
  1567. if forceDeepSeekRates && isDeepSeekModel(model) {
  1568. cloned := *pricing
  1569. if strings.Contains(strings.ToLower(strings.TrimSpace(model)), "deepseek-v4-pro") {
  1570. cloned.InputPricePerToken = deepseekProOffPeakInputPrice
  1571. cloned.OutputPricePerToken = deepseekProOffPeakOutputPrice
  1572. cloned.CacheReadPricePerToken = deepseekProOffPeakCacheRead
  1573. } else {
  1574. // deepseek-v4-flash / deepseek-v4-flash-vision-exp 与其余 deepseek-* 共用 flash 价。
  1575. cloned.InputPricePerToken = deepseekFlashOffPeakInputPrice
  1576. cloned.OutputPricePerToken = deepseekFlashOffPeakOutputPrice
  1577. cloned.CacheReadPricePerToken = deepseekFlashOffPeakCacheRead
  1578. }
  1579. return &cloned
  1580. }
  1581. normalized := normalizeKnownOpenAICodexModel(model)
  1582. isGPT56 := isOpenAIGPT56Model(normalized)
  1583. needsMaxReasoningEffortMultiplier := isClaudeFable51Model(model) && pricing.MaxReasoningEffortMultiplier == nil
  1584. needsCacheCreationPolicy := isGPT56 && !pricing.CacheCreationPriceExplicit && (pricing.CacheCreationPricePerToken <= 0 ||
  1585. (pricing.InputPricePerTokenPriority > 0 && pricing.CacheCreationPricePerTokenPriority <= 0))
  1586. fastRatio := openAIModelFastPricingRatio(normalized)
  1587. if !needsCacheCreationPolicy && fastRatio <= 0 && !needsMaxReasoningEffortMultiplier {
  1588. return pricing
  1589. }
  1590. cloned := *pricing
  1591. if needsMaxReasoningEffortMultiplier {
  1592. cloned.MaxReasoningEffortMultiplier = defaultMaxReasoningEffortMultiplier(model)
  1593. }
  1594. if isGPT56 && !cloned.CacheCreationPriceExplicit {
  1595. if cloned.CacheCreationPricePerToken <= 0 {
  1596. cloned.CacheCreationPricePerToken = cloned.InputPricePerToken * 1.25
  1597. }
  1598. if cloned.CacheCreationPricePerTokenPriority <= 0 {
  1599. cloned.CacheCreationPricePerTokenPriority = cloned.InputPricePerTokenPriority * 1.25
  1600. }
  1601. }
  1602. if fastRatio > 0 {
  1603. enforceOpenAIFastPricingRatio(&cloned, fastRatio)
  1604. }
  1605. return &cloned
  1606. }
  1607. // openAIModelFastPricingRatio 返回业务口径下 OpenAI GPT 模型 Fast/priority
  1608. // 的标准价倍率:gpt-5.6 / gpt-6-astra / gpt-5.4 为 2x,gpt-5.5 为 2.5x。未定义 Fast
  1609. // 档的模型(如 gpt-5.5-pro、gpt-5.4-mini/nano)返回 0。
  1610. func openAIModelFastPricingRatio(normalized string) float64 {
  1611. switch normalized {
  1612. case "gpt-5.4", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra":
  1613. return 2.0
  1614. case "gpt-5.5":
  1615. return 2.5
  1616. default:
  1617. if isOpenAIGPT6AstraModel(normalized) {
  1618. return 2.0
  1619. }
  1620. return 0
  1621. }
  1622. }
  1623. // enforceOpenAIFastPricingRatio 把 priority 档价格改写为「标准价 × ratio」。
  1624. // 本地/远程 LiteLLM 目录可能只带官方旧口径(如 gpt-5.5 priority 仍标 2x),
  1625. // 直接采用会导致 Fast 模式少计费;这里按业务倍率兜底修正,且对已正确的
  1626. // fallback 条目(2x/2.5x)是幂等的。computeTokenBreakdown 在 priority 价格
  1627. // 存在时走显式档位价、不再叠加通用 tier 倍率,因此不会重复乘价。
  1628. func enforceOpenAIFastPricingRatio(pricing *ModelPricing, ratio float64) {
  1629. if pricing == nil || ratio <= 0 {
  1630. return
  1631. }
  1632. pricing.InputPricePerTokenPriority = pricing.InputPricePerToken * ratio
  1633. pricing.OutputPricePerTokenPriority = pricing.OutputPricePerToken * ratio
  1634. if pricing.CacheReadPricePerToken > 0 {
  1635. pricing.CacheReadPricePerTokenPriority = pricing.CacheReadPricePerToken * ratio
  1636. }
  1637. if pricing.CacheCreationPricePerToken > 0 {
  1638. pricing.CacheCreationPricePerTokenPriority = pricing.CacheCreationPricePerToken * ratio
  1639. }
  1640. }
  1641. // longContextMultiplierOrOne 把未配置(≤0)的长上下文倍率归一为 1。
  1642. func longContextMultiplierOrOne(m float64) float64 {
  1643. if m <= 0 {
  1644. return 1
  1645. }
  1646. return m
  1647. }
  1648. func (s *BillingService) shouldApplySessionLongContextPricing(tokens UsageTokens, pricing *ModelPricing) bool {
  1649. if pricing == nil || pricing.LongContextInputThreshold <= 0 {
  1650. return false
  1651. }
  1652. if pricing.LongContextInputMultiplier <= 1 && pricing.LongContextOutputMultiplier <= 1 {
  1653. return false
  1654. }
  1655. totalInputTokens := tokens.InputTokens + tokens.CacheCreationTokens + tokens.CacheReadTokens
  1656. if pricing.LongContextThresholdInclusive {
  1657. return totalInputTokens >= pricing.LongContextInputThreshold
  1658. }
  1659. return totalInputTokens > pricing.LongContextInputThreshold
  1660. }
  1661. // CalculateCostWithConfig 使用配置中的默认倍率计算费用
  1662. func (s *BillingService) CalculateCostWithConfig(model string, tokens UsageTokens) (*CostBreakdown, error) {
  1663. multiplier := s.cfg.Default.RateMultiplier
  1664. if multiplier <= 0 {
  1665. multiplier = 1.0
  1666. }
  1667. return s.CalculateCost(model, tokens, multiplier)
  1668. }
  1669. // ListSupportedModels 列出所有支持的模型(现在总是返回true,因为有模糊匹配)
  1670. func (s *BillingService) ListSupportedModels() []string {
  1671. models := make([]string, 0)
  1672. // 返回回退价格支持的模型系列
  1673. for model := range s.fallbackPrices {
  1674. models = append(models, model)
  1675. }
  1676. return models
  1677. }
  1678. // IsModelSupported 检查模型是否支持(现在总是返回true,因为有模糊匹配回退)
  1679. func (s *BillingService) IsModelSupported(model string) bool {
  1680. // 所有Claude模型都有回退价格支持
  1681. modelLower := strings.ToLower(model)
  1682. return strings.Contains(modelLower, "claude") ||
  1683. strings.Contains(modelLower, "opus") ||
  1684. strings.Contains(modelLower, "sonnet") ||
  1685. strings.Contains(modelLower, "haiku")
  1686. }
  1687. // GetEstimatedCost 估算费用(用于前端展示)
  1688. func (s *BillingService) GetEstimatedCost(model string, estimatedInputTokens, estimatedOutputTokens int) (float64, error) {
  1689. tokens := UsageTokens{
  1690. InputTokens: estimatedInputTokens,
  1691. OutputTokens: estimatedOutputTokens,
  1692. }
  1693. breakdown, err := s.CalculateCostWithConfig(model, tokens)
  1694. if err != nil {
  1695. return 0, err
  1696. }
  1697. return breakdown.ActualCost, nil
  1698. }
  1699. // GetPricingServiceStatus 获取价格服务状态
  1700. func (s *BillingService) GetPricingServiceStatus() map[string]any {
  1701. if s.pricingService != nil {
  1702. return s.pricingService.GetStatus()
  1703. }
  1704. return map[string]any{
  1705. "model_count": len(s.fallbackPrices),
  1706. "last_updated": "using fallback",
  1707. "local_hash": "N/A",
  1708. }
  1709. }
  1710. // ForceUpdatePricing 强制更新价格数据
  1711. func (s *BillingService) ForceUpdatePricing() error {
  1712. if s.pricingService != nil {
  1713. return s.pricingService.ForceUpdate()
  1714. }
  1715. return fmt.Errorf("pricing service not initialized")
  1716. }
  1717. // ImagePriceConfig 图片计费配置
  1718. type ImagePriceConfig struct {
  1719. Price1K *float64 // 1K 尺寸价格(nil 表示使用默认值)
  1720. Price2K *float64 // 2K 尺寸价格(nil 表示使用默认值)
  1721. Price4K *float64 // 4K 尺寸价格(nil 表示使用默认值)
  1722. }
  1723. // VideoPriceConfig 视频生成计费配置。所有价格均为**每秒**单价(USD/s),与 xAI 官方计费口径一致。
  1724. type VideoPriceConfig struct {
  1725. Price480P *float64 // 480p 每秒价格(nil 表示使用默认值)
  1726. Price720P *float64 // 720p 每秒价格(nil 表示使用默认值)
  1727. Price1080P *float64 // 1080p 每秒价格(nil 表示使用默认值)
  1728. // ModelPrices is optional per-model-family override: family → resolution → USD/s.
  1729. // When set for a model, it wins over Price* flat columns for that model only.
  1730. ModelPrices map[string]map[string]float64
  1731. }
  1732. const (
  1733. defaultImageGenerationPrice = 0.134
  1734. defaultGrokImagineImagePrice1K = 0.02
  1735. defaultGrokImagineImagePrice2K = 0.02
  1736. defaultGrokImagineImageQualityPrice1K = 0.05
  1737. defaultGrokImagineImageQualityPrice2K = 0.07
  1738. defaultGrokImagineImage20Price1K = 0.06 // default quality is Medium
  1739. defaultGrokImagineImage20Price2K = 0.08
  1740. // 视频默认价为 xAI 官方**每秒**输出价格(USD/s),总价 = 每秒价 × 时长(秒)。
  1741. defaultGrokImagineVideoPrice480P = 0.05
  1742. defaultGrokImagineVideoPrice720P = 0.07
  1743. defaultGrokImagineVideo15Price480P = 0.08
  1744. defaultGrokImagineVideo15Price720P = 0.14
  1745. defaultGrokImagineVideo15Price1080P = 0.25
  1746. // Codex alpha/search 网页搜索单次默认价:OpenAI 官方 web search 定价 $10/1000 次。
  1747. defaultWebSearchPricePerCall = 0.01
  1748. // xAI server-side web/X search and code execution are $5/1000 calls.
  1749. defaultSearchPricePer1k = 5.0
  1750. // Generic realtime defaults to think-fast-1.0; think-fast-2.0 can be
  1751. // configured independently through per-model group/channel pricing.
  1752. defaultAudioRealtimePricePerMin = 0.05
  1753. defaultAudioTTSPricePerMillionChars = 15.0
  1754. defaultAudioSTTPricePerHour = 0.10
  1755. )
  1756. // CalculateWebSearchCost 计算 Codex alpha/search 网页搜索按次费用。
  1757. // callCount: 搜索调用次数(每次请求为 1)
  1758. // groupPrice: 分组配置的单次价格(nil 表示使用默认价 0.01;0 表示免费)
  1759. // rateMultiplier: 分组费率倍数
  1760. func (s *BillingService) CalculateWebSearchCost(callCount int, groupPrice *float64, rateMultiplier float64) *CostBreakdown {
  1761. if callCount <= 0 {
  1762. return &CostBreakdown{}
  1763. }
  1764. unitPrice := defaultWebSearchPricePerCall
  1765. if groupPrice != nil && *groupPrice >= 0 {
  1766. unitPrice = *groupPrice
  1767. }
  1768. totalCost := unitPrice * float64(callCount)
  1769. // 应用倍率(保存时强制 > 0;负数按 0 处理避免按 1x 误扣)
  1770. if rateMultiplier < 0 {
  1771. rateMultiplier = 0
  1772. }
  1773. return &CostBreakdown{
  1774. TotalCost: totalCost,
  1775. ActualCost: totalCost * rateMultiplier,
  1776. BillingMode: string(BillingModePerRequest),
  1777. }
  1778. }
  1779. // CalculateSearchCost bills search/tool invocations (e.g. web_search) per 1k calls.
  1780. // groupPricePer1k: nil → defaultSearchPricePer1k; explicit 0 → free; >0 → that rate.
  1781. func (s *BillingService) CalculateSearchCost(numCalls int, groupPricePer1k *float64, rateMultiplier float64) *CostBreakdown {
  1782. if numCalls <= 0 {
  1783. return &CostBreakdown{}
  1784. }
  1785. pricePer1k := defaultSearchPricePer1k
  1786. if groupPricePer1k != nil {
  1787. if *groupPricePer1k < 0 {
  1788. return &CostBreakdown{}
  1789. }
  1790. pricePer1k = *groupPricePer1k
  1791. }
  1792. if pricePer1k == 0 {
  1793. return &CostBreakdown{}
  1794. }
  1795. if rateMultiplier < 0 {
  1796. rateMultiplier = 0
  1797. }
  1798. unit := pricePer1k / 1000.0
  1799. total := unit * float64(numCalls)
  1800. return &CostBreakdown{
  1801. TotalCost: total,
  1802. ActualCost: total * rateMultiplier,
  1803. BillingMode: string(BillingModePerRequest),
  1804. }
  1805. }
  1806. type audioPriceConfig struct {
  1807. RealtimePerMin *float64
  1808. TTSPerMChars *float64
  1809. STTPerHour *float64
  1810. }
  1811. // CalculateAudioCost supports realtime (per min), tts (per M chars), stt (per hr).
  1812. // Missing group prices use defaults; explicit 0 means free for that mode.
  1813. func (s *BillingService) CalculateAudioCost(mode string, durationOrUnits float64, groupConfig *audioPriceConfig, rateMultiplier float64) *CostBreakdown {
  1814. if durationOrUnits <= 0 {
  1815. return &CostBreakdown{}
  1816. }
  1817. var unitPrice float64
  1818. switch strings.ToLower(mode) {
  1819. case "realtime":
  1820. unitPrice = defaultAudioRealtimePricePerMin
  1821. if groupConfig != nil && groupConfig.RealtimePerMin != nil {
  1822. unitPrice = *groupConfig.RealtimePerMin
  1823. }
  1824. case "tts":
  1825. unitPrice = defaultAudioTTSPricePerMillionChars
  1826. if groupConfig != nil && groupConfig.TTSPerMChars != nil {
  1827. unitPrice = *groupConfig.TTSPerMChars
  1828. }
  1829. case "stt":
  1830. unitPrice = defaultAudioSTTPricePerHour
  1831. if groupConfig != nil && groupConfig.STTPerHour != nil {
  1832. unitPrice = *groupConfig.STTPerHour
  1833. }
  1834. default:
  1835. return &CostBreakdown{}
  1836. }
  1837. if unitPrice <= 0 {
  1838. return &CostBreakdown{}
  1839. }
  1840. if rateMultiplier < 0 {
  1841. rateMultiplier = 0
  1842. }
  1843. total := unitPrice * durationOrUnits
  1844. return &CostBreakdown{
  1845. TotalCost: total,
  1846. ActualCost: total * rateMultiplier,
  1847. BillingMode: string(BillingModePerRequest),
  1848. }
  1849. }
  1850. // CalculateImageCost 计算图片生成费用
  1851. // model: 请求的模型名称(用于获取 LiteLLM 默认价格)
  1852. // imageSize: 图片尺寸 "1K", "2K", "4K"
  1853. // imageCount: 生成的图片数量
  1854. // groupConfig: 分组配置的价格(可能为 nil,表示使用默认值)
  1855. // rateMultiplier: 费率倍数
  1856. func (s *BillingService) CalculateImageCost(model string, imageSize string, imageCount int, groupConfig *ImagePriceConfig, rateMultiplier float64) *CostBreakdown {
  1857. if imageCount <= 0 {
  1858. return &CostBreakdown{}
  1859. }
  1860. imageSize = NormalizeImageBillingTierOrDefault(imageSize)
  1861. // 获取单价
  1862. unitPrice := s.getImageUnitPrice(model, imageSize, groupConfig)
  1863. // 计算总费用
  1864. totalCost := unitPrice * float64(imageCount)
  1865. // 应用倍率(保存时强制 > 0;负数按 0 处理避免按 1x 误扣)
  1866. if rateMultiplier < 0 {
  1867. rateMultiplier = 0
  1868. }
  1869. actualCost := totalCost * rateMultiplier
  1870. return &CostBreakdown{
  1871. TotalCost: totalCost,
  1872. ActualCost: actualCost,
  1873. BillingMode: string(BillingModeImage),
  1874. }
  1875. }
  1876. // CalculateVideoCost 计算视频生成费用(按秒计费,与 xAI 口径一致)。
  1877. // model: 请求的模型名称(用于获取默认价格)
  1878. // resolution: 视频分辨率 "480p", "720p", "1080p"
  1879. // videoCount: 生成的视频数量
  1880. // durationSeconds: 单个视频时长(秒),<=0 时按上游默认时长计
  1881. // groupConfig: 分组配置的每秒价格(可能为 nil,表示使用默认值)
  1882. // rateMultiplier: 费率倍数
  1883. func (s *BillingService) CalculateVideoCost(model string, resolution string, videoCount int, durationSeconds int, groupConfig *VideoPriceConfig, rateMultiplier float64) *CostBreakdown {
  1884. if videoCount <= 0 {
  1885. return &CostBreakdown{}
  1886. }
  1887. resolution = NormalizeVideoBillingResolutionOrDefault(resolution)
  1888. durationSeconds = NormalizeVideoBillingDurationSecondsOrDefault(durationSeconds)
  1889. perSecondPrice := s.getVideoUnitPrice(model, resolution, groupConfig)
  1890. totalCost := perSecondPrice * float64(durationSeconds) * float64(videoCount)
  1891. if rateMultiplier < 0 {
  1892. rateMultiplier = 0
  1893. }
  1894. actualCost := totalCost * rateMultiplier
  1895. return &CostBreakdown{
  1896. TotalCost: totalCost,
  1897. ActualCost: actualCost,
  1898. BillingMode: string(BillingModeVideo),
  1899. }
  1900. }
  1901. // getImageUnitPrice 获取图片单价
  1902. func (s *BillingService) getImageUnitPrice(model string, imageSize string, groupConfig *ImagePriceConfig) float64 {
  1903. // 优先使用分组配置的价格
  1904. if groupConfig != nil {
  1905. switch imageSize {
  1906. case "1K":
  1907. if groupConfig.Price1K != nil {
  1908. return *groupConfig.Price1K
  1909. }
  1910. case "2K":
  1911. if groupConfig.Price2K != nil {
  1912. return *groupConfig.Price2K
  1913. }
  1914. case "4K":
  1915. if groupConfig.Price4K != nil {
  1916. return *groupConfig.Price4K
  1917. }
  1918. }
  1919. }
  1920. // 回退到 LiteLLM 默认价格
  1921. return s.getDefaultImagePrice(model, imageSize)
  1922. }
  1923. func (s *BillingService) getVideoUnitPrice(model string, resolution string, groupConfig *VideoPriceConfig) float64 {
  1924. // Order: (a) per-model map (b) flat group video_price_* (c) model-aware code defaults.
  1925. if groupConfig != nil {
  1926. if price := LookupVideoModelPrice(groupConfig.ModelPrices, model, resolution); price != nil {
  1927. return *price
  1928. }
  1929. switch NormalizeVideoBillingResolutionOrDefault(resolution) {
  1930. case VideoBillingResolution480P:
  1931. if groupConfig.Price480P != nil {
  1932. return *groupConfig.Price480P
  1933. }
  1934. case VideoBillingResolution720P:
  1935. if groupConfig.Price720P != nil {
  1936. return *groupConfig.Price720P
  1937. }
  1938. case VideoBillingResolution1080P:
  1939. if groupConfig.Price1080P != nil {
  1940. return *groupConfig.Price1080P
  1941. }
  1942. }
  1943. }
  1944. return s.getDefaultVideoPrice(model, resolution)
  1945. }
  1946. // getDefaultImagePrice 获取 LiteLLM 默认图片价格
  1947. func (s *BillingService) getDefaultImagePrice(model string, imageSize string) float64 {
  1948. if price, ok := getDefaultGrokImagineImagePrice(model, imageSize); ok {
  1949. return price
  1950. }
  1951. basePrice := 0.0
  1952. // 从 PricingService 获取 output_cost_per_image
  1953. if s.pricingService != nil {
  1954. pricing := s.pricingService.GetModelPricing(model)
  1955. if pricing != nil && pricing.OutputCostPerImage > 0 {
  1956. basePrice = pricing.OutputCostPerImage
  1957. }
  1958. }
  1959. // 如果没有找到价格,使用硬编码默认值($0.134,来自 gemini-3-pro-image-preview)
  1960. if basePrice <= 0 {
  1961. basePrice = defaultImageGenerationPrice
  1962. }
  1963. // 2K 尺寸 1.5 倍,4K 尺寸翻倍
  1964. if imageSize == "2K" {
  1965. return basePrice * 1.5
  1966. }
  1967. if imageSize == "4K" {
  1968. return basePrice * 2
  1969. }
  1970. return basePrice
  1971. }
  1972. func (s *BillingService) getDefaultVideoPrice(model string, resolution string) float64 {
  1973. if price, ok := getDefaultGrokImagineVideoPrice(model, resolution); ok {
  1974. return price
  1975. }
  1976. // The bundled LiteLLM schema does not expose an output video generation price.
  1977. // Keep the historical model default as the fallback (interpreted as a per-second
  1978. // rate; today only Grok models reach video billing, so this path is a safety net),
  1979. // while letting group-level video prices override it independently from image prices.
  1980. return s.getDefaultImagePrice(model, ImageBillingSize2K)
  1981. }
  1982. func getDefaultGrokImagineImagePrice(model string, imageSize string) (float64, bool) {
  1983. model = strings.ToLower(strings.TrimSpace(model))
  1984. switch model {
  1985. case "grok-imagine-image-2.0":
  1986. return getGrokImagineImageTierPrice(
  1987. imageSize,
  1988. defaultGrokImagineImage20Price1K,
  1989. defaultGrokImagineImage20Price2K,
  1990. ), true
  1991. case "grok-imagine-image-quality":
  1992. return getGrokImagineImageTierPrice(
  1993. imageSize,
  1994. defaultGrokImagineImageQualityPrice1K,
  1995. defaultGrokImagineImageQualityPrice2K,
  1996. ), true
  1997. case "grok-imagine", "grok-imagine-image", "grok-imagine-edit":
  1998. return getGrokImagineImageTierPrice(
  1999. imageSize,
  2000. defaultGrokImagineImagePrice1K,
  2001. defaultGrokImagineImagePrice2K,
  2002. ), true
  2003. default:
  2004. return 0, false
  2005. }
  2006. }
  2007. func getGrokImagineImageTierPrice(imageSize string, price1K float64, price2K float64) float64 {
  2008. switch NormalizeImageBillingTierOrDefault(imageSize) {
  2009. case ImageBillingSize1K:
  2010. return price1K
  2011. case ImageBillingSize2K, ImageBillingSize4K:
  2012. return price2K
  2013. default:
  2014. return price2K
  2015. }
  2016. }
  2017. func getDefaultGrokImagineVideoPrice(model string, resolution string) (float64, bool) {
  2018. model = strings.ToLower(strings.TrimSpace(model))
  2019. switch {
  2020. case strings.HasPrefix(model, "grok-imagine-video-1.5"):
  2021. switch NormalizeVideoBillingResolutionOrDefault(resolution) {
  2022. case VideoBillingResolution480P:
  2023. return defaultGrokImagineVideo15Price480P, true
  2024. case VideoBillingResolution720P:
  2025. return defaultGrokImagineVideo15Price720P, true
  2026. case VideoBillingResolution1080P:
  2027. return defaultGrokImagineVideo15Price1080P, true
  2028. default:
  2029. return defaultGrokImagineVideo15Price480P, true
  2030. }
  2031. case strings.HasPrefix(model, "grok-imagine-video"):
  2032. switch NormalizeVideoBillingResolutionOrDefault(resolution) {
  2033. case VideoBillingResolution480P:
  2034. return defaultGrokImagineVideoPrice480P, true
  2035. case VideoBillingResolution720P, VideoBillingResolution1080P:
  2036. return defaultGrokImagineVideoPrice720P, true
  2037. default:
  2038. return defaultGrokImagineVideoPrice480P, true
  2039. }
  2040. default:
  2041. return 0, false
  2042. }
  2043. }