Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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()
Expand Down
9 changes: 8 additions & 1 deletion lib/llm/minimax.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
23 changes: 21 additions & 2 deletions test/llm-minimax-integration.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
25 changes: 17 additions & 8 deletions test/llm-minimax.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(() =>
Expand All @@ -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) => {
Expand All @@ -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',
}),
});
});
Expand All @@ -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');
Expand Down
Loading