diff --git a/README.md b/README.md index a3e24280..74875e4d 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ Langtrace automatically captures traces from the following vendors and framework | xAI | ✅ | ✅ | | Groq | ✅ | ✅ | | Perplexity | ✅ | ✅ | +| MiniMax | ✅ | ❌ | | Gemini | ✅ | ✅ | | AWS Bedrock | ✅ | ✅ | | Mistral | ❌ | ✅ | diff --git a/__tests__/minimax/minimax-integration.test.ts b/__tests__/minimax/minimax-integration.test.ts new file mode 100644 index 00000000..a45b63cf --- /dev/null +++ b/__tests__/minimax/minimax-integration.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect } from "vitest"; + +// Integration tests for MiniMax provider +// These tests verify the end-to-end integration of MiniMax components + +describe("MiniMax Integration - Provider Registration", () => { + it("should have MiniMax registered in all required constants", async () => { + const { LLM_VENDORS, LLM_VENDOR_APIS, SUPPORTED_VENDORS, MINIMAX_PRICING } = + await import("../../lib/constants"); + + // Verify consistent registration across all provider lists + const vendorEntry = LLM_VENDORS.find((v) => v.value === "minimax"); + const apiEntry = LLM_VENDOR_APIS.find( + (v) => v.value === "MINIMAX_API_KEY" + ); + + expect(vendorEntry).toBeDefined(); + expect(apiEntry).toBeDefined(); + expect(SUPPORTED_VENDORS.MINIMAX).toBe("MiniMax"); + expect(Object.keys(MINIMAX_PRICING).length).toBeGreaterThan(0); + }); + + it("should have matching models between types and pricing", async () => { + const { MINIMAX_PRICING } = await import("../../lib/constants"); + const { minimaxModels } = await import( + "../../lib/types/playground_types" + ); + + // Every model in the dropdown should have pricing + for (const model of minimaxModels) { + expect( + MINIMAX_PRICING[model.value], + `Missing pricing for model: ${model.value}` + ).toBeDefined(); + } + }); + + it("should have chat handler aligned with API route", async () => { + const handlers = await import( + "../../components/playground/chat-handlers" + ); + + // Verify the handler exists and is a function + expect(typeof handlers.minimaxHandler).toBe("function"); + + // The handler should accept two parameters (llm config and api key) + expect(handlers.minimaxHandler.length).toBe(2); + }); +}); + +describe("MiniMax Integration - API Route File", () => { + it("should have MiniMax API route at correct path", async () => { + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + expect(fs.existsSync(routePath)).toBe(true); + }); + + it("should configure OpenAI client with MiniMax base URL", async () => { + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + const content = fs.readFileSync(routePath, "utf-8"); + + expect(content).toContain("https://api.minimax.io/v1"); + expect(content).toContain("new OpenAI"); + }); + + it("should implement temperature clamping in route", async () => { + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + const content = fs.readFileSync(routePath, "utf-8"); + + expect(content).toContain("Math.min(Math.max(data.temperature, 0), 1)"); + }); + + it("should support streaming via OpenAIStream", async () => { + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + const content = fs.readFileSync(routePath, "utf-8"); + + expect(content).toContain("OpenAIStream"); + expect(content).toContain("StreamingTextResponse"); + }); + + it("should follow same pattern as perplexity route", async () => { + const fs = await import("fs"); + const path = await import("path"); + const minimaxPath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + const perplexityPath = path.resolve( + __dirname, + "../../app/api/chat/perplexity/route.ts" + ); + const minimaxContent = fs.readFileSync(minimaxPath, "utf-8"); + const perplexityContent = fs.readFileSync(perplexityPath, "utf-8"); + + // Both should use OpenAI client, OpenAIStream, StreamingTextResponse + expect(minimaxContent).toContain("import OpenAI from"); + expect(perplexityContent).toContain("import OpenAI from"); + expect(minimaxContent).toContain("OpenAIStream"); + expect(perplexityContent).toContain("OpenAIStream"); + }); +}); + +describe("MiniMax Integration - Vendor Metadata", () => { + it("should have vendor badge color for minimax", async () => { + const fs = await import("fs"); + const path = await import("path"); + const vendorPath = path.resolve( + __dirname, + "../../components/shared/vendor-metadata.tsx" + ); + const content = fs.readFileSync(vendorPath, "utf-8"); + + expect(content).toContain('vendor.includes("minimax")'); + }); + + it("should have vendor color for minimax", async () => { + const fs = await import("fs"); + const path = await import("path"); + const vendorPath = path.resolve( + __dirname, + "../../components/shared/vendor-metadata.tsx" + ); + const content = fs.readFileSync(vendorPath, "utf-8"); + + // Should appear in both vendorBadgeColor and vendorColor + const matches = content.match(/minimax/g); + expect(matches).toBeDefined(); + expect(matches!.length).toBeGreaterThanOrEqual(3); // badge, color, and logo + }); +}); diff --git a/__tests__/minimax/minimax-unit.test.ts b/__tests__/minimax/minimax-unit.test.ts new file mode 100644 index 00000000..2b1afcef --- /dev/null +++ b/__tests__/minimax/minimax-unit.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect } from "vitest"; + +// Unit tests for MiniMax constants and types +describe("MiniMax Constants", () => { + it("should include MiniMax in LLM_VENDORS", async () => { + const { LLM_VENDORS } = await import("../../lib/constants"); + const minimax = LLM_VENDORS.find((v) => v.value === "minimax"); + expect(minimax).toBeDefined(); + expect(minimax?.label).toBe("MiniMax"); + }); + + it("should include MINIMAX_API_KEY in LLM_VENDOR_APIS", async () => { + const { LLM_VENDOR_APIS } = await import("../../lib/constants"); + const minimax = LLM_VENDOR_APIS.find( + (v) => v.value === "MINIMAX_API_KEY" + ); + expect(minimax).toBeDefined(); + expect(minimax?.label).toBe("MiniMax"); + }); + + it("should include MINIMAX in SUPPORTED_VENDORS", async () => { + const { SUPPORTED_VENDORS } = await import("../../lib/constants"); + expect(SUPPORTED_VENDORS.MINIMAX).toBe("MiniMax"); + }); + + it("should have MINIMAX_PRICING with correct models", async () => { + const { MINIMAX_PRICING } = await import("../../lib/constants"); + expect(MINIMAX_PRICING).toBeDefined(); + expect(MINIMAX_PRICING["MiniMax-M2.7"]).toBeDefined(); + expect(MINIMAX_PRICING["MiniMax-M2.7-highspeed"]).toBeDefined(); + expect(MINIMAX_PRICING["MiniMax-M2.5"]).toBeDefined(); + expect(MINIMAX_PRICING["MiniMax-M2.5-highspeed"]).toBeDefined(); + }); + + it("should have valid pricing format for all MiniMax models", async () => { + const { MINIMAX_PRICING } = await import("../../lib/constants"); + for (const [model, pricing] of Object.entries(MINIMAX_PRICING)) { + expect(pricing.input).toBeGreaterThan(0); + expect(pricing.output).toBeGreaterThan(0); + expect(typeof pricing.input).toBe("number"); + expect(typeof pricing.output).toBe("number"); + } + }); +}); + +describe("MiniMax Models", () => { + it("should export minimaxModels array", async () => { + const { minimaxModels } = await import( + "../../lib/types/playground_types" + ); + expect(minimaxModels).toBeDefined(); + expect(Array.isArray(minimaxModels)).toBe(true); + expect(minimaxModels.length).toBe(4); + }); + + it("should have correct model values", async () => { + const { minimaxModels } = await import( + "../../lib/types/playground_types" + ); + const values = minimaxModels.map((m) => m.value); + expect(values).toContain("MiniMax-M2.7"); + expect(values).toContain("MiniMax-M2.7-highspeed"); + expect(values).toContain("MiniMax-M2.5"); + expect(values).toContain("MiniMax-M2.5-highspeed"); + }); + + it("should have labels for all models", async () => { + const { minimaxModels } = await import( + "../../lib/types/playground_types" + ); + for (const model of minimaxModels) { + expect(model.label).toBeDefined(); + expect(model.label.length).toBeGreaterThan(0); + } + }); +}); + +describe("MiniMax Settings Interface", () => { + it("should allow creating MiniMaxSettings objects", async () => { + const settings = { + messages: [ + { id: "1", role: "user" as const, content: "Hello" }, + ], + model: "MiniMax-M2.7", + temperature: 0.7, + max_tokens: 1024, + stream: false, + top_p: 0.9, + }; + expect(settings.model).toBe("MiniMax-M2.7"); + expect(settings.temperature).toBe(0.7); + expect(settings.max_tokens).toBe(1024); + }); + + it("should accept temperature in [0, 1] range", () => { + const validTemperatures = [0, 0.1, 0.5, 0.7, 1.0]; + for (const temp of validTemperatures) { + expect(temp >= 0 && temp <= 1).toBe(true); + } + }); +}); + +describe("MiniMax Chat Handler", () => { + it("should export minimaxHandler function", async () => { + const handlers = await import( + "../../components/playground/chat-handlers" + ); + expect(handlers.minimaxHandler).toBeDefined(); + expect(typeof handlers.minimaxHandler).toBe("function"); + }); +}); + +describe("MiniMax API Route", () => { + it("should have route file at correct path", async () => { + // Verify the route file exists by checking the file system + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + expect(fs.existsSync(routePath)).toBe(true); + }); + + it("should contain POST handler and OpenAI client with MiniMax baseURL", async () => { + const fs = await import("fs"); + const path = await import("path"); + const routePath = path.resolve( + __dirname, + "../../app/api/chat/minimax/route.ts" + ); + const content = fs.readFileSync(routePath, "utf-8"); + + // Verify key implementation details + expect(content).toContain("export async function POST"); + expect(content).toContain("https://api.minimax.io/v1"); + expect(content).toContain("OpenAI"); + expect(content).toContain("OpenAIStream"); + expect(content).toContain("Math.min(Math.max"); + }); +}); + +describe("MiniMax Temperature Clamping", () => { + it("should clamp temperature values to [0, 1]", () => { + // Simulate the clamping logic from the chat handler and route + const clamp = (temp: number) => Math.min(Math.max(temp, 0), 1); + + expect(clamp(0)).toBe(0); + expect(clamp(0.5)).toBe(0.5); + expect(clamp(1)).toBe(1); + expect(clamp(1.5)).toBe(1); + expect(clamp(2)).toBe(1); + expect(clamp(-0.5)).toBe(0); + }); +}); + +describe("MiniMax Pricing Calculations", () => { + it("should have M2.7 priced higher than M2.7-highspeed", async () => { + const { MINIMAX_PRICING } = await import("../../lib/constants"); + expect(MINIMAX_PRICING["MiniMax-M2.7"].input).toBeGreaterThan( + MINIMAX_PRICING["MiniMax-M2.7-highspeed"].input + ); + expect(MINIMAX_PRICING["MiniMax-M2.7"].output).toBeGreaterThan( + MINIMAX_PRICING["MiniMax-M2.7-highspeed"].output + ); + }); + + it("should have M2.5 priced higher than M2.5-highspeed", async () => { + const { MINIMAX_PRICING } = await import("../../lib/constants"); + expect(MINIMAX_PRICING["MiniMax-M2.5"].input).toBeGreaterThan( + MINIMAX_PRICING["MiniMax-M2.5-highspeed"].input + ); + expect(MINIMAX_PRICING["MiniMax-M2.5"].output).toBeGreaterThan( + MINIMAX_PRICING["MiniMax-M2.5-highspeed"].output + ); + }); +}); diff --git a/app/api/chat/minimax/route.ts b/app/api/chat/minimax/route.ts new file mode 100644 index 00000000..7adf795c --- /dev/null +++ b/app/api/chat/minimax/route.ts @@ -0,0 +1,41 @@ +import { OpenAIStream, StreamingTextResponse } from "ai"; +import { NextResponse } from "next/server"; +import OpenAI from "openai"; + +export async function POST(req: Request) { + try { + const data = await req.json(); + const isStream = data.stream; + const apiKey = data.apiKey; + + delete data.apiKey; + + // MiniMax uses an OpenAI-compatible API + const minimax = new OpenAI({ + apiKey: apiKey || "", + baseURL: "https://api.minimax.io/v1", + }); + + // Clamp temperature to MiniMax's supported range [0, 1] + if (data.temperature !== undefined) { + data.temperature = Math.min(Math.max(data.temperature, 0), 1); + } + + const response = await minimax.chat.completions.create({ + ...data, + }); + + // Convert the response into a friendly text-stream + if (isStream) { + const stream = OpenAIStream(response as any); + return new StreamingTextResponse(stream); + } + + return NextResponse.json(response); + } catch (error: any) { + return NextResponse.json({ + error: error?.message || "Something went wrong", + status: error?.status || 500, + }); + } +} diff --git a/components/playground/chat-handlers.ts b/components/playground/chat-handlers.ts index 3fae0a58..c84da584 100644 --- a/components/playground/chat-handlers.ts +++ b/components/playground/chat-handlers.ts @@ -2,6 +2,7 @@ import { AnthropicChatInterface, CohereChatInterface, GroqChatInterface, + MiniMaxChatInterface, OpenAIChatInterface, PerplexityChatInterface, } from "@/lib/types/playground_types"; @@ -346,3 +347,44 @@ export async function perplexityHandler( return response; } + +export async function minimaxHandler( + llm: MiniMaxChatInterface, + apiKey: string +): Promise { + const body: any = {}; + if (llm.settings.messages.length > 0) { + body.messages = llm.settings.messages.map((m) => { + return { content: m.content, role: m.role }; + }); + } + if (llm.settings.model) { + body.model = llm.settings.model; + } + if (llm.settings.temperature !== undefined) { + // MiniMax temperature range is [0, 1] + body.temperature = Math.min(Math.max(llm.settings.temperature, 0), 1); + } + if (llm.settings.max_tokens) { + body.max_tokens = llm.settings.max_tokens; + } + if (llm.settings.stream !== undefined) { + body.stream = llm.settings.stream; + } + if (llm.settings.top_p) { + body.top_p = llm.settings.top_p; + } + + // Get the API key from the browser store + body.apiKey = apiKey; + + const response = await fetch("/api/chat/minimax", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + return response; +} diff --git a/components/playground/model-dropdown.tsx b/components/playground/model-dropdown.tsx index 8dc4f4c1..c39da5f7 100644 --- a/components/playground/model-dropdown.tsx +++ b/components/playground/model-dropdown.tsx @@ -18,6 +18,7 @@ import { anthropicModels, cohereModels, groqModels, + minimaxModels, openAIModels, perplexityModels, } from "@/lib/types/playground_types"; @@ -42,6 +43,8 @@ export function ModelsDropDown({ models = groqModels; } else if (vendor === "perplexity") { models = perplexityModels; + } else if (vendor === "minimax") { + models = minimaxModels; } return ( diff --git a/components/playground/settings-sheet.tsx b/components/playground/settings-sheet.tsx index 4a9772fe..27aef3d4 100644 --- a/components/playground/settings-sheet.tsx +++ b/components/playground/settings-sheet.tsx @@ -21,6 +21,7 @@ import { AnthropicSettings, CohereSettings, GroqSettings, + MiniMaxSettings, OpenAISettings, PerplexitySettings, } from "@/lib/types/playground_types"; @@ -2913,3 +2914,258 @@ export function PerplexitySettingsSheet({ ); } + +export function MiniMaxSettingsSheet({ + settings, + setSettings, +}: { + settings: MiniMaxSettings; + setSettings: any; +}) { + const [open, setOpen] = useState(false); + const [advancedSettings, setAdvancedSettings] = useState(false); + const schema = z.object({ + model: z.string(), + stream: z.boolean().optional(), + max_tokens: z.union([z.number().positive(), z.nan()]).optional(), + temperature: z + .union([ + z.number().min(0), + z.number().max(1), + z.nan(), + ]) + .optional(), + top_p: z.union([z.number(), z.nan()]).optional(), + }); + + const SettingsForm = useForm({ + resolver: zodResolver(schema), + defaultValues: { + model: settings.model ?? "MiniMax-M2.7", + stream: settings.stream ?? false, + max_tokens: settings.max_tokens ?? undefined, + temperature: settings.temperature ?? 0.7, + top_p: settings.top_p ?? 1, + }, + }); + + return ( + + + + + + + Chat Settings + + Configure the settings for the MiniMax chat. + + +
+ { + try { + const newSettings: any = {}; + if (data.stream !== undefined) { + newSettings["stream"] = data.stream; + } + if (data.model !== undefined && (data.model as string) !== "") { + newSettings["model"] = data.model; + } else { + throw new Error("Model is required"); + } + if (data.max_tokens !== undefined && !isNaN(data.max_tokens)) { + newSettings["max_tokens"] = data.max_tokens; + } + if ( + data.temperature !== undefined && + !isNaN(data.temperature) + ) { + if (!(data.temperature >= 0 && data.temperature <= 1)) { + throw new Error("Temperature must be between 0 and 1"); + } + newSettings["temperature"] = data.temperature; + } + if (data.top_p !== undefined && !isNaN(data.top_p)) { + newSettings["top_p"] = data.top_p; + } + setSettings({ ...settings, ...newSettings }); + toast.success("Settings saved"); + setOpen(false); + } catch (error: any) { + toast.error("Error saving settings", { + description: error?.message, + }); + } + })} + className="mt-6 px-2 flex flex-col gap-4 overflow-y-scroll h-screen pb-48" + > + ( + + + Model + + + + + + + + )} + /> + ( + + +
+ field.onChange(checked)} + checked={field.value} + /> +
+ + +
+
+
+ +
+ )} + /> +
+ ( + + + Max Tokens + + + + { + field.onChange(e.target.valueAsNumber); + }} + type="number" + placeholder="Max Tokens" + /> + + + + )} + /> + ( + + + Temperature + + + + { + field.onChange(e.target.valueAsNumber); + }} + type="number" + placeholder="Temperature" + /> + + + + )} + /> +
+ + {advancedSettings && ( + <> +
+ ( + + + Top P + + + + { + field.onChange(e.target.valueAsNumber); + }} + type="number" + placeholder="Top P" + /> + + + + )} + /> +
+ + )} + + + + + +
+
+ ); +} diff --git a/components/shared/vendor-metadata.tsx b/components/shared/vendor-metadata.tsx index 850e8a00..776bf71f 100644 --- a/components/shared/vendor-metadata.tsx +++ b/components/shared/vendor-metadata.tsx @@ -78,6 +78,10 @@ export function vendorBadgeColor(vendor: string) { return "bg-blue-500"; } + if (vendor.includes("minimax")) { + return "bg-indigo-500"; + } + if (vendor.startsWith("arch")) { return "bg-white"; } @@ -202,6 +206,10 @@ export function vendorColor(vendor: string) { return "bg-blue-200"; } + if (vendor.includes("minimax")) { + return "bg-indigo-200"; + } + if (vendor.includes("litellm")) { return "bg-blue-100"; } @@ -691,6 +699,21 @@ export function VendorLogo({ ); } + if (vendor.includes("minimax")) { + const color = vendorColor("minimax"); + return ( +
+ MM +
+ ); + } + if (vendor.includes("cleanlab")) { const color = vendorColor("cleanlab"); return ( diff --git a/lib/constants.ts b/lib/constants.ts index 48c7a032..c1e12d8b 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -544,6 +544,26 @@ export const AZURE_PRICING: Record = { }, }; +// https://platform.minimaxi.com/document/Price +export const MINIMAX_PRICING: Record = { + "MiniMax-M2.7": { + input: 0.0011, + output: 0.0055, + }, + "MiniMax-M2.7-highspeed": { + input: 0.00044, + output: 0.0022, + }, + "MiniMax-M2.5": { + input: 0.0011, + output: 0.0055, + }, + "MiniMax-M2.5-highspeed": { + input: 0.00044, + output: 0.0022, + }, +}; + export const GEMINI_PRICING: Record = { "gemini-1.5-pro": { input: 0.00125, // $1.25 per 1M tokens = $0.00125 per 1K tokens @@ -607,6 +627,10 @@ export const LLM_VENDOR_APIS = [ value: "PERPLEXITY_API_KEY", label: "Perplexity", }, + { + value: "MINIMAX_API_KEY", + label: "MiniMax", + }, ]; export const LLM_VENDORS = [ @@ -630,6 +654,10 @@ export const LLM_VENDORS = [ value: "perplexity", label: "Perplexity", }, + { + value: "minimax", + label: "MiniMax", + }, ]; export const SUPPORTED_VENDORS: Record = { @@ -658,6 +686,7 @@ export const SUPPORTED_VENDORS: Record = { GEMINI: "Gemini", EMBEDCHAIN: "Embedchain", VERCEL: "Vercel", + MINIMAX: "MiniMax", }; export const jsontheme = { diff --git a/lib/types/playground_types.ts b/lib/types/playground_types.ts index 2d4f669e..526372f3 100644 --- a/lib/types/playground_types.ts +++ b/lib/types/playground_types.ts @@ -202,6 +202,25 @@ export const perplexityModels = [ }, ]; +export const minimaxModels = [ + { + value: "MiniMax-M2.7", + label: "MiniMax M2.7", + }, + { + value: "MiniMax-M2.7-highspeed", + label: "MiniMax M2.7 Highspeed", + }, + { + value: "MiniMax-M2.5", + label: "MiniMax M2.5", + }, + { + value: "MiniMax-M2.5-highspeed", + label: "MiniMax M2.5 Highspeed (204K)", + }, +]; + export enum OpenAIRole { "user" = "user", "assistant" = "assistant", @@ -304,6 +323,15 @@ export interface PerplexitySettings { frequency_penalty?: number; } +export interface MiniMaxSettings { + messages: Conversation[]; + model: string; + max_tokens?: number; + temperature?: number; + top_p?: number; + stream?: boolean; +} + export interface ChatInterface { id: string; vendor: string; @@ -312,7 +340,8 @@ export interface ChatInterface { | AnthropicSettings | CohereSettings | GroqSettings - | PerplexitySettings; + | PerplexitySettings + | MiniMaxSettings; } export interface OpenAIChatInterface extends ChatInterface { @@ -334,3 +363,7 @@ export interface GroqChatInterface extends ChatInterface { export interface PerplexityChatInterface extends ChatInterface { settings: PerplexitySettings; } + +export interface MiniMaxChatInterface extends ChatInterface { + settings: MiniMaxSettings; +} diff --git a/lib/utils.ts b/lib/utils.ts index 4a902218..dd69bf06 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -17,6 +17,7 @@ import { GEMINI_PRICING, GROQ_PRICING, LangTraceAttributes, + MINIMAX_PRICING, MISTRAL_PRICING, OPENAI_PRICING, PERPLEXITY_PRICING, @@ -549,6 +550,8 @@ export function calculatePriceFromUsage( vendor = "groq"; } else if (model.includes("deepseek")) { vendor = "deepseek"; + } else if (model.includes("MiniMax")) { + vendor = "minimax"; } } @@ -599,6 +602,8 @@ export function calculatePriceFromUsage( costTable = MISTRAL_PRICING[model]; } else if (model.includes("deepseek")) { costTable = DEEPSEEK_PRICING[model]; + } else if (model.includes("MiniMax")) { + costTable = MINIMAX_PRICING[model]; } } else if (vendor === "openai" || vendor === "azure") { // check if model is present as key in OPENAI_PRICING @@ -672,6 +677,8 @@ export function calculatePriceFromUsage( costTable = MISTRAL_PRICING[model]; } else if (vendor === "deepseek") { costTable = DEEPSEEK_PRICING[model]; + } else if (vendor === "minimax") { + costTable = MINIMAX_PRICING[model]; } if (costTable) { const total = diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..7022acd4 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; +import path from "path"; + +export default defineConfig({ + test: { + include: ["__tests__/**/*.test.ts"], + globals: true, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "."), + }, + }, +});