channel_handler_test.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. //go:build unit
  2. package admin
  3. import (
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "time"
  9. "github.com/Wei-Shaw/sub2api/internal/service"
  10. "github.com/gin-gonic/gin"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // ---------------------------------------------------------------------------
  14. // helpers
  15. // ---------------------------------------------------------------------------
  16. func float64Ptr(v float64) *float64 { return &v }
  17. func intPtr(v int) *int { return &v }
  18. // ---------------------------------------------------------------------------
  19. // 1. channelToResponse
  20. // ---------------------------------------------------------------------------
  21. func TestChannelToResponse_NilInput(t *testing.T) {
  22. require.Nil(t, channelToResponse(nil))
  23. }
  24. func TestChannelToResponse_FullChannel(t *testing.T) {
  25. now := time.Date(2025, 6, 1, 12, 0, 0, 0, time.UTC)
  26. ch := &service.Channel{
  27. ID: 42,
  28. Name: "test-channel",
  29. Description: "desc",
  30. Status: "active",
  31. BillingModelSource: "upstream",
  32. RestrictModels: true,
  33. CreatedAt: now,
  34. UpdatedAt: now.Add(time.Hour),
  35. GroupIDs: []int64{1, 2, 3},
  36. ModelPricing: []service.ChannelModelPricing{
  37. {
  38. ID: 10,
  39. Platform: "openai",
  40. Models: []string{"gpt-4"},
  41. BillingMode: service.BillingModeToken,
  42. InputPrice: float64Ptr(0.01),
  43. OutputPrice: float64Ptr(0.03),
  44. CacheWritePrice: float64Ptr(0.005),
  45. CacheReadPrice: float64Ptr(0.002),
  46. PerRequestPrice: float64Ptr(0.5),
  47. },
  48. },
  49. ModelMapping: map[string]map[string]string{
  50. "anthropic": {"claude-3-haiku": "claude-haiku-3"},
  51. },
  52. }
  53. resp := channelToResponse(ch)
  54. require.NotNil(t, resp)
  55. require.Equal(t, int64(42), resp.ID)
  56. require.Equal(t, "test-channel", resp.Name)
  57. require.Equal(t, "desc", resp.Description)
  58. require.Equal(t, "active", resp.Status)
  59. require.Equal(t, "upstream", resp.BillingModelSource)
  60. require.True(t, resp.RestrictModels)
  61. require.Equal(t, []int64{1, 2, 3}, resp.GroupIDs)
  62. require.Equal(t, "2025-06-01T12:00:00Z", resp.CreatedAt)
  63. require.Equal(t, "2025-06-01T13:00:00Z", resp.UpdatedAt)
  64. // model mapping
  65. require.Len(t, resp.ModelMapping, 1)
  66. require.Equal(t, "claude-haiku-3", resp.ModelMapping["anthropic"]["claude-3-haiku"])
  67. // pricing
  68. require.Len(t, resp.ModelPricing, 1)
  69. p := resp.ModelPricing[0]
  70. require.Equal(t, int64(10), p.ID)
  71. require.Equal(t, "openai", p.Platform)
  72. require.Equal(t, []string{"gpt-4"}, p.Models)
  73. require.Equal(t, "token", p.BillingMode)
  74. require.Equal(t, float64Ptr(0.01), p.InputPrice)
  75. require.Equal(t, float64Ptr(0.03), p.OutputPrice)
  76. require.Equal(t, float64Ptr(0.005), p.CacheWritePrice)
  77. require.Equal(t, float64Ptr(0.002), p.CacheReadPrice)
  78. require.Equal(t, float64Ptr(0.5), p.PerRequestPrice)
  79. require.Empty(t, p.Intervals)
  80. }
  81. func TestChannelToResponse_EmptyDefaults(t *testing.T) {
  82. now := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
  83. ch := &service.Channel{
  84. ID: 1,
  85. Name: "ch",
  86. BillingModelSource: service.BillingModelSourceChannelMapped,
  87. CreatedAt: now,
  88. UpdatedAt: now,
  89. GroupIDs: nil,
  90. ModelMapping: nil,
  91. ModelPricing: []service.ChannelModelPricing{
  92. {
  93. Platform: "",
  94. BillingMode: "",
  95. Models: []string{"m1"},
  96. },
  97. },
  98. }
  99. // handler 层 channelToResponse 现在是纯透传:BillingModelSource 的空值兜底
  100. // 已下放到 service 层(Create/GetByID/List/Update/ListAvailable 出口统一处理),
  101. // 因此这里构造 fixture 时直接传入归一化后的值。
  102. resp := channelToResponse(ch)
  103. require.Equal(t, "channel_mapped", resp.BillingModelSource)
  104. require.NotNil(t, resp.GroupIDs)
  105. require.Empty(t, resp.GroupIDs)
  106. require.NotNil(t, resp.ModelMapping)
  107. require.Empty(t, resp.ModelMapping)
  108. require.Len(t, resp.ModelPricing, 1)
  109. require.Equal(t, "anthropic", resp.ModelPricing[0].Platform)
  110. require.Equal(t, "token", resp.ModelPricing[0].BillingMode)
  111. }
  112. func TestChannelToResponse_BillingModelSourcePassthrough(t *testing.T) {
  113. // handler 不再兜底 BillingModelSource:空值应原样透传(由 service 层负责默认回填)。
  114. ch := &service.Channel{
  115. ID: 1,
  116. Name: "ch",
  117. BillingModelSource: "",
  118. CreatedAt: time.Now(),
  119. UpdatedAt: time.Now(),
  120. }
  121. resp := channelToResponse(ch)
  122. require.Equal(t, "", resp.BillingModelSource, "handler 应纯透传,默认值由 service.normalizeBillingModelSource 负责")
  123. }
  124. func TestChannelToResponse_NilModels(t *testing.T) {
  125. now := time.Now()
  126. ch := &service.Channel{
  127. ID: 1,
  128. Name: "ch",
  129. CreatedAt: now,
  130. UpdatedAt: now,
  131. ModelPricing: []service.ChannelModelPricing{
  132. {
  133. Models: nil,
  134. },
  135. },
  136. }
  137. resp := channelToResponse(ch)
  138. require.Len(t, resp.ModelPricing, 1)
  139. require.NotNil(t, resp.ModelPricing[0].Models)
  140. require.Empty(t, resp.ModelPricing[0].Models)
  141. }
  142. func TestChannelToResponse_WithIntervals(t *testing.T) {
  143. now := time.Now()
  144. ch := &service.Channel{
  145. ID: 1,
  146. Name: "ch",
  147. CreatedAt: now,
  148. UpdatedAt: now,
  149. ModelPricing: []service.ChannelModelPricing{
  150. {
  151. Models: []string{"m1"},
  152. BillingMode: service.BillingModePerRequest,
  153. Intervals: []service.PricingInterval{
  154. {
  155. ID: 100,
  156. MinTokens: 0,
  157. MaxTokens: intPtr(1000),
  158. TierLabel: "1K",
  159. InputPrice: float64Ptr(0.01),
  160. OutputPrice: float64Ptr(0.02),
  161. CacheWritePrice: float64Ptr(0.003),
  162. CacheReadPrice: float64Ptr(0.001),
  163. PerRequestPrice: float64Ptr(0.1),
  164. SortOrder: 1,
  165. },
  166. {
  167. ID: 101,
  168. MinTokens: 1000,
  169. MaxTokens: nil,
  170. TierLabel: "unlimited",
  171. SortOrder: 2,
  172. },
  173. },
  174. },
  175. },
  176. }
  177. resp := channelToResponse(ch)
  178. require.Len(t, resp.ModelPricing, 1)
  179. intervals := resp.ModelPricing[0].Intervals
  180. require.Len(t, intervals, 2)
  181. iv0 := intervals[0]
  182. require.Equal(t, int64(100), iv0.ID)
  183. require.Equal(t, 0, iv0.MinTokens)
  184. require.Equal(t, intPtr(1000), iv0.MaxTokens)
  185. require.Equal(t, "1K", iv0.TierLabel)
  186. require.Equal(t, float64Ptr(0.01), iv0.InputPrice)
  187. require.Equal(t, float64Ptr(0.02), iv0.OutputPrice)
  188. require.Equal(t, float64Ptr(0.003), iv0.CacheWritePrice)
  189. require.Equal(t, float64Ptr(0.001), iv0.CacheReadPrice)
  190. require.Equal(t, float64Ptr(0.1), iv0.PerRequestPrice)
  191. require.Equal(t, 1, iv0.SortOrder)
  192. iv1 := intervals[1]
  193. require.Equal(t, int64(101), iv1.ID)
  194. require.Equal(t, 1000, iv1.MinTokens)
  195. require.Nil(t, iv1.MaxTokens)
  196. require.Equal(t, "unlimited", iv1.TierLabel)
  197. require.Equal(t, 2, iv1.SortOrder)
  198. }
  199. func TestChannelToResponse_MultipleEntries(t *testing.T) {
  200. now := time.Now()
  201. ch := &service.Channel{
  202. ID: 1,
  203. Name: "multi",
  204. CreatedAt: now,
  205. UpdatedAt: now,
  206. ModelPricing: []service.ChannelModelPricing{
  207. {
  208. ID: 1,
  209. Platform: "anthropic",
  210. Models: []string{"claude-sonnet-4"},
  211. BillingMode: service.BillingModeToken,
  212. InputPrice: float64Ptr(0.003),
  213. OutputPrice: float64Ptr(0.015),
  214. },
  215. {
  216. ID: 2,
  217. Platform: "openai",
  218. Models: []string{"gpt-4", "gpt-4o"},
  219. BillingMode: service.BillingModePerRequest,
  220. PerRequestPrice: float64Ptr(1.0),
  221. },
  222. {
  223. ID: 3,
  224. Platform: "gemini",
  225. Models: []string{"gemini-2.5-pro"},
  226. BillingMode: service.BillingModeImage,
  227. ImageOutputPrice: float64Ptr(0.05),
  228. PerRequestPrice: float64Ptr(0.2),
  229. },
  230. },
  231. }
  232. resp := channelToResponse(ch)
  233. require.Len(t, resp.ModelPricing, 3)
  234. require.Equal(t, int64(1), resp.ModelPricing[0].ID)
  235. require.Equal(t, "anthropic", resp.ModelPricing[0].Platform)
  236. require.Equal(t, []string{"claude-sonnet-4"}, resp.ModelPricing[0].Models)
  237. require.Equal(t, "token", resp.ModelPricing[0].BillingMode)
  238. require.Equal(t, int64(2), resp.ModelPricing[1].ID)
  239. require.Equal(t, "openai", resp.ModelPricing[1].Platform)
  240. require.Equal(t, []string{"gpt-4", "gpt-4o"}, resp.ModelPricing[1].Models)
  241. require.Equal(t, "per_request", resp.ModelPricing[1].BillingMode)
  242. require.Equal(t, int64(3), resp.ModelPricing[2].ID)
  243. require.Equal(t, "gemini", resp.ModelPricing[2].Platform)
  244. require.Equal(t, []string{"gemini-2.5-pro"}, resp.ModelPricing[2].Models)
  245. require.Equal(t, "image", resp.ModelPricing[2].BillingMode)
  246. require.Equal(t, float64Ptr(0.05), resp.ModelPricing[2].ImageOutputPrice)
  247. }
  248. // ---------------------------------------------------------------------------
  249. // 2. pricingRequestToService
  250. // ---------------------------------------------------------------------------
  251. func TestPricingRequestToService_Defaults(t *testing.T) {
  252. tests := []struct {
  253. name string
  254. req channelModelPricingRequest
  255. wantField string // which default field to check
  256. wantValue string
  257. }{
  258. {
  259. name: "empty billing mode defaults to token",
  260. req: channelModelPricingRequest{
  261. Models: []string{"m1"},
  262. BillingMode: "",
  263. },
  264. wantField: "BillingMode",
  265. wantValue: string(service.BillingModeToken),
  266. },
  267. {
  268. name: "empty platform stays empty",
  269. req: channelModelPricingRequest{
  270. Models: []string{"m1"},
  271. Platform: "",
  272. },
  273. wantField: "Platform",
  274. wantValue: "",
  275. },
  276. }
  277. for _, tt := range tests {
  278. t.Run(tt.name, func(t *testing.T) {
  279. result := pricingRequestToService([]channelModelPricingRequest{tt.req}, true)
  280. require.Len(t, result, 1)
  281. switch tt.wantField {
  282. case "BillingMode":
  283. require.Equal(t, service.BillingMode(tt.wantValue), result[0].BillingMode)
  284. case "Platform":
  285. require.Equal(t, tt.wantValue, result[0].Platform)
  286. }
  287. })
  288. }
  289. }
  290. func TestPricingRequestToService_WithAllFields(t *testing.T) {
  291. reqs := []channelModelPricingRequest{
  292. {
  293. Platform: "openai",
  294. Models: []string{"gpt-4", "gpt-4o"},
  295. BillingMode: "per_request",
  296. InputPrice: float64Ptr(0.01),
  297. OutputPrice: float64Ptr(0.03),
  298. CacheWritePrice: float64Ptr(0.005),
  299. CacheWrite1hPrice: float64Ptr(0.008),
  300. CacheReadPrice: float64Ptr(0.002),
  301. ImageOutputPrice: float64Ptr(0.04),
  302. PerRequestPrice: float64Ptr(0.5),
  303. },
  304. }
  305. result := pricingRequestToService(reqs, true)
  306. require.Len(t, result, 1)
  307. r := result[0]
  308. require.Equal(t, "openai", r.Platform)
  309. require.Equal(t, []string{"gpt-4", "gpt-4o"}, r.Models)
  310. require.Equal(t, service.BillingModePerRequest, r.BillingMode)
  311. require.Equal(t, float64Ptr(0.01), r.InputPrice)
  312. require.Equal(t, float64Ptr(0.03), r.OutputPrice)
  313. require.Equal(t, float64Ptr(0.005), r.CacheWritePrice)
  314. require.Equal(t, float64Ptr(0.008), r.CacheWrite1hPrice)
  315. require.Equal(t, float64Ptr(0.002), r.CacheReadPrice)
  316. require.Equal(t, float64Ptr(0.04), r.ImageOutputPrice)
  317. require.Equal(t, float64Ptr(0.5), r.PerRequestPrice)
  318. }
  319. func TestPricingRequestToService_WithIntervals(t *testing.T) {
  320. reqs := []channelModelPricingRequest{
  321. {
  322. Models: []string{"m1"},
  323. BillingMode: "per_request",
  324. Intervals: []pricingIntervalRequest{
  325. {
  326. MinTokens: 0,
  327. MaxTokens: intPtr(2000),
  328. TierLabel: "small",
  329. InputPrice: float64Ptr(0.01),
  330. OutputPrice: float64Ptr(0.02),
  331. CacheWritePrice: float64Ptr(0.003),
  332. CacheWrite1hPrice: float64Ptr(0.006),
  333. CacheReadPrice: float64Ptr(0.001),
  334. PerRequestPrice: float64Ptr(0.1),
  335. SortOrder: 1,
  336. },
  337. {
  338. MinTokens: 2000,
  339. MaxTokens: nil,
  340. TierLabel: "large",
  341. SortOrder: 2,
  342. },
  343. },
  344. },
  345. }
  346. result := pricingRequestToService(reqs, true)
  347. require.Len(t, result, 1)
  348. require.Len(t, result[0].Intervals, 2)
  349. iv0 := result[0].Intervals[0]
  350. require.Equal(t, 0, iv0.MinTokens)
  351. require.Equal(t, intPtr(2000), iv0.MaxTokens)
  352. require.Equal(t, "small", iv0.TierLabel)
  353. require.Equal(t, float64Ptr(0.01), iv0.InputPrice)
  354. require.Equal(t, float64Ptr(0.02), iv0.OutputPrice)
  355. require.Equal(t, float64Ptr(0.003), iv0.CacheWritePrice)
  356. require.Equal(t, float64Ptr(0.006), iv0.CacheWrite1hPrice)
  357. require.Equal(t, float64Ptr(0.001), iv0.CacheReadPrice)
  358. require.Equal(t, float64Ptr(0.1), iv0.PerRequestPrice)
  359. require.Equal(t, 1, iv0.SortOrder)
  360. iv1 := result[0].Intervals[1]
  361. require.Equal(t, 2000, iv1.MinTokens)
  362. require.Nil(t, iv1.MaxTokens)
  363. require.Equal(t, "large", iv1.TierLabel)
  364. require.Equal(t, 2, iv1.SortOrder)
  365. }
  366. func TestPricingRequestToService_EmptySlice(t *testing.T) {
  367. result := pricingRequestToService([]channelModelPricingRequest{}, true)
  368. require.NotNil(t, result)
  369. require.Empty(t, result)
  370. }
  371. func TestPricingRequestToService_NilPriceFields(t *testing.T) {
  372. reqs := []channelModelPricingRequest{
  373. {
  374. Models: []string{"m1"},
  375. BillingMode: "token",
  376. // all price fields are nil by default
  377. },
  378. }
  379. result := pricingRequestToService(reqs, true)
  380. require.Len(t, result, 1)
  381. r := result[0]
  382. require.Nil(t, r.InputPrice)
  383. require.Nil(t, r.OutputPrice)
  384. require.Nil(t, r.CacheWritePrice)
  385. require.Nil(t, r.CacheReadPrice)
  386. require.Nil(t, r.ImageOutputPrice)
  387. require.Nil(t, r.PerRequestPrice)
  388. }
  389. func TestPricingRequestToService_TimePricing(t *testing.T) {
  390. req := channelModelPricingRequest{
  391. Models: []string{"gpt-5"},
  392. BillingMode: "token",
  393. TimePricing: &channelTimePricingRequest{
  394. Timezone: "Asia/Shanghai",
  395. WeekdaysOnly: true,
  396. Periods: []channelTimePricingPeriodRequest{{
  397. StartTime: "09:00", EndTime: "12:00", Multiplier: 2,
  398. }},
  399. },
  400. }
  401. got := pricingRequestToService([]channelModelPricingRequest{req}, true)
  402. require.Equal(t, "Asia/Shanghai", got[0].TimePricing.Timezone)
  403. require.True(t, got[0].TimePricing.WeekdaysOnly)
  404. require.Equal(t, 2.0, got[0].TimePricing.Periods[0].Multiplier)
  405. }
  406. func TestPricingRequestToService_TimePricingNil(t *testing.T) {
  407. got := pricingRequestToService([]channelModelPricingRequest{{Models: []string{"gpt-5"}}}, true)
  408. require.Nil(t, got[0].TimePricing)
  409. }
  410. // 账号成本统计规则不支持倍率:allowChannelMultipliers=false 时必须丢弃,
  411. // 避免渠道倍率意外污染账号成本口径。
  412. func TestPricingRequestToService_MultipliersGatedByFlag(t *testing.T) {
  413. req := channelModelPricingRequest{
  414. Models: []string{"gpt-5"},
  415. BillingMode: "token",
  416. FastMultiplier: float64Ptr(2.5),
  417. FlexMultiplier: float64Ptr(0.5),
  418. MaxReasoningEffortMultiplier: float64Ptr(3),
  419. Intervals: []pricingIntervalRequest{{
  420. MinTokens: 272000,
  421. InputMultiplier: float64Ptr(2),
  422. OutputMultiplier: float64Ptr(1.5),
  423. CacheWriteMultiplier: float64Ptr(2),
  424. CacheReadMultiplier: float64Ptr(2),
  425. }},
  426. }
  427. allowed := pricingRequestToService([]channelModelPricingRequest{req}, true)
  428. require.Equal(t, float64Ptr(2.5), allowed[0].FastMultiplier)
  429. require.Equal(t, float64Ptr(0.5), allowed[0].FlexMultiplier)
  430. require.Equal(t, float64Ptr(3), allowed[0].MaxReasoningEffortMultiplier)
  431. require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].InputMultiplier)
  432. require.Equal(t, float64Ptr(1.5), allowed[0].Intervals[0].OutputMultiplier)
  433. require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheWriteMultiplier)
  434. require.Equal(t, float64Ptr(2), allowed[0].Intervals[0].CacheReadMultiplier)
  435. dropped := pricingRequestToService([]channelModelPricingRequest{req}, false)
  436. require.Nil(t, dropped[0].FastMultiplier)
  437. require.Nil(t, dropped[0].FlexMultiplier)
  438. require.Nil(t, dropped[0].MaxReasoningEffortMultiplier)
  439. require.Nil(t, dropped[0].Intervals[0].InputMultiplier)
  440. require.Nil(t, dropped[0].Intervals[0].OutputMultiplier)
  441. require.Nil(t, dropped[0].Intervals[0].CacheWriteMultiplier)
  442. require.Nil(t, dropped[0].Intervals[0].CacheReadMultiplier)
  443. // 非倍率字段不受开关影响
  444. require.Equal(t, 272000, dropped[0].Intervals[0].MinTokens)
  445. }
  446. func TestPricingToResponse_TimePricing(t *testing.T) {
  447. got := pricingToResponse(&service.ChannelModelPricing{
  448. BillingMode: service.BillingModeToken,
  449. TimePricing: &service.ChannelTimePricing{
  450. Timezone: "Asia/Shanghai",
  451. WeekdaysOnly: true,
  452. Periods: []service.ChannelTimePricingPeriod{{
  453. StartTime: "14:00", EndTime: "18:00", Multiplier: 1.25,
  454. }},
  455. },
  456. })
  457. require.NotNil(t, got.TimePricing)
  458. require.Equal(t, "Asia/Shanghai", got.TimePricing.Timezone)
  459. require.True(t, got.TimePricing.WeekdaysOnly)
  460. require.Equal(t, 1.25, got.TimePricing.Periods[0].Multiplier)
  461. }
  462. func TestPricingToResponse_TimePricingNil(t *testing.T) {
  463. got := pricingToResponse(&service.ChannelModelPricing{})
  464. require.Nil(t, got.TimePricing)
  465. }
  466. // ---------------------------------------------------------------------------
  467. // 3. SyncPricingModels handler
  468. // ---------------------------------------------------------------------------
  469. func setupSyncPricingModelsRouter(pricingSvc *service.PricingService) *gin.Engine {
  470. gin.SetMode(gin.TestMode)
  471. router := gin.New()
  472. h := &ChannelHandler{pricingService: pricingSvc}
  473. router.GET("/channels/pricing/sync-models", h.SyncPricingModels)
  474. return router
  475. }
  476. func TestSyncPricingModels_MissingPlatform(t *testing.T) {
  477. svc := service.NewPricingService(nil, nil)
  478. router := setupSyncPricingModelsRouter(svc)
  479. req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models", nil)
  480. w := httptest.NewRecorder()
  481. router.ServeHTTP(w, req)
  482. require.Equal(t, http.StatusBadRequest, w.Code)
  483. }
  484. func TestSyncPricingModels_UnsupportedPlatform(t *testing.T) {
  485. svc := service.NewPricingService(nil, nil)
  486. router := setupSyncPricingModelsRouter(svc)
  487. req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform=unknown", nil)
  488. w := httptest.NewRecorder()
  489. router.ServeHTTP(w, req)
  490. require.Equal(t, http.StatusBadRequest, w.Code)
  491. }
  492. func TestSyncPricingModels_ValidPlatform_EmptyService(t *testing.T) {
  493. svc := service.NewPricingService(nil, nil)
  494. router := setupSyncPricingModelsRouter(svc)
  495. for _, platform := range []string{"anthropic", "openai", "gemini", "antigravity", "grok", "kimi", "zhipu", "deepseek", "minimax"} {
  496. req := httptest.NewRequest(http.MethodGet, "/channels/pricing/sync-models?platform="+platform, nil)
  497. w := httptest.NewRecorder()
  498. router.ServeHTTP(w, req)
  499. require.Equal(t, http.StatusOK, w.Code, "platform=%s", platform)
  500. var body struct {
  501. Data struct {
  502. Models []string `json:"models"`
  503. } `json:"data"`
  504. }
  505. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  506. require.NotNil(t, body.Data.Models, "models must not be null for platform=%s", platform)
  507. }
  508. }
  509. func setupModelDefaultPricingRouter() *gin.Engine {
  510. gin.SetMode(gin.TestMode)
  511. router := gin.New()
  512. h := &ChannelHandler{billingService: service.NewBillingService(nil, nil)}
  513. router.GET("/channels/model-pricing", h.GetModelDefaultPricing)
  514. return router
  515. }
  516. func TestGetModelDefaultPricing_ReturnsFable51CacheTTLs(t *testing.T) {
  517. router := setupModelDefaultPricingRouter()
  518. req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-fable-5-1", nil)
  519. w := httptest.NewRecorder()
  520. router.ServeHTTP(w, req)
  521. require.Equal(t, http.StatusOK, w.Code)
  522. var body struct {
  523. Data struct {
  524. Found bool `json:"found"`
  525. CacheWritePrice float64 `json:"cache_write_price"`
  526. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  527. MaxReasoningEffortMultiplier *float64 `json:"max_reasoning_effort_multiplier"`
  528. } `json:"data"`
  529. }
  530. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  531. require.True(t, body.Data.Found)
  532. require.InDelta(t, 12.5e-6, body.Data.CacheWritePrice, 1e-12)
  533. require.NotNil(t, body.Data.CacheWrite1hPrice)
  534. require.InDelta(t, 20e-6, *body.Data.CacheWrite1hPrice, 1e-12)
  535. require.NotNil(t, body.Data.MaxReasoningEffortMultiplier)
  536. require.Equal(t, 3.0, *body.Data.MaxReasoningEffortMultiplier)
  537. }
  538. func TestGetModelDefaultPricing_OmitsUnsupportedCache1hPrice(t *testing.T) {
  539. router := setupModelDefaultPricingRouter()
  540. req := httptest.NewRequest(http.MethodGet, "/channels/model-pricing?model=claude-sonnet-4", nil)
  541. w := httptest.NewRecorder()
  542. router.ServeHTTP(w, req)
  543. require.Equal(t, http.StatusOK, w.Code)
  544. var body struct {
  545. Data struct {
  546. Found bool `json:"found"`
  547. CacheWrite1hPrice *float64 `json:"cache_write_1h_price"`
  548. } `json:"data"`
  549. }
  550. require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body))
  551. require.True(t, body.Data.Found)
  552. require.Nil(t, body.Data.CacheWrite1hPrice)
  553. }