Skip to content

Commit 195155b

Browse files
octo-patchPR Botomeraplak
authored
fix: use OpenAI-compatible adapter for MiniMax provider (#1167)
* fix: use OpenAI-compatible adapter for MiniMax provider instead of Anthropic The auto-generated registry incorrectly configured MiniMax to use @ai-sdk/anthropic with an invalid /anthropic/v1 base URL. MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1. Changes: - Add MiniMax and MiniMax-CN entries to EXTRA_PROVIDER_REGISTRY with correct @ai-sdk/openai-compatible adapter and API URLs - Reorder STATIC_PROVIDER_REGISTRY so EXTRA entries take precedence over auto-generated ones in provider registration - Fix STATIC_PROVIDER_MAP construction to ensure EXTRA entries override auto-generated entries in config lookups - Update provider documentation with correct package, base URL, and complete model list (M2.7, M2.5 + highspeed variants) - Add 8 unit tests verifying correct adapter selection, base URL resolution, API key handling, and model variant support Signed-off-by: octopus <octopus@github.com> * chore: add changeset --------- Signed-off-by: octopus <octopus@github.com> Co-authored-by: PR Bot <pr-bot@minimaxi.com> Co-authored-by: Omer Aplak <omeraplak@gmail.com>
1 parent 5d6d386 commit 195155b

5 files changed

Lines changed: 228 additions & 29 deletions

File tree

.changeset/tall-birds-invite.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@voltagent/core": patch
3+
---
4+
5+
fix: use OpenAI-compatible adapter for MiniMax provider
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
// Track calls to createOpenAICompatible across module resets
4+
let createOpenAICompatibleCalls: unknown[][] = [];
5+
let createAnthropicCalls: unknown[][] = [];
6+
7+
// Mock @voltagent/internal to avoid build dependency
8+
vi.mock("@voltagent/internal", () => ({
9+
safeStringify: (value: unknown) => JSON.stringify(value),
10+
}));
11+
12+
// Mock @ai-sdk/openai-compatible with a tracking wrapper
13+
vi.mock("@ai-sdk/openai-compatible", () => ({
14+
createOpenAICompatible: (...args: unknown[]) => {
15+
createOpenAICompatibleCalls.push(args);
16+
const mockModel = {
17+
modelId: "mock-model",
18+
specificationVersion: "v1",
19+
provider: "minimax",
20+
};
21+
return {
22+
languageModel: () => mockModel,
23+
chatModel: () => mockModel,
24+
};
25+
},
26+
}));
27+
28+
// Mock @ai-sdk/anthropic to track if it's called for MiniMax
29+
vi.mock("@ai-sdk/anthropic", () => ({
30+
createAnthropic: (...args: unknown[]) => {
31+
createAnthropicCalls.push(args);
32+
return {
33+
languageModel: () => ({ modelId: "anthropic-model" }),
34+
chatModel: () => ({ modelId: "anthropic-model" }),
35+
};
36+
},
37+
anthropic: {
38+
languageModel: () => ({ modelId: "anthropic-model" }),
39+
chatModel: () => ({ modelId: "anthropic-model" }),
40+
},
41+
}));
42+
43+
describe("MiniMax provider registry", () => {
44+
const originalEnv = { ...process.env };
45+
46+
beforeEach(() => {
47+
createOpenAICompatibleCalls = [];
48+
createAnthropicCalls = [];
49+
(globalThis as Record<string, unknown>).___voltagent_model_provider_registry = undefined;
50+
process.env = { ...originalEnv };
51+
});
52+
53+
afterEach(() => {
54+
process.env = originalEnv;
55+
(globalThis as Record<string, unknown>).___voltagent_model_provider_registry = undefined;
56+
});
57+
58+
it("should list minimax as a registered provider", async () => {
59+
const { ModelProviderRegistry } = await import("./model-provider-registry");
60+
const registry = ModelProviderRegistry.getInstance();
61+
const providers = registry.listProviders();
62+
expect(providers).toContain("minimax");
63+
});
64+
65+
it("should list minimax-cn as a registered provider", async () => {
66+
const { ModelProviderRegistry } = await import("./model-provider-registry");
67+
const registry = ModelProviderRegistry.getInstance();
68+
const providers = registry.listProviders();
69+
expect(providers).toContain("minimax-cn");
70+
});
71+
72+
it("should load minimax provider via @ai-sdk/openai-compatible", async () => {
73+
process.env.MINIMAX_API_KEY = "test-key-minimax";
74+
75+
const { ModelProviderRegistry } = await import("./model-provider-registry");
76+
const registry = ModelProviderRegistry.getInstance();
77+
const model = await registry.resolveLanguageModel("minimax/MiniMax-M2.7");
78+
79+
expect(model).toBeDefined();
80+
expect(createOpenAICompatibleCalls.length).toBeGreaterThan(0);
81+
82+
const lastCall = createOpenAICompatibleCalls[createOpenAICompatibleCalls.length - 1];
83+
const config = lastCall[0] as Record<string, unknown>;
84+
expect(config.name).toBe("minimax");
85+
expect(config.baseURL).toBe("https://api.minimax.io/v1");
86+
expect(config.apiKey).toBe("test-key-minimax");
87+
});
88+
89+
it("should load minimax-cn provider with China base URL", async () => {
90+
process.env.MINIMAX_API_KEY = "test-key-minimax-cn";
91+
92+
const { ModelProviderRegistry } = await import("./model-provider-registry");
93+
const registry = ModelProviderRegistry.getInstance();
94+
const model = await registry.resolveLanguageModel("minimax-cn/MiniMax-M2.7");
95+
96+
expect(model).toBeDefined();
97+
98+
// Find the call for minimax-cn
99+
const cnCall = createOpenAICompatibleCalls.find((call) => {
100+
const config = call[0] as Record<string, unknown>;
101+
return config.name === "minimax-cn";
102+
});
103+
expect(cnCall).toBeDefined();
104+
const config = cnCall![0] as Record<string, unknown>;
105+
expect(config.baseURL).toBe("https://api.minimaxi.com/v1");
106+
expect(config.apiKey).toBe("test-key-minimax-cn");
107+
});
108+
109+
it("should support MINIMAX_BASE_URL override", async () => {
110+
process.env.MINIMAX_API_KEY = "test-key";
111+
process.env.MINIMAX_BASE_URL = "https://custom.minimax.io/v1";
112+
113+
const { ModelProviderRegistry } = await import("./model-provider-registry");
114+
const registry = ModelProviderRegistry.getInstance();
115+
await registry.resolveLanguageModel("minimax/MiniMax-M2.7");
116+
117+
const minimaxCall = createOpenAICompatibleCalls.find((call) => {
118+
const config = call[0] as Record<string, unknown>;
119+
return config.name === "minimax";
120+
});
121+
expect(minimaxCall).toBeDefined();
122+
const config = minimaxCall![0] as Record<string, unknown>;
123+
expect(config.baseURL).toBe("https://custom.minimax.io/v1");
124+
});
125+
126+
it("should throw if MINIMAX_API_KEY is not set", async () => {
127+
delete process.env.MINIMAX_API_KEY;
128+
129+
const { ModelProviderRegistry } = await import("./model-provider-registry");
130+
const registry = ModelProviderRegistry.getInstance();
131+
132+
await expect(registry.resolveLanguageModel("minimax/MiniMax-M2.7")).rejects.toThrow(
133+
/MINIMAX_API_KEY/,
134+
);
135+
});
136+
137+
it("should resolve multiple MiniMax model variants", async () => {
138+
process.env.MINIMAX_API_KEY = "test-key";
139+
140+
const { ModelProviderRegistry } = await import("./model-provider-registry");
141+
const registry = ModelProviderRegistry.getInstance();
142+
143+
const modelIds = [
144+
"MiniMax-M2.7",
145+
"MiniMax-M2.7-highspeed",
146+
"MiniMax-M2.5",
147+
"MiniMax-M2.5-highspeed",
148+
];
149+
150+
for (const modelId of modelIds) {
151+
const model = await registry.resolveLanguageModel(`minimax/${modelId}`);
152+
expect(model).toBeDefined();
153+
}
154+
});
155+
156+
it("should not use @ai-sdk/anthropic adapter for minimax", async () => {
157+
process.env.MINIMAX_API_KEY = "test-key";
158+
159+
const anthropicCallsBefore = createAnthropicCalls.length;
160+
161+
const { ModelProviderRegistry } = await import("./model-provider-registry");
162+
const registry = ModelProviderRegistry.getInstance();
163+
await registry.resolveLanguageModel("minimax/MiniMax-M2.7");
164+
165+
// Anthropic adapter should NOT have been called for MiniMax
166+
expect(createAnthropicCalls.length).toBe(anthropicCallsBefore);
167+
// OpenAI-compatible adapter SHOULD have been called
168+
expect(createOpenAICompatibleCalls.length).toBeGreaterThan(0);
169+
});
170+
});

packages/core/src/registries/model-provider-registry.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,40 @@ const EXTRA_PROVIDER_REGISTRY: ModelProviderRegistryEntry[] = [
144144
npm: "ollama-ai-provider-v2",
145145
doc: "https://ollama.com",
146146
},
147+
{
148+
id: "minimax",
149+
name: "MiniMax",
150+
npm: "@ai-sdk/openai-compatible",
151+
api: "https://api.minimax.io/v1",
152+
env: ["MINIMAX_API_KEY"],
153+
doc: "https://platform.minimax.io/docs/guides/quickstart",
154+
},
155+
{
156+
id: "minimax-cn",
157+
name: "MiniMax (China)",
158+
npm: "@ai-sdk/openai-compatible",
159+
api: "https://api.minimaxi.com/v1",
160+
env: ["MINIMAX_API_KEY"],
161+
doc: "https://platform.minimaxi.com/docs/guides/quickstart",
162+
},
147163
];
148164

165+
// EXTRA entries first so they take precedence over auto-generated entries
166+
// (registerProviderConfig skips IDs that are already registered)
149167
const STATIC_PROVIDER_REGISTRY = [
150-
...Object.values(MODEL_PROVIDER_REGISTRY),
151168
...EXTRA_PROVIDER_REGISTRY,
169+
...Object.values(MODEL_PROVIDER_REGISTRY),
152170
];
153171

154-
const STATIC_PROVIDER_MAP = new Map(
155-
STATIC_PROVIDER_REGISTRY.map((entry) => [normalizeProviderId(entry.id), entry]),
156-
);
172+
// For Map lookups, auto-generated entries go first so EXTRA entries override them
173+
const STATIC_PROVIDER_MAP = new Map([
174+
...Object.values(MODEL_PROVIDER_REGISTRY).map(
175+
(entry) => [normalizeProviderId(entry.id), entry] as const,
176+
),
177+
...EXTRA_PROVIDER_REGISTRY.map(
178+
(entry) => [normalizeProviderId(entry.id), entry] as const,
179+
),
180+
]);
157181

158182
declare global {
159183
// eslint-disable-next-line no-var

website/models-docs/providers/minimax-cn.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
title: MiniMax (China)
33
---
44

5-
<!-- THIS FILE IS AUTO-GENERATED BY website/scripts/generate-model-docs.js. DO NOT EDIT MANUALLY. -->
6-
75
# MiniMax (China)
86

9-
Use `minimax-cn/<model>` with VoltAgent's model router.
7+
Use `minimax-cn/<model>` with VoltAgent's model router. This provider routes to the China-region endpoint at `https://api.minimaxi.com/v1`.
108

119
## Quick start
1210

@@ -16,7 +14,7 @@ import { Agent } from "@voltagent/core";
1614
const agent = new Agent({
1715
name: "minimax-cn-agent",
1816
instructions: "You are a helpful assistant",
19-
model: "minimax-cn/MiniMax-M2",
17+
model: "minimax-cn/MiniMax-M2.7",
2018
});
2119
```
2220

@@ -26,11 +24,11 @@ const agent = new Agent({
2624

2725
## Provider package
2826

29-
`@ai-sdk/anthropic`
27+
`@ai-sdk/openai-compatible`
3028

3129
## Default base URL
3230

33-
`https://api.minimaxi.com/anthropic/v1`
31+
`https://api.minimaxi.com/v1`
3432

3533
You can override the base URL by setting `MINIMAX_CN_BASE_URL`.
3634

@@ -40,10 +38,11 @@ You can override the base URL by setting `MINIMAX_CN_BASE_URL`.
4038

4139
## Models
4240

43-
<details>
44-
<summary>Show models (2)</summary>
45-
46-
- MiniMax-M2
47-
- MiniMax-M2.1
48-
49-
</details>
41+
| Model | Context | Description |
42+
|---|---|---|
43+
| MiniMax-M2.7 | 1M tokens | Latest flagship model |
44+
| MiniMax-M2.7-highspeed | 1M tokens | Optimized for speed |
45+
| MiniMax-M2.5 | 1M tokens | Previous generation |
46+
| MiniMax-M2.5-highspeed | 204K tokens | Fast inference |
47+
| MiniMax-M2.1 | 1M tokens | Legacy |
48+
| MiniMax-M2 | 1M tokens | Legacy |

website/models-docs/providers/minimax.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
title: MiniMax
33
---
44

5-
<!-- THIS FILE IS AUTO-GENERATED BY website/scripts/generate-model-docs.js. DO NOT EDIT MANUALLY. -->
6-
75
# MiniMax
86

97
Use `minimax/<model>` with VoltAgent's model router.
108

9+
MiniMax provides an OpenAI-compatible API at `https://api.minimax.io/v1`. For users in China, use the `minimax-cn` provider which routes to `https://api.minimaxi.com/v1`.
10+
1111
## Quick start
1212

1313
```ts
@@ -16,7 +16,7 @@ import { Agent } from "@voltagent/core";
1616
const agent = new Agent({
1717
name: "minimax-agent",
1818
instructions: "You are a helpful assistant",
19-
model: "minimax/MiniMax-M2",
19+
model: "minimax/MiniMax-M2.7",
2020
});
2121
```
2222

@@ -26,11 +26,11 @@ const agent = new Agent({
2626

2727
## Provider package
2828

29-
`@ai-sdk/anthropic`
29+
`@ai-sdk/openai-compatible`
3030

3131
## Default base URL
3232

33-
`https://api.minimax.io/anthropic/v1`
33+
`https://api.minimax.io/v1`
3434

3535
You can override the base URL by setting `MINIMAX_BASE_URL`.
3636

@@ -40,10 +40,11 @@ You can override the base URL by setting `MINIMAX_BASE_URL`.
4040

4141
## Models
4242

43-
<details>
44-
<summary>Show models (2)</summary>
45-
46-
- MiniMax-M2
47-
- MiniMax-M2.1
48-
49-
</details>
43+
| Model | Context | Description |
44+
|---|---|---|
45+
| MiniMax-M2.7 | 1M tokens | Latest flagship model |
46+
| MiniMax-M2.7-highspeed | 1M tokens | Optimized for speed |
47+
| MiniMax-M2.5 | 1M tokens | Previous generation |
48+
| MiniMax-M2.5-highspeed | 204K tokens | Fast inference |
49+
| MiniMax-M2.1 | 1M tokens | Legacy |
50+
| MiniMax-M2 | 1M tokens | Legacy |

0 commit comments

Comments
 (0)