|
| 1 | +import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; |
| 2 | +import { |
| 3 | + parseModelString, |
| 4 | + getLocaleModel, |
| 5 | + validateAndGetApiKeys, |
| 6 | + createAiModel, |
| 7 | + getKeyFromEnv, |
| 8 | +} from "./model-factory"; |
| 9 | + |
| 10 | +vi.mock("dotenv", () => ({ |
| 11 | + config: vi.fn(() => ({ parsed: {} })), |
| 12 | +})); |
| 13 | + |
| 14 | +describe("model-factory", () => { |
| 15 | + describe("parseModelString", () => { |
| 16 | + it("should parse provider:model format correctly", () => { |
| 17 | + const result = parseModelString("openai:gpt-4"); |
| 18 | + expect(result).toEqual({ provider: "openai", name: "gpt-4" }); |
| 19 | + }); |
| 20 | + |
| 21 | + it("should handle model names with colons", () => { |
| 22 | + const result = parseModelString("openai:ft:gpt-4:my-org:custom:id"); |
| 23 | + expect(result).toEqual({ |
| 24 | + provider: "openai", |
| 25 | + name: "ft:gpt-4:my-org:custom:id", |
| 26 | + }); |
| 27 | + }); |
| 28 | + |
| 29 | + it("should handle simple model names with dashes", () => { |
| 30 | + const result = parseModelString("groq:llama3-8b-8192"); |
| 31 | + expect(result).toEqual({ provider: "groq", name: "llama3-8b-8192" }); |
| 32 | + }); |
| 33 | + |
| 34 | + it("should return undefined for invalid format", () => { |
| 35 | + expect(parseModelString("invalid")).toBeUndefined(); |
| 36 | + expect(parseModelString("")).toBeUndefined(); |
| 37 | + }); |
| 38 | + }); |
| 39 | + |
| 40 | + describe("getLocaleModel", () => { |
| 41 | + it("should match exact locale pair", () => { |
| 42 | + const config = { |
| 43 | + "en:fr": "openai:gpt-4", |
| 44 | + "*:*": "groq:llama3-8b-8192", |
| 45 | + }; |
| 46 | + |
| 47 | + const result = getLocaleModel(config, "en", "fr"); |
| 48 | + expect(result).toEqual({ provider: "openai", name: "gpt-4" }); |
| 49 | + }); |
| 50 | + |
| 51 | + it("should fallback to *:targetLocale wildcard pattern", () => { |
| 52 | + const config = { |
| 53 | + "*:fr": "openai:gpt-3.5-turbo", |
| 54 | + "*:*": "groq:llama3-8b-8192", |
| 55 | + }; |
| 56 | + |
| 57 | + const result = getLocaleModel(config, "en", "fr"); |
| 58 | + expect(result).toEqual({ provider: "openai", name: "gpt-3.5-turbo" }); |
| 59 | + }); |
| 60 | + |
| 61 | + it("should fallback to sourceLocale:* wildcard pattern", () => { |
| 62 | + const config = { |
| 63 | + "en:*": "openai:gpt-4", |
| 64 | + "*:*": "groq:llama3-8b-8192", |
| 65 | + }; |
| 66 | + |
| 67 | + const result = getLocaleModel(config, "en", "de"); |
| 68 | + expect(result).toEqual({ provider: "openai", name: "gpt-4" }); |
| 69 | + }); |
| 70 | + |
| 71 | + it("should return undefined when no match found", () => { |
| 72 | + const config = { |
| 73 | + "en:fr": "openai:gpt-4", |
| 74 | + }; |
| 75 | + |
| 76 | + const result = getLocaleModel(config, "de", "es"); |
| 77 | + expect(result).toBeUndefined(); |
| 78 | + }); |
| 79 | + }); |
| 80 | + |
| 81 | + describe("validateAndGetApiKeys", () => { |
| 82 | + const originalEnv = process.env; |
| 83 | + |
| 84 | + beforeEach(() => { |
| 85 | + process.env = { ...originalEnv }; |
| 86 | + }); |
| 87 | + |
| 88 | + afterEach(() => { |
| 89 | + process.env = originalEnv; |
| 90 | + }); |
| 91 | + |
| 92 | + it("should validate and return API keys for configured providers", () => { |
| 93 | + process.env.OPENAI_API_KEY = "test-openai-key"; |
| 94 | + process.env.GROQ_API_KEY = "test-groq-key"; |
| 95 | + |
| 96 | + const config = { |
| 97 | + "*:fr": "openai:gpt-4", |
| 98 | + "*:es": "groq:llama3-8b-8192", |
| 99 | + }; |
| 100 | + |
| 101 | + const result = validateAndGetApiKeys(config); |
| 102 | + expect(result).toEqual({ |
| 103 | + openai: "test-openai-key", |
| 104 | + groq: "test-groq-key", |
| 105 | + }); |
| 106 | + }); |
| 107 | + |
| 108 | + it("should skip providers that don't require API keys (like Ollama)", () => { |
| 109 | + const config = { |
| 110 | + "*:*": "ollama:llama3", |
| 111 | + }; |
| 112 | + |
| 113 | + // Should not throw even without OLLAMA_API_KEY |
| 114 | + const result = validateAndGetApiKeys(config); |
| 115 | + expect(result).toEqual({}); |
| 116 | + }); |
| 117 | + |
| 118 | + it("should throw error for missing required API keys", () => { |
| 119 | + const config = { |
| 120 | + "*:*": "openai:gpt-4", |
| 121 | + }; |
| 122 | + |
| 123 | + expect(() => validateAndGetApiKeys(config)).toThrow(); |
| 124 | + }); |
| 125 | + |
| 126 | + it("should validate lingo.dev provider when specified", () => { |
| 127 | + process.env.LINGODOTDEV_API_KEY = "test-lingo-key"; |
| 128 | + |
| 129 | + const result = validateAndGetApiKeys("lingo.dev"); |
| 130 | + expect(result).toEqual({ |
| 131 | + "lingo.dev": "test-lingo-key", |
| 132 | + }); |
| 133 | + }); |
| 134 | + |
| 135 | + it("should throw error for unknown provider", () => { |
| 136 | + const config = { |
| 137 | + "*:*": "unknownprovider:model", |
| 138 | + }; |
| 139 | + |
| 140 | + expect(() => validateAndGetApiKeys(config)).toThrow( |
| 141 | + /Unknown provider "unknownprovider"/, |
| 142 | + ); |
| 143 | + }); |
| 144 | + }); |
| 145 | + |
| 146 | + describe("createAiModel", () => { |
| 147 | + const originalEnv = process.env; |
| 148 | + |
| 149 | + beforeEach(() => { |
| 150 | + process.env = { ...originalEnv }; |
| 151 | + }); |
| 152 | + |
| 153 | + afterEach(() => { |
| 154 | + process.env = originalEnv; |
| 155 | + }); |
| 156 | + |
| 157 | + it("should support OpenAI with custom baseURL from env", () => { |
| 158 | + process.env.OPENAI_BASE_URL = "https://api.studio.nebius.ai/v1/"; |
| 159 | + process.env.OPENAI_API_KEY = "test-key"; |
| 160 | + |
| 161 | + const model = { provider: "openai", name: "gpt-4" }; |
| 162 | + const keys = { openai: "test-key" }; |
| 163 | + |
| 164 | + const result = createAiModel(model, keys); |
| 165 | + expect(result).toBeDefined(); |
| 166 | + }); |
| 167 | + |
| 168 | + it("should create OpenAI model without baseURL when not set", () => { |
| 169 | + delete process.env.OPENAI_BASE_URL; |
| 170 | + process.env.OPENAI_API_KEY = "test-key"; |
| 171 | + |
| 172 | + const model = { provider: "openai", name: "gpt-4" }; |
| 173 | + const keys = { openai: "test-key" }; |
| 174 | + |
| 175 | + const result = createAiModel(model, keys); |
| 176 | + expect(result).toBeDefined(); |
| 177 | + }); |
| 178 | + |
| 179 | + it("should create Groq model", () => { |
| 180 | + const model = { provider: "groq", name: "llama3-8b-8192" }; |
| 181 | + const keys = { groq: "test-groq-key" }; |
| 182 | + |
| 183 | + const result = createAiModel(model, keys); |
| 184 | + expect(result).toBeDefined(); |
| 185 | + }); |
| 186 | + |
| 187 | + it("should create Google model", () => { |
| 188 | + const model = { provider: "google", name: "gemini-pro" }; |
| 189 | + const keys = { google: "test-google-key" }; |
| 190 | + |
| 191 | + const result = createAiModel(model, keys); |
| 192 | + expect(result).toBeDefined(); |
| 193 | + }); |
| 194 | + |
| 195 | + it("should create OpenRouter model", () => { |
| 196 | + const model = { provider: "openrouter", name: "anthropic/claude-3-opus" }; |
| 197 | + const keys = { openrouter: "test-openrouter-key" }; |
| 198 | + |
| 199 | + const result = createAiModel(model, keys); |
| 200 | + expect(result).toBeDefined(); |
| 201 | + }); |
| 202 | + |
| 203 | + it("should create Ollama model without API key", () => { |
| 204 | + const model = { provider: "ollama", name: "llama3" }; |
| 205 | + const keys = {}; |
| 206 | + |
| 207 | + const result = createAiModel(model, keys); |
| 208 | + expect(result).toBeDefined(); |
| 209 | + }); |
| 210 | + |
| 211 | + it("should create Mistral model", () => { |
| 212 | + const model = { provider: "mistral", name: "mistral-large-latest" }; |
| 213 | + const keys = { mistral: "test-mistral-key" }; |
| 214 | + |
| 215 | + const result = createAiModel(model, keys); |
| 216 | + expect(result).toBeDefined(); |
| 217 | + }); |
| 218 | + |
| 219 | + it("should throw error for unsupported provider", () => { |
| 220 | + const model = { provider: "unsupported", name: "model" }; |
| 221 | + const keys = {}; |
| 222 | + |
| 223 | + expect(() => createAiModel(model, keys)).toThrow( |
| 224 | + /Provider "unsupported" is not supported/, |
| 225 | + ); |
| 226 | + }); |
| 227 | + }); |
| 228 | + |
| 229 | + describe("getKeyFromEnv", () => { |
| 230 | + const originalEnv = process.env; |
| 231 | + |
| 232 | + beforeEach(() => { |
| 233 | + process.env = { ...originalEnv }; |
| 234 | + }); |
| 235 | + |
| 236 | + afterEach(() => { |
| 237 | + process.env = originalEnv; |
| 238 | + }); |
| 239 | + |
| 240 | + it("should get key from process.env", () => { |
| 241 | + process.env.TEST_KEY = "test-value"; |
| 242 | + expect(getKeyFromEnv("TEST_KEY")).toBe("test-value"); |
| 243 | + }); |
| 244 | + |
| 245 | + it("should return undefined for missing key", () => { |
| 246 | + expect(getKeyFromEnv("NONEXISTENT_KEY")).toBeUndefined(); |
| 247 | + }); |
| 248 | + }); |
| 249 | +}); |
0 commit comments