Skip to content
Closed
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 README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ providers:
apiKey: sk-ant-...
```

支持的服务商:**OpenAI**、**Anthropic**、**Google Gemini**、**DeepSeek** 以及任何兼容 OpenAI API 的服务。
支持的服务商:**OpenAI**、**Anthropic**、**Google Gemini**、**DeepSeek**、**MiniMax** 以及任何兼容 OpenAI API 的服务。

> **推荐模型:** **Gemini 3 Flash** — 效果与速度的最佳平衡。追求最高质量可选 **Gemini 3.1 Pro**(速度较慢)。
>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ providers:
apiKey: sk-ant-...
```

Supported providers: **OpenAI**, **Anthropic**, **Google Gemini**, **DeepSeek**, and any OpenAI-compatible API.
Supported providers: **OpenAI**, **Anthropic**, **Google Gemini**, **DeepSeek**, **MiniMax**, and any OpenAI-compatible API.

> **Recommended model:** **Gemini 3 Flash** — best balance of quality and speed. For highest quality (at slower speed), try **Gemini 3.1 Pro**.
>
Expand Down
100 changes: 100 additions & 0 deletions lib/ai/__tests__/providers.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Integration tests for MiniMax provider — validates that getModel()
* creates a valid LanguageModel instance via the Anthropic SDK adapter.
*
* These tests do NOT make real API calls; they verify that the provider
* configuration integrates correctly with the Vercel AI SDK factory
* functions and that the resulting model object has the expected shape.
*/

import { describe, it, expect } from 'vitest';
import { getModel, PROVIDERS } from '../providers';

describe('MiniMax getModel() integration', () => {
it('should create a model instance for MiniMax-M2.7', () => {
const { model, modelInfo } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.7',
apiKey: 'test-key-not-used',
});

expect(model).toBeDefined();
expect(model.modelId).toBe('MiniMax-M2.7');
expect(modelInfo).toBeDefined();
expect(modelInfo!.name).toBe('MiniMax M2.7');
});

it('should create a model instance for MiniMax-M2.5-highspeed', () => {
const { model, modelInfo } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.5-highspeed',
apiKey: 'test-key-not-used',
});

expect(model).toBeDefined();
expect(model.modelId).toBe('MiniMax-M2.5-highspeed');
expect(modelInfo).toBeDefined();
expect(modelInfo!.name).toBe('MiniMax M2.5 Highspeed');
});

it('should create a model instance for MiniMax-M2.5', () => {
const { model, modelInfo } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.5',
apiKey: 'test-key-not-used',
});

expect(model).toBeDefined();
expect(model.modelId).toBe('MiniMax-M2.5');
expect(modelInfo).toBeDefined();
});

it('should throw when API key is missing', () => {
expect(() =>
getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.7',
apiKey: '',
}),
).toThrow('API key required for provider: minimax');
});

it('should use the provider default base URL', () => {
const provider = PROVIDERS.minimax;
expect(provider.defaultBaseUrl).toBe(
'https://api.minimaxi.com/anthropic/v1',
);

// The model is created with the provider's default base URL
const { model } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.7',
apiKey: 'test-key-not-used',
});
expect(model).toBeDefined();
});

it('should allow overriding the base URL', () => {
const { model } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.7',
apiKey: 'test-key-not-used',
baseUrl: 'https://custom-proxy.example.com/v1',
});
expect(model).toBeDefined();
});

it('should use anthropic provider type for model creation', () => {
// Verify the provider type is 'anthropic' which routes through createAnthropic()
expect(PROVIDERS.minimax.type).toBe('anthropic');

// The model should be created successfully via the anthropic code path
const { model } = getModel({
providerId: 'minimax',
modelId: 'MiniMax-M2.7',
apiKey: 'test-key-not-used',
});
expect(model).toBeDefined();
expect(model.provider).toContain('anthropic');
});
});
163 changes: 163 additions & 0 deletions lib/ai/__tests__/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Unit tests for AI providers configuration — MiniMax provider
*/

import { describe, it, expect } from 'vitest';
import {
PROVIDERS,
getProvider,
getModelInfo,
parseModelString,
getAllModels,
} from '../providers';

describe('MiniMax provider configuration', () => {
it('should be registered as a built-in provider', () => {
expect(PROVIDERS.minimax).toBeDefined();
expect(PROVIDERS.minimax.id).toBe('minimax');
expect(PROVIDERS.minimax.name).toBe('MiniMax');
});

it('should use the Anthropic-compatible API type', () => {
expect(PROVIDERS.minimax.type).toBe('anthropic');
});

it('should have a valid default base URL', () => {
expect(PROVIDERS.minimax.defaultBaseUrl).toBe(
'https://api.minimaxi.com/anthropic/v1',
);
});

it('should require an API key', () => {
expect(PROVIDERS.minimax.requiresApiKey).toBe(true);
});

it('should have an icon path', () => {
expect(PROVIDERS.minimax.icon).toBe('/logos/minimax.svg');
});

it('should include MiniMax-M2.7 as the flagship model', () => {
const m27 = PROVIDERS.minimax.models.find(
(m) => m.id === 'MiniMax-M2.7',
);
expect(m27).toBeDefined();
expect(m27!.name).toBe('MiniMax M2.7');
expect(m27!.contextWindow).toBe(204800);
expect(m27!.outputWindow).toBe(8192);
});

it('should list MiniMax-M2.7 as the first (top) model', () => {
expect(PROVIDERS.minimax.models[0].id).toBe('MiniMax-M2.7');
});

it('should include MiniMax-M2.5', () => {
const m25 = PROVIDERS.minimax.models.find(
(m) => m.id === 'MiniMax-M2.5',
);
expect(m25).toBeDefined();
expect(m25!.name).toBe('MiniMax M2.5');
});

it('should include MiniMax-M2.5-highspeed', () => {
const m25hs = PROVIDERS.minimax.models.find(
(m) => m.id === 'MiniMax-M2.5-highspeed',
);
expect(m25hs).toBeDefined();
expect(m25hs!.name).toBe('MiniMax M2.5 Highspeed');
expect(m25hs!.contextWindow).toBe(204800);
});

it('should include legacy models (M2.1, M2.1-lightning, M2)', () => {
const m21 = PROVIDERS.minimax.models.find(
(m) => m.id === 'MiniMax-M2.1',
);
const m21l = PROVIDERS.minimax.models.find(
(m) => m.id === 'MiniMax-M2.1-lightning',
);
const m2 = PROVIDERS.minimax.models.find((m) => m.id === 'MiniMax-M2');
expect(m21).toBeDefined();
expect(m21l).toBeDefined();
expect(m2).toBeDefined();
});

it('should have 6 models in total', () => {
expect(PROVIDERS.minimax.models).toHaveLength(6);
});

it('all models should support streaming and tools', () => {
for (const model of PROVIDERS.minimax.models) {
expect(model.capabilities?.streaming).toBe(true);
expect(model.capabilities?.tools).toBe(true);
}
});

it('no models should claim vision support', () => {
for (const model of PROVIDERS.minimax.models) {
expect(model.capabilities?.vision).toBe(false);
}
});
});

describe('getProvider() for MiniMax', () => {
it('should return the MiniMax provider', () => {
const provider = getProvider('minimax');
expect(provider).toBeDefined();
expect(provider!.id).toBe('minimax');
});
});

describe('getModelInfo() for MiniMax models', () => {
it('should return info for MiniMax-M2.7', () => {
const info = getModelInfo('minimax', 'MiniMax-M2.7');
expect(info).toBeDefined();
expect(info!.name).toBe('MiniMax M2.7');
});

it('should return info for MiniMax-M2.5-highspeed', () => {
const info = getModelInfo('minimax', 'MiniMax-M2.5-highspeed');
expect(info).toBeDefined();
expect(info!.name).toBe('MiniMax M2.5 Highspeed');
});

it('should return undefined for unknown model', () => {
const info = getModelInfo('minimax', 'MiniMax-M99');
expect(info).toBeUndefined();
});
});

describe('parseModelString() with MiniMax', () => {
it('should parse "minimax:MiniMax-M2.7"', () => {
const result = parseModelString('minimax:MiniMax-M2.7');
expect(result.providerId).toBe('minimax');
expect(result.modelId).toBe('MiniMax-M2.7');
});

it('should parse "minimax:MiniMax-M2.5-highspeed"', () => {
const result = parseModelString('minimax:MiniMax-M2.5-highspeed');
expect(result.providerId).toBe('minimax');
expect(result.modelId).toBe('MiniMax-M2.5-highspeed');
});
});

describe('getAllModels() includes MiniMax', () => {
it('should include MiniMax in the grouped results', () => {
const all = getAllModels();
const minimaxGroup = all.find((g) => g.provider.id === 'minimax');
expect(minimaxGroup).toBeDefined();
expect(minimaxGroup!.models.length).toBeGreaterThanOrEqual(6);
});
});

describe('MiniMax model ordering', () => {
it('should list models from newest to oldest', () => {
const ids = PROVIDERS.minimax.models.map((m) => m.id);
expect(ids).toEqual([
'MiniMax-M2.7',
'MiniMax-M2.5',
'MiniMax-M2.5-highspeed',
'MiniMax-M2.1',
'MiniMax-M2.1-lightning',
'MiniMax-M2',
]);
});
});
14 changes: 14 additions & 0 deletions lib/ai/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,13 +668,27 @@ export const PROVIDERS: Record<ProviderId, ProviderConfig> = {
requiresApiKey: true,
icon: '/logos/minimax.svg',
models: [
{
id: 'MiniMax-M2.7',
name: 'MiniMax M2.7',
contextWindow: 204800,
outputWindow: 8192,
capabilities: { streaming: true, tools: true, vision: false },
},
{
id: 'MiniMax-M2.5',
name: 'MiniMax M2.5',
contextWindow: 204800,
outputWindow: 8192,
capabilities: { streaming: true, tools: true, vision: false },
},
{
id: 'MiniMax-M2.5-highspeed',
name: 'MiniMax M2.5 Highspeed',
contextWindow: 204800,
outputWindow: 8192,
capabilities: { streaming: true, tools: true, vision: false },
},
{
id: 'MiniMax-M2.1',
name: 'MiniMax M2.1',
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"start": "next start",
"lint": "eslint",
"check": "prettier . --check",
"format": "prettier . --write"
"format": "prettier . --write",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@ai-sdk/anthropic": "^3.0.23",
Expand Down Expand Up @@ -114,6 +116,7 @@
"tailwindcss": "^4",
"tslib": "^2.8.0",
"typescript": "^5",
"vitest": "^4.1.0",
"vue-to-react": "^1.0.0"
},
"pnpm": {
Expand Down
14 changes: 14 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, '.'),
},
},
test: {
include: ['**/*.test.ts'],
environment: 'node',
},
});