From 38c311d793e8f4b51a1605b6d547031e9bd0fba5 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Wed, 25 Mar 2026 17:31:38 +0800 Subject: [PATCH] feat: add MiniMax as LLM provider Add MiniMax AI as a first-class LLM provider using the existing OpenAI-compatible connector pattern (same approach as DeepSeek and TogetherAI). Changes: - Register MiniMax in BuiltinLLMProviders enum - Wire MiniMax to OpenAIConnector in LLMService (register + init) - Add 4 MiniMax models: M2.7, M2.7-highspeed, M2.5, M2.5-highspeed - Add 10 unit tests and 5 integration tests - Update README to list MiniMax as supported connector --- README.md | 2 +- .../LLMManager/LLM.service/index.ts | 2 + .../core/src/subsystems/LLMManager/models.ts | 73 ++++++++ packages/core/src/types/LLM.types.ts | 1 + .../005-LLM/07-llm-minimax.test.ts | 156 ++++++++++++++++++ .../tests/unit/005-LLM/07-llm-minimax.test.ts | 143 ++++++++++++++++ 6 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 packages/sdk/tests/integration/005-LLM/07-llm-minimax.test.ts create mode 100644 packages/sdk/tests/unit/005-LLM/07-llm-minimax.test.ts diff --git a/README.md b/README.md index 718611426..e20068320 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ The **SRE** is the core runtime environment that powers SmythOS. Think of it as **Supported Connectors:** - **Storage**: Local, S3, Google Cloud, Azure -- **LLM**: OpenAI, Anthropic, Google AI, AWS Bedrock, Groq, Perplexity +- **LLM**: OpenAI, Anthropic, Google AI, AWS Bedrock, Groq, Perplexity, MiniMax - **VectorDB**: Pinecone, Milvus, RAMVec - **Cache**: RAM, Redis - **Vault**: JSON File, AWS Secrets Manager, HashiCorp diff --git a/packages/core/src/subsystems/LLMManager/LLM.service/index.ts b/packages/core/src/subsystems/LLMManager/LLM.service/index.ts index 20adddebf..d389b3c37 100644 --- a/packages/core/src/subsystems/LLMManager/LLM.service/index.ts +++ b/packages/core/src/subsystems/LLMManager/LLM.service/index.ts @@ -27,6 +27,7 @@ export class LLMService extends ConnectorServiceProvider { ConnectorService.register(TConnectorService.LLM, 'xAI', xAIConnector); ConnectorService.register(TConnectorService.LLM, 'Perplexity', PerplexityConnector); ConnectorService.register(TConnectorService.LLM, 'Ollama', OllamaConnector); + ConnectorService.register(TConnectorService.LLM, 'MiniMax', OpenAIConnector); } public init() { @@ -43,5 +44,6 @@ export class LLMService extends ConnectorServiceProvider { ConnectorService.init(TConnectorService.LLM, 'xAI'); ConnectorService.init(TConnectorService.LLM, 'Perplexity'); ConnectorService.init(TConnectorService.LLM, 'Ollama'); + ConnectorService.init(TConnectorService.LLM, 'MiniMax'); } } diff --git a/packages/core/src/subsystems/LLMManager/models.ts b/packages/core/src/subsystems/LLMManager/models.ts index 856dcf4f8..8eecb93ea 100644 --- a/packages/core/src/subsystems/LLMManager/models.ts +++ b/packages/core/src/subsystems/LLMManager/models.ts @@ -2235,6 +2235,79 @@ export const models = { // #endregion Together AI Models ========================== + // #region MiniMax Models ========================== + + 'minimax-m2.7': { + llm: 'MiniMax', + + label: 'MiniMax M2.7', + modelId: 'MiniMax-M2.7', + provider: 'MiniMax', + features: ['text', 'tools'], + tags: ['New', 'Personal'], + tokens: 0, + completionTokens: 0, + enabled: false, + keyOptions: { tokens: 1_000_000, completionTokens: 16_384, enabled: true }, + + baseURL: 'https://api.minimax.io/v1', + + credentials: 'vault', + }, + 'minimax-m2.7-highspeed': { + llm: 'MiniMax', + + label: 'MiniMax M2.7 Highspeed', + modelId: 'MiniMax-M2.7-highspeed', + provider: 'MiniMax', + features: ['text', 'tools'], + tags: ['New', 'Personal'], + tokens: 0, + completionTokens: 0, + enabled: false, + keyOptions: { tokens: 1_000_000, completionTokens: 16_384, enabled: true }, + + baseURL: 'https://api.minimax.io/v1', + + credentials: 'vault', + }, + 'minimax-m2.5': { + llm: 'MiniMax', + + label: 'MiniMax M2.5', + modelId: 'MiniMax-M2.5', + provider: 'MiniMax', + features: ['text', 'tools'], + tags: ['Personal'], + tokens: 0, + completionTokens: 0, + enabled: false, + keyOptions: { tokens: 204_000, completionTokens: 16_384, enabled: true }, + + baseURL: 'https://api.minimax.io/v1', + + credentials: 'vault', + }, + 'minimax-m2.5-highspeed': { + llm: 'MiniMax', + + label: 'MiniMax M2.5 Highspeed', + modelId: 'MiniMax-M2.5-highspeed', + provider: 'MiniMax', + features: ['text', 'tools'], + tags: ['Personal'], + tokens: 0, + completionTokens: 0, + enabled: false, + keyOptions: { tokens: 204_000, completionTokens: 16_384, enabled: true }, + + baseURL: 'https://api.minimax.io/v1', + + credentials: 'vault', + }, + + // #endregion MiniMax Models ========================== + // #region Image Generation Models ============================ // #region OpenAI Models gpt-image-1 diff --git a/packages/core/src/types/LLM.types.ts b/packages/core/src/types/LLM.types.ts index 3225bdfd7..12739404e 100644 --- a/packages/core/src/types/LLM.types.ts +++ b/packages/core/src/types/LLM.types.ts @@ -303,6 +303,7 @@ export const BuiltinLLMProviders = { xAI: 'xAI', Perplexity: 'Perplexity', Ollama: 'Ollama', + MiniMax: 'MiniMax', } as const; // Base provider type export type TBuiltinLLMProvider = (typeof BuiltinLLMProviders)[keyof typeof BuiltinLLMProviders]; diff --git a/packages/sdk/tests/integration/005-LLM/07-llm-minimax.test.ts b/packages/sdk/tests/integration/005-LLM/07-llm-minimax.test.ts new file mode 100644 index 000000000..eccf4df4d --- /dev/null +++ b/packages/sdk/tests/integration/005-LLM/07-llm-minimax.test.ts @@ -0,0 +1,156 @@ +// prettier-ignore-file +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { LLM, LLMInstance, TLLMEvent } from '../../../src'; + +// Mock @smythos/sre dependencies for integration-style tests +vi.mock('@smythos/sre', async () => { + const EventEmitter = (await import('events')).EventEmitter; + class DummyRequester { + constructor(public candidate?: any) {} + async request(params: any) { + const userMsg = params?.messages?.find((m: any) => m.role === 'user')?.content || ''; + const sysMsg = params?.messages?.find((m: any) => m.role === 'system')?.content || ''; + return { + content: (sysMsg ? sysMsg + ' ' : '') + `Echo: ${userMsg}`, + finishReason: 'stop', + } as any; + } + async streamRequest(params: any) { + const emitter = new EventEmitter(); + setTimeout(() => { + const userMsg = params?.messages?.find((m: any) => m.role === 'user')?.content || ''; + emitter.emit('content', 'Echo: ' + userMsg); + emitter.emit('end'); + }, 0); + return emitter as any; + } + } + return { + TLLMProvider: { + OpenAI: 'OpenAI', + MiniMax: 'MiniMax', + Anthropic: 'Anthropic', + GoogleAI: 'GoogleAI', + DeepSeek: 'DeepSeek', + Groq: 'Groq', + TogetherAI: 'TogetherAI', + Bedrock: 'Bedrock', + VertexAI: 'VertexAI', + xAI: 'xAI', + Perplexity: 'Perplexity', + Ollama: 'Ollama', + Echo: 'Echo', + }, + TLLMEvent: { + Data: 'data', + Content: 'content', + Thinking: 'thinking', + End: 'end', + Abort: 'abort', + Error: 'error', + ToolInfo: 'toolInfo', + ToolCall: 'toolCall', + ToolResult: 'toolResult', + Usage: 'usage', + Interrupted: 'interrupted', + Fallback: 'fallback', + Requested: 'requested', + }, + DEFAULT_TEAM_ID: 'default', + AccessCandidate: { + team: (id: string) => ({ type: 'team', id }), + }, + ConnectorService: { + getModelsProviderConnector() { + return { + requester() { + return { + async getModels() { + return { + 'MiniMax-M2.7': { tokens: 1000000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.7-highspeed': { tokens: 1000000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.5': { tokens: 204000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.5-highspeed': { tokens: 204000, completionTokens: 16384, keyOptions: {} }, + } as any; + }, + } as any; + }, + } as any; + }, + getLLMConnector() { + return { + user(candidate: any) { + return new DummyRequester(candidate) as any; + }, + } as any; + }, + }, + BinaryInput: class {}, + SRE: { init: vi.fn(), ready: vi.fn().mockResolvedValue(true), initializing: false }, + } as any; +}); + +describe('LLM - MiniMax integration', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('MiniMax M2.7 prompt returns expected content', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7'); + const res = await llm.prompt('Tell me a joke'); + expect(res).toContain('Echo: Tell me a joke'); + expect(typeof res).toBe('string'); + }); + + it('MiniMax M2.7-highspeed prompt returns expected content', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7-highspeed'); + const res = await llm.prompt('What is AI?'); + expect(res).toContain('Echo: What is AI?'); + }); + + it('MiniMax M2.5 prompt returns expected content', async () => { + const llm = LLM.MiniMax('MiniMax-M2.5'); + const res = await llm.prompt('Hello MiniMax'); + expect(res).toContain('Echo: Hello MiniMax'); + }); + + it('MiniMax streaming works correctly', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7', { temperature: 0.7 }); + const streamEvents = await llm.prompt('Stream test').stream(); + + return new Promise((resolve, reject) => { + let receivedContent = false; + + streamEvents.on('content', (content: string) => { + expect(content).toContain('Echo: Stream test'); + receivedContent = true; + }); + + streamEvents.on('end', () => { + expect(receivedContent).toBe(true); + resolve(); + }); + + streamEvents.on('error', (error: any) => { + reject(error); + }); + + // Timeout safety + setTimeout(() => { + if (!receivedContent) { + reject(new Error('Streaming timed out')); + } + }, 5000); + }); + }); + + it('MiniMax with custom temperature and maxTokens', async () => { + const llm = LLM.MiniMax('MiniMax-M2.5-highspeed', { + temperature: 0.3, + maxTokens: 500, + }); + const res = await llm.prompt('Custom params test'); + expect(res).toContain('Echo: Custom params test'); + }); +}); diff --git a/packages/sdk/tests/unit/005-LLM/07-llm-minimax.test.ts b/packages/sdk/tests/unit/005-LLM/07-llm-minimax.test.ts new file mode 100644 index 000000000..be7997a09 --- /dev/null +++ b/packages/sdk/tests/unit/005-LLM/07-llm-minimax.test.ts @@ -0,0 +1,143 @@ +// prettier-ignore-file +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { LLM, LLMInstance, Model } from '../../../src'; + +// Mock @smythos/sre dependencies used by LLMInstance +vi.mock('@smythos/sre', async () => { + const EventEmitter = (await import('events')).EventEmitter; + class DummyRequester { + constructor(public candidate?: any) {} + async request(params: any) { + const userMsg = params?.messages?.find((m: any) => m.role === 'user')?.content || ''; + const sysMsg = params?.messages?.find((m: any) => m.role === 'system')?.content || ''; + return { + content: (sysMsg ? sysMsg + ' ' : '') + `Echo: ${userMsg}`, + finishReason: 'stop', + } as any; + } + async streamRequest(params: any) { + const emitter = new EventEmitter(); + setTimeout(() => { + const userMsg = params?.messages?.find((m: any) => m.role === 'user')?.content || ''; + emitter.emit('content', 'Echo: ' + userMsg); + emitter.emit('end'); + }, 0); + return emitter as any; + } + } + return { + TLLMProvider: { + OpenAI: 'OpenAI', + MiniMax: 'MiniMax', + Anthropic: 'Anthropic', + GoogleAI: 'GoogleAI', + DeepSeek: 'DeepSeek', + Groq: 'Groq', + TogetherAI: 'TogetherAI', + Bedrock: 'Bedrock', + VertexAI: 'VertexAI', + xAI: 'xAI', + Perplexity: 'Perplexity', + Ollama: 'Ollama', + Echo: 'Echo', + }, + DEFAULT_TEAM_ID: 'default', + AccessCandidate: { + team: (id: string) => ({ type: 'team', id }), + }, + ConnectorService: { + getModelsProviderConnector() { + return { + requester() { + return { + async getModels() { + return { + 'MiniMax-M2.7': { tokens: 1000000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.7-highspeed': { tokens: 1000000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.5': { tokens: 204000, completionTokens: 16384, keyOptions: {} }, + 'MiniMax-M2.5-highspeed': { tokens: 204000, completionTokens: 16384, keyOptions: {} }, + } as any; + }, + } as any; + }, + } as any; + }, + getLLMConnector() { + return { + user(candidate: any) { + return new DummyRequester(candidate) as any; + }, + } as any; + }, + }, + BinaryInput: class {}, + SRE: { init: vi.fn(), ready: vi.fn().mockResolvedValue(true), initializing: false }, + } as any; +}); + +describe('LLM - MiniMax provider', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates MiniMax LLM via factory with model string and prompts', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7', { temperature: 0.7 }); + expect(llm).toBeInstanceOf(LLMInstance); + const res = await llm.prompt('What is the capital of France?'); + expect(res).toContain('Echo:'); + expect(typeof res).toBe('string'); + }); + + it('creates MiniMax LLM via factory with params object and prompts', async () => { + const llm = LLM.MiniMax({ model: 'MiniMax-M2.5', maxTokens: 100 }); + expect(llm).toBeInstanceOf(LLMInstance); + const res = await llm.prompt('Say hi'); + expect(res).toContain('Echo: Say hi'); + }); + + it('creates MiniMax M2.7-highspeed model instance', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7-highspeed'); + expect(llm).toBeInstanceOf(LLMInstance); + const res = await llm.prompt('Hello'); + expect(res).toContain('Echo: Hello'); + }); + + it('creates MiniMax M2.5-highspeed model instance', async () => { + const llm = LLM.MiniMax('MiniMax-M2.5-highspeed', { temperature: 0.5 }); + expect(llm).toBeInstanceOf(LLMInstance); + const res = await llm.prompt('Test'); + expect(res).toContain('Echo: Test'); + }); + + it('applies behavior from model settings for MiniMax', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7', { behavior: 'PREFIX>' }); + const res = await llm.prompt('Hello'); + expect(res.startsWith('PREFIX>')).toBeTruthy(); + }); + + it('overrides behavior via prompt options for MiniMax', async () => { + const llm = LLM.MiniMax('MiniMax-M2.7', { behavior: 'BASE>' }); + const res = await llm.prompt('Hello', { behavior: 'OVERRIDE>' }); + expect(res.startsWith('OVERRIDE>')).toBeTruthy(); + expect(res).not.toContain('BASE> Hello'); + }); + + it('creates MiniMax model via Model factory', () => { + const model = Model.MiniMax('MiniMax-M2.7'); + expect(model).toBeDefined(); + }); + + it('creates MiniMax model via Model factory with params', () => { + const model = Model.MiniMax({ model: 'MiniMax-M2.5', temperature: 0.8 }); + expect(model).toBeDefined(); + }); + + it('LLM.MiniMax exists as a factory function', () => { + expect(typeof LLM.MiniMax).toBe('function'); + }); + + it('Model.MiniMax exists as a factory function', () => { + expect(typeof Model.MiniMax).toBe('function'); + }); +});