1
0

service_usage.go 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202
  1. package ocm
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "math"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/sagernet/sing-box/log"
  12. E "github.com/sagernet/sing/common/exceptions"
  13. )
  14. type UsageStats struct {
  15. RequestCount int `json:"request_count"`
  16. InputTokens int64 `json:"input_tokens"`
  17. OutputTokens int64 `json:"output_tokens"`
  18. CachedTokens int64 `json:"cached_tokens"`
  19. }
  20. func (u *UsageStats) UnmarshalJSON(data []byte) error {
  21. type Alias UsageStats
  22. aux := &struct {
  23. *Alias
  24. PromptTokens int64 `json:"prompt_tokens"`
  25. CompletionTokens int64 `json:"completion_tokens"`
  26. }{
  27. Alias: (*Alias)(u),
  28. }
  29. err := json.Unmarshal(data, aux)
  30. if err != nil {
  31. return err
  32. }
  33. if u.InputTokens == 0 && aux.PromptTokens > 0 {
  34. u.InputTokens = aux.PromptTokens
  35. }
  36. if u.OutputTokens == 0 && aux.CompletionTokens > 0 {
  37. u.OutputTokens = aux.CompletionTokens
  38. }
  39. return nil
  40. }
  41. type CostCombination struct {
  42. Model string `json:"model"`
  43. ServiceTier string `json:"service_tier,omitempty"`
  44. ContextWindow int `json:"context_window"`
  45. WeekStartUnix int64 `json:"week_start_unix,omitempty"`
  46. Total UsageStats `json:"total"`
  47. ByUser map[string]UsageStats `json:"by_user"`
  48. }
  49. type AggregatedUsage struct {
  50. LastUpdated time.Time `json:"last_updated"`
  51. Combinations []CostCombination `json:"combinations"`
  52. mutex sync.Mutex
  53. filePath string
  54. logger log.ContextLogger
  55. lastSaveTime time.Time
  56. pendingSave bool
  57. saveTimer *time.Timer
  58. saveMutex sync.Mutex
  59. }
  60. type UsageStatsJSON struct {
  61. RequestCount int `json:"request_count"`
  62. InputTokens int64 `json:"input_tokens"`
  63. OutputTokens int64 `json:"output_tokens"`
  64. CachedTokens int64 `json:"cached_tokens"`
  65. CostUSD float64 `json:"cost_usd"`
  66. }
  67. type CostCombinationJSON struct {
  68. Model string `json:"model"`
  69. ServiceTier string `json:"service_tier,omitempty"`
  70. ContextWindow int `json:"context_window"`
  71. WeekStartUnix int64 `json:"week_start_unix,omitempty"`
  72. Total UsageStatsJSON `json:"total"`
  73. ByUser map[string]UsageStatsJSON `json:"by_user"`
  74. }
  75. type CostsSummaryJSON struct {
  76. TotalUSD float64 `json:"total_usd"`
  77. ByUser map[string]float64 `json:"by_user"`
  78. ByWeek map[string]float64 `json:"by_week,omitempty"`
  79. ByUserAndWeek map[string]map[string]float64 `json:"by_user_and_week,omitempty"`
  80. }
  81. type AggregatedUsageJSON struct {
  82. LastUpdated time.Time `json:"last_updated"`
  83. Costs CostsSummaryJSON `json:"costs"`
  84. Combinations []CostCombinationJSON `json:"combinations"`
  85. }
  86. type WeeklyCycleHint struct {
  87. WindowMinutes int64
  88. ResetAt time.Time
  89. }
  90. type ModelPricing struct {
  91. InputPrice float64
  92. OutputPrice float64
  93. CachedInputPrice float64
  94. }
  95. type modelFamily struct {
  96. pattern *regexp.Regexp
  97. pricing ModelPricing
  98. premiumPricing *ModelPricing
  99. }
  100. const (
  101. serviceTierAuto = "auto"
  102. serviceTierDefault = "default"
  103. serviceTierFlex = "flex"
  104. serviceTierPriority = "priority"
  105. serviceTierScale = "scale"
  106. )
  107. const (
  108. contextWindowStandard = 272000
  109. contextWindowPremium = 1050000
  110. premiumContextThreshold = 272000
  111. )
  112. var (
  113. gpt52Pricing = ModelPricing{
  114. InputPrice: 1.75,
  115. OutputPrice: 14.0,
  116. CachedInputPrice: 0.175,
  117. }
  118. gpt5Pricing = ModelPricing{
  119. InputPrice: 1.25,
  120. OutputPrice: 10.0,
  121. CachedInputPrice: 0.125,
  122. }
  123. gpt5MiniPricing = ModelPricing{
  124. InputPrice: 0.25,
  125. OutputPrice: 2.0,
  126. CachedInputPrice: 0.025,
  127. }
  128. gpt5NanoPricing = ModelPricing{
  129. InputPrice: 0.05,
  130. OutputPrice: 0.4,
  131. CachedInputPrice: 0.005,
  132. }
  133. gpt52CodexPricing = ModelPricing{
  134. InputPrice: 1.75,
  135. OutputPrice: 14.0,
  136. CachedInputPrice: 0.175,
  137. }
  138. gpt51CodexPricing = ModelPricing{
  139. InputPrice: 1.25,
  140. OutputPrice: 10.0,
  141. CachedInputPrice: 0.125,
  142. }
  143. gpt51CodexMiniPricing = ModelPricing{
  144. InputPrice: 0.25,
  145. OutputPrice: 2.0,
  146. CachedInputPrice: 0.025,
  147. }
  148. gpt54StandardPricing = ModelPricing{
  149. InputPrice: 2.5,
  150. OutputPrice: 15.0,
  151. CachedInputPrice: 0.25,
  152. }
  153. gpt54PremiumPricing = ModelPricing{
  154. InputPrice: 5.0,
  155. OutputPrice: 22.5,
  156. CachedInputPrice: 0.5,
  157. }
  158. gpt54ProPricing = ModelPricing{
  159. InputPrice: 30.0,
  160. OutputPrice: 180.0,
  161. CachedInputPrice: 30.0,
  162. }
  163. gpt54ProPremiumPricing = ModelPricing{
  164. InputPrice: 60.0,
  165. OutputPrice: 270.0,
  166. CachedInputPrice: 60.0,
  167. }
  168. gpt52ProPricing = ModelPricing{
  169. InputPrice: 21.0,
  170. OutputPrice: 168.0,
  171. CachedInputPrice: 21.0,
  172. }
  173. gpt5ProPricing = ModelPricing{
  174. InputPrice: 15.0,
  175. OutputPrice: 120.0,
  176. CachedInputPrice: 15.0,
  177. }
  178. gpt54FlexPricing = ModelPricing{
  179. InputPrice: 1.25,
  180. OutputPrice: 7.5,
  181. CachedInputPrice: 0.125,
  182. }
  183. gpt54PremiumFlexPricing = ModelPricing{
  184. InputPrice: 2.5,
  185. OutputPrice: 11.25,
  186. CachedInputPrice: 0.25,
  187. }
  188. gpt54ProFlexPricing = ModelPricing{
  189. InputPrice: 15.0,
  190. OutputPrice: 90.0,
  191. CachedInputPrice: 15.0,
  192. }
  193. gpt54ProPremiumFlexPricing = ModelPricing{
  194. InputPrice: 30.0,
  195. OutputPrice: 135.0,
  196. CachedInputPrice: 30.0,
  197. }
  198. gpt52FlexPricing = ModelPricing{
  199. InputPrice: 0.875,
  200. OutputPrice: 7.0,
  201. CachedInputPrice: 0.0875,
  202. }
  203. gpt5FlexPricing = ModelPricing{
  204. InputPrice: 0.625,
  205. OutputPrice: 5.0,
  206. CachedInputPrice: 0.0625,
  207. }
  208. gpt5MiniFlexPricing = ModelPricing{
  209. InputPrice: 0.125,
  210. OutputPrice: 1.0,
  211. CachedInputPrice: 0.0125,
  212. }
  213. gpt5NanoFlexPricing = ModelPricing{
  214. InputPrice: 0.025,
  215. OutputPrice: 0.2,
  216. CachedInputPrice: 0.0025,
  217. }
  218. gpt54PriorityPricing = ModelPricing{
  219. InputPrice: 5.0,
  220. OutputPrice: 30.0,
  221. CachedInputPrice: 0.5,
  222. }
  223. gpt54PremiumPriorityPricing = ModelPricing{
  224. InputPrice: 10.0,
  225. OutputPrice: 45.0,
  226. CachedInputPrice: 1.0,
  227. }
  228. gpt52PriorityPricing = ModelPricing{
  229. InputPrice: 3.5,
  230. OutputPrice: 28.0,
  231. CachedInputPrice: 0.35,
  232. }
  233. gpt5PriorityPricing = ModelPricing{
  234. InputPrice: 2.5,
  235. OutputPrice: 20.0,
  236. CachedInputPrice: 0.25,
  237. }
  238. gpt5MiniPriorityPricing = ModelPricing{
  239. InputPrice: 0.45,
  240. OutputPrice: 3.6,
  241. CachedInputPrice: 0.045,
  242. }
  243. gpt52CodexPriorityPricing = ModelPricing{
  244. InputPrice: 3.5,
  245. OutputPrice: 28.0,
  246. CachedInputPrice: 0.35,
  247. }
  248. gpt51CodexPriorityPricing = ModelPricing{
  249. InputPrice: 2.5,
  250. OutputPrice: 20.0,
  251. CachedInputPrice: 0.25,
  252. }
  253. gpt4oPricing = ModelPricing{
  254. InputPrice: 2.5,
  255. OutputPrice: 10.0,
  256. CachedInputPrice: 1.25,
  257. }
  258. gpt4oMiniPricing = ModelPricing{
  259. InputPrice: 0.15,
  260. OutputPrice: 0.6,
  261. CachedInputPrice: 0.075,
  262. }
  263. gpt4oAudioPricing = ModelPricing{
  264. InputPrice: 2.5,
  265. OutputPrice: 10.0,
  266. CachedInputPrice: 2.5,
  267. }
  268. gpt4oMiniAudioPricing = ModelPricing{
  269. InputPrice: 0.15,
  270. OutputPrice: 0.6,
  271. CachedInputPrice: 0.15,
  272. }
  273. gptAudioMiniPricing = ModelPricing{
  274. InputPrice: 0.6,
  275. OutputPrice: 2.4,
  276. CachedInputPrice: 0.6,
  277. }
  278. o1Pricing = ModelPricing{
  279. InputPrice: 15.0,
  280. OutputPrice: 60.0,
  281. CachedInputPrice: 7.5,
  282. }
  283. o1ProPricing = ModelPricing{
  284. InputPrice: 150.0,
  285. OutputPrice: 600.0,
  286. CachedInputPrice: 150.0,
  287. }
  288. o1MiniPricing = ModelPricing{
  289. InputPrice: 1.1,
  290. OutputPrice: 4.4,
  291. CachedInputPrice: 0.55,
  292. }
  293. o3MiniPricing = ModelPricing{
  294. InputPrice: 1.1,
  295. OutputPrice: 4.4,
  296. CachedInputPrice: 0.55,
  297. }
  298. o3Pricing = ModelPricing{
  299. InputPrice: 2.0,
  300. OutputPrice: 8.0,
  301. CachedInputPrice: 0.5,
  302. }
  303. o3ProPricing = ModelPricing{
  304. InputPrice: 20.0,
  305. OutputPrice: 80.0,
  306. CachedInputPrice: 20.0,
  307. }
  308. o3DeepResearchPricing = ModelPricing{
  309. InputPrice: 10.0,
  310. OutputPrice: 40.0,
  311. CachedInputPrice: 2.5,
  312. }
  313. o4MiniPricing = ModelPricing{
  314. InputPrice: 1.1,
  315. OutputPrice: 4.4,
  316. CachedInputPrice: 0.275,
  317. }
  318. o4MiniDeepResearchPricing = ModelPricing{
  319. InputPrice: 2.0,
  320. OutputPrice: 8.0,
  321. CachedInputPrice: 0.5,
  322. }
  323. o3FlexPricing = ModelPricing{
  324. InputPrice: 1.0,
  325. OutputPrice: 4.0,
  326. CachedInputPrice: 0.25,
  327. }
  328. o4MiniFlexPricing = ModelPricing{
  329. InputPrice: 0.55,
  330. OutputPrice: 2.2,
  331. CachedInputPrice: 0.138,
  332. }
  333. o3PriorityPricing = ModelPricing{
  334. InputPrice: 3.5,
  335. OutputPrice: 14.0,
  336. CachedInputPrice: 0.875,
  337. }
  338. o4MiniPriorityPricing = ModelPricing{
  339. InputPrice: 2.0,
  340. OutputPrice: 8.0,
  341. CachedInputPrice: 0.5,
  342. }
  343. gpt41Pricing = ModelPricing{
  344. InputPrice: 2.0,
  345. OutputPrice: 8.0,
  346. CachedInputPrice: 0.5,
  347. }
  348. gpt41MiniPricing = ModelPricing{
  349. InputPrice: 0.4,
  350. OutputPrice: 1.6,
  351. CachedInputPrice: 0.1,
  352. }
  353. gpt41NanoPricing = ModelPricing{
  354. InputPrice: 0.1,
  355. OutputPrice: 0.4,
  356. CachedInputPrice: 0.025,
  357. }
  358. gpt41PriorityPricing = ModelPricing{
  359. InputPrice: 3.5,
  360. OutputPrice: 14.0,
  361. CachedInputPrice: 0.875,
  362. }
  363. gpt41MiniPriorityPricing = ModelPricing{
  364. InputPrice: 0.7,
  365. OutputPrice: 2.8,
  366. CachedInputPrice: 0.175,
  367. }
  368. gpt41NanoPriorityPricing = ModelPricing{
  369. InputPrice: 0.2,
  370. OutputPrice: 0.8,
  371. CachedInputPrice: 0.05,
  372. }
  373. gpt4oPriorityPricing = ModelPricing{
  374. InputPrice: 4.25,
  375. OutputPrice: 17.0,
  376. CachedInputPrice: 2.125,
  377. }
  378. gpt4oMiniPriorityPricing = ModelPricing{
  379. InputPrice: 0.25,
  380. OutputPrice: 1.0,
  381. CachedInputPrice: 0.125,
  382. }
  383. standardModelFamilies = []modelFamily{
  384. {
  385. pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
  386. pricing: gpt54ProPricing,
  387. premiumPricing: &gpt54ProPremiumPricing,
  388. },
  389. {
  390. pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
  391. pricing: gpt54StandardPricing,
  392. premiumPricing: &gpt54PremiumPricing,
  393. },
  394. {
  395. pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
  396. pricing: gpt52CodexPricing,
  397. },
  398. {
  399. pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
  400. pricing: gpt52CodexPricing,
  401. },
  402. {
  403. pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
  404. pricing: gpt51CodexPricing,
  405. },
  406. {
  407. pattern: regexp.MustCompile(`^gpt-5\.1-codex-mini(?:$|-)`),
  408. pricing: gpt51CodexMiniPricing,
  409. },
  410. {
  411. pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
  412. pricing: gpt51CodexPricing,
  413. },
  414. {
  415. pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`),
  416. pricing: gpt51CodexMiniPricing,
  417. },
  418. {
  419. pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
  420. pricing: gpt51CodexPricing,
  421. },
  422. {
  423. pattern: regexp.MustCompile(`^gpt-5\.2-chat-latest$`),
  424. pricing: gpt52Pricing,
  425. },
  426. {
  427. pattern: regexp.MustCompile(`^gpt-5\.1-chat-latest$`),
  428. pricing: gpt5Pricing,
  429. },
  430. {
  431. pattern: regexp.MustCompile(`^gpt-5-chat-latest$`),
  432. pricing: gpt5Pricing,
  433. },
  434. {
  435. pattern: regexp.MustCompile(`^gpt-5\.2-pro(?:$|-)`),
  436. pricing: gpt52ProPricing,
  437. },
  438. {
  439. pattern: regexp.MustCompile(`^gpt-5-pro(?:$|-)`),
  440. pricing: gpt5ProPricing,
  441. },
  442. {
  443. pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
  444. pricing: gpt5MiniPricing,
  445. },
  446. {
  447. pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
  448. pricing: gpt5NanoPricing,
  449. },
  450. {
  451. pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
  452. pricing: gpt52Pricing,
  453. },
  454. {
  455. pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
  456. pricing: gpt5Pricing,
  457. },
  458. {
  459. pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
  460. pricing: gpt5Pricing,
  461. },
  462. {
  463. pattern: regexp.MustCompile(`^o4-mini-deep-research(?:$|-)`),
  464. pricing: o4MiniDeepResearchPricing,
  465. },
  466. {
  467. pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
  468. pricing: o4MiniPricing,
  469. },
  470. {
  471. pattern: regexp.MustCompile(`^o3-pro(?:$|-)`),
  472. pricing: o3ProPricing,
  473. },
  474. {
  475. pattern: regexp.MustCompile(`^o3-deep-research(?:$|-)`),
  476. pricing: o3DeepResearchPricing,
  477. },
  478. {
  479. pattern: regexp.MustCompile(`^o3-mini(?:$|-)`),
  480. pricing: o3MiniPricing,
  481. },
  482. {
  483. pattern: regexp.MustCompile(`^o3(?:$|-)`),
  484. pricing: o3Pricing,
  485. },
  486. {
  487. pattern: regexp.MustCompile(`^o1-pro(?:$|-)`),
  488. pricing: o1ProPricing,
  489. },
  490. {
  491. pattern: regexp.MustCompile(`^o1-mini(?:$|-)`),
  492. pricing: o1MiniPricing,
  493. },
  494. {
  495. pattern: regexp.MustCompile(`^o1(?:$|-)`),
  496. pricing: o1Pricing,
  497. },
  498. {
  499. pattern: regexp.MustCompile(`^gpt-4o-mini-audio(?:$|-)`),
  500. pricing: gpt4oMiniAudioPricing,
  501. },
  502. {
  503. pattern: regexp.MustCompile(`^gpt-audio-mini(?:$|-)`),
  504. pricing: gptAudioMiniPricing,
  505. },
  506. {
  507. pattern: regexp.MustCompile(`^(?:gpt-4o-audio|gpt-audio)(?:$|-)`),
  508. pricing: gpt4oAudioPricing,
  509. },
  510. {
  511. pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
  512. pricing: gpt41NanoPricing,
  513. },
  514. {
  515. pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
  516. pricing: gpt41MiniPricing,
  517. },
  518. {
  519. pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
  520. pricing: gpt41Pricing,
  521. },
  522. {
  523. pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
  524. pricing: gpt4oMiniPricing,
  525. },
  526. {
  527. pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
  528. pricing: gpt4oPricing,
  529. },
  530. {
  531. pattern: regexp.MustCompile(`^chatgpt-4o(?:$|-)`),
  532. pricing: gpt4oPricing,
  533. },
  534. }
  535. flexModelFamilies = []modelFamily{
  536. {
  537. pattern: regexp.MustCompile(`^gpt-5\.4-pro(?:$|-)`),
  538. pricing: gpt54ProFlexPricing,
  539. premiumPricing: &gpt54ProPremiumFlexPricing,
  540. },
  541. {
  542. pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
  543. pricing: gpt54FlexPricing,
  544. premiumPricing: &gpt54PremiumFlexPricing,
  545. },
  546. {
  547. pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
  548. pricing: gpt5MiniFlexPricing,
  549. },
  550. {
  551. pattern: regexp.MustCompile(`^gpt-5-nano(?:$|-)`),
  552. pricing: gpt5NanoFlexPricing,
  553. },
  554. {
  555. pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
  556. pricing: gpt52FlexPricing,
  557. },
  558. {
  559. pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
  560. pricing: gpt5FlexPricing,
  561. },
  562. {
  563. pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
  564. pricing: gpt5FlexPricing,
  565. },
  566. {
  567. pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
  568. pricing: o4MiniFlexPricing,
  569. },
  570. {
  571. pattern: regexp.MustCompile(`^o3(?:$|-)`),
  572. pricing: o3FlexPricing,
  573. },
  574. }
  575. priorityModelFamilies = []modelFamily{
  576. {
  577. pattern: regexp.MustCompile(`^gpt-5\.4(?:$|-)`),
  578. pricing: gpt54PriorityPricing,
  579. premiumPricing: &gpt54PremiumPriorityPricing,
  580. },
  581. {
  582. pattern: regexp.MustCompile(`^gpt-5\.3-codex(?:$|-)`),
  583. pricing: gpt52CodexPriorityPricing,
  584. },
  585. {
  586. pattern: regexp.MustCompile(`^gpt-5\.2-codex(?:$|-)`),
  587. pricing: gpt52CodexPriorityPricing,
  588. },
  589. {
  590. pattern: regexp.MustCompile(`^gpt-5\.1-codex-max(?:$|-)`),
  591. pricing: gpt51CodexPriorityPricing,
  592. },
  593. {
  594. pattern: regexp.MustCompile(`^gpt-5\.1-codex(?:$|-)`),
  595. pricing: gpt51CodexPriorityPricing,
  596. },
  597. {
  598. pattern: regexp.MustCompile(`^gpt-5-codex-mini(?:$|-)`),
  599. pricing: gpt5MiniPriorityPricing,
  600. },
  601. {
  602. pattern: regexp.MustCompile(`^gpt-5-codex(?:$|-)`),
  603. pricing: gpt51CodexPriorityPricing,
  604. },
  605. {
  606. pattern: regexp.MustCompile(`^gpt-5-mini(?:$|-)`),
  607. pricing: gpt5MiniPriorityPricing,
  608. },
  609. {
  610. pattern: regexp.MustCompile(`^gpt-5\.2(?:$|-)`),
  611. pricing: gpt52PriorityPricing,
  612. },
  613. {
  614. pattern: regexp.MustCompile(`^gpt-5\.1(?:$|-)`),
  615. pricing: gpt5PriorityPricing,
  616. },
  617. {
  618. pattern: regexp.MustCompile(`^gpt-5(?:$|-)`),
  619. pricing: gpt5PriorityPricing,
  620. },
  621. {
  622. pattern: regexp.MustCompile(`^o4-mini(?:$|-)`),
  623. pricing: o4MiniPriorityPricing,
  624. },
  625. {
  626. pattern: regexp.MustCompile(`^o3(?:$|-)`),
  627. pricing: o3PriorityPricing,
  628. },
  629. {
  630. pattern: regexp.MustCompile(`^gpt-4\.1-nano(?:$|-)`),
  631. pricing: gpt41NanoPriorityPricing,
  632. },
  633. {
  634. pattern: regexp.MustCompile(`^gpt-4\.1-mini(?:$|-)`),
  635. pricing: gpt41MiniPriorityPricing,
  636. },
  637. {
  638. pattern: regexp.MustCompile(`^gpt-4\.1(?:$|-)`),
  639. pricing: gpt41PriorityPricing,
  640. },
  641. {
  642. pattern: regexp.MustCompile(`^gpt-4o-mini(?:$|-)`),
  643. pricing: gpt4oMiniPriorityPricing,
  644. },
  645. {
  646. pattern: regexp.MustCompile(`^gpt-4o(?:$|-)`),
  647. pricing: gpt4oPriorityPricing,
  648. },
  649. }
  650. )
  651. func modelFamiliesForTier(serviceTier string) []modelFamily {
  652. switch serviceTier {
  653. case serviceTierFlex:
  654. return flexModelFamilies
  655. case serviceTierPriority:
  656. return priorityModelFamilies
  657. default:
  658. return standardModelFamilies
  659. }
  660. }
  661. func findPricingInFamilies(model string, contextWindow int, modelFamilies []modelFamily) (ModelPricing, bool) {
  662. isPremium := contextWindow >= contextWindowPremium
  663. for _, family := range modelFamilies {
  664. if family.pattern.MatchString(model) {
  665. if isPremium && family.premiumPricing != nil {
  666. return *family.premiumPricing, true
  667. }
  668. return family.pricing, true
  669. }
  670. }
  671. return ModelPricing{}, false
  672. }
  673. func hasPremiumPricingInFamilies(model string, modelFamilies []modelFamily) bool {
  674. for _, family := range modelFamilies {
  675. if family.pattern.MatchString(model) {
  676. return family.premiumPricing != nil
  677. }
  678. }
  679. return false
  680. }
  681. func normalizeServiceTier(serviceTier string) string {
  682. switch strings.ToLower(strings.TrimSpace(serviceTier)) {
  683. case "", serviceTierAuto, serviceTierDefault:
  684. return serviceTierDefault
  685. case serviceTierFlex:
  686. return serviceTierFlex
  687. case serviceTierPriority:
  688. return serviceTierPriority
  689. case serviceTierScale:
  690. // Scale-tier requests are prepaid differently and not listed in this usage file.
  691. return serviceTierDefault
  692. default:
  693. return serviceTierDefault
  694. }
  695. }
  696. func getPricing(model string, serviceTier string, contextWindow int) ModelPricing {
  697. normalizedServiceTier := normalizeServiceTier(serviceTier)
  698. families := modelFamiliesForTier(normalizedServiceTier)
  699. if pricing, found := findPricingInFamilies(model, contextWindow, families); found {
  700. return pricing
  701. }
  702. normalizedModel := normalizeGPT5Model(model)
  703. if normalizedModel != model {
  704. if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, families); found {
  705. return pricing
  706. }
  707. }
  708. if normalizedServiceTier != serviceTierDefault {
  709. if pricing, found := findPricingInFamilies(model, contextWindow, standardModelFamilies); found {
  710. return pricing
  711. }
  712. if normalizedModel != model {
  713. if pricing, found := findPricingInFamilies(normalizedModel, contextWindow, standardModelFamilies); found {
  714. return pricing
  715. }
  716. }
  717. }
  718. return gpt4oPricing
  719. }
  720. func detectContextWindow(model string, serviceTier string, inputTokens int64) int {
  721. if inputTokens <= premiumContextThreshold {
  722. return contextWindowStandard
  723. }
  724. normalizedServiceTier := normalizeServiceTier(serviceTier)
  725. families := modelFamiliesForTier(normalizedServiceTier)
  726. if hasPremiumPricingInFamilies(model, families) {
  727. return contextWindowPremium
  728. }
  729. normalizedModel := normalizeGPT5Model(model)
  730. if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, families) {
  731. return contextWindowPremium
  732. }
  733. if normalizedServiceTier != serviceTierDefault {
  734. if hasPremiumPricingInFamilies(model, standardModelFamilies) {
  735. return contextWindowPremium
  736. }
  737. if normalizedModel != model && hasPremiumPricingInFamilies(normalizedModel, standardModelFamilies) {
  738. return contextWindowPremium
  739. }
  740. }
  741. return contextWindowStandard
  742. }
  743. func normalizeGPT5Model(model string) string {
  744. if !strings.HasPrefix(model, "gpt-5.") {
  745. return model
  746. }
  747. switch {
  748. case strings.Contains(model, "-codex-mini"):
  749. return "gpt-5.1-codex-mini"
  750. case strings.Contains(model, "-codex-max"):
  751. return "gpt-5.1-codex-max"
  752. case strings.Contains(model, "-codex"):
  753. return "gpt-5.3-codex"
  754. case strings.Contains(model, "-chat-latest"):
  755. return "gpt-5.2-chat-latest"
  756. case strings.Contains(model, "-pro"):
  757. return "gpt-5.4-pro"
  758. case strings.Contains(model, "-mini"):
  759. return "gpt-5-mini"
  760. case strings.Contains(model, "-nano"):
  761. return "gpt-5-nano"
  762. default:
  763. return "gpt-5.4"
  764. }
  765. }
  766. func calculateCost(stats UsageStats, model string, serviceTier string, contextWindow int) float64 {
  767. pricing := getPricing(model, serviceTier, contextWindow)
  768. regularInputTokens := stats.InputTokens - stats.CachedTokens
  769. if regularInputTokens < 0 {
  770. regularInputTokens = 0
  771. }
  772. cost := (float64(regularInputTokens)*pricing.InputPrice +
  773. float64(stats.OutputTokens)*pricing.OutputPrice +
  774. float64(stats.CachedTokens)*pricing.CachedInputPrice) / 1_000_000
  775. return math.Round(cost*100) / 100
  776. }
  777. func roundCost(cost float64) float64 {
  778. return math.Round(cost*100) / 100
  779. }
  780. func normalizeCombinations(combinations []CostCombination) {
  781. for index := range combinations {
  782. combinations[index].ServiceTier = normalizeServiceTier(combinations[index].ServiceTier)
  783. if combinations[index].ContextWindow <= 0 {
  784. combinations[index].ContextWindow = contextWindowStandard
  785. }
  786. if combinations[index].ByUser == nil {
  787. combinations[index].ByUser = make(map[string]UsageStats)
  788. }
  789. }
  790. }
  791. func addUsageToCombinations(combinations *[]CostCombination, model string, serviceTier string, contextWindow int, weekStartUnix int64, user string, inputTokens, outputTokens, cachedTokens int64) {
  792. var matchedCombination *CostCombination
  793. for index := range *combinations {
  794. combination := &(*combinations)[index]
  795. combinationServiceTier := normalizeServiceTier(combination.ServiceTier)
  796. if combination.ServiceTier != combinationServiceTier {
  797. combination.ServiceTier = combinationServiceTier
  798. }
  799. if combination.Model == model && combinationServiceTier == serviceTier && combination.ContextWindow == contextWindow && combination.WeekStartUnix == weekStartUnix {
  800. matchedCombination = combination
  801. break
  802. }
  803. }
  804. if matchedCombination == nil {
  805. newCombination := CostCombination{
  806. Model: model,
  807. ServiceTier: serviceTier,
  808. ContextWindow: contextWindow,
  809. WeekStartUnix: weekStartUnix,
  810. Total: UsageStats{},
  811. ByUser: make(map[string]UsageStats),
  812. }
  813. *combinations = append(*combinations, newCombination)
  814. matchedCombination = &(*combinations)[len(*combinations)-1]
  815. }
  816. matchedCombination.Total.RequestCount++
  817. matchedCombination.Total.InputTokens += inputTokens
  818. matchedCombination.Total.OutputTokens += outputTokens
  819. matchedCombination.Total.CachedTokens += cachedTokens
  820. if user != "" {
  821. userStats := matchedCombination.ByUser[user]
  822. userStats.RequestCount++
  823. userStats.InputTokens += inputTokens
  824. userStats.OutputTokens += outputTokens
  825. userStats.CachedTokens += cachedTokens
  826. matchedCombination.ByUser[user] = userStats
  827. }
  828. }
  829. func buildCombinationJSON(combinations []CostCombination, aggregateUserCosts map[string]float64) ([]CostCombinationJSON, float64) {
  830. result := make([]CostCombinationJSON, len(combinations))
  831. var totalCost float64
  832. for index, combination := range combinations {
  833. combinationTotalCost := calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
  834. totalCost += combinationTotalCost
  835. combinationJSON := CostCombinationJSON{
  836. Model: combination.Model,
  837. ServiceTier: combination.ServiceTier,
  838. ContextWindow: combination.ContextWindow,
  839. WeekStartUnix: combination.WeekStartUnix,
  840. Total: UsageStatsJSON{
  841. RequestCount: combination.Total.RequestCount,
  842. InputTokens: combination.Total.InputTokens,
  843. OutputTokens: combination.Total.OutputTokens,
  844. CachedTokens: combination.Total.CachedTokens,
  845. CostUSD: combinationTotalCost,
  846. },
  847. ByUser: make(map[string]UsageStatsJSON),
  848. }
  849. for user, userStats := range combination.ByUser {
  850. userCost := calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
  851. if aggregateUserCosts != nil {
  852. aggregateUserCosts[user] += userCost
  853. }
  854. combinationJSON.ByUser[user] = UsageStatsJSON{
  855. RequestCount: userStats.RequestCount,
  856. InputTokens: userStats.InputTokens,
  857. OutputTokens: userStats.OutputTokens,
  858. CachedTokens: userStats.CachedTokens,
  859. CostUSD: userCost,
  860. }
  861. }
  862. result[index] = combinationJSON
  863. }
  864. return result, roundCost(totalCost)
  865. }
  866. func formatUTCOffsetLabel(timestamp time.Time) string {
  867. _, offsetSeconds := timestamp.Zone()
  868. sign := "+"
  869. if offsetSeconds < 0 {
  870. sign = "-"
  871. offsetSeconds = -offsetSeconds
  872. }
  873. offsetHours := offsetSeconds / 3600
  874. offsetMinutes := (offsetSeconds % 3600) / 60
  875. if offsetMinutes == 0 {
  876. return fmt.Sprintf("UTC%s%d", sign, offsetHours)
  877. }
  878. return fmt.Sprintf("UTC%s%d:%02d", sign, offsetHours, offsetMinutes)
  879. }
  880. func formatWeekStartKey(cycleStartAt time.Time) string {
  881. localCycleStart := cycleStartAt.In(time.Local)
  882. return fmt.Sprintf("%s %s", localCycleStart.Format("2006-01-02 15:04:05"), formatUTCOffsetLabel(localCycleStart))
  883. }
  884. func buildByWeekCost(combinations []CostCombination) map[string]float64 {
  885. byWeek := make(map[string]float64)
  886. for _, combination := range combinations {
  887. if combination.WeekStartUnix <= 0 {
  888. continue
  889. }
  890. weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
  891. weekKey := formatWeekStartKey(weekStartAt)
  892. byWeek[weekKey] += calculateCost(combination.Total, combination.Model, combination.ServiceTier, combination.ContextWindow)
  893. }
  894. for weekKey, weekCost := range byWeek {
  895. byWeek[weekKey] = roundCost(weekCost)
  896. }
  897. return byWeek
  898. }
  899. func buildByUserAndWeekCost(combinations []CostCombination) map[string]map[string]float64 {
  900. byUserAndWeek := make(map[string]map[string]float64)
  901. for _, combination := range combinations {
  902. if combination.WeekStartUnix <= 0 {
  903. continue
  904. }
  905. weekStartAt := time.Unix(combination.WeekStartUnix, 0).UTC()
  906. weekKey := formatWeekStartKey(weekStartAt)
  907. for user, userStats := range combination.ByUser {
  908. userWeeks, exists := byUserAndWeek[user]
  909. if !exists {
  910. userWeeks = make(map[string]float64)
  911. byUserAndWeek[user] = userWeeks
  912. }
  913. userWeeks[weekKey] += calculateCost(userStats, combination.Model, combination.ServiceTier, combination.ContextWindow)
  914. }
  915. }
  916. for _, weekCosts := range byUserAndWeek {
  917. for weekKey, cost := range weekCosts {
  918. weekCosts[weekKey] = roundCost(cost)
  919. }
  920. }
  921. return byUserAndWeek
  922. }
  923. func deriveWeekStartUnix(cycleHint *WeeklyCycleHint) int64 {
  924. if cycleHint == nil || cycleHint.WindowMinutes <= 0 || cycleHint.ResetAt.IsZero() {
  925. return 0
  926. }
  927. windowDuration := time.Duration(cycleHint.WindowMinutes) * time.Minute
  928. return cycleHint.ResetAt.UTC().Add(-windowDuration).Unix()
  929. }
  930. func (u *AggregatedUsage) ToJSON() *AggregatedUsageJSON {
  931. u.mutex.Lock()
  932. defer u.mutex.Unlock()
  933. result := &AggregatedUsageJSON{
  934. LastUpdated: u.LastUpdated,
  935. Costs: CostsSummaryJSON{
  936. TotalUSD: 0,
  937. ByUser: make(map[string]float64),
  938. ByWeek: make(map[string]float64),
  939. },
  940. }
  941. globalCombinationsJSON, totalCost := buildCombinationJSON(u.Combinations, result.Costs.ByUser)
  942. result.Combinations = globalCombinationsJSON
  943. result.Costs.TotalUSD = totalCost
  944. result.Costs.ByWeek = buildByWeekCost(u.Combinations)
  945. if len(result.Costs.ByWeek) == 0 {
  946. result.Costs.ByWeek = nil
  947. }
  948. result.Costs.ByUserAndWeek = buildByUserAndWeekCost(u.Combinations)
  949. if len(result.Costs.ByUserAndWeek) == 0 {
  950. result.Costs.ByUserAndWeek = nil
  951. }
  952. for user, cost := range result.Costs.ByUser {
  953. result.Costs.ByUser[user] = roundCost(cost)
  954. }
  955. return result
  956. }
  957. func (u *AggregatedUsage) Load() error {
  958. u.mutex.Lock()
  959. defer u.mutex.Unlock()
  960. u.LastUpdated = time.Time{}
  961. u.Combinations = nil
  962. data, err := os.ReadFile(u.filePath)
  963. if err != nil {
  964. if os.IsNotExist(err) {
  965. return nil
  966. }
  967. return err
  968. }
  969. var temp struct {
  970. LastUpdated time.Time `json:"last_updated"`
  971. Combinations []CostCombination `json:"combinations"`
  972. }
  973. err = json.Unmarshal(data, &temp)
  974. if err != nil {
  975. return err
  976. }
  977. u.LastUpdated = temp.LastUpdated
  978. u.Combinations = temp.Combinations
  979. normalizeCombinations(u.Combinations)
  980. return nil
  981. }
  982. func (u *AggregatedUsage) Save() error {
  983. jsonData := u.ToJSON()
  984. data, err := json.MarshalIndent(jsonData, "", " ")
  985. if err != nil {
  986. return err
  987. }
  988. tmpFile := u.filePath + ".tmp"
  989. err = os.WriteFile(tmpFile, data, 0o644)
  990. if err != nil {
  991. return err
  992. }
  993. defer os.Remove(tmpFile)
  994. err = os.Rename(tmpFile, u.filePath)
  995. if err == nil {
  996. u.saveMutex.Lock()
  997. u.lastSaveTime = time.Now()
  998. u.saveMutex.Unlock()
  999. }
  1000. return err
  1001. }
  1002. func (u *AggregatedUsage) AddUsage(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string) error {
  1003. return u.AddUsageWithCycleHint(model, contextWindow, inputTokens, outputTokens, cachedTokens, serviceTier, user, time.Now(), nil)
  1004. }
  1005. func (u *AggregatedUsage) AddUsageWithCycleHint(model string, contextWindow int, inputTokens, outputTokens, cachedTokens int64, serviceTier string, user string, observedAt time.Time, cycleHint *WeeklyCycleHint) error {
  1006. if model == "" {
  1007. return E.New("model cannot be empty")
  1008. }
  1009. if contextWindow <= 0 {
  1010. return E.New("contextWindow must be positive")
  1011. }
  1012. normalizedServiceTier := normalizeServiceTier(serviceTier)
  1013. if observedAt.IsZero() {
  1014. observedAt = time.Now()
  1015. }
  1016. u.mutex.Lock()
  1017. defer u.mutex.Unlock()
  1018. u.LastUpdated = observedAt
  1019. weekStartUnix := deriveWeekStartUnix(cycleHint)
  1020. addUsageToCombinations(&u.Combinations, model, normalizedServiceTier, contextWindow, weekStartUnix, user, inputTokens, outputTokens, cachedTokens)
  1021. go u.scheduleSave()
  1022. return nil
  1023. }
  1024. func (u *AggregatedUsage) scheduleSave() {
  1025. const saveInterval = time.Minute
  1026. u.saveMutex.Lock()
  1027. defer u.saveMutex.Unlock()
  1028. timeSinceLastSave := time.Since(u.lastSaveTime)
  1029. if timeSinceLastSave >= saveInterval {
  1030. go u.saveAsync()
  1031. return
  1032. }
  1033. if u.pendingSave {
  1034. return
  1035. }
  1036. u.pendingSave = true
  1037. remainingTime := saveInterval - timeSinceLastSave
  1038. u.saveTimer = time.AfterFunc(remainingTime, func() {
  1039. u.saveMutex.Lock()
  1040. u.pendingSave = false
  1041. u.saveMutex.Unlock()
  1042. u.saveAsync()
  1043. })
  1044. }
  1045. func (u *AggregatedUsage) saveAsync() {
  1046. err := u.Save()
  1047. if err != nil {
  1048. if u.logger != nil {
  1049. u.logger.Error("save usage statistics: ", err)
  1050. }
  1051. }
  1052. }
  1053. func (u *AggregatedUsage) cancelPendingSave() {
  1054. u.saveMutex.Lock()
  1055. defer u.saveMutex.Unlock()
  1056. if u.saveTimer != nil {
  1057. u.saveTimer.Stop()
  1058. u.saveTimer = nil
  1059. }
  1060. u.pendingSave = false
  1061. }