Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/tall-birds-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@voltagent/core": patch
---

fix: use OpenAI-compatible adapter for MiniMax provider
170 changes: 170 additions & 0 deletions packages/core/src/registries/model-provider-registry-minimax.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

// Track calls to createOpenAICompatible across module resets
let createOpenAICompatibleCalls: unknown[][] = [];
let createAnthropicCalls: unknown[][] = [];

// Mock @voltagent/internal to avoid build dependency
vi.mock("@voltagent/internal", () => ({
safeStringify: (value: unknown) => JSON.stringify(value),
}));

// Mock @ai-sdk/openai-compatible with a tracking wrapper
vi.mock("@ai-sdk/openai-compatible", () => ({
createOpenAICompatible: (...args: unknown[]) => {
createOpenAICompatibleCalls.push(args);
const mockModel = {
modelId: "mock-model",
specificationVersion: "v1",
provider: "minimax",
};
return {
languageModel: () => mockModel,
chatModel: () => mockModel,
};
},
}));

// Mock @ai-sdk/anthropic to track if it's called for MiniMax
vi.mock("@ai-sdk/anthropic", () => ({
createAnthropic: (...args: unknown[]) => {
createAnthropicCalls.push(args);
return {
languageModel: () => ({ modelId: "anthropic-model" }),
chatModel: () => ({ modelId: "anthropic-model" }),
};
},
anthropic: {
languageModel: () => ({ modelId: "anthropic-model" }),
chatModel: () => ({ modelId: "anthropic-model" }),
},
}));

describe("MiniMax provider registry", () => {
const originalEnv = { ...process.env };

beforeEach(() => {
createOpenAICompatibleCalls = [];
createAnthropicCalls = [];
(globalThis as Record<string, unknown>).___voltagent_model_provider_registry = undefined;
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
(globalThis as Record<string, unknown>).___voltagent_model_provider_registry = undefined;
});

it("should list minimax as a registered provider", async () => {
const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
const providers = registry.listProviders();
expect(providers).toContain("minimax");
});

it("should list minimax-cn as a registered provider", async () => {
const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
const providers = registry.listProviders();
expect(providers).toContain("minimax-cn");
});

it("should load minimax provider via @ai-sdk/openai-compatible", async () => {
process.env.MINIMAX_API_KEY = "test-key-minimax";

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
const model = await registry.resolveLanguageModel("minimax/MiniMax-M2.7");

expect(model).toBeDefined();
expect(createOpenAICompatibleCalls.length).toBeGreaterThan(0);

const lastCall = createOpenAICompatibleCalls[createOpenAICompatibleCalls.length - 1];
const config = lastCall[0] as Record<string, unknown>;
expect(config.name).toBe("minimax");
expect(config.baseURL).toBe("https://api.minimax.io/v1");
expect(config.apiKey).toBe("test-key-minimax");
});

it("should load minimax-cn provider with China base URL", async () => {
process.env.MINIMAX_API_KEY = "test-key-minimax-cn";

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
const model = await registry.resolveLanguageModel("minimax-cn/MiniMax-M2.7");

expect(model).toBeDefined();

// Find the call for minimax-cn
const cnCall = createOpenAICompatibleCalls.find((call) => {
const config = call[0] as Record<string, unknown>;
return config.name === "minimax-cn";
});
expect(cnCall).toBeDefined();
const config = cnCall![0] as Record<string, unknown>;
expect(config.baseURL).toBe("https://api.minimaxi.com/v1");
expect(config.apiKey).toBe("test-key-minimax-cn");
});

it("should support MINIMAX_BASE_URL override", async () => {
process.env.MINIMAX_API_KEY = "test-key";
process.env.MINIMAX_BASE_URL = "https://custom.minimax.io/v1";

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
await registry.resolveLanguageModel("minimax/MiniMax-M2.7");

const minimaxCall = createOpenAICompatibleCalls.find((call) => {
const config = call[0] as Record<string, unknown>;
return config.name === "minimax";
});
expect(minimaxCall).toBeDefined();
const config = minimaxCall![0] as Record<string, unknown>;
expect(config.baseURL).toBe("https://custom.minimax.io/v1");
});

it("should throw if MINIMAX_API_KEY is not set", async () => {
delete process.env.MINIMAX_API_KEY;

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();

await expect(registry.resolveLanguageModel("minimax/MiniMax-M2.7")).rejects.toThrow(
/MINIMAX_API_KEY/,
);
});

it("should resolve multiple MiniMax model variants", async () => {
process.env.MINIMAX_API_KEY = "test-key";

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();

const modelIds = [
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
];

for (const modelId of modelIds) {
const model = await registry.resolveLanguageModel(`minimax/${modelId}`);
expect(model).toBeDefined();
}
});

it("should not use @ai-sdk/anthropic adapter for minimax", async () => {
process.env.MINIMAX_API_KEY = "test-key";

const anthropicCallsBefore = createAnthropicCalls.length;

const { ModelProviderRegistry } = await import("./model-provider-registry");
const registry = ModelProviderRegistry.getInstance();
await registry.resolveLanguageModel("minimax/MiniMax-M2.7");

// Anthropic adapter should NOT have been called for MiniMax
expect(createAnthropicCalls.length).toBe(anthropicCallsBefore);
// OpenAI-compatible adapter SHOULD have been called
expect(createOpenAICompatibleCalls.length).toBeGreaterThan(0);
});
});
32 changes: 28 additions & 4 deletions packages/core/src/registries/model-provider-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,16 +144,40 @@ const EXTRA_PROVIDER_REGISTRY: ModelProviderRegistryEntry[] = [
npm: "ollama-ai-provider-v2",
doc: "https://ollama.com",
},
{
id: "minimax",
name: "MiniMax",
npm: "@ai-sdk/openai-compatible",
api: "https://api.minimax.io/v1",
env: ["MINIMAX_API_KEY"],
doc: "https://platform.minimax.io/docs/guides/quickstart",
},
{
id: "minimax-cn",
name: "MiniMax (China)",
npm: "@ai-sdk/openai-compatible",
api: "https://api.minimaxi.com/v1",
env: ["MINIMAX_API_KEY"],
doc: "https://platform.minimaxi.com/docs/guides/quickstart",
},
];

// EXTRA entries first so they take precedence over auto-generated entries
// (registerProviderConfig skips IDs that are already registered)
const STATIC_PROVIDER_REGISTRY = [
...Object.values(MODEL_PROVIDER_REGISTRY),
...EXTRA_PROVIDER_REGISTRY,
...Object.values(MODEL_PROVIDER_REGISTRY),
];

const STATIC_PROVIDER_MAP = new Map(
STATIC_PROVIDER_REGISTRY.map((entry) => [normalizeProviderId(entry.id), entry]),
);
// For Map lookups, auto-generated entries go first so EXTRA entries override them
const STATIC_PROVIDER_MAP = new Map([
...Object.values(MODEL_PROVIDER_REGISTRY).map(
(entry) => [normalizeProviderId(entry.id), entry] as const,
),
...EXTRA_PROVIDER_REGISTRY.map(
(entry) => [normalizeProviderId(entry.id), entry] as const,
),
]);

declare global {
// eslint-disable-next-line no-var
Expand Down
25 changes: 12 additions & 13 deletions website/models-docs/providers/minimax-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
title: MiniMax (China)
---

<!-- THIS FILE IS AUTO-GENERATED BY website/scripts/generate-model-docs.js. DO NOT EDIT MANUALLY. -->

# MiniMax (China)

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

## Quick start

Expand All @@ -16,7 +14,7 @@ import { Agent } from "@voltagent/core";
const agent = new Agent({
name: "minimax-cn-agent",
instructions: "You are a helpful assistant",
model: "minimax-cn/MiniMax-M2",
model: "minimax-cn/MiniMax-M2.7",
});
```

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

## Provider package

`@ai-sdk/anthropic`
`@ai-sdk/openai-compatible`

## Default base URL

`https://api.minimaxi.com/anthropic/v1`
`https://api.minimaxi.com/v1`

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

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

## Models

<details>
<summary>Show models (2)</summary>

- MiniMax-M2
- MiniMax-M2.1

</details>
| Model | Context | Description |
|---|---|---|
| MiniMax-M2.7 | 1M tokens | Latest flagship model |
| MiniMax-M2.7-highspeed | 1M tokens | Optimized for speed |
| MiniMax-M2.5 | 1M tokens | Previous generation |
| MiniMax-M2.5-highspeed | 204K tokens | Fast inference |
| MiniMax-M2.1 | 1M tokens | Legacy |
| MiniMax-M2 | 1M tokens | Legacy |
25 changes: 13 additions & 12 deletions website/models-docs/providers/minimax.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
title: MiniMax
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Manual edits in an auto-generated provider doc (and removal of the generated-file warning) create doc drift and overwrite risk when the generation script runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At website/models-docs/providers/minimax.md, line 9:

<comment>Manual edits in an auto-generated provider doc (and removal of the generated-file warning) create doc drift and overwrite risk when the generation script runs.</comment>

<file context>
@@ -2,12 +2,12 @@
 
 Use `minimax/<model>` with VoltAgent's model router.
 
+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`.
+
 ## Quick start
</file context>
Fix with Cubic

---

<!-- THIS FILE IS AUTO-GENERATED BY website/scripts/generate-model-docs.js. DO NOT EDIT MANUALLY. -->

# MiniMax

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

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`.

## Quick start

```ts
Expand All @@ -16,7 +16,7 @@ import { Agent } from "@voltagent/core";
const agent = new Agent({
name: "minimax-agent",
instructions: "You are a helpful assistant",
model: "minimax/MiniMax-M2",
model: "minimax/MiniMax-M2.7",
});
```

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

## Provider package

`@ai-sdk/anthropic`
`@ai-sdk/openai-compatible`

## Default base URL

`https://api.minimax.io/anthropic/v1`
`https://api.minimax.io/v1`

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

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

## Models

<details>
<summary>Show models (2)</summary>

- MiniMax-M2
- MiniMax-M2.1

</details>
| Model | Context | Description |
|---|---|---|
| MiniMax-M2.7 | 1M tokens | Latest flagship model |
| MiniMax-M2.7-highspeed | 1M tokens | Optimized for speed |
| MiniMax-M2.5 | 1M tokens | Previous generation |
| MiniMax-M2.5-highspeed | 204K tokens | Fast inference |
| MiniMax-M2.1 | 1M tokens | Legacy |
| MiniMax-M2 | 1M tokens | Legacy |
Loading