diff --git a/.env.example b/.env.example index 674e8823..2fcc83a4 100644 --- a/.env.example +++ b/.env.example @@ -38,7 +38,7 @@ LLM_PROVIDER= # Not needed for codex (uses ~/.codex/auth.json) or ollama (local) LLM_API_KEY= # Optional override. Each provider has a sensible default: -# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto | minimax: MiniMax-M2.5 | ollama: llama3.1:8b | grok: grok-4-latest +# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto | minimax: MiniMax-M3 (also supports MiniMax-M2.7) | ollama: llama3.1:8b | grok: grok-4-latest LLM_MODEL= # Ollama base URL (only needed if not using default http://localhost:11434) OLLAMA_BASE_URL= diff --git a/README.md b/README.md index a05f962a..1c8d308d 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ Set `LLM_PROVIDER` to one of: `anthropic`, `openai`, `gemini`, `codex`, `openrou | `gemini` | `LLM_API_KEY` | gemini-3.1-pro | | `openrouter` | `LLM_API_KEY` | openrouter/auto | | `codex` | None (uses `~/.codex/auth.json`) | gpt-5.3-codex | -| `minimax` | `LLM_API_KEY` | MiniMax-M2.5 | +| `minimax` | `LLM_API_KEY` | MiniMax-M3 | | `mistral` | `LLM_API_KEY` | mistral-large-latest | | `grok` | `LLM_API_KEY` | grok-4-latest | @@ -311,7 +311,7 @@ crucix/ │ │ ├── grok.mjs # Grok │ │ ├── openrouter.mjs # OpenRouter (Unified API) │ │ ├── codex.mjs # Codex (ChatGPT subscription) -│ │ ├── minimax.mjs # MiniMax (M2.5, 204K context) +│ │ ├── minimax.mjs # MiniMax (M3, also supports M2.7) │ │ ├── mistral.mjs # Mistral AI │ │ ├── ideas.mjs # LLM-powered trade idea generation │ │ └── index.mjs # Factory: createLLMProvider() diff --git a/lib/llm/minimax.mjs b/lib/llm/minimax.mjs index 7c9160bf..6c205d4c 100644 --- a/lib/llm/minimax.mjs +++ b/lib/llm/minimax.mjs @@ -3,12 +3,19 @@ import { LLMProvider } from './provider.mjs'; +// Default text model and the full set of supported text model IDs. +// `modelIds` is the authoritative list of currently supported models; the +// default is the first entry. An explicit `LLM_MODEL` override always wins. +const MINIMAX_DEFAULT_MODEL = 'MiniMax-M3'; +const MINIMAX_MODEL_IDS = ['MiniMax-M3', 'MiniMax-M2.7']; + export class MiniMaxProvider extends LLMProvider { constructor(config) { super(config); this.name = 'minimax'; this.apiKey = config.apiKey; - this.model = config.model || 'MiniMax-M2.5'; + this.model = config.model || MINIMAX_DEFAULT_MODEL; + this.modelIds = MINIMAX_MODEL_IDS; } get isConfigured() { return !!this.apiKey; } diff --git a/test/llm-minimax-integration.test.mjs b/test/llm-minimax-integration.test.mjs index 08b0c7c8..2b542755 100644 --- a/test/llm-minimax-integration.test.mjs +++ b/test/llm-minimax-integration.test.mjs @@ -9,8 +9,27 @@ import { MiniMaxProvider } from '../lib/llm/minimax.mjs'; const API_KEY = process.env.MINIMAX_API_KEY; describe('MiniMax integration', { skip: !API_KEY && 'MINIMAX_API_KEY not set' }, () => { - it('should complete a prompt with MiniMax-M2.5', async () => { - const provider = new MiniMaxProvider({ apiKey: API_KEY, model: 'MiniMax-M2.5' }); + it('should complete a prompt with MiniMax-M3', async () => { + const provider = new MiniMaxProvider({ apiKey: API_KEY, model: 'MiniMax-M3' }); + assert.equal(provider.isConfigured, true); + + const result = await provider.complete( + 'You are a helpful assistant. Respond in exactly one sentence.', + 'What is 2+2?', + { maxTokens: 128, timeout: 30000 } + ); + + assert.ok(result.text.length > 0, 'Response text should not be empty'); + assert.ok(result.usage.inputTokens > 0, 'Should report input tokens'); + assert.ok(result.usage.outputTokens > 0, 'Should report output tokens'); + assert.ok(result.model, 'Should report model name'); + console.log(` Response: ${result.text}`); + console.log(` Tokens: ${result.usage.inputTokens} in / ${result.usage.outputTokens} out`); + console.log(` Model: ${result.model}`); + }); + + it('should complete a prompt with MiniMax-M2.7', async () => { + const provider = new MiniMaxProvider({ apiKey: API_KEY, model: 'MiniMax-M2.7' }); assert.equal(provider.isConfigured, true); const result = await provider.complete( diff --git a/test/llm-minimax.test.mjs b/test/llm-minimax.test.mjs index f2d3b7d5..84fe273f 100644 --- a/test/llm-minimax.test.mjs +++ b/test/llm-minimax.test.mjs @@ -12,13 +12,22 @@ describe('MiniMaxProvider', () => { it('should set defaults correctly', () => { const provider = new MiniMaxProvider({ apiKey: 'sk-test' }); assert.equal(provider.name, 'minimax'); - assert.equal(provider.model, 'MiniMax-M2.5'); + assert.equal(provider.model, 'MiniMax-M3'); + assert.deepEqual(provider.modelIds, ['MiniMax-M3', 'MiniMax-M2.7']); assert.equal(provider.isConfigured, true); }); it('should accept custom model', () => { - const provider = new MiniMaxProvider({ apiKey: 'sk-test', model: 'MiniMax-M2.5-highspeed' }); - assert.equal(provider.model, 'MiniMax-M2.5-highspeed'); + const provider = new MiniMaxProvider({ apiKey: 'sk-test', model: 'MiniMax-M2.7' }); + assert.equal(provider.model, 'MiniMax-M2.7'); + // explicit model does not change the supported model set + assert.deepEqual(provider.modelIds, ['MiniMax-M3', 'MiniMax-M2.7']); + }); + + it('should expose both supported model ids', () => { + const provider = new MiniMaxProvider({ apiKey: 'sk-test' }); + assert.ok(provider.modelIds.includes('MiniMax-M3'), 'should include MiniMax-M3'); + assert.ok(provider.modelIds.includes('MiniMax-M2.7'), 'should include MiniMax-M2.7'); }); it('should report not configured without API key', () => { @@ -50,7 +59,7 @@ describe('MiniMaxProvider', () => { const mockResponse = { choices: [{ message: { content: 'Hello from MiniMax' } }], usage: { prompt_tokens: 10, completion_tokens: 5 }, - model: 'MiniMax-M2.5', + model: 'MiniMax-M3', }; const originalFetch = globalThis.fetch; globalThis.fetch = mock.fn(() => @@ -61,14 +70,14 @@ describe('MiniMaxProvider', () => { assert.equal(result.text, 'Hello from MiniMax'); assert.equal(result.usage.inputTokens, 10); assert.equal(result.usage.outputTokens, 5); - assert.equal(result.model, 'MiniMax-M2.5'); + assert.equal(result.model, 'MiniMax-M3'); } finally { globalThis.fetch = originalFetch; } }); it('should send correct request format', async () => { - const provider = new MiniMaxProvider({ apiKey: 'sk-test-key', model: 'MiniMax-M2.5' }); + const provider = new MiniMaxProvider({ apiKey: 'sk-test-key', model: 'MiniMax-M3' }); let capturedUrl, capturedOpts; const originalFetch = globalThis.fetch; globalThis.fetch = mock.fn((url, opts) => { @@ -79,7 +88,7 @@ describe('MiniMaxProvider', () => { json: () => Promise.resolve({ choices: [{ message: { content: 'ok' } }], usage: { prompt_tokens: 1, completion_tokens: 1 }, - model: 'MiniMax-M2.5', + model: 'MiniMax-M3', }), }); }); @@ -91,7 +100,7 @@ describe('MiniMaxProvider', () => { assert.equal(headers['Content-Type'], 'application/json'); assert.equal(headers['Authorization'], 'Bearer sk-test-key'); const body = JSON.parse(capturedOpts.body); - assert.equal(body.model, 'MiniMax-M2.5'); + assert.equal(body.model, 'MiniMax-M3'); assert.equal(body.max_tokens, 2048); assert.equal(body.messages[0].role, 'system'); assert.equal(body.messages[0].content, 'system prompt');