available-models.test.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import { describe, expect, test, vi } from "vitest";
  2. // Mock dependencies that cause import issues
  3. vi.mock("@/lib/proxy-agent", () => ({
  4. createProxyAgentForProvider: vi.fn(),
  5. }));
  6. vi.mock("@/repository/key", () => ({
  7. validateApiKeyAndGetUser: vi.fn(),
  8. }));
  9. vi.mock("@/app/v1/_lib/proxy/provider-selector", () => ({
  10. checkProviderGroupMatch: vi.fn(),
  11. }));
  12. vi.mock("@/repository/provider", () => ({
  13. findAllProviders: vi.fn().mockResolvedValue([]),
  14. }));
  15. vi.mock("@/lib/utils/provider-schedule", () => ({
  16. isProviderActiveNow: vi.fn().mockReturnValue(true),
  17. }));
  18. vi.mock("@/lib/utils/timezone", () => ({
  19. resolveSystemTimezone: vi.fn().mockResolvedValue("UTC"),
  20. }));
  21. import {
  22. formatAnthropicResponse,
  23. formatGeminiResponse,
  24. formatOpenAIResponse,
  25. getProviderTypesForFormat,
  26. inferOwner,
  27. type FetchedModel,
  28. } from "@/app/v1/_lib/models/available-models";
  29. import type {
  30. AnthropicModelsResponse,
  31. GeminiModelsResponse,
  32. OpenAIModelsResponse,
  33. } from "@/types/models";
  34. describe("inferOwner - 根据模型 ID 推断所有者", () => {
  35. describe("Anthropic 模型", () => {
  36. test("claude-* 模型应返回 anthropic", () => {
  37. expect(inferOwner("claude-3-opus-20240229")).toBe("anthropic");
  38. expect(inferOwner("claude-3-sonnet-20240229")).toBe("anthropic");
  39. expect(inferOwner("claude-3-haiku-20240307")).toBe("anthropic");
  40. expect(inferOwner("claude-2.1")).toBe("anthropic");
  41. expect(inferOwner("claude-instant-1.2")).toBe("anthropic");
  42. });
  43. });
  44. describe("OpenAI 模型", () => {
  45. test("gpt-* 模型应返回 openai", () => {
  46. expect(inferOwner("gpt-4")).toBe("openai");
  47. expect(inferOwner("gpt-4-turbo")).toBe("openai");
  48. expect(inferOwner("gpt-3.5-turbo")).toBe("openai");
  49. expect(inferOwner("gpt-4o")).toBe("openai");
  50. });
  51. test("o1* 模型应返回 openai", () => {
  52. expect(inferOwner("o1-preview")).toBe("openai");
  53. expect(inferOwner("o1-mini")).toBe("openai");
  54. expect(inferOwner("o1")).toBe("openai");
  55. });
  56. test("o3* 模型应返回 openai", () => {
  57. expect(inferOwner("o3-mini")).toBe("openai");
  58. expect(inferOwner("o3")).toBe("openai");
  59. });
  60. });
  61. describe("Google 模型", () => {
  62. test("gemini-* 模型应返回 google", () => {
  63. expect(inferOwner("gemini-pro")).toBe("google");
  64. expect(inferOwner("gemini-1.5-pro")).toBe("google");
  65. expect(inferOwner("gemini-1.5-flash")).toBe("google");
  66. expect(inferOwner("gemini-2.0-flash-exp")).toBe("google");
  67. });
  68. });
  69. describe("DeepSeek 模型", () => {
  70. test("deepseek* 模型应返回 deepseek", () => {
  71. expect(inferOwner("deepseek-chat")).toBe("deepseek");
  72. expect(inferOwner("deepseek-coder")).toBe("deepseek");
  73. expect(inferOwner("deepseek-v3")).toBe("deepseek");
  74. });
  75. });
  76. describe("Alibaba 模型", () => {
  77. test("qwen* 模型应返回 alibaba", () => {
  78. expect(inferOwner("qwen-turbo")).toBe("alibaba");
  79. expect(inferOwner("qwen-plus")).toBe("alibaba");
  80. expect(inferOwner("qwen-max")).toBe("alibaba");
  81. });
  82. });
  83. describe("未知模型", () => {
  84. test("无法识别的模型应返回 unknown", () => {
  85. expect(inferOwner("llama-2-70b")).toBe("unknown");
  86. expect(inferOwner("mistral-7b")).toBe("unknown");
  87. expect(inferOwner("custom-model")).toBe("unknown");
  88. });
  89. });
  90. });
  91. describe("getProviderTypesForFormat - 客户端格式到 Provider 类型映射", () => {
  92. test("claude 格式应返回 claude 和 claude-auth 类型", () => {
  93. expect(getProviderTypesForFormat("claude")).toEqual(["claude", "claude-auth"]);
  94. });
  95. test("openai 格式应返回 codex 和 openai-compatible 类型", () => {
  96. expect(getProviderTypesForFormat("openai")).toEqual(["codex", "openai-compatible"]);
  97. });
  98. test("gemini 格式应返回 gemini 和 gemini-cli 类型", () => {
  99. expect(getProviderTypesForFormat("gemini")).toEqual(["gemini", "gemini-cli"]);
  100. });
  101. test("gemini-cli 格式应返回 gemini 和 gemini-cli 类型", () => {
  102. expect(getProviderTypesForFormat("gemini-cli")).toEqual(["gemini", "gemini-cli"]);
  103. });
  104. test("response 格式应仅返回 codex 类型", () => {
  105. expect(getProviderTypesForFormat("response")).toEqual(["codex"]);
  106. });
  107. });
  108. describe("formatOpenAIResponse - OpenAI 格式响应", () => {
  109. test("空模型列表应返回空 data 数组", () => {
  110. const result: OpenAIModelsResponse = formatOpenAIResponse([]);
  111. expect(result.object).toBe("list");
  112. expect(result.data).toEqual([]);
  113. });
  114. test("应正确格式化模型列表", () => {
  115. const models: FetchedModel[] = [
  116. { id: "gpt-4" },
  117. { id: "claude-3-opus-20240229" },
  118. { id: "gemini-pro" },
  119. ];
  120. const result: OpenAIModelsResponse = formatOpenAIResponse(models);
  121. expect(result.object).toBe("list");
  122. expect(result.data).toHaveLength(3);
  123. expect(result.data[0].id).toBe("gpt-4");
  124. expect(result.data[0].object).toBe("model");
  125. expect(result.data[0].owned_by).toBe("openai");
  126. expect(typeof result.data[0].created).toBe("number");
  127. expect(result.data[1].id).toBe("claude-3-opus-20240229");
  128. expect(result.data[1].owned_by).toBe("anthropic");
  129. expect(result.data[2].id).toBe("gemini-pro");
  130. expect(result.data[2].owned_by).toBe("google");
  131. });
  132. test("created 时间戳应为当前时间(秒)", () => {
  133. const before = Math.floor(Date.now() / 1000);
  134. const result: OpenAIModelsResponse = formatOpenAIResponse([{ id: "test" }]);
  135. const after = Math.floor(Date.now() / 1000);
  136. expect(result.data[0].created).toBeGreaterThanOrEqual(before);
  137. expect(result.data[0].created).toBeLessThanOrEqual(after);
  138. });
  139. });
  140. describe("formatAnthropicResponse - Anthropic 格式响应", () => {
  141. test("空模型列表应返回空 data 数组", () => {
  142. const result: AnthropicModelsResponse = formatAnthropicResponse([]);
  143. expect(result.data).toEqual([]);
  144. expect(result.has_more).toBe(false);
  145. });
  146. test("应正确格式化模型列表", () => {
  147. const models: FetchedModel[] = [
  148. {
  149. id: "claude-3-opus-20240229",
  150. displayName: "Claude 3 Opus",
  151. createdAt: "2024-02-29T00:00:00Z",
  152. },
  153. { id: "claude-3-sonnet-20240229" },
  154. ];
  155. const result: AnthropicModelsResponse = formatAnthropicResponse(models);
  156. expect(result.has_more).toBe(false);
  157. expect(result.data).toHaveLength(2);
  158. expect(result.data[0].id).toBe("claude-3-opus-20240229");
  159. expect(result.data[0].type).toBe("model");
  160. expect(result.data[0].display_name).toBe("Claude 3 Opus");
  161. expect(result.data[0].created_at).toBe("2024-02-29T00:00:00Z");
  162. expect(result.data[1].id).toBe("claude-3-sonnet-20240229");
  163. expect(result.data[1].display_name).toBe("claude-3-sonnet-20240229");
  164. expect(result.data[1].created_at).toBeDefined();
  165. });
  166. test("缺少 displayName 时应使用 id 作为 display_name", () => {
  167. const result: AnthropicModelsResponse = formatAnthropicResponse([{ id: "test-model" }]);
  168. expect(result.data[0].display_name).toBe("test-model");
  169. });
  170. });
  171. describe("formatGeminiResponse - Gemini 格式响应", () => {
  172. test("空模型列表应返回空 models 数组", () => {
  173. const result: GeminiModelsResponse = formatGeminiResponse([]);
  174. expect(result.models).toEqual([]);
  175. });
  176. test("应正确格式化模型列表", () => {
  177. const models: FetchedModel[] = [
  178. { id: "gemini-pro", displayName: "Gemini Pro" },
  179. { id: "gemini-1.5-flash" },
  180. ];
  181. const result: GeminiModelsResponse = formatGeminiResponse(models);
  182. expect(result.models).toHaveLength(2);
  183. expect(result.models[0].name).toBe("models/gemini-pro");
  184. expect(result.models[0].displayName).toBe("Gemini Pro");
  185. expect(result.models[0].supportedGenerationMethods).toEqual(["generateContent"]);
  186. expect(result.models[1].name).toBe("models/gemini-1.5-flash");
  187. expect(result.models[1].displayName).toBe("gemini-1.5-flash");
  188. });
  189. test("模型名称应添加 models/ 前缀", () => {
  190. const result: GeminiModelsResponse = formatGeminiResponse([{ id: "test-model" }]);
  191. expect(result.models[0].name).toBe("models/test-model");
  192. });
  193. test("缺少 displayName 时应使用 id", () => {
  194. const result: GeminiModelsResponse = formatGeminiResponse([{ id: "test-model" }]);
  195. expect(result.models[0].displayName).toBe("test-model");
  196. });
  197. });