Skip to content

Commit ccc2ebd

Browse files
PR Botkapelame
authored andcommitted
feat: add MiniMax as LLM provider
Add MiniMax as a first-class LLM provider to the Portkey AI Gateway, supporting MiniMax-M2.7 and MiniMax-M2.7-highspeed models via their OpenAI-compatible API at https://api.minimax.io/v1. - Add provider files (api.ts, chatComplete.ts, index.ts) with streaming support and response transforms - Register provider in globals, provider index, providers.json, and models.json - Use open-ai-base for chat completion params with temperature default of 1.0 - Exclude unsupported params: logit_bias, logprobs, top_logprobs - Add 22 unit tests covering config, transforms, and streaming - Add 3 integration tests with real API validation - Add test variables entry for MINIMAX_API_KEY Co-authored-by: PR Bot <pr-bot@minimaxi.com> Signed-off-by: kapelame <kapelame@users.noreply.github.com>
1 parent 351692f commit ccc2ebd

10 files changed

Lines changed: 548 additions & 0 deletions

File tree

src/data/models.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15425,6 +15425,22 @@
1542515425
"id": "openai"
1542615426
},
1542715427
"name": "Text Embedding Ada 002"
15428+
},
15429+
{
15430+
"id": "MiniMax-M2.7",
15431+
"object": "model",
15432+
"provider": {
15433+
"id": "minimax"
15434+
},
15435+
"name": "MiniMax M2.7"
15436+
},
15437+
{
15438+
"id": "MiniMax-M2.7-highspeed",
15439+
"object": "model",
15440+
"provider": {
15441+
"id": "minimax"
15442+
},
15443+
"name": "MiniMax M2.7 Highspeed"
1542815444
}
1542915445
]
1543015446
}

src/data/providers.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,13 @@
323323
"object": "provider",
324324
"description": "Dashscope provides intelligent analytics solutions designed to help businesses visualize and interpret complex data effectively. Their platform integrates advanced machine learning algorithms to generate real-time insights from diverse datasets, enabling organizations to make informed decisions quickly. Dashscope focuses on enhancing data accessibility and usability, empowering teams to leverage analytics for strategic planning and operational efficiency.",
325325
"base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1"
326+
},
327+
{
328+
"id": "minimax",
329+
"name": "MiniMax",
330+
"object": "provider",
331+
"description": "MiniMax is a leading AI company that develops advanced large language models including MiniMax-M2.7 and MiniMax-M2.7-highspeed. Their models support 204,800 tokens context window and are accessible via an OpenAI-compatible API, enabling seamless integration for text generation, chat, and tool-calling applications.",
332+
"base_url": "https://api.minimax.io/v1"
326333
}
327334
]
328335
}

src/globals.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export const ORACLE: string = 'oracle';
113113
export const IO_INTELLIGENCE: string = 'iointelligence';
114114
export const AIBADGR: string = 'aibadgr';
115115
export const OVHCLOUD: string = 'ovhcloud';
116+
export const MINIMAX: string = 'minimax';
116117

117118
export const VALID_PROVIDERS = [
118119
ANTHROPIC,
@@ -189,6 +190,7 @@ export const VALID_PROVIDERS = [
189190
IO_INTELLIGENCE,
190191
AIBADGR,
191192
OVHCLOUD,
193+
MINIMAX,
192194
];
193195

194196
export const CONTENT_TYPES = {

src/providers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import OracleConfig from './oracle';
7474
import IOIntelligenceConfig from './iointelligence';
7575
import AIBadgrConfig from './aibadgr';
7676
import OVHcloudConfig from './ovhcloud';
77+
import MiniMaxConfig from './minimax';
7778

7879
const Providers: { [key: string]: ProviderConfigs } = {
7980
openai: OpenAIConfig,
@@ -148,6 +149,7 @@ const Providers: { [key: string]: ProviderConfigs } = {
148149
iointelligence: IOIntelligenceConfig,
149150
aibadgr: AIBadgrConfig,
150151
ovhcloud: OVHcloudConfig,
152+
minimax: MiniMaxConfig,
151153
};
152154

153155
export default Providers;

src/providers/minimax/api.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ProviderAPIConfig } from '../types';
2+
3+
const MiniMaxAPIConfig: ProviderAPIConfig = {
4+
getBaseURL: () => 'https://api.minimax.io/v1',
5+
headers: ({ providerOptions }) => {
6+
return {
7+
Authorization: `Bearer ${providerOptions.apiKey}`,
8+
};
9+
},
10+
getEndpoint: ({ fn }) => {
11+
switch (fn) {
12+
case 'chatComplete':
13+
return '/chat/completions';
14+
default:
15+
return '';
16+
}
17+
},
18+
};
19+
20+
export default MiniMaxAPIConfig;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { MINIMAX } from '../../globals';
2+
import { ChatCompletionResponse, ErrorResponse } from '../types';
3+
import {
4+
generateErrorResponse,
5+
generateInvalidProviderResponseError,
6+
} from '../utils';
7+
8+
export interface MiniMaxChatCompleteResponse extends ChatCompletionResponse {}
9+
10+
export interface MiniMaxErrorResponse extends ErrorResponse {}
11+
12+
export interface MiniMaxStreamChunk {
13+
id: string;
14+
object: string;
15+
created: number;
16+
model: string;
17+
choices: {
18+
delta: {
19+
role?: string | null;
20+
content?: string;
21+
tool_calls?: object[];
22+
};
23+
index: number;
24+
finish_reason: string | null;
25+
}[];
26+
usage?: {
27+
prompt_tokens: number;
28+
completion_tokens: number;
29+
total_tokens: number;
30+
};
31+
}
32+
33+
export const MiniMaxChatCompleteResponseTransform: (
34+
response: MiniMaxChatCompleteResponse | MiniMaxErrorResponse,
35+
responseStatus: number
36+
) => ChatCompletionResponse | ErrorResponse = (response, responseStatus) => {
37+
if ('error' in response && responseStatus !== 200) {
38+
return generateErrorResponse(
39+
{
40+
message: response.error.message,
41+
type: response.error.type,
42+
param: null,
43+
code: response.error.code?.toString() || null,
44+
},
45+
MINIMAX
46+
);
47+
}
48+
49+
if ('choices' in response) {
50+
return {
51+
id: response.id,
52+
object: response.object,
53+
created: response.created,
54+
model: response.model,
55+
provider: MINIMAX,
56+
choices: response.choices.map((c) => ({
57+
index: c.index,
58+
message: c.message,
59+
logprobs: c.logprobs,
60+
finish_reason: c.finish_reason,
61+
})),
62+
usage: {
63+
prompt_tokens: response.usage?.prompt_tokens || 0,
64+
completion_tokens: response.usage?.completion_tokens || 0,
65+
total_tokens: response.usage?.total_tokens || 0,
66+
},
67+
};
68+
}
69+
70+
return generateInvalidProviderResponseError(response, MINIMAX);
71+
};
72+
73+
export const MiniMaxChatCompleteStreamChunkTransform: (
74+
response: string
75+
) => string = (responseChunk) => {
76+
let chunk = responseChunk.trim();
77+
chunk = chunk.replace(/^data: /, '');
78+
chunk = chunk.trim();
79+
if (chunk === '[DONE]') {
80+
return `data: ${chunk}\n\n`;
81+
}
82+
83+
const parsedChunk: MiniMaxStreamChunk = JSON.parse(chunk);
84+
return (
85+
`data: ${JSON.stringify({
86+
id: parsedChunk.id,
87+
object: parsedChunk.object,
88+
created: parsedChunk.created,
89+
model: parsedChunk.model,
90+
provider: MINIMAX,
91+
choices:
92+
parsedChunk.choices && parsedChunk.choices.length > 0
93+
? [
94+
{
95+
index: parsedChunk.choices[0].index || 0,
96+
delta: {
97+
role: parsedChunk.choices[0].delta?.role || undefined,
98+
content: parsedChunk.choices[0].delta?.content || '',
99+
tool_calls:
100+
parsedChunk.choices[0].delta?.tool_calls || undefined,
101+
},
102+
finish_reason: parsedChunk.choices[0].finish_reason || null,
103+
},
104+
]
105+
: [],
106+
usage: parsedChunk.usage
107+
? {
108+
prompt_tokens: parsedChunk.usage.prompt_tokens || 0,
109+
completion_tokens: parsedChunk.usage.completion_tokens || 0,
110+
total_tokens: parsedChunk.usage.total_tokens || 0,
111+
}
112+
: undefined,
113+
})}` + '\n\n'
114+
);
115+
};

src/providers/minimax/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { MINIMAX } from '../../globals';
2+
import { chatCompleteParams, responseTransformers } from '../open-ai-base';
3+
import { ProviderConfigs } from '../types';
4+
import MiniMaxAPIConfig from './api';
5+
import { MiniMaxChatCompleteStreamChunkTransform } from './chatComplete';
6+
7+
const MiniMaxConfig: ProviderConfigs = {
8+
chatComplete: chatCompleteParams(
9+
['logit_bias', 'logprobs', 'top_logprobs', 'parallel_tool_calls'],
10+
{ temperature: 1 },
11+
{
12+
response_format: {
13+
param: 'response_format',
14+
default: null,
15+
},
16+
}
17+
),
18+
api: MiniMaxAPIConfig,
19+
responseTransforms: {
20+
...responseTransformers(MINIMAX, {
21+
chatComplete: true,
22+
}),
23+
'stream-chatComplete': MiniMaxChatCompleteStreamChunkTransform,
24+
},
25+
};
26+
27+
export default MiniMaxConfig;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Integration tests for MiniMax provider.
3+
* These tests call the real MiniMax API and require MINIMAX_API_KEY env var.
4+
* Run with: npx jest src/tests/minimax.integration.test.ts
5+
*/
6+
7+
const MINIMAX_API_KEY = process.env.MINIMAX_API_KEY;
8+
const BASE_URL = 'https://api.minimax.io/v1';
9+
10+
const skipIfNoKey = MINIMAX_API_KEY ? describe : describe.skip;
11+
12+
skipIfNoKey('MiniMax integration tests', () => {
13+
test('non-streaming chat completion with MiniMax-M2.7', async () => {
14+
const response = await fetch(`${BASE_URL}/chat/completions`, {
15+
method: 'POST',
16+
headers: {
17+
Authorization: `Bearer ${MINIMAX_API_KEY}`,
18+
'Content-Type': 'application/json',
19+
},
20+
body: JSON.stringify({
21+
model: 'MiniMax-M2.7',
22+
messages: [
23+
{ role: 'system', content: 'You are a helpful assistant.' },
24+
{ role: 'user', content: 'Say hello in one word.' },
25+
],
26+
max_tokens: 30,
27+
temperature: 1.0,
28+
stream: false,
29+
}),
30+
});
31+
32+
expect(response.status).toBe(200);
33+
const data = await response.json();
34+
expect(data).toHaveProperty('id');
35+
expect(data).toHaveProperty('model');
36+
expect(data).toHaveProperty('choices');
37+
expect(data.choices.length).toBeGreaterThan(0);
38+
expect(data.choices[0]).toHaveProperty('message');
39+
expect(data.choices[0].message).toHaveProperty('content');
40+
expect(data).toHaveProperty('usage');
41+
expect(data.usage).toHaveProperty('prompt_tokens');
42+
expect(data.usage).toHaveProperty('completion_tokens');
43+
expect(data.usage).toHaveProperty('total_tokens');
44+
});
45+
46+
test('streaming chat completion with MiniMax-M2.7-highspeed', async () => {
47+
const response = await fetch(`${BASE_URL}/chat/completions`, {
48+
method: 'POST',
49+
headers: {
50+
Authorization: `Bearer ${MINIMAX_API_KEY}`,
51+
'Content-Type': 'application/json',
52+
},
53+
body: JSON.stringify({
54+
model: 'MiniMax-M2.7-highspeed',
55+
messages: [{ role: 'user', content: 'Say hi' }],
56+
max_tokens: 20,
57+
temperature: 1.0,
58+
stream: true,
59+
}),
60+
});
61+
62+
expect(response.status).toBe(200);
63+
const text = await response.text();
64+
const lines = text
65+
.split('\n')
66+
.filter((line: string) => line.startsWith('data: '));
67+
expect(lines.length).toBeGreaterThan(0);
68+
69+
// Check that the last meaningful chunk or a data chunk has proper structure
70+
const firstLine = lines[0];
71+
const firstData = JSON.parse(firstLine.replace('data: ', ''));
72+
expect(firstData).toHaveProperty('id');
73+
expect(firstData).toHaveProperty('model');
74+
expect(firstData).toHaveProperty('choices');
75+
76+
// Verify at least one chunk has finish_reason
77+
const hasFinishReason = lines.some((line: string) => {
78+
try {
79+
const parsed = JSON.parse(line.replace('data: ', ''));
80+
return parsed.choices?.[0]?.finish_reason !== null;
81+
} catch {
82+
return false;
83+
}
84+
});
85+
expect(hasFinishReason).toBe(true);
86+
});
87+
88+
test('error handling with invalid API key', async () => {
89+
const response = await fetch(`${BASE_URL}/chat/completions`, {
90+
method: 'POST',
91+
headers: {
92+
Authorization: 'Bearer invalid-key',
93+
'Content-Type': 'application/json',
94+
},
95+
body: JSON.stringify({
96+
model: 'MiniMax-M2.7',
97+
messages: [{ role: 'user', content: 'Hello' }],
98+
max_tokens: 10,
99+
}),
100+
});
101+
102+
expect(response.status).not.toBe(200);
103+
});
104+
});

0 commit comments

Comments
 (0)