pricing-resolution.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { describe, expect, test } from "vitest";
  2. import type { ModelPrice } from "@/types/model-price";
  3. import { resolvePricingForModelRecords } from "@/lib/utils/pricing-resolution";
  4. function makeRecord(
  5. modelName: string,
  6. priceData: ModelPrice["priceData"],
  7. source: ModelPrice["source"] = "litellm"
  8. ): ModelPrice {
  9. const now = new Date("2026-03-06T00:00:00.000Z");
  10. return {
  11. id: Math.floor(Math.random() * 100000),
  12. modelName,
  13. priceData,
  14. source,
  15. createdAt: now,
  16. updatedAt: now,
  17. };
  18. }
  19. describe("resolvePricingForModelRecords", () => {
  20. test("falls back from chatgpt to openai pricing for gpt-5.4 alias models", () => {
  21. const aliasRecord = makeRecord("gpt-5.4", {
  22. mode: "responses",
  23. model_family: "gpt",
  24. litellm_provider: "chatgpt",
  25. pricing: {
  26. openai: {
  27. input_cost_per_token: 0.0000025,
  28. output_cost_per_token: 0.000015,
  29. cache_read_input_token_cost: 2.5e-7,
  30. },
  31. openrouter: {
  32. input_cost_per_token: 0.0000025,
  33. output_cost_per_token: 0.000015,
  34. cache_read_input_token_cost: 2.5e-7,
  35. },
  36. },
  37. });
  38. const resolved = resolvePricingForModelRecords({
  39. provider: {
  40. id: 1,
  41. name: "ChatGPT",
  42. url: "https://chatgpt.com/backend-api/codex",
  43. } as never,
  44. primaryModelName: "gpt-5.4",
  45. fallbackModelName: null,
  46. primaryRecord: aliasRecord,
  47. fallbackRecord: null,
  48. });
  49. expect(resolved).not.toBeNull();
  50. expect(resolved?.resolvedPricingProviderKey).toBe("openai");
  51. expect(resolved?.source).toBe("official_fallback");
  52. expect(resolved?.priceData.input_cost_per_token).toBe(0.0000025);
  53. });
  54. test("falls back from redirected date model to alias model for provider-specific pricing", () => {
  55. const datedRecord = makeRecord("gpt-5.4-2026-03-05", {
  56. mode: "responses",
  57. model_family: "gpt",
  58. litellm_provider: "openai",
  59. input_cost_per_token: 0.0000025,
  60. output_cost_per_token: 0.000015,
  61. cache_read_input_token_cost: 2.5e-7,
  62. pricing: {
  63. openai: {
  64. input_cost_per_token: 0.0000025,
  65. output_cost_per_token: 0.000015,
  66. cache_read_input_token_cost: 2.5e-7,
  67. },
  68. },
  69. });
  70. const aliasRecord = makeRecord("gpt-5.4", {
  71. mode: "responses",
  72. model_family: "gpt",
  73. litellm_provider: "chatgpt",
  74. pricing: {
  75. openrouter: {
  76. input_cost_per_token: 0.0000025,
  77. output_cost_per_token: 0.000015,
  78. cache_read_input_token_cost: 2.5e-7,
  79. },
  80. },
  81. });
  82. const resolved = resolvePricingForModelRecords({
  83. provider: {
  84. id: 2,
  85. name: "OpenRouter",
  86. url: "https://openrouter.ai/api/v1",
  87. } as never,
  88. primaryModelName: "gpt-5.4-2026-03-05",
  89. fallbackModelName: "gpt-5.4",
  90. primaryRecord: datedRecord,
  91. fallbackRecord: aliasRecord,
  92. });
  93. expect(resolved).not.toBeNull();
  94. expect(resolved?.resolvedModelName).toBe("gpt-5.4");
  95. expect(resolved?.resolvedPricingProviderKey).toBe("openrouter");
  96. expect(resolved?.source).toBe("cloud_model_fallback");
  97. });
  98. test("prefers local manual prices over cloud multi-provider pricing", () => {
  99. const manualRecord = makeRecord(
  100. "gpt-5.4",
  101. {
  102. mode: "responses",
  103. input_cost_per_token: 0.0000099,
  104. output_cost_per_token: 0.0000199,
  105. selected_pricing_provider: "manual-custom",
  106. },
  107. "manual"
  108. );
  109. const cloudRecord = makeRecord("gpt-5.4", {
  110. mode: "responses",
  111. pricing: {
  112. openai: {
  113. input_cost_per_token: 0.0000025,
  114. output_cost_per_token: 0.000015,
  115. },
  116. },
  117. });
  118. const resolved = resolvePricingForModelRecords({
  119. provider: {
  120. id: 1,
  121. name: "ChatGPT",
  122. url: "https://chatgpt.com/backend-api/codex",
  123. } as never,
  124. primaryModelName: "gpt-5.4",
  125. fallbackModelName: null,
  126. primaryRecord: manualRecord,
  127. fallbackRecord: cloudRecord,
  128. });
  129. expect(resolved).not.toBeNull();
  130. expect(resolved?.source).toBe("local_manual");
  131. expect(resolved?.priceData.input_cost_per_token).toBe(0.0000099);
  132. expect(resolved?.resolvedPricingProviderKey).toBe("manual-custom");
  133. });
  134. });