usage_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. package model_test
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/labring/aiproxy/core/model"
  6. )
  7. func TestPrice_ValidateConditionalPrices(t *testing.T) {
  8. tests := []struct {
  9. name string
  10. price model.Price
  11. wantErr bool
  12. }{
  13. {
  14. name: "Empty conditional prices",
  15. price: model.Price{
  16. ConditionalPrices: []model.ConditionalPrice{},
  17. },
  18. wantErr: false,
  19. },
  20. {
  21. name: "Nil conditional prices",
  22. price: model.Price{
  23. ConditionalPrices: nil,
  24. },
  25. wantErr: false,
  26. },
  27. {
  28. name: "Valid single condition",
  29. price: model.Price{
  30. ConditionalPrices: []model.ConditionalPrice{
  31. {
  32. Condition: model.PriceCondition{
  33. InputTokenMin: 0,
  34. InputTokenMax: 32000,
  35. OutputTokenMin: 0,
  36. OutputTokenMax: 200,
  37. },
  38. Price: model.Price{
  39. InputPrice: 0.0008,
  40. OutputPrice: 0.002,
  41. },
  42. },
  43. },
  44. },
  45. wantErr: false,
  46. },
  47. {
  48. name: "Valid multiple conditions - doubao-seed-1.6 example",
  49. price: model.Price{
  50. ConditionalPrices: []model.ConditionalPrice{
  51. {
  52. Condition: model.PriceCondition{
  53. InputTokenMin: 0,
  54. InputTokenMax: 32000,
  55. OutputTokenMin: 0,
  56. OutputTokenMax: 200,
  57. },
  58. Price: model.Price{
  59. InputPrice: 0.0008,
  60. OutputPrice: 0.002,
  61. },
  62. },
  63. {
  64. Condition: model.PriceCondition{
  65. InputTokenMin: 0,
  66. InputTokenMax: 32000,
  67. OutputTokenMin: 201,
  68. OutputTokenMax: 16000,
  69. },
  70. Price: model.Price{
  71. InputPrice: 0.0008,
  72. OutputPrice: 0.008,
  73. },
  74. },
  75. {
  76. Condition: model.PriceCondition{
  77. InputTokenMin: 32001,
  78. InputTokenMax: 128000,
  79. },
  80. Price: model.Price{
  81. InputPrice: 0.0012,
  82. OutputPrice: 0.016,
  83. },
  84. },
  85. {
  86. Condition: model.PriceCondition{
  87. InputTokenMin: 128001,
  88. InputTokenMax: 256000,
  89. },
  90. Price: model.Price{
  91. InputPrice: 0.0024,
  92. OutputPrice: 0.024,
  93. },
  94. },
  95. },
  96. },
  97. wantErr: false,
  98. },
  99. {
  100. name: "Invalid input token range - min > max",
  101. price: model.Price{
  102. ConditionalPrices: []model.ConditionalPrice{
  103. {
  104. Condition: model.PriceCondition{
  105. InputTokenMin: 32000,
  106. InputTokenMax: 1000, // min > max
  107. },
  108. Price: model.Price{
  109. InputPrice: 0.0008,
  110. OutputPrice: 0.002,
  111. },
  112. },
  113. },
  114. },
  115. wantErr: true,
  116. },
  117. {
  118. name: "Invalid output token range - min > max",
  119. price: model.Price{
  120. ConditionalPrices: []model.ConditionalPrice{
  121. {
  122. Condition: model.PriceCondition{
  123. InputTokenMin: 0,
  124. InputTokenMax: 32000,
  125. OutputTokenMin: 1000,
  126. OutputTokenMax: 500, // min > max
  127. },
  128. Price: model.Price{
  129. InputPrice: 0.0008,
  130. OutputPrice: 0.002,
  131. },
  132. },
  133. },
  134. },
  135. wantErr: true,
  136. },
  137. {
  138. name: "Overlapping input ranges with overlapping output ranges",
  139. price: model.Price{
  140. ConditionalPrices: []model.ConditionalPrice{
  141. {
  142. Condition: model.PriceCondition{
  143. InputTokenMin: 0,
  144. InputTokenMax: 32000,
  145. OutputTokenMin: 0,
  146. OutputTokenMax: 500,
  147. },
  148. Price: model.Price{
  149. InputPrice: 0.0008,
  150. OutputPrice: 0.002,
  151. },
  152. },
  153. {
  154. Condition: model.PriceCondition{
  155. InputTokenMin: 20000, // overlaps with previous
  156. InputTokenMax: 50000,
  157. OutputTokenMin: 200, // overlaps with previous
  158. OutputTokenMax: 1000,
  159. },
  160. Price: model.Price{
  161. InputPrice: 0.0012,
  162. OutputPrice: 0.008,
  163. },
  164. },
  165. },
  166. },
  167. wantErr: true,
  168. },
  169. {
  170. name: "Overlapping input ranges but non-overlapping output ranges (valid)",
  171. price: model.Price{
  172. ConditionalPrices: []model.ConditionalPrice{
  173. {
  174. Condition: model.PriceCondition{
  175. InputTokenMin: 0,
  176. InputTokenMax: 32000,
  177. OutputTokenMin: 0,
  178. OutputTokenMax: 200,
  179. },
  180. Price: model.Price{
  181. InputPrice: 0.0008,
  182. OutputPrice: 0.002,
  183. },
  184. },
  185. {
  186. Condition: model.PriceCondition{
  187. InputTokenMin: 0, // same input range
  188. InputTokenMax: 32000, // same input range
  189. OutputTokenMin: 201, // non-overlapping output range
  190. OutputTokenMax: 16000,
  191. },
  192. Price: model.Price{
  193. InputPrice: 0.0008,
  194. OutputPrice: 0.008,
  195. },
  196. },
  197. },
  198. },
  199. wantErr: false,
  200. },
  201. {
  202. name: "Improperly ordered conditions",
  203. price: model.Price{
  204. ConditionalPrices: []model.ConditionalPrice{
  205. {
  206. Condition: model.PriceCondition{
  207. InputTokenMin: 32001,
  208. InputTokenMax: 128000,
  209. },
  210. Price: model.Price{
  211. InputPrice: 0.0012,
  212. OutputPrice: 0.016,
  213. },
  214. },
  215. {
  216. Condition: model.PriceCondition{
  217. InputTokenMin: 0,
  218. InputTokenMax: 32000, // should come before the previous one
  219. },
  220. Price: model.Price{
  221. InputPrice: 0.0008,
  222. OutputPrice: 0.002,
  223. },
  224. },
  225. },
  226. },
  227. wantErr: true,
  228. },
  229. {
  230. name: "Valid consecutive ranges",
  231. price: model.Price{
  232. ConditionalPrices: []model.ConditionalPrice{
  233. {
  234. Condition: model.PriceCondition{
  235. InputTokenMin: 0,
  236. InputTokenMax: 32000,
  237. },
  238. Price: model.Price{
  239. InputPrice: 0.0008,
  240. OutputPrice: 0.002,
  241. },
  242. },
  243. {
  244. Condition: model.PriceCondition{
  245. InputTokenMin: 32001, // consecutive with previous
  246. InputTokenMax: 128000,
  247. },
  248. Price: model.Price{
  249. InputPrice: 0.0012,
  250. OutputPrice: 0.016,
  251. },
  252. },
  253. },
  254. },
  255. wantErr: false,
  256. },
  257. {
  258. name: "Gap between ranges (valid)",
  259. price: model.Price{
  260. ConditionalPrices: []model.ConditionalPrice{
  261. {
  262. Condition: model.PriceCondition{
  263. InputTokenMin: 0,
  264. InputTokenMax: 32000,
  265. },
  266. Price: model.Price{
  267. InputPrice: 0.0008,
  268. OutputPrice: 0.002,
  269. },
  270. },
  271. {
  272. Condition: model.PriceCondition{
  273. InputTokenMin: 50000, // gap between 32000 and 50000
  274. InputTokenMax: 128000,
  275. },
  276. Price: model.Price{
  277. InputPrice: 0.0012,
  278. OutputPrice: 0.016,
  279. },
  280. },
  281. },
  282. },
  283. wantErr: false,
  284. },
  285. {
  286. name: "Unbounded ranges (zero values)",
  287. price: model.Price{
  288. ConditionalPrices: []model.ConditionalPrice{
  289. {
  290. Condition: model.PriceCondition{
  291. InputTokenMin: 0, // unbounded min
  292. InputTokenMax: 0, // unbounded max
  293. },
  294. Price: model.Price{
  295. InputPrice: 0.001,
  296. OutputPrice: 0.002,
  297. },
  298. },
  299. },
  300. },
  301. wantErr: false,
  302. },
  303. {
  304. name: "Mixed bounded and unbounded ranges",
  305. price: model.Price{
  306. ConditionalPrices: []model.ConditionalPrice{
  307. {
  308. Condition: model.PriceCondition{
  309. InputTokenMin: 0,
  310. InputTokenMax: 32000,
  311. },
  312. Price: model.Price{
  313. InputPrice: 0.0008,
  314. OutputPrice: 0.002,
  315. },
  316. },
  317. {
  318. Condition: model.PriceCondition{
  319. InputTokenMin: 32001,
  320. InputTokenMax: 0, // unbounded max
  321. },
  322. Price: model.Price{
  323. InputPrice: 0.0012,
  324. OutputPrice: 0.016,
  325. },
  326. },
  327. },
  328. },
  329. wantErr: false,
  330. },
  331. }
  332. for _, tt := range tests {
  333. t.Run(tt.name, func(t *testing.T) {
  334. err := tt.price.ValidateConditionalPrices()
  335. if tt.wantErr {
  336. if err == nil {
  337. t.Errorf("%s: ValidateConditionalPrices() expected error but got nil", tt.name)
  338. }
  339. return
  340. }
  341. if err != nil {
  342. t.Errorf("%s: ValidateConditionalPrices() unexpected error = %v", tt.name, err)
  343. }
  344. })
  345. }
  346. }
  347. func TestPrice_ValidateConditionalPrices_WithTime(t *testing.T) {
  348. now := time.Now().Unix()
  349. future := now + 3600 // 1 hour from now
  350. past := now - 3600 // 1 hour ago
  351. tests := []struct {
  352. name string
  353. price model.Price
  354. wantErr bool
  355. }{
  356. {
  357. name: "Valid time range - future time window",
  358. price: model.Price{
  359. ConditionalPrices: []model.ConditionalPrice{
  360. {
  361. Condition: model.PriceCondition{
  362. InputTokenMin: 0,
  363. InputTokenMax: 32000,
  364. StartTime: now,
  365. EndTime: future,
  366. },
  367. Price: model.Price{
  368. InputPrice: 0.0008,
  369. OutputPrice: 0.002,
  370. },
  371. },
  372. },
  373. },
  374. wantErr: false,
  375. },
  376. {
  377. name: "Valid time range - no end time",
  378. price: model.Price{
  379. ConditionalPrices: []model.ConditionalPrice{
  380. {
  381. Condition: model.PriceCondition{
  382. InputTokenMin: 0,
  383. InputTokenMax: 32000,
  384. StartTime: now,
  385. EndTime: 0, // no end limit
  386. },
  387. Price: model.Price{
  388. InputPrice: 0.0008,
  389. OutputPrice: 0.002,
  390. },
  391. },
  392. },
  393. },
  394. wantErr: false,
  395. },
  396. {
  397. name: "Valid time range - no start time",
  398. price: model.Price{
  399. ConditionalPrices: []model.ConditionalPrice{
  400. {
  401. Condition: model.PriceCondition{
  402. InputTokenMin: 0,
  403. InputTokenMax: 32000,
  404. StartTime: 0, // no start limit
  405. EndTime: future,
  406. },
  407. Price: model.Price{
  408. InputPrice: 0.0008,
  409. OutputPrice: 0.002,
  410. },
  411. },
  412. },
  413. },
  414. wantErr: false,
  415. },
  416. {
  417. name: "Invalid time range - start time >= end time",
  418. price: model.Price{
  419. ConditionalPrices: []model.ConditionalPrice{
  420. {
  421. Condition: model.PriceCondition{
  422. InputTokenMin: 0,
  423. InputTokenMax: 32000,
  424. StartTime: future,
  425. EndTime: now, // end before start
  426. },
  427. Price: model.Price{
  428. InputPrice: 0.0008,
  429. OutputPrice: 0.002,
  430. },
  431. },
  432. },
  433. },
  434. wantErr: true,
  435. },
  436. {
  437. name: "Invalid time range - start time equals end time",
  438. price: model.Price{
  439. ConditionalPrices: []model.ConditionalPrice{
  440. {
  441. Condition: model.PriceCondition{
  442. InputTokenMin: 0,
  443. InputTokenMax: 32000,
  444. StartTime: now,
  445. EndTime: now, // same time
  446. },
  447. Price: model.Price{
  448. InputPrice: 0.0008,
  449. OutputPrice: 0.002,
  450. },
  451. },
  452. },
  453. },
  454. wantErr: true,
  455. },
  456. {
  457. name: "Multiple conditions with different time ranges",
  458. price: model.Price{
  459. ConditionalPrices: []model.ConditionalPrice{
  460. {
  461. Condition: model.PriceCondition{
  462. InputTokenMin: 0,
  463. InputTokenMax: 32000,
  464. StartTime: past,
  465. EndTime: now,
  466. },
  467. Price: model.Price{
  468. InputPrice: 0.0008,
  469. OutputPrice: 0.002,
  470. },
  471. },
  472. {
  473. Condition: model.PriceCondition{
  474. InputTokenMin: 0,
  475. InputTokenMax: 32000,
  476. StartTime: now,
  477. EndTime: future,
  478. },
  479. Price: model.Price{
  480. InputPrice: 0.001,
  481. OutputPrice: 0.003,
  482. },
  483. },
  484. },
  485. },
  486. wantErr: false,
  487. },
  488. }
  489. for _, tt := range tests {
  490. t.Run(tt.name, func(t *testing.T) {
  491. err := tt.price.ValidateConditionalPrices()
  492. if tt.wantErr {
  493. if err == nil {
  494. t.Errorf("%s: ValidateConditionalPrices() expected error but got nil", tt.name)
  495. }
  496. return
  497. }
  498. if err != nil {
  499. t.Errorf("%s: ValidateConditionalPrices() unexpected error = %v", tt.name, err)
  500. }
  501. })
  502. }
  503. }
  504. func TestPrice_SelectConditionalPrice_WithTime(t *testing.T) {
  505. now := time.Now().Unix()
  506. past := now - 3600 // 1 hour ago
  507. future := now + 3600 // 1 hour from now
  508. farFuture := now + 7200 // 2 hours from now
  509. tests := []struct {
  510. name string
  511. price model.Price
  512. usage model.Usage
  513. expectedInput float64
  514. expectedOutput float64
  515. }{
  516. {
  517. name: "Select price within active time range",
  518. price: model.Price{
  519. InputPrice: 0.001,
  520. OutputPrice: 0.002,
  521. ConditionalPrices: []model.ConditionalPrice{
  522. {
  523. Condition: model.PriceCondition{
  524. InputTokenMin: 0,
  525. InputTokenMax: 32000,
  526. StartTime: past,
  527. EndTime: future,
  528. },
  529. Price: model.Price{
  530. InputPrice: 0.0005,
  531. OutputPrice: 0.001,
  532. },
  533. },
  534. },
  535. },
  536. usage: model.Usage{
  537. InputTokens: 1000,
  538. OutputTokens: 500,
  539. },
  540. expectedInput: 0.0005,
  541. expectedOutput: 0.001,
  542. },
  543. {
  544. name: "Fallback to default price when time range not active (before start)",
  545. price: model.Price{
  546. InputPrice: 0.001,
  547. OutputPrice: 0.002,
  548. ConditionalPrices: []model.ConditionalPrice{
  549. {
  550. Condition: model.PriceCondition{
  551. InputTokenMin: 0,
  552. InputTokenMax: 32000,
  553. StartTime: future,
  554. EndTime: farFuture,
  555. },
  556. Price: model.Price{
  557. InputPrice: 0.0005,
  558. OutputPrice: 0.001,
  559. },
  560. },
  561. },
  562. },
  563. usage: model.Usage{
  564. InputTokens: 1000,
  565. OutputTokens: 500,
  566. },
  567. expectedInput: 0.001,
  568. expectedOutput: 0.002,
  569. },
  570. {
  571. name: "Fallback to default price when time range expired",
  572. price: model.Price{
  573. InputPrice: 0.001,
  574. OutputPrice: 0.002,
  575. ConditionalPrices: []model.ConditionalPrice{
  576. {
  577. Condition: model.PriceCondition{
  578. InputTokenMin: 0,
  579. InputTokenMax: 32000,
  580. StartTime: past - 7200, // 3 hours ago
  581. EndTime: past, // 1 hour ago
  582. },
  583. Price: model.Price{
  584. InputPrice: 0.0005,
  585. OutputPrice: 0.001,
  586. },
  587. },
  588. },
  589. },
  590. usage: model.Usage{
  591. InputTokens: 1000,
  592. OutputTokens: 500,
  593. },
  594. expectedInput: 0.001,
  595. expectedOutput: 0.002,
  596. },
  597. {
  598. name: "Select first matching price with multiple time-based conditions",
  599. price: model.Price{
  600. InputPrice: 0.001,
  601. OutputPrice: 0.002,
  602. ConditionalPrices: []model.ConditionalPrice{
  603. {
  604. Condition: model.PriceCondition{
  605. InputTokenMin: 0,
  606. InputTokenMax: 32000,
  607. StartTime: past,
  608. EndTime: future,
  609. },
  610. Price: model.Price{
  611. InputPrice: 0.0005,
  612. OutputPrice: 0.001,
  613. },
  614. },
  615. {
  616. Condition: model.PriceCondition{
  617. InputTokenMin: 0,
  618. InputTokenMax: 32000,
  619. StartTime: past,
  620. EndTime: farFuture, // broader time range
  621. },
  622. Price: model.Price{
  623. InputPrice: 0.0008,
  624. OutputPrice: 0.0015,
  625. },
  626. },
  627. },
  628. },
  629. usage: model.Usage{
  630. InputTokens: 1000,
  631. OutputTokens: 500,
  632. },
  633. expectedInput: 0.0005,
  634. expectedOutput: 0.001,
  635. },
  636. {
  637. name: "Time range with no end time (ongoing promotion)",
  638. price: model.Price{
  639. InputPrice: 0.001,
  640. OutputPrice: 0.002,
  641. ConditionalPrices: []model.ConditionalPrice{
  642. {
  643. Condition: model.PriceCondition{
  644. InputTokenMin: 0,
  645. InputTokenMax: 32000,
  646. StartTime: past,
  647. EndTime: 0, // no end time
  648. },
  649. Price: model.Price{
  650. InputPrice: 0.0005,
  651. OutputPrice: 0.001,
  652. },
  653. },
  654. },
  655. },
  656. usage: model.Usage{
  657. InputTokens: 1000,
  658. OutputTokens: 500,
  659. },
  660. expectedInput: 0.0005,
  661. expectedOutput: 0.001,
  662. },
  663. {
  664. name: "Time range with no start time (promotion until end)",
  665. price: model.Price{
  666. InputPrice: 0.001,
  667. OutputPrice: 0.002,
  668. ConditionalPrices: []model.ConditionalPrice{
  669. {
  670. Condition: model.PriceCondition{
  671. InputTokenMin: 0,
  672. InputTokenMax: 32000,
  673. StartTime: 0, // no start time
  674. EndTime: future,
  675. },
  676. Price: model.Price{
  677. InputPrice: 0.0005,
  678. OutputPrice: 0.001,
  679. },
  680. },
  681. },
  682. },
  683. usage: model.Usage{
  684. InputTokens: 1000,
  685. OutputTokens: 500,
  686. },
  687. expectedInput: 0.0005,
  688. expectedOutput: 0.001,
  689. },
  690. }
  691. for _, tt := range tests {
  692. t.Run(tt.name, func(t *testing.T) {
  693. selectedPrice := tt.price.SelectConditionalPrice(tt.usage)
  694. if float64(selectedPrice.InputPrice) != tt.expectedInput {
  695. t.Errorf("%s: expected input price %v, got %v",
  696. tt.name, tt.expectedInput, float64(selectedPrice.InputPrice))
  697. }
  698. if float64(selectedPrice.OutputPrice) != tt.expectedOutput {
  699. t.Errorf("%s: expected output price %v, got %v",
  700. tt.name, tt.expectedOutput, float64(selectedPrice.OutputPrice))
  701. }
  702. })
  703. }
  704. }