diff --git a/apps/mongostory/README.md b/apps/mongostory/README.md index 42d5d51..6e65203 100644 --- a/apps/mongostory/README.md +++ b/apps/mongostory/README.md @@ -41,7 +41,7 @@ MongoStory is a cloud-native platform designed to empower content creators, edit - **API Routes**: Next.js API routes for server-side functionality - **Database**: MongoDB for flexible document storage - **Vector Search**: MongoDB Atlas Vector Search for semantic content operations -- **AI Integration**: Integration with AI models via AI SDK - xAI (Grok) +- **AI Integration**: Integration with AI models via AI SDK - xAI (Grok) or [MiniMax](https://platform.minimax.io/) (M2.7) ### AI Integration - **Content Generation**: AI-powered content creation and suggestions @@ -71,7 +71,7 @@ MongoStory leverages MongoDB's document model for flexible content storage and i - Node.js 18+ and npm/yarn - MongoDB Atlas account - AI API keys: -- - Grok AI API key +- - Grok AI API key (or MiniMax API key) - - Voyage AI API Key. ### Installation @@ -90,10 +90,21 @@ npm install ``` openssl rand -base64 32 ``` -- `XAI_API_KEY`: API key for AI services +- `XAI_API_KEY`: API key for AI services (default provider) - `VOYAGE_API_KEY`: API key for vector embeddings - `NEXT_PUBLIC_APP_URL`: The main domain of the app (eg. http://localhost:3000). +#### Using MiniMax as the LLM Provider + +To use [MiniMax](https://platform.minimax.io/) instead of xAI/Grok, set the following environment variables: + +``` +LLM_PROVIDER=minimax +MINIMAX_API_KEY=your_minimax_api_key +``` + +Supported MiniMax models: `MiniMax-M2.7` (default), `MiniMax-M2.7-highspeed`. + ### Trigger for content embedding: Set a `VOYAGE_API_KEY` - Value + Secret on the triggers app. diff --git a/apps/mongostory/__tests__/llm-provider.test.ts b/apps/mongostory/__tests__/llm-provider.test.ts new file mode 100644 index 0000000..93ec155 --- /dev/null +++ b/apps/mongostory/__tests__/llm-provider.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock @ai-sdk/xai +const mockXaiModel = { modelId: 'grok-model', provider: 'xai' }; +vi.mock('@ai-sdk/xai', () => ({ + xai: vi.fn().mockReturnValue(mockXaiModel), +})); + +// Mock @ai-sdk/openai +const mockMinimaxModel = { modelId: 'minimax-model', provider: 'minimax' }; +const mockProviderFn = vi.fn().mockReturnValue(mockMinimaxModel); +vi.mock('@ai-sdk/openai', () => ({ + createOpenAI: vi.fn().mockReturnValue(mockProviderFn), +})); + +describe('getLLMModel', () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('defaults to xai provider when LLM_PROVIDER is not set', async () => { + delete process.env.LLM_PROVIDER; + const { getLLMModel } = await import('../lib/llm-provider'); + const { xai } = await import('@ai-sdk/xai'); + + const model = getLLMModel(); + + expect(model).toBeDefined(); + expect(xai).toHaveBeenCalledWith('grok-2-1212'); + }); + + it('uses xai provider when LLM_PROVIDER is "xai"', async () => { + process.env.LLM_PROVIDER = 'xai'; + const { getLLMModel } = await import('../lib/llm-provider'); + const { xai } = await import('@ai-sdk/xai'); + + getLLMModel(); + + expect(xai).toHaveBeenCalledWith('grok-2-1212'); + }); + + it('uses minimax provider when LLM_PROVIDER is "minimax"', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-minimax-key'; + const { getLLMModel } = await import('../lib/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: 'https://api.minimax.io/v1', + apiKey: 'test-minimax-key', + }) + ); + }); + + it('uses custom MINIMAX_BASE_URL when provided', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + process.env.MINIMAX_BASE_URL = 'https://api.minimaxi.com/v1'; + const { getLLMModel } = await import('../lib/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: 'https://api.minimaxi.com/v1', + apiKey: 'test-key', + }) + ); + }); + + it('uses MiniMax-M2.7 as default model for minimax provider', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + const { getLLMModel } = await import('../lib/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + const providerFn = (createOpenAI as any).mock.results[0].value; + expect(providerFn).toHaveBeenCalledWith('MiniMax-M2.7'); + }); + + it('uses custom modelId when provided for minimax', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + const { getLLMModel } = await import('../lib/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel({ modelId: 'MiniMax-M2.7-highspeed' }); + + const providerFn = (createOpenAI as any).mock.results[0].value; + expect(providerFn).toHaveBeenCalledWith('MiniMax-M2.7-highspeed'); + }); + + it('uses custom modelId when provided for xai', async () => { + delete process.env.LLM_PROVIDER; + const { getLLMModel } = await import('../lib/llm-provider'); + const { xai } = await import('@ai-sdk/xai'); + + getLLMModel({ modelId: 'grok-3' }); + + expect(xai).toHaveBeenCalledWith('grok-3'); + }); +}); diff --git a/apps/mongostory/__tests__/minimax-integration.test.ts b/apps/mongostory/__tests__/minimax-integration.test.ts new file mode 100644 index 0000000..c88b66b --- /dev/null +++ b/apps/mongostory/__tests__/minimax-integration.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest'; + +const API_KEY = process.env.MINIMAX_API_KEY; +const BASE_URL = process.env.MINIMAX_BASE_URL || 'https://api.minimax.io/v1'; + +describe.skipIf(!API_KEY)('MiniMax Integration', () => { + it('completes a basic chat request with MiniMax-M2.7', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Say "test passed" and nothing else.' }], + max_tokens: 20, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.choices).toBeDefined(); + expect(data.choices.length).toBeGreaterThan(0); + expect(data.choices[0].message.content).toBeTruthy(); + }, 30000); + + it('generates structured content analysis with MiniMax-M2.7', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7', + messages: [ + { + role: 'system', + content: 'You are a content quality expert. Return a JSON object with readabilityScore (1-10), clarity (1-10), and suggestions (array of strings).', + }, + { + role: 'user', + content: 'Evaluate the quality of this content: "MongoDB is a popular NoSQL database that stores data in flexible, JSON-like documents."', + }, + ], + max_tokens: 200, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.choices[0].message.content).toBeTruthy(); + }, 30000); + + it('handles streaming responses', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Count from 1 to 5.' }], + max_tokens: 50, + stream: true, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const reader = response.body!.getReader(); + const decoder = new TextDecoder(); + let chunks = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const text = decoder.decode(value, { stream: true }); + if (text.includes('data:')) chunks++; + } + + expect(chunks).toBeGreaterThan(1); + }, 30000); +}); diff --git a/apps/mongostory/app/api/clusters/regenerate/route.ts b/apps/mongostory/app/api/clusters/regenerate/route.ts index 508d184..5fc85b2 100644 --- a/apps/mongostory/app/api/clusters/regenerate/route.ts +++ b/apps/mongostory/app/api/clusters/regenerate/route.ts @@ -3,8 +3,8 @@ import clientPromise from "@/lib/mongodb" import { ObjectId } from "mongodb" import { generateEmbedding } from "@/lib/embeddings" import { performVectorSearch } from "@/lib/vector-search" -import { xai } from "@ai-sdk/xai" import { generateText } from "ai" +import { getLLMModel } from "@/lib/llm-provider" export async function POST() { try { @@ -131,7 +131,7 @@ async function generateClusterLabel(keywords: string[], contentSamples: string[] // Take a sample of content to provide context const contentSample = contentSamples.slice(0, 3).join("\n\n").substring(0, 1000) - const model = xai("grok-2-1212") + const model = getLLMModel() const { text } = await generateText({ model, prompt: `Generate a concise, descriptive label (3-5 words) for a content cluster with these keywords: ${keywords.join(", ")}. diff --git a/apps/mongostory/app/api/content/[id]/ai-revise/route.ts b/apps/mongostory/app/api/content/[id]/ai-revise/route.ts index 45f5af5..ff0b13b 100644 --- a/apps/mongostory/app/api/content/[id]/ai-revise/route.ts +++ b/apps/mongostory/app/api/content/[id]/ai-revise/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server" -import { xai } from "@ai-sdk/xai" import { generateText } from "ai" import clientPromise from "@/lib/mongodb" +import { getLLMModel } from "@/lib/llm-provider" import { ObjectId } from "mongodb" export async function POST(req: Request, { params }: { params: { id: string } }) { @@ -29,7 +29,7 @@ export async function POST(req: Request, { params }: { params: { id: string } }) ) } - const model = xai("grok-2-1212") + const model = getLLMModel() // Generate revised content based on analysis const { text: revisedContent } = await generateText({ diff --git a/apps/mongostory/app/api/content/[id]/translate/route.ts b/apps/mongostory/app/api/content/[id]/translate/route.ts index 6ec4fb2..c03cef7 100644 --- a/apps/mongostory/app/api/content/[id]/translate/route.ts +++ b/apps/mongostory/app/api/content/[id]/translate/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server" import { generateText } from "ai" -import { xai } from "@ai-sdk/xai" import clientPromise from "@/lib/mongodb" +import { getLLMModel } from "@/lib/llm-provider" import { ObjectId } from "mongodb" const SUPPORTED_LANGUAGES = { @@ -35,7 +35,7 @@ export async function POST(req: Request, { params }: { params: { id: string } }) return NextResponse.json({ message: "Translation already exists" }, { status: 200 }) } - const model = xai("grok-2-1212") + const model = getLLMModel() // Translate title const { text: translatedTitle } = await generateText({ diff --git a/apps/mongostory/app/api/generate-content/route.ts b/apps/mongostory/app/api/generate-content/route.ts index 45d35e9..0501bd4 100644 --- a/apps/mongostory/app/api/generate-content/route.ts +++ b/apps/mongostory/app/api/generate-content/route.ts @@ -1,6 +1,6 @@ import { generateText } from "ai" -import { xai } from "@ai-sdk/xai" import { NextResponse } from "next/server" +import { getLLMModel } from "@/lib/llm-provider" type ExpertiseLevel = "student" | "mid-level" | "expert" @@ -40,7 +40,7 @@ const topicSuggestions = { export async function POST(req: Request) { try { const { topic, expertiseLevel } = await req.json() - const model = xai("grok-2-1212") + const model = getLLMModel() // Generate title with a specific prompt for concise titles const { text: titleResponse } = await generateText({ diff --git a/apps/mongostory/app/api/generate/route.ts b/apps/mongostory/app/api/generate/route.ts index 9f9a835..23c0ab2 100644 --- a/apps/mongostory/app/api/generate/route.ts +++ b/apps/mongostory/app/api/generate/route.ts @@ -1,6 +1,6 @@ -import { xai } from "@ai-sdk/xai" import { generateText } from "ai" import { NextResponse } from "next/server" +import { getLLMModel } from "@/lib/llm-provider" export async function POST(req: Request) { try { @@ -8,31 +8,31 @@ export async function POST(req: Request) { // Generate content summary const { text: summary } = await generateText({ - model: xai("grok-2-1212"), + model: getLLMModel(), prompt: `Summarize the following content in 2-3 sentences: ${content}`, }) // Generate SEO optimized title const { text: seoTitle } = await generateText({ - model: xai("grok-2-1212"), + model: getLLMModel(), prompt: `Generate an SEO-optimized title for this content: ${content}`, }) // Generate SEO description const { text: seoDescription } = await generateText({ - model: xai("grok-2-1212"), + model: getLLMModel(), prompt: `Write a compelling meta description (under 160 characters) for this content: ${content}`, }) // Analyze sentiment const { text: sentiment } = await generateText({ - model: xai("grok-2-1212"), + model: getLLMModel(), prompt: `Analyze the sentiment and emotional tone of this content. Include percentage breakdowns of detected emotions: ${content}`, }) // Generate tag recommendations const { text: tagSuggestions } = await generateText({ - model: xai("grok-2-1212"), + model: getLLMModel(), prompt: `Suggest 5-7 relevant tags for this content, separated by commas: ${content}`, }) diff --git a/apps/mongostory/lib/ai-agent.ts b/apps/mongostory/lib/ai-agent.ts index 9053874..0d4aac5 100644 --- a/apps/mongostory/lib/ai-agent.ts +++ b/apps/mongostory/lib/ai-agent.ts @@ -1,7 +1,7 @@ -import { xai } from "@ai-sdk/xai" import { generateText, generateObject } from "ai" import { z } from "zod" import clientPromise from "@/lib/mongodb" +import { getLLMModel } from "@/lib/llm-provider" // Helper function to clamp number within range const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max) @@ -68,7 +68,7 @@ async function getEnabledFeatures() { } export async function analyzeContent(content: string, title: string, selectedFeatures: string[]) { - const model = xai("grok-2-1212") + const model = getLLMModel() const enabledFeatures = await getEnabledFeatures() const analysisPromises = [] diff --git a/apps/mongostory/lib/llm-provider.ts b/apps/mongostory/lib/llm-provider.ts new file mode 100644 index 0000000..abc5582 --- /dev/null +++ b/apps/mongostory/lib/llm-provider.ts @@ -0,0 +1,29 @@ +import { xai } from "@ai-sdk/xai" +import { createOpenAI } from "@ai-sdk/openai" + +export type LLMProvider = "xai" | "minimax" + +const LLM_PROVIDER = (process.env.LLM_PROVIDER as LLMProvider) || "xai" + +/** + * Returns the appropriate LLM model based on the LLM_PROVIDER environment variable. + * + * Supported providers: + * - xai (default): Uses xAI/Grok models (grok-2-1212) + * - minimax: Uses MiniMax models (MiniMax-M2.7, MiniMax-M2.7-highspeed) + * via OpenAI-compatible API at https://api.minimax.io/v1 + */ +export function getLLMModel(options?: { modelId?: string }) { + switch (LLM_PROVIDER) { + case "minimax": { + const minimax = createOpenAI({ + baseURL: process.env.MINIMAX_BASE_URL || "https://api.minimax.io/v1", + apiKey: process.env.MINIMAX_API_KEY, + }) + return minimax(options?.modelId || "MiniMax-M2.7") + } + case "xai": + default: + return xai(options?.modelId || "grok-2-1212") + } +} diff --git a/apps/mongostory/lib/social-media-ai-agent.ts b/apps/mongostory/lib/social-media-ai-agent.ts index a6f4c5e..99f5754 100644 --- a/apps/mongostory/lib/social-media-ai-agent.ts +++ b/apps/mongostory/lib/social-media-ai-agent.ts @@ -1,5 +1,5 @@ -import { xai } from "@ai-sdk/xai" import { generateText } from "ai" +import { getLLMModel } from "@/lib/llm-provider" interface SocialMediaPost { content: string @@ -11,7 +11,7 @@ export async function generateSocialMediaPost( platform: string, articleUrl?: string, // Add articleUrl parameter ): Promise { - const model = xai("grok-2-1212") + const model = getLLMModel() const prompt = `Generate a social media post for ${platform} based on this article: Title: ${title} diff --git a/apps/mongostory/package.json b/apps/mongostory/package.json index f7f1380..3f77035 100644 --- a/apps/mongostory/package.json +++ b/apps/mongostory/package.json @@ -9,6 +9,7 @@ "lint": "next lint" }, "dependencies": { + "@ai-sdk/openai": "latest", "@ai-sdk/xai": "latest", "@aws-sdk/credential-providers": "latest", "@hookform/resolvers": "^3.9.1", diff --git a/apps/vercel_sdk_hr_agent/README.md b/apps/vercel_sdk_hr_agent/README.md index 1b83d85..6c3cdac 100644 --- a/apps/vercel_sdk_hr_agent/README.md +++ b/apps/vercel_sdk_hr_agent/README.md @@ -18,7 +18,8 @@ The HR Team Matcher is built with the following technologies: - **MongoDB**: Database with Vector Search for semantic skill matching - **Vercel AI SDK**: Agentic AI capabilities with multi-step reasoning - **Voyage AI**: Generation of text embeddings for semantic search -- **OpenAI**: Language model for team analysis and recommendations +- **OpenAI**: Language model for team analysis and recommendations (default) +- **MiniMax**: Alternative LLM provider via OpenAI-compatible API ([MiniMax-M2.7](https://platform.minimax.io/docs/api-reference/text-openai-api)) - **Tailwind CSS**: Styling ## How It Works @@ -35,7 +36,7 @@ The HR Team Matcher is built with the following technologies: - Node.js 18.x or higher - MongoDB Atlas account (with Vector Search capability) -- OpenAI API key +- OpenAI API key (or MiniMax API key) - Voyage AI API key ### Environment Setup @@ -52,6 +53,19 @@ The HR Team Matcher is built with the following technologies: VOYAGE_API_KEY=your_voyage_api_key ``` +#### Using MiniMax as the LLM Provider + +To use [MiniMax](https://platform.minimax.io/) instead of OpenAI, set the following environment variables: + +``` +LLM_PROVIDER=minimax +MINIMAX_API_KEY=your_minimax_api_key +VOYAGE_API_KEY=your_voyage_api_key +MONGODB_URI=your_mongodb_connection_string +``` + +Supported MiniMax models: `MiniMax-M2.7` (default), `MiniMax-M2.7-highspeed`. + ### Database Setup 1. Create a MongoDB Atlas cluster diff --git a/apps/vercel_sdk_hr_agent/__tests__/llm-provider.test.ts b/apps/vercel_sdk_hr_agent/__tests__/llm-provider.test.ts new file mode 100644 index 0000000..6fb247f --- /dev/null +++ b/apps/vercel_sdk_hr_agent/__tests__/llm-provider.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock @ai-sdk/openai +vi.mock('@ai-sdk/openai', () => { + const mockModel = { modelId: 'mock-model', provider: 'mock-provider' }; + const mockProviderFn = vi.fn().mockReturnValue(mockModel); + return { + createOpenAI: vi.fn().mockReturnValue(mockProviderFn), + }; +}); + +describe('getLLMModel', () => { + const originalEnv = process.env; + + beforeEach(() => { + vi.resetModules(); + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it('defaults to openai provider when LLM_PROVIDER is not set', async () => { + delete process.env.LLM_PROVIDER; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + const model = getLLMModel(); + + expect(model).toBeDefined(); + // Should call createOpenAI with OpenAI config (no custom baseURL) + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: undefined, + }) + ); + }); + + it('uses openai provider when LLM_PROVIDER is "openai"', async () => { + process.env.LLM_PROVIDER = 'openai'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + apiKey: undefined, + }) + ); + }); + + it('uses minimax provider when LLM_PROVIDER is "minimax"', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-minimax-key'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: 'https://api.minimax.io/v1', + apiKey: 'test-minimax-key', + }) + ); + }); + + it('uses custom MINIMAX_BASE_URL when provided', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + process.env.MINIMAX_BASE_URL = 'https://api.minimaxi.com/v1'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + expect(createOpenAI).toHaveBeenCalledWith( + expect.objectContaining({ + baseURL: 'https://api.minimaxi.com/v1', + apiKey: 'test-key', + }) + ); + }); + + it('uses custom modelId when provided for minimax', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel({ modelId: 'MiniMax-M2.7-highspeed' }); + + const providerFn = (createOpenAI as any).mock.results[0].value; + expect(providerFn).toHaveBeenCalledWith('MiniMax-M2.7-highspeed'); + }); + + it('uses default MiniMax-M2.7 model when no modelId for minimax', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + const providerFn = (createOpenAI as any).mock.results[0].value; + expect(providerFn).toHaveBeenCalledWith('MiniMax-M2.7'); + }); + + it('uses default o3-mini model for openai provider', async () => { + delete process.env.LLM_PROVIDER; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + getLLMModel(); + + const providerFn = (createOpenAI as any).mock.results[0].value; + expect(providerFn).toHaveBeenCalledWith('o3-mini', { structuredOutputs: true }); + }); + + it('passes structuredOutputs only for openai provider', async () => { + process.env.LLM_PROVIDER = 'minimax'; + process.env.MINIMAX_API_KEY = 'test-key'; + const { getLLMModel } = await import('../utils/llm-provider'); + const { createOpenAI } = await import('@ai-sdk/openai'); + + // Clear mock call history before this specific test + const providerFn = (createOpenAI as any).mock.results[0].value; + providerFn.mockClear(); + + getLLMModel(); + + // MiniMax should be called with just the model name, no structuredOutputs + expect(providerFn).toHaveBeenCalledWith('MiniMax-M2.7'); + expect(providerFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/vercel_sdk_hr_agent/__tests__/minimax-integration.test.ts b/apps/vercel_sdk_hr_agent/__tests__/minimax-integration.test.ts new file mode 100644 index 0000000..7cb7612 --- /dev/null +++ b/apps/vercel_sdk_hr_agent/__tests__/minimax-integration.test.ts @@ -0,0 +1,88 @@ +import { describe, it, expect } from 'vitest'; + +const API_KEY = process.env.MINIMAX_API_KEY; +const BASE_URL = process.env.MINIMAX_BASE_URL || 'https://api.minimax.io/v1'; + +describe.skipIf(!API_KEY)('MiniMax Integration', () => { + it('completes a basic chat request with MiniMax-M2.7', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'Say "test passed" and nothing else.' }], + max_tokens: 20, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.choices).toBeDefined(); + expect(data.choices.length).toBeGreaterThan(0); + expect(data.choices[0].message.content).toBeTruthy(); + }, 30000); + + it('supports tool calling with MiniMax-M2.7', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7', + messages: [{ role: 'user', content: 'What is the weather in San Francisco?' }], + tools: [ + { + type: 'function', + function: { + name: 'get_weather', + description: 'Get the current weather for a location', + parameters: { + type: 'object', + properties: { + location: { type: 'string', description: 'City name' }, + }, + required: ['location'], + }, + }, + }, + ], + max_tokens: 100, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.choices).toBeDefined(); + expect(data.choices.length).toBeGreaterThan(0); + // Model should either call the tool or provide a text response + const choice = data.choices[0]; + expect(choice.message).toBeDefined(); + }, 30000); + + it('completes a request with MiniMax-M2.7-highspeed', async () => { + const response = await fetch(`${BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: 'MiniMax-M2.7-highspeed', + messages: [{ role: 'user', content: 'Count from 1 to 3.' }], + max_tokens: 50, + temperature: 1.0, + }), + }); + + expect(response.ok).toBe(true); + const data = await response.json(); + expect(data.choices[0].message.content).toBeTruthy(); + }, 30000); +}); diff --git a/apps/vercel_sdk_hr_agent/app/api/build-team/route.ts b/apps/vercel_sdk_hr_agent/app/api/build-team/route.ts index c5ea5b4..40a486f 100644 --- a/apps/vercel_sdk_hr_agent/app/api/build-team/route.ts +++ b/apps/vercel_sdk_hr_agent/app/api/build-team/route.ts @@ -1,5 +1,4 @@ import { NextRequest, NextResponse } from 'next/server'; -import { openai } from '@ai-sdk/openai'; import { generateText, tool } from 'ai'; import { z } from 'zod'; import { @@ -9,6 +8,7 @@ import { saveTeamToDatabase, generateTeamRecommendation } from '../../../utils/tools'; +import { getLLMModel } from '../../../utils/llm-provider'; export async function POST(req: NextRequest) { try { @@ -29,7 +29,7 @@ export async function POST(req: NextRequest) { async function buildTeam(projectDescription: string) { const { steps , toolCalls } = await generateText({ - model: openai('o3-mini', { structuredOutputs: true }), + model: getLLMModel(), tools: { analyzeProjectRequirements, searchEmployeesBySkill, diff --git a/apps/vercel_sdk_hr_agent/utils/llm-provider.ts b/apps/vercel_sdk_hr_agent/utils/llm-provider.ts new file mode 100644 index 0000000..88d2c7f --- /dev/null +++ b/apps/vercel_sdk_hr_agent/utils/llm-provider.ts @@ -0,0 +1,32 @@ +import { createOpenAI } from '@ai-sdk/openai'; + +export type LLMProvider = 'openai' | 'minimax'; + +const LLM_PROVIDER = (process.env.LLM_PROVIDER as LLMProvider) || 'openai'; + +/** + * Returns the appropriate LLM model based on the LLM_PROVIDER environment variable. + * + * Supported providers: + * - openai (default): Uses OpenAI models (o3-mini) + * - minimax: Uses MiniMax models (MiniMax-M2.7, MiniMax-M2.7-highspeed) + * via OpenAI-compatible API at https://api.minimax.io/v1 + */ +export function getLLMModel(options?: { modelId?: string }) { + switch (LLM_PROVIDER) { + case 'minimax': { + const minimax = createOpenAI({ + baseURL: process.env.MINIMAX_BASE_URL || 'https://api.minimax.io/v1', + apiKey: process.env.MINIMAX_API_KEY, + }); + return minimax(options?.modelId || 'MiniMax-M2.7'); + } + case 'openai': + default: { + const openai = createOpenAI({ + apiKey: process.env.OPENAI_API_KEY, + }); + return openai(options?.modelId || 'o3-mini', { structuredOutputs: true }); + } + } +}