From 9e16ee9b81027ef4357ddbf9346cdddbf0d989e6 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Fri, 20 Mar 2026 21:33:17 +0800 Subject: [PATCH 1/2] 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 --- .../model-provider-registry-minimax.spec.ts | 170 ++++++++++++++++++ .../src/registries/model-provider-registry.ts | 32 +++- website/models-docs/providers/minimax-cn.md | 25 ++- website/models-docs/providers/minimax.md | 25 +-- 4 files changed, 223 insertions(+), 29 deletions(-) create mode 100644 packages/core/src/registries/model-provider-registry-minimax.spec.ts diff --git a/packages/core/src/registries/model-provider-registry-minimax.spec.ts b/packages/core/src/registries/model-provider-registry-minimax.spec.ts new file mode 100644 index 000000000..8e027d2c2 --- /dev/null +++ b/packages/core/src/registries/model-provider-registry-minimax.spec.ts @@ -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).___voltagent_model_provider_registry = undefined; + process.env = { ...originalEnv }; + }); + + afterEach(() => { + process.env = originalEnv; + (globalThis as Record).___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; + 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; + return config.name === "minimax-cn"; + }); + expect(cnCall).toBeDefined(); + const config = cnCall![0] as Record; + 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; + return config.name === "minimax"; + }); + expect(minimaxCall).toBeDefined(); + const config = minimaxCall![0] as Record; + 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); + }); +}); diff --git a/packages/core/src/registries/model-provider-registry.ts b/packages/core/src/registries/model-provider-registry.ts index cdad24cdf..192478c0d 100644 --- a/packages/core/src/registries/model-provider-registry.ts +++ b/packages/core/src/registries/model-provider-registry.ts @@ -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 diff --git a/website/models-docs/providers/minimax-cn.md b/website/models-docs/providers/minimax-cn.md index f01932b5f..3f47facd8 100644 --- a/website/models-docs/providers/minimax-cn.md +++ b/website/models-docs/providers/minimax-cn.md @@ -2,11 +2,9 @@ title: MiniMax (China) --- - - # MiniMax (China) -Use `minimax-cn/` with VoltAgent's model router. +Use `minimax-cn/` with VoltAgent's model router. This provider routes to the China-region endpoint at `https://api.minimaxi.com/v1`. ## Quick start @@ -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", }); ``` @@ -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`. @@ -40,10 +38,11 @@ You can override the base URL by setting `MINIMAX_CN_BASE_URL`. ## Models -
- Show models (2) - -- MiniMax-M2 -- MiniMax-M2.1 - -
+| 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 | diff --git a/website/models-docs/providers/minimax.md b/website/models-docs/providers/minimax.md index f2a81d668..3d269516b 100644 --- a/website/models-docs/providers/minimax.md +++ b/website/models-docs/providers/minimax.md @@ -2,12 +2,12 @@ title: MiniMax --- - - # MiniMax Use `minimax/` 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 @@ -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", }); ``` @@ -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`. @@ -40,10 +40,11 @@ You can override the base URL by setting `MINIMAX_BASE_URL`. ## Models -
- Show models (2) - -- MiniMax-M2 -- MiniMax-M2.1 - -
+| 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 | From e82dfe94cb0d02dbc1ae28cbce4bb08816535a58 Mon Sep 17 00:00:00 2001 From: Omer Aplak Date: Mon, 30 Mar 2026 20:40:46 +0300 Subject: [PATCH 2/2] chore: add changeset --- .changeset/tall-birds-invite.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tall-birds-invite.md diff --git a/.changeset/tall-birds-invite.md b/.changeset/tall-birds-invite.md new file mode 100644 index 000000000..169561439 --- /dev/null +++ b/.changeset/tall-birds-invite.md @@ -0,0 +1,5 @@ +--- +"@voltagent/core": patch +--- + +fix: use OpenAI-compatible adapter for MiniMax provider