provider-selector-group-priority.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { describe, expect, it } from "vitest";
  2. import type { Provider } from "@/types/provider";
  3. import { ProxyProviderResolver } from "@/app/v1/_lib/proxy/provider-selector";
  4. function makeProvider(overrides: Partial<Provider>): Provider {
  5. return {
  6. id: 1,
  7. name: "test",
  8. url: "https://api.example.com",
  9. key: "sk-test",
  10. providerVendorId: null,
  11. isEnabled: true,
  12. weight: 1,
  13. priority: 0,
  14. groupPriorities: null,
  15. costMultiplier: 1,
  16. groupTag: null,
  17. providerType: "claude",
  18. preserveClientIp: false,
  19. modelRedirects: null,
  20. allowedModels: null,
  21. mcpPassthroughType: "none",
  22. mcpPassthroughUrl: null,
  23. limit5hUsd: null,
  24. limitDailyUsd: null,
  25. dailyResetMode: "fixed",
  26. dailyResetTime: "00:00",
  27. limitWeeklyUsd: null,
  28. limitMonthlyUsd: null,
  29. limitTotalUsd: null,
  30. totalCostResetAt: null,
  31. limitConcurrentSessions: 0,
  32. maxRetryAttempts: null,
  33. circuitBreakerFailureThreshold: 5,
  34. circuitBreakerOpenDuration: 1800000,
  35. circuitBreakerHalfOpenSuccessThreshold: 2,
  36. proxyUrl: null,
  37. proxyFallbackToDirect: false,
  38. firstByteTimeoutStreamingMs: 30000,
  39. streamingIdleTimeoutMs: 10000,
  40. requestTimeoutNonStreamingMs: 600000,
  41. websiteUrl: null,
  42. faviconUrl: null,
  43. cacheTtlPreference: null,
  44. context1mPreference: null,
  45. codexReasoningEffortPreference: null,
  46. codexReasoningSummaryPreference: null,
  47. codexTextVerbosityPreference: null,
  48. codexParallelToolCallsPreference: null,
  49. anthropicMaxTokensPreference: null,
  50. anthropicThinkingBudgetPreference: null,
  51. geminiGoogleSearchPreference: null,
  52. tpm: null,
  53. rpm: null,
  54. rpd: null,
  55. cc: null,
  56. createdAt: new Date(),
  57. updatedAt: new Date(),
  58. ...overrides,
  59. };
  60. }
  61. describe("resolveEffectivePriority", () => {
  62. it("returns global priority when no groupPriorities", () => {
  63. const provider = makeProvider({ priority: 5, groupPriorities: null });
  64. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli")).toBe(5);
  65. });
  66. it("returns group-specific priority when override exists", () => {
  67. const provider = makeProvider({
  68. priority: 5,
  69. groupPriorities: { cli: 0, chat: 2 },
  70. });
  71. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli")).toBe(0);
  72. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "chat")).toBe(2);
  73. });
  74. it("falls back to global when group not in overrides", () => {
  75. const provider = makeProvider({
  76. priority: 5,
  77. groupPriorities: { cli: 0 },
  78. });
  79. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "chat")).toBe(5);
  80. });
  81. it("returns global priority when userGroup is null", () => {
  82. const provider = makeProvider({
  83. priority: 5,
  84. groupPriorities: { cli: 0 },
  85. });
  86. expect(ProxyProviderResolver.resolveEffectivePriority(provider, null)).toBe(5);
  87. });
  88. it("handles group priority of 0 correctly (not falsy)", () => {
  89. const provider = makeProvider({
  90. priority: 5,
  91. groupPriorities: { cli: 0 },
  92. });
  93. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli")).toBe(0);
  94. });
  95. it("handles comma-separated user groups (multi-group)", () => {
  96. const provider = makeProvider({
  97. priority: 10,
  98. groupPriorities: { cli: 2, admin: 5, chat: 8 },
  99. });
  100. // Multi-group "cli,admin" should match both and take minimum (2)
  101. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli,admin")).toBe(2);
  102. // Multi-group "admin,chat" should take minimum (5)
  103. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "admin,chat")).toBe(5);
  104. });
  105. it("falls back to global when no group in multi-group matches", () => {
  106. const provider = makeProvider({
  107. priority: 10,
  108. groupPriorities: { cli: 2 },
  109. });
  110. // "admin,chat" has no matching overrides, should fall back to global (10)
  111. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "admin,chat")).toBe(10);
  112. });
  113. it("handles partial match in multi-group", () => {
  114. const provider = makeProvider({
  115. priority: 10,
  116. groupPriorities: { cli: 3 },
  117. });
  118. // "cli,admin" - only "cli" matches, should return 3
  119. expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli,admin")).toBe(3);
  120. });
  121. });
  122. describe("selectTopPriority with group context", () => {
  123. // Access private method via bracket notation for testing
  124. const selectTopPriority = (providers: Provider[], userGroup?: string | null) =>
  125. (ProxyProviderResolver as any).selectTopPriority(providers, userGroup);
  126. it("selects providers by group-aware priority", () => {
  127. const providerA = makeProvider({
  128. id: 1,
  129. name: "A",
  130. priority: 5,
  131. groupPriorities: { cli: 0 },
  132. });
  133. const providerB = makeProvider({
  134. id: 2,
  135. name: "B",
  136. priority: 0,
  137. groupPriorities: null,
  138. });
  139. // cli group: A has effective priority 0, B has effective priority 0
  140. const result = selectTopPriority([providerA, providerB], "cli");
  141. expect(result).toHaveLength(2);
  142. expect(result.map((p: Provider) => p.id).sort()).toEqual([1, 2]);
  143. });
  144. it("without group context, uses global priority", () => {
  145. const providerA = makeProvider({
  146. id: 1,
  147. name: "A",
  148. priority: 5,
  149. groupPriorities: { cli: 0 },
  150. });
  151. const providerB = makeProvider({
  152. id: 2,
  153. name: "B",
  154. priority: 0,
  155. groupPriorities: null,
  156. });
  157. // no group: A has priority 5, B has priority 0 -> only B selected
  158. const result = selectTopPriority([providerA, providerB], null);
  159. expect(result).toHaveLength(1);
  160. expect(result[0].id).toBe(2);
  161. });
  162. it("group override changes which providers are top priority", () => {
  163. const providerA = makeProvider({
  164. id: 1,
  165. name: "A",
  166. priority: 5,
  167. groupPriorities: { chat: 1 },
  168. });
  169. const providerB = makeProvider({
  170. id: 2,
  171. name: "B",
  172. priority: 3,
  173. groupPriorities: null,
  174. });
  175. // chat group: A=1, B=3 -> only A
  176. const chatResult = selectTopPriority([providerA, providerB], "chat");
  177. expect(chatResult).toHaveLength(1);
  178. expect(chatResult[0].id).toBe(1);
  179. // no group: A=5, B=3 -> only B
  180. const noGroupResult = selectTopPriority([providerA, providerB], null);
  181. expect(noGroupResult).toHaveLength(1);
  182. expect(noGroupResult[0].id).toBe(2);
  183. });
  184. });