Skip to content
Open
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
6 changes: 5 additions & 1 deletion docs/src/content/docs/modules/backend.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The following table depicts supported providers. Each provider requires specific
| Google Gemini | ✅ | ✅ | GEMINI_CHAT_MODEL<br/>**GEMINI_API_KEY**<br/>GEMINI_API_HEADERS |
| MistralAI | ✅ | ✅ | MISTRALAI_CHAT_MODEL<br/>MISTRALAI_EMBEDDING_MODEL<br />**MISTRALAI_API_KEY**<br />MISTRALAI_API_BASE |
| Transformers | ✅ | ✅ | TRANSFORMERS_CHAT_MODEL<br/>HF_TOKEN|
| MiniMax | ✅ | ❌ | MINIMAX_CHAT_MODEL<br/>**MINIMAX_API_KEY**<br/>MINIMAX_API_BASE<br/>MINIMAX_API_HEADERS |
| MiniMax | ✅ | ❌ | MINIMAX_CHAT_MODEL<br/>**MINIMAX_API_KEY**<br/>MINIMAX_API_BASE<br/>MINIMAX_API_REGION<br/>MINIMAX_API_HEADERS |

<Tip>
If you don't see your provider raise an issue [here](https://github.com/i-am-bee/beeai-framework/issues).
Expand All @@ -49,6 +49,10 @@ or [TypeScript](https://github.com/i-am-bee/beeai-framework/blob/main/typescript
Google Gemini, MistralAI, and Transformers are supported in Python only. The Transformers chat model does not support tool calling.
</Note>

<Note>
MiniMax serves the same API from two regional gateways. Select one with `MINIMAX_API_REGION` (or the `region` option): `global` (default, `https://api.minimax.io/v1`) or `cn` (`https://api.minimaxi.com/v1`). An explicit `MINIMAX_API_BASE` / `base_url` always takes precedence, so you no longer have to look up and type the regional URL by hand.
</Note>

---

### Backend initialization
Expand Down
14 changes: 12 additions & 2 deletions python/beeai_framework/adapters/minimax/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

from beeai_framework.adapters.minimax.backend.chat import MiniMaxChatModel
from beeai_framework.adapters.minimax.backend.chat import (
MINIMAX_API_BASE,
MINIMAX_API_BASE_CN,
MiniMaxChatModel,
resolve_minimax_base_url,
)

__all__ = ["MiniMaxChatModel"]
__all__ = [
"MINIMAX_API_BASE",
"MINIMAX_API_BASE_CN",
"MiniMaxChatModel",
"resolve_minimax_base_url",
]
49 changes: 47 additions & 2 deletions python/beeai_framework/adapters/minimax/backend/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@
logger = Logger(__name__)

MINIMAX_API_BASE = "https://api.minimax.io/v1"
MINIMAX_API_BASE_CN = "https://api.minimaxi.com/v1"

# Region identifiers accepted by ``region`` / the MINIMAX_API_REGION env var,
# mapped to the matching OpenAI-compatible base URL. MiniMax serves the same API
# from two regional gateways; this mapping lets callers select one by name
# instead of discovering and typing the base URL by hand.
MINIMAX_REGION_BASE_URLS = {
"global": MINIMAX_API_BASE,
"global_en": MINIMAX_API_BASE,
"cn": MINIMAX_API_BASE_CN,
"cn_zh": MINIMAX_API_BASE_CN,
}
DEFAULT_MINIMAX_REGION = "global"


def resolve_minimax_base_url(region: str | None) -> str:
"""
Resolve a MiniMax region identifier to its OpenAI-compatible base URL.

An empty or missing region resolves to the global endpoint. Unknown
identifiers raise ``ValueError`` listing the supported regions.
"""
normalized = (region or "").strip().lower()
if not normalized:
return MINIMAX_API_BASE
try:
return MINIMAX_REGION_BASE_URLS[normalized]
except KeyError:
raise ValueError(
f"Unknown MiniMax region '{region}'. Supported regions: {', '.join(sorted(MINIMAX_REGION_BASE_URLS))}."
) from None


class MiniMaxChatModel(LiteLLMChatModel):
Expand All @@ -22,6 +53,12 @@ class MiniMaxChatModel(LiteLLMChatModel):
MiniMax provides an OpenAI-compatible API. This adapter routes requests
through LiteLLM's OpenAI provider with the MiniMax base URL.

MiniMax exposes the same API from two regional gateways: the global
endpoint (https://api.minimax.io/v1) and the CN endpoint
(https://api.minimaxi.com/v1). Select one with the ``region`` argument or
the MINIMAX_API_REGION environment variable, or override it entirely with
an explicit ``base_url`` / MINIMAX_API_BASE.

Available models include MiniMax-M3 (default), MiniMax-M2.7,
and MiniMax-M2.7-highspeed.
"""
Expand All @@ -37,6 +74,7 @@ def __init__(
*,
api_key: str | None = None,
base_url: str | None = None,
region: str | None = None,
**kwargs: Unpack[ChatModelKwargs],
) -> None:
"""
Expand All @@ -48,7 +86,13 @@ def __init__(
and then defaults to 'MiniMax-M3'.
api_key: The MiniMax API key. Falls back to MINIMAX_API_KEY env var.
base_url: The MiniMax API base URL. Falls back to MINIMAX_API_BASE
env var, then defaults to 'https://api.minimax.io/v1'.
env var, then to the endpoint selected by ``region``, and finally
to the global endpoint 'https://api.minimax.io/v1'.
region: The MiniMax region whose endpoint is used when neither
``base_url`` nor MINIMAX_API_BASE is set. Accepts 'global'
(alias 'global_en', https://api.minimax.io/v1) or 'cn'
(alias 'cn_zh', https://api.minimaxi.com/v1). Falls back to the
MINIMAX_API_REGION env var, then to the global endpoint.
**kwargs: Additional settings to configure the provider.
"""
super().__init__(
Expand All @@ -57,14 +101,15 @@ def __init__(
**kwargs,
)

region = region if region is not None else os.getenv("MINIMAX_API_REGION")
self._assert_setting_value("api_key", api_key, envs=["MINIMAX_API_KEY"])
self._assert_setting_value(
"base_url",
base_url,
envs=["MINIMAX_API_BASE"],
aliases=["api_base"],
allow_empty=True,
fallback=MINIMAX_API_BASE,
fallback=resolve_minimax_base_url(region),
)
self._settings["extra_headers"] = utils.parse_extra_headers(
self._settings.get("extra_headers"), os.getenv("MINIMAX_API_HEADERS")
Expand Down
4 changes: 4 additions & 0 deletions python/examples/backend/providers/minimax.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ async def minimax_from_name() -> None:


async def minimax_sync() -> None:
# Select a regional gateway by name instead of hard-coding the URL:
# region="global" -> https://api.minimax.io/v1 (default)
# region="cn" -> https://api.minimaxi.com/v1
# An explicit base_url / MINIMAX_API_BASE still takes precedence.
llm = MiniMaxChatModel("MiniMax-M3")
user_message = UserMessage("what is the capital of Massachusetts?")
response = await llm.run([user_message])
Expand Down
78 changes: 77 additions & 1 deletion python/tests/adapters/minimax/test_minimax_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

import pytest

from beeai_framework.adapters.minimax.backend.chat import MINIMAX_API_BASE, MiniMaxChatModel
from beeai_framework.adapters.minimax.backend.chat import (
MINIMAX_API_BASE,
MINIMAX_API_BASE_CN,
MiniMaxChatModel,
resolve_minimax_base_url,
)
from beeai_framework.backend.chat import ChatModel
from beeai_framework.backend.constants import BackendProviders

Expand Down Expand Up @@ -109,3 +114,74 @@ def test_load_from_alias(self) -> None:
model = ChatModel.from_name("minimax:MiniMax-M2.7")
assert isinstance(model, MiniMaxChatModel)
assert model.model_id == "MiniMax-M2.7"


class TestMiniMaxRegionSelection:
"""Test regional (global / CN) endpoint selection."""

def test_resolve_global_variants(self) -> None:
assert resolve_minimax_base_url("global") == MINIMAX_API_BASE
assert resolve_minimax_base_url("global_en") == MINIMAX_API_BASE

def test_resolve_cn_variants(self) -> None:
assert resolve_minimax_base_url("cn") == MINIMAX_API_BASE_CN
assert resolve_minimax_base_url("cn_zh") == MINIMAX_API_BASE_CN

def test_resolve_is_case_insensitive_and_trimmed(self) -> None:
assert resolve_minimax_base_url(" CN ") == MINIMAX_API_BASE_CN

def test_resolve_empty_defaults_to_global(self) -> None:
assert resolve_minimax_base_url(None) == MINIMAX_API_BASE
assert resolve_minimax_base_url("") == MINIMAX_API_BASE

def test_resolve_unknown_region_raises(self) -> None:
with pytest.raises(ValueError, match=r"Unknown MiniMax region"):
resolve_minimax_base_url("mars")

def test_endpoints_differ(self) -> None:
assert MINIMAX_API_BASE == "https://api.minimax.io/v1"
assert MINIMAX_API_BASE_CN == "https://api.minimaxi.com/v1"
assert MINIMAX_API_BASE != MINIMAX_API_BASE_CN

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_region_param_selects_cn(self) -> None:
model = MiniMaxChatModel(region="cn")
assert model._settings.get("base_url") == MINIMAX_API_BASE_CN

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_region_param_selects_global(self) -> None:
model = MiniMaxChatModel(region="global")
assert model._settings.get("base_url") == MINIMAX_API_BASE

@patch.dict(
os.environ,
{"MINIMAX_API_KEY": "test-key-123", "MINIMAX_API_REGION": "cn_zh"},
)
def test_region_from_env_selects_cn(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("base_url") == MINIMAX_API_BASE_CN

@patch.dict(
os.environ,
{"MINIMAX_API_KEY": "test-key-123", "MINIMAX_API_REGION": "cn"},
)
def test_explicit_base_url_overrides_region(self) -> None:
model = MiniMaxChatModel(base_url="https://proxy.example.com/v1", region="cn")
assert model._settings.get("base_url") == "https://proxy.example.com/v1"

@patch.dict(
os.environ,
{
"MINIMAX_API_KEY": "test-key-123",
"MINIMAX_API_BASE": "https://api.minimax.io/v1",
"MINIMAX_API_REGION": "cn",
},
)
def test_api_base_env_overrides_region(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("base_url") == MINIMAX_API_BASE

@patch.dict(os.environ, {"MINIMAX_API_KEY": "test-key-123"})
def test_default_without_region_is_global(self) -> None:
model = MiniMaxChatModel()
assert model._settings.get("base_url") == MINIMAX_API_BASE
7 changes: 6 additions & 1 deletion typescript/examples/backend/providers/minimax.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ const llm = new MiniMaxChatModel(
// {},
// {
// apiKey: "MINIMAX_API_KEY",
// baseURL: "https://api.minimax.io/v1",
// // Pick a regional gateway by name instead of hard-coding the URL:
// // "global" -> https://api.minimax.io/v1 (default)
// // "cn" -> https://api.minimaxi.com/v1
// region: "global",
// // ...or override the base URL directly (takes precedence over region):
// // baseURL: "https://api.minimax.io/v1",
// },
);

Expand Down
63 changes: 56 additions & 7 deletions typescript/src/adapters/minimax/backend/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,70 @@ import { getEnv } from "@/internals/env.js";
import { BackendClient } from "@/backend/client.js";
import { parseHeadersFromEnv, vercelFetcher } from "@/adapters/vercel/backend/utils.js";

const MINIMAX_API_BASE = "https://api.minimax.io/v1";
export const MINIMAX_API_BASE = "https://api.minimax.io/v1";
export const MINIMAX_API_BASE_CN = "https://api.minimaxi.com/v1";

export type MiniMaxClientSettings = OpenAIProviderSettings;
/**
* Region identifiers accepted by `region` / the MINIMAX_API_REGION env var,
* mapped to the matching OpenAI-compatible base URL. MiniMax serves the same
* API from two regional gateways; this mapping lets callers select one by name
* instead of discovering and typing the base URL by hand.
*/
export const MINIMAX_REGION_BASE_URLS = {
global: MINIMAX_API_BASE,
global_en: MINIMAX_API_BASE,
cn: MINIMAX_API_BASE_CN,
cn_zh: MINIMAX_API_BASE_CN,
} as const;

export type MiniMaxRegion = keyof typeof MINIMAX_REGION_BASE_URLS;

/**
* Resolve a MiniMax region identifier to its OpenAI-compatible base URL.
*
* An empty or missing region resolves to the global endpoint. Unknown
* identifiers throw an error listing the supported regions.
*/
export function resolveMiniMaxBaseURL(region?: string): string {
const normalized = region?.trim().toLowerCase();
if (!normalized) {
return MINIMAX_API_BASE;
}
const baseURL = (MINIMAX_REGION_BASE_URLS as Record<string, string>)[normalized];
if (!baseURL) {
throw new Error(
`Unknown MiniMax region "${region}". Supported regions: ${Object.keys(
MINIMAX_REGION_BASE_URLS,
).join(", ")}.`,
);
}
return baseURL;
}

export interface MiniMaxClientSettings extends OpenAIProviderSettings {
/**
* Region whose endpoint is used when neither `baseURL` nor MINIMAX_API_BASE
* is set. Falls back to the MINIMAX_API_REGION env var, then to the global
* endpoint.
*/
region?: MiniMaxRegion;
}

export class MiniMaxClient extends BackendClient<MiniMaxClientSettings, OpenAIProvider> {
protected create(): OpenAIProvider {
const { region, ...settings } = this.settings ?? {};
return createOpenAI({
...this.settings,
apiKey: this.settings?.apiKey || getEnv("MINIMAX_API_KEY"),
baseURL: this.settings?.baseURL || getEnv("MINIMAX_API_BASE", MINIMAX_API_BASE),
...settings,
apiKey: settings.apiKey || getEnv("MINIMAX_API_KEY"),
baseURL:
settings.baseURL ||
getEnv("MINIMAX_API_BASE") ||
resolveMiniMaxBaseURL(region ?? getEnv("MINIMAX_API_REGION")),
headers: {
...parseHeadersFromEnv("MINIMAX_API_HEADERS"),
...this.settings?.headers,
...settings.headers,
},
fetch: vercelFetcher(this.settings?.fetch),
fetch: vercelFetcher(settings.fetch),
});
}
}
44 changes: 43 additions & 1 deletion typescript/tests/e2e/adapters/minimax.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { BackendProviders } from "@/backend/constants.js";
import { MiniMaxChatModel } from "@/adapters/minimax/backend/chat.js";
import { MiniMaxClient } from "@/adapters/minimax/backend/client.js";
import {
MiniMaxClient,
MINIMAX_API_BASE,
MINIMAX_API_BASE_CN,
resolveMiniMaxBaseURL,
} from "@/adapters/minimax/backend/client.js";

describe("MiniMax Provider Registration", () => {
it("should be registered in BackendProviders", () => {
Expand Down Expand Up @@ -98,3 +103,40 @@ describe("MiniMaxChatModel", () => {
expect(model).toBeInstanceOf(MiniMaxChatModel);
});
});

describe("MiniMax regional endpoints", () => {
it("should expose distinct global and CN endpoints", () => {
expect(MINIMAX_API_BASE).toBe("https://api.minimax.io/v1");
expect(MINIMAX_API_BASE_CN).toBe("https://api.minimaxi.com/v1");
expect(MINIMAX_API_BASE).not.toBe(MINIMAX_API_BASE_CN);
});

it("should resolve global region variants", () => {
expect(resolveMiniMaxBaseURL("global")).toBe(MINIMAX_API_BASE);
expect(resolveMiniMaxBaseURL("global_en")).toBe(MINIMAX_API_BASE);
});

it("should resolve CN region variants", () => {
expect(resolveMiniMaxBaseURL("cn")).toBe(MINIMAX_API_BASE_CN);
expect(resolveMiniMaxBaseURL("cn_zh")).toBe(MINIMAX_API_BASE_CN);
});

it("should be case-insensitive and trim whitespace", () => {
expect(resolveMiniMaxBaseURL(" CN ")).toBe(MINIMAX_API_BASE_CN);
});

it("should default to the global endpoint when region is missing", () => {
expect(resolveMiniMaxBaseURL()).toBe(MINIMAX_API_BASE);
expect(resolveMiniMaxBaseURL("")).toBe(MINIMAX_API_BASE);
});

it("should throw for an unknown region", () => {
expect(() => resolveMiniMaxBaseURL("mars")).toThrowError(/Unknown MiniMax region/);
});

it("should build a client for the CN region without throwing", () => {
const client = new MiniMaxClient({ apiKey: "test-key", region: "cn" });
expect(client).toBeDefined();
expect(client.instance).toBeDefined();
});
});
Loading