diff --git a/docs/src/content/docs/modules/backend.mdx b/docs/src/content/docs/modules/backend.mdx index fbd5ee0c9..675099d79 100644 --- a/docs/src/content/docs/modules/backend.mdx +++ b/docs/src/content/docs/modules/backend.mdx @@ -37,7 +37,7 @@ The following table depicts supported providers. Each provider requires specific | Google Gemini | ✅ | ✅ | GEMINI_CHAT_MODEL
**GEMINI_API_KEY**
GEMINI_API_HEADERS | | MistralAI | ✅ | ✅ | MISTRALAI_CHAT_MODEL
MISTRALAI_EMBEDDING_MODEL
**MISTRALAI_API_KEY**
MISTRALAI_API_BASE | | Transformers | ✅ | ✅ | TRANSFORMERS_CHAT_MODEL
HF_TOKEN| -| MiniMax | ✅ | ❌ | MINIMAX_CHAT_MODEL
**MINIMAX_API_KEY**
MINIMAX_API_BASE
MINIMAX_API_HEADERS | +| MiniMax | ✅ | ❌ | MINIMAX_CHAT_MODEL
**MINIMAX_API_KEY**
MINIMAX_API_BASE
MINIMAX_API_REGION
MINIMAX_API_HEADERS | If you don't see your provider raise an issue [here](https://github.com/i-am-bee/beeai-framework/issues). @@ -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. + + 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. + + --- ### Backend initialization diff --git a/python/beeai_framework/adapters/minimax/__init__.py b/python/beeai_framework/adapters/minimax/__init__.py index 128f60bff..96dd29bbf 100644 --- a/python/beeai_framework/adapters/minimax/__init__.py +++ b/python/beeai_framework/adapters/minimax/__init__.py @@ -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", +] diff --git a/python/beeai_framework/adapters/minimax/backend/chat.py b/python/beeai_framework/adapters/minimax/backend/chat.py index 897f66ab3..670a1d0f6 100644 --- a/python/beeai_framework/adapters/minimax/backend/chat.py +++ b/python/beeai_framework/adapters/minimax/backend/chat.py @@ -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): @@ -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. """ @@ -37,6 +74,7 @@ def __init__( *, api_key: str | None = None, base_url: str | None = None, + region: str | None = None, **kwargs: Unpack[ChatModelKwargs], ) -> None: """ @@ -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__( @@ -57,6 +101,7 @@ 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", @@ -64,7 +109,7 @@ def __init__( 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") diff --git a/python/examples/backend/providers/minimax.py b/python/examples/backend/providers/minimax.py index 72b7bc24b..9aad9c75a 100644 --- a/python/examples/backend/providers/minimax.py +++ b/python/examples/backend/providers/minimax.py @@ -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]) diff --git a/python/tests/adapters/minimax/test_minimax_chat.py b/python/tests/adapters/minimax/test_minimax_chat.py index 845478a2a..475fad1d7 100644 --- a/python/tests/adapters/minimax/test_minimax_chat.py +++ b/python/tests/adapters/minimax/test_minimax_chat.py @@ -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 @@ -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 diff --git a/typescript/examples/backend/providers/minimax.ts b/typescript/examples/backend/providers/minimax.ts index c412a8266..89290bd90 100644 --- a/typescript/examples/backend/providers/minimax.ts +++ b/typescript/examples/backend/providers/minimax.ts @@ -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", // }, ); diff --git a/typescript/src/adapters/minimax/backend/client.ts b/typescript/src/adapters/minimax/backend/client.ts index b058b7a76..7aed3f2ad 100644 --- a/typescript/src/adapters/minimax/backend/client.ts +++ b/typescript/src/adapters/minimax/backend/client.ts @@ -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)[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 { 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), }); } } diff --git a/typescript/tests/e2e/adapters/minimax.test.ts b/typescript/tests/e2e/adapters/minimax.test.ts index 33fcf6f54..15d7a3f2d 100644 --- a/typescript/tests/e2e/adapters/minimax.test.ts +++ b/typescript/tests/e2e/adapters/minimax.test.ts @@ -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", () => { @@ -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(); + }); +});