From f94346ba850f67d6c2db6a2e84a69370dde10496 Mon Sep 17 00:00:00 2001 From: Octopus Date: Mon, 6 Apr 2026 11:50:20 -0500 Subject: [PATCH] feat: add MiniMax provider support - Add agent-framework-minimax package with Anthropic-compatible API integration - Support MiniMax-M2.7 and MiniMax-M2.7-highspeed models - Add MINIMAX_API_KEY environment variable support - Add unit and integration tests --- python/packages/minimax/AGENTS.md | 44 +++ python/packages/minimax/LICENSE | 21 ++ python/packages/minimax/README.md | 61 +++++ .../agent_framework_minimax/__init__.py | 17 ++ .../agent_framework_minimax/_chat_client.py | 256 ++++++++++++++++++ python/packages/minimax/pyproject.toml | 99 +++++++ python/packages/minimax/tests/conftest.py | 55 ++++ .../minimax/tests/test_minimax_client.py | 236 ++++++++++++++++ 8 files changed, 789 insertions(+) create mode 100644 python/packages/minimax/AGENTS.md create mode 100644 python/packages/minimax/LICENSE create mode 100644 python/packages/minimax/README.md create mode 100644 python/packages/minimax/agent_framework_minimax/__init__.py create mode 100644 python/packages/minimax/agent_framework_minimax/_chat_client.py create mode 100644 python/packages/minimax/pyproject.toml create mode 100644 python/packages/minimax/tests/conftest.py create mode 100644 python/packages/minimax/tests/test_minimax_client.py diff --git a/python/packages/minimax/AGENTS.md b/python/packages/minimax/AGENTS.md new file mode 100644 index 0000000000..b3e1359f0a --- /dev/null +++ b/python/packages/minimax/AGENTS.md @@ -0,0 +1,44 @@ +# MiniMax Package (agent-framework-minimax) + +Integration with MiniMax's Anthropic-compatible API for Microsoft Agent Framework. + +## Main Classes + +- **`MiniMaxClient`** - Chat client for MiniMax models via Anthropic-compatible API +- **`RawMiniMaxClient`** - Raw client without middleware or telemetry +- **`MiniMaxSettings`** - Settings TypedDict for MiniMax configuration + +## Supported Models + +| Model ID | Description | +|----------|-------------| +| `MiniMax-M2.7` | Peak Performance. Ultimate Value. Master the Complex | +| `MiniMax-M2.7-highspeed` | Same performance, faster and more agile | + +## Usage + +```python +from agent_framework_minimax import MiniMaxClient + +# Set MINIMAX_API_KEY environment variable, then: +client = MiniMaxClient(model="MiniMax-M2.7") +response = await client.get_response("Hello from MiniMax!") +``` + +## Import Path + +```python +from agent_framework_minimax import MiniMaxClient +``` + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `MINIMAX_API_KEY` | Yes | MiniMax API key | +| `MINIMAX_CHAT_MODEL` | No | Default model to use | +| `MINIMAX_BASE_URL` | No | Override base URL (default: `https://api.minimax.io/anthropic`) | + +## API Reference + +- Chat (Anthropic Compatible): https://platform.minimax.io/docs/api-reference/text-anthropic-api diff --git a/python/packages/minimax/LICENSE b/python/packages/minimax/LICENSE new file mode 100644 index 0000000000..9e841e7a26 --- /dev/null +++ b/python/packages/minimax/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/python/packages/minimax/README.md b/python/packages/minimax/README.md new file mode 100644 index 0000000000..27fcf370d4 --- /dev/null +++ b/python/packages/minimax/README.md @@ -0,0 +1,61 @@ +# Get Started with Microsoft Agent Framework MiniMax + +Please install this package via pip: + +```bash +pip install agent-framework-minimax --pre +``` + +## MiniMax Integration + +The MiniMax integration enables communication with MiniMax's Anthropic-compatible API, +allowing your Agent Framework applications to leverage MiniMax's powerful language models. + +### Environment Variables + +Set the following environment variables before using the client: + +```bash +export MINIMAX_API_KEY="your_minimax_api_key" +``` + +### Basic Usage Example + +```python +import asyncio +from agent_framework_minimax import MiniMaxClient + +async def main(): + client = MiniMaxClient(model="MiniMax-M2.7") + response = await client.get_response("Hello! Tell me about yourself.") + print(response.messages[0].text) + +asyncio.run(main()) +``` + +### Streaming Example + +```python +import asyncio +from agent_framework_minimax import MiniMaxClient + +async def main(): + client = MiniMaxClient(model="MiniMax-M2.7") + async for update in await client.get_streaming_response("Tell me a short story."): + if update.text: + print(update.text, end="", flush=True) + print() + +asyncio.run(main()) +``` + +### Supported Models + +| Model | Description | +|-------|-------------| +| `MiniMax-M2.7` | Peak Performance. Ultimate Value. Master the Complex | +| `MiniMax-M2.7-highspeed` | Same performance, faster and more agile | + +### API Documentation + +- Chat (Anthropic Compatible): https://platform.minimax.io/docs/api-reference/text-anthropic-api diff --git a/python/packages/minimax/agent_framework_minimax/__init__.py b/python/packages/minimax/agent_framework_minimax/__init__.py new file mode 100644 index 0000000000..476e196fd3 --- /dev/null +++ b/python/packages/minimax/agent_framework_minimax/__init__.py @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft. All rights reserved. + +import importlib.metadata + +from ._chat_client import MiniMaxClient, MiniMaxSettings, RawMiniMaxClient + +try: + __version__ = importlib.metadata.version(__name__) +except importlib.metadata.PackageNotFoundError: + __version__ = "0.0.0" # Fallback for development mode + +__all__ = [ + "MiniMaxClient", + "MiniMaxSettings", + "RawMiniMaxClient", + "__version__", +] diff --git a/python/packages/minimax/agent_framework_minimax/_chat_client.py b/python/packages/minimax/agent_framework_minimax/_chat_client.py new file mode 100644 index 0000000000..dfbde55e90 --- /dev/null +++ b/python/packages/minimax/agent_framework_minimax/_chat_client.py @@ -0,0 +1,256 @@ +# Copyright (c) Microsoft. All rights reserved. + +from __future__ import annotations + +import logging +import sys +from collections.abc import AsyncIterable, Awaitable, Sequence +from typing import Any, ClassVar, Final, Generic, Mapping, TypedDict + +from agent_framework import ( + AGENT_FRAMEWORK_USER_AGENT, + ChatAndFunctionMiddlewareTypes, + ChatMiddlewareLayer, + ChatResponse, + ChatResponseUpdate, + FunctionInvocationConfiguration, + FunctionInvocationLayer, + Message, + ResponseStream, +) +from agent_framework._settings import SecretString, load_settings +from agent_framework.observability import ChatTelemetryLayer +from anthropic import AsyncAnthropic + +from agent_framework_anthropic._chat_client import AnthropicOptionsT, RawAnthropicClient + +if sys.version_info >= (3, 12): + from typing import override # type: ignore # pragma: no cover +else: + from typing_extensions import override # type: ignore # pragma: no cover + +__all__ = [ + "MiniMaxClient", + "MiniMaxSettings", + "RawMiniMaxClient", +] + +logger = logging.getLogger("agent_framework.minimax") + +MINIMAX_DEFAULT_BASE_URL: Final[str] = "https://api.minimax.io/anthropic" + +MINIMAX_MODELS: Final[list[str]] = [ + "MiniMax-M2.7", + "MiniMax-M2.7-highspeed", +] + +# Parameters not supported by MiniMax Anthropic-compatible API +MINIMAX_UNSUPPORTED_PARAMS: Final[frozenset[str]] = frozenset([ + "betas", + "top_k", + "stop_sequences", + "service_tier", + "mcp_servers", + "context_management", + "container", + "thinking", + "output_format", + "additional_beta_flags", +]) + + +class MiniMaxSettings(TypedDict, total=False): + """MiniMax Project settings. + + Settings are resolved in this order: explicit keyword arguments, values from an + explicitly provided .env file, then environment variables with the prefix + 'MINIMAX_'. + + Keys: + api_key: The MiniMax API key. + chat_model: The MiniMax chat model. + base_url: Optional custom base URL for the MiniMax API. + """ + + api_key: SecretString | None + chat_model: str | None + base_url: str | None + + +class RawMiniMaxClient( + RawAnthropicClient[AnthropicOptionsT], + Generic[AnthropicOptionsT], +): + """Raw MiniMax chat client using Anthropic-compatible API. + + Warning: + **This class should not normally be used directly.** It does not include middleware, + telemetry, or function invocation support that you most likely need. + Use ``MiniMaxClient`` instead for a fully-featured client with all layers applied. + """ + + OTEL_PROVIDER_NAME: ClassVar[str] = "minimax" # type: ignore[reportIncompatibleVariableOverride, misc] + + def __init__( + self, + *, + api_key: str | None = None, + model: str | None = None, + base_url: str | None = None, + anthropic_client: AsyncAnthropic | None = None, + additional_properties: dict[str, Any] | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a raw MiniMax client. + + Keyword Args: + api_key: The MiniMax API key to use for authentication. + model: The model to use (e.g. ``"MiniMax-M2.7"``). + base_url: Optional base URL override. Defaults to ``https://api.minimax.io/anthropic``. + anthropic_client: An existing AsyncAnthropic client to use with a custom base_url. + additional_properties: Additional properties stored on the client instance. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. + + Examples: + .. code-block:: python + + from agent_framework_minimax import MiniMaxClient + + # Using environment variables (set MINIMAX_API_KEY) + client = MiniMaxClient(model="MiniMax-M2.7") + + # Or passing parameters directly + client = MiniMaxClient( + model="MiniMax-M2.7", + api_key="your_minimax_api_key", + ) + """ + settings = load_settings( + MiniMaxSettings, + env_prefix="MINIMAX_", + api_key=api_key, + chat_model=model, + base_url=base_url, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + + api_key_secret = settings.get("api_key") + model_setting = settings.get("chat_model") + base_url_setting = settings.get("base_url") or MINIMAX_DEFAULT_BASE_URL + + if anthropic_client is None: + if api_key_secret is None: + raise ValueError( + "MiniMax API key is required. Set via 'api_key' parameter " + "or 'MINIMAX_API_KEY' environment variable." + ) + anthropic_client = AsyncAnthropic( + api_key=api_key_secret.get_secret_value(), + base_url=base_url_setting, + default_headers={"User-Agent": AGENT_FRAMEWORK_USER_AGENT}, + ) + + super().__init__( + model=model_setting, + anthropic_client=anthropic_client, + additional_properties=additional_properties, + ) + + @override + def _inner_get_response( + self, + *, + messages: Sequence[Message], + options: Mapping[str, Any], + stream: bool = False, + **kwargs: Any, + ) -> Awaitable[ChatResponse] | ResponseStream[ChatResponseUpdate, ChatResponse]: + """Execute a chat request against MiniMax's Anthropic-compatible API. + + Overrides the parent to use the standard messages API (not beta) and + filters out parameters unsupported by MiniMax. + """ + run_options = self._prepare_options(messages, options, **kwargs) + + # Remove params not supported by MiniMax Anthropic-compatible API + for param in MINIMAX_UNSUPPORTED_PARAMS: + run_options.pop(param, None) + + if stream: + async def _stream() -> AsyncIterable[ChatResponseUpdate]: + async for chunk in await self.anthropic_client.messages.create(**run_options, stream=True): # type: ignore[misc] + parsed_chunk = self._process_stream_event(chunk) # type: ignore[arg-type] + if parsed_chunk: + yield parsed_chunk + + return self._build_response_stream(_stream(), response_format=options.get("response_format")) + + async def _get_response() -> ChatResponse: + message = await self.anthropic_client.messages.create(**run_options, stream=False) # type: ignore[misc] + return self._process_message(message, options) # type: ignore[arg-type] + + return _get_response() + + +class MiniMaxClient( # type: ignore[misc] + FunctionInvocationLayer[AnthropicOptionsT], + ChatMiddlewareLayer[AnthropicOptionsT], + ChatTelemetryLayer[AnthropicOptionsT], + RawMiniMaxClient[AnthropicOptionsT], + Generic[AnthropicOptionsT], +): + """MiniMax chat client with middleware, telemetry, and function invocation support. + + Uses MiniMax's Anthropic-compatible API (https://api.minimax.io/anthropic). + Supported models: ``MiniMax-M2.7``, ``MiniMax-M2.7-highspeed``. + + Examples: + .. code-block:: python + + from agent_framework_minimax import MiniMaxClient + + # Set MINIMAX_API_KEY environment variable, then: + client = MiniMaxClient(model="MiniMax-M2.7") + response = await client.get_response("Hello from MiniMax!") + """ + + def __init__( + self, + *, + api_key: str | None = None, + model: str | None = None, + base_url: str | None = None, + anthropic_client: AsyncAnthropic | None = None, + additional_properties: dict[str, Any] | None = None, + middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None, + function_invocation_configuration: FunctionInvocationConfiguration | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + ) -> None: + """Initialize a MiniMax client. + + Keyword Args: + api_key: The MiniMax API key to use for authentication. + model: The model to use (e.g. ``"MiniMax-M2.7"``). + base_url: Optional base URL override. Defaults to ``https://api.minimax.io/anthropic``. + anthropic_client: An existing AsyncAnthropic client to use with a custom base_url. + additional_properties: Additional properties stored on the client instance. + middleware: Optional middleware to apply to the client. + function_invocation_configuration: Optional function invocation configuration override. + env_file_path: Path to environment file for loading settings. + env_file_encoding: Encoding of the environment file. + """ + super().__init__( + api_key=api_key, + model=model, + base_url=base_url, + anthropic_client=anthropic_client, + additional_properties=additional_properties, + middleware=middleware, + function_invocation_configuration=function_invocation_configuration, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) diff --git a/python/packages/minimax/pyproject.toml b/python/packages/minimax/pyproject.toml new file mode 100644 index 0000000000..9254547bce --- /dev/null +++ b/python/packages/minimax/pyproject.toml @@ -0,0 +1,99 @@ +[project] +name = "agent-framework-minimax" +description = "MiniMax integration for Microsoft Agent Framework." +authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}] +readme = "README.md" +requires-python = ">=3.10" +version = "1.0.0b260402" +license-files = ["LICENSE"] +urls.homepage = "https://aka.ms/agent-framework" +urls.source = "https://github.com/microsoft/agent-framework/tree/main/python" +urls.release_notes = "https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true" +urls.issues = "https://github.com/microsoft/agent-framework/issues" +classifiers = [ + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] +dependencies = [ + "agent-framework-core>=1.0.0,<2", + "agent-framework-anthropic>=1.0.0,<2", + "anthropic>=0.80.0,<0.80.1", +] + +[tool.uv] +prerelease = "if-necessary-or-explicit" +environments = [ + "sys_platform == 'darwin'", + "sys_platform == 'linux'", + "sys_platform == 'win32'" +] + +[tool.uv-dynamic-versioning] +fallback-version = "0.0.0" + +[tool.pytest.ini_options] +testpaths = 'tests' +addopts = "-ra -q -r fEX" +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:Support for class-based `config` is deprecated:DeprecationWarning:pydantic.*" +] +timeout = 120 +markers = [ + "integration: marks tests as integration tests that require external services", +] + +[tool.ruff] +extend = "../../pyproject.toml" + +[tool.coverage.run] +omit = [ + "**/__init__.py" +] + +[tool.pyright] +extends = "../../pyproject.toml" +exclude = ['tests'] + +[tool.mypy] +plugins = ['pydantic.mypy'] +strict = true +python_version = "3.10" +ignore_missing_imports = true +disallow_untyped_defs = true +no_implicit_optional = true +check_untyped_defs = true +warn_return_any = true +show_error_codes = true +warn_unused_ignores = false +disallow_incomplete_defs = true +disallow_untyped_decorators = true + +[tool.bandit] +targets = ["agent_framework_minimax"] +exclude_dirs = ["tests"] + +[tool.poe] +executor.type = "uv" +include = "../../shared_tasks.toml" + +[tool.poe.tasks.mypy] +help = "Run MyPy for this package." +cmd = "mypy --config-file $POE_ROOT/pyproject.toml agent_framework_minimax" + +[tool.poe.tasks.test] +help = "Run the default unit test suite for this package." +cmd = 'pytest -m "not integration" --cov=agent_framework_minimax --cov-report=term-missing:skip-covered -n auto --dist worksteal tests' + +[build-system] +requires = ["flit-core >= 3.11,<4.0"] +build-backend = "flit_core.buildapi" diff --git a/python/packages/minimax/tests/conftest.py b/python/packages/minimax/tests/conftest.py new file mode 100644 index 0000000000..644ee7e7f1 --- /dev/null +++ b/python/packages/minimax/tests/conftest.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft. All rights reserved. +from typing import Any +from unittest.mock import AsyncMock, MagicMock + +from pytest import fixture + + +@fixture +def exclude_list(request: Any) -> list[str]: + """Fixture that returns a list of environment variables to exclude.""" + return request.param if hasattr(request, "param") else [] + + +@fixture +def override_env_param_dict(request: Any) -> dict[str, str]: + """Fixture that returns a dict of environment variables to override.""" + return request.param if hasattr(request, "param") else {} + + +@fixture +def minimax_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): # type: ignore + """Fixture to set environment variables for MiniMaxSettings.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = { + "MINIMAX_API_KEY": "test-minimax-api-key-12345", + "MINIMAX_CHAT_MODEL": "MiniMax-M2.7", + } + + env_vars.update(override_env_param_dict) # type: ignore + + for key, value in env_vars.items(): + if key in exclude_list: + monkeypatch.delenv(key, raising=False) # type: ignore + continue + monkeypatch.setenv(key, value) # type: ignore + + return env_vars + + +@fixture +def mock_minimax_client() -> MagicMock: + """Fixture that provides a mock AsyncAnthropic client for MiniMax.""" + mock_client = MagicMock() + mock_client.base_url = "https://api.minimax.io/anthropic" + + # Mock messages property (non-beta) + mock_client.messages = MagicMock() + mock_client.messages.create = AsyncMock() + + return mock_client diff --git a/python/packages/minimax/tests/test_minimax_client.py b/python/packages/minimax/tests/test_minimax_client.py new file mode 100644 index 0000000000..2c81f52295 --- /dev/null +++ b/python/packages/minimax/tests/test_minimax_client.py @@ -0,0 +1,236 @@ +# Copyright (c) Microsoft. All rights reserved. +import os +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest +from agent_framework import ( + ChatMiddlewareLayer, + FunctionInvocationLayer, + Message, +) +from agent_framework._tools import normalize_function_invocation_configuration + +USER_MESSAGE = [Message(role="user", contents=["Hello"])] +from agent_framework.observability import ChatTelemetryLayer +from anthropic.types import ( + Message as AnthropicMessage, + TextBlock, + Usage, +) + +from agent_framework_minimax import MiniMaxClient, RawMiniMaxClient +from agent_framework_minimax._chat_client import ( + MINIMAX_DEFAULT_BASE_URL, + MINIMAX_MODELS, + MINIMAX_UNSUPPORTED_PARAMS, + MiniMaxSettings, +) + +skip_if_minimax_integration_tests_disabled = pytest.mark.skipif( + os.getenv("MINIMAX_API_KEY", "") in ("", "test-minimax-api-key-12345"), + reason="No real MINIMAX_API_KEY provided; skipping integration tests.", +) + + +def create_test_minimax_client( + mock_anthropic_client: MagicMock, + model: str | None = "MiniMax-M2.7", +) -> MiniMaxClient: + """Helper function to create MiniMaxClient instances for testing.""" + client = object.__new__(MiniMaxClient) + + client.anthropic_client = mock_anthropic_client + client.model = model + client._last_call_id_name = None + client._last_call_content_type = None + client._tool_name_aliases = {} + client.additional_properties = {} + client.middleware = None + client.additional_beta_flags = [] + client.chat_middleware = [] + client.function_middleware = [] + client._cached_chat_middleware_pipeline = None + client._cached_function_middleware_pipeline = None + client.function_invocation_configuration = normalize_function_invocation_configuration(None) + + return client + + +def make_anthropic_message(text: str = "Hello from MiniMax!") -> AnthropicMessage: + """Create a mock Anthropic Message response.""" + return AnthropicMessage( + id="msg_test123", + type="message", + role="assistant", + content=[TextBlock(type="text", text=text)], + model="MiniMax-M2.7", + stop_reason="end_turn", + stop_sequence=None, + usage=Usage(input_tokens=10, output_tokens=5), + ) + + +# Settings Tests + + +def test_minimax_settings_from_env(minimax_unit_test_env: dict[str, str]) -> None: + """Test that MiniMaxSettings loads correctly from environment variables.""" + from agent_framework._settings import load_settings + + settings = load_settings( + MiniMaxSettings, + env_prefix="MINIMAX_", + ) + assert settings.get("chat_model") == "MiniMax-M2.7" + assert settings.get("api_key") is not None + + +def test_minimax_default_base_url() -> None: + """Test the default base URL constant.""" + assert MINIMAX_DEFAULT_BASE_URL == "https://api.minimax.io/anthropic" + + +def test_minimax_models_list() -> None: + """Test that the supported model list contains the expected models.""" + assert "MiniMax-M2.7" in MINIMAX_MODELS + assert "MiniMax-M2.7-highspeed" in MINIMAX_MODELS + + +def test_minimax_unsupported_params() -> None: + """Test that the unsupported params set contains key MiniMax-incompatible params.""" + assert "betas" in MINIMAX_UNSUPPORTED_PARAMS + assert "top_k" in MINIMAX_UNSUPPORTED_PARAMS + assert "service_tier" in MINIMAX_UNSUPPORTED_PARAMS + assert "thinking" in MINIMAX_UNSUPPORTED_PARAMS + assert "output_format" in MINIMAX_UNSUPPORTED_PARAMS + + +# Client Initialization Tests + + +def test_raw_minimax_client_raises_without_api_key(minimax_unit_test_env: dict[str, str], monkeypatch) -> None: # type: ignore + """Test that RawMiniMaxClient raises ValueError when no API key is provided.""" + monkeypatch.delenv("MINIMAX_API_KEY", raising=False) + with pytest.raises(ValueError, match="MiniMax API key is required"): + RawMiniMaxClient(model="MiniMax-M2.7") + + +def test_raw_minimax_client_init(minimax_unit_test_env: dict[str, str]) -> None: + """Test that RawMiniMaxClient initializes correctly with environment variables.""" + client = RawMiniMaxClient() + assert client.model == "MiniMax-M2.7" + assert MINIMAX_DEFAULT_BASE_URL in str(client.anthropic_client.base_url) + + +def test_raw_minimax_client_custom_base_url(minimax_unit_test_env: dict[str, str]) -> None: + """Test that RawMiniMaxClient respects a custom base_url.""" + custom_url = "https://api.minimaxi.com/anthropic" + client = RawMiniMaxClient(base_url=custom_url) + assert custom_url in str(client.anthropic_client.base_url) + + +def test_raw_minimax_client_with_explicit_anthropic_client(mock_minimax_client: MagicMock) -> None: + """Test that RawMiniMaxClient accepts a pre-built AsyncAnthropic client.""" + client = RawMiniMaxClient(model="MiniMax-M2.7", anthropic_client=mock_minimax_client) + assert client.anthropic_client is mock_minimax_client + assert client.model == "MiniMax-M2.7" + + +def test_minimax_client_has_middleware_layers(minimax_unit_test_env: dict[str, str]) -> None: + """Test that MiniMaxClient includes all expected middleware layers.""" + client = MiniMaxClient() + assert isinstance(client, FunctionInvocationLayer) + assert isinstance(client, ChatMiddlewareLayer) + assert isinstance(client, ChatTelemetryLayer) + assert isinstance(client, RawMiniMaxClient) + + +# Response Tests + + +@pytest.mark.asyncio +async def test_minimax_get_response(mock_minimax_client: MagicMock) -> None: + """Test that MiniMaxClient.get_response returns a valid ChatResponse.""" + mock_minimax_client.messages.create = AsyncMock( + return_value=make_anthropic_message("Hello!") + ) + + client = create_test_minimax_client(mock_minimax_client) + response = await client.get_response(USER_MESSAGE) + + assert response is not None + assert len(response.messages) > 0 + assert response.messages[0].text == "Hello!" + + +@pytest.mark.asyncio +async def test_minimax_uses_messages_not_beta(mock_minimax_client: MagicMock) -> None: + """Test that MiniMaxClient uses messages.create (not beta.messages.create).""" + mock_minimax_client.messages.create = AsyncMock( + return_value=make_anthropic_message("OK") + ) + + client = create_test_minimax_client(mock_minimax_client) + await client.get_response(USER_MESSAGE) + + # Should call messages.create, not beta.messages.create + mock_minimax_client.messages.create.assert_called_once() + assert not hasattr(mock_minimax_client, "beta") or not mock_minimax_client.beta.messages.create.called + + +@pytest.mark.asyncio +async def test_minimax_filters_unsupported_params(mock_minimax_client: MagicMock) -> None: + """Test that unsupported params are filtered out before calling the API.""" + mock_minimax_client.messages.create = AsyncMock( + return_value=make_anthropic_message("OK") + ) + + client = create_test_minimax_client(mock_minimax_client) + await client.get_response(USER_MESSAGE) + + call_kwargs = mock_minimax_client.messages.create.call_args[1] + for param in MINIMAX_UNSUPPORTED_PARAMS: + assert param not in call_kwargs, f"Unsupported param '{param}' was passed to API" + + +@pytest.mark.asyncio +async def test_minimax_uses_correct_model(mock_minimax_client: MagicMock) -> None: + """Test that MiniMaxClient passes the correct model to the API.""" + mock_minimax_client.messages.create = AsyncMock( + return_value=make_anthropic_message("OK") + ) + + client = create_test_minimax_client(mock_minimax_client, model="MiniMax-M2.7-highspeed") + await client.get_response(USER_MESSAGE) + + call_kwargs = mock_minimax_client.messages.create.call_args[1] + assert call_kwargs.get("model") == "MiniMax-M2.7-highspeed" + + +# Integration Tests (require real MINIMAX_API_KEY) + + +@skip_if_minimax_integration_tests_disabled +@pytest.mark.integration +@pytest.mark.asyncio +async def test_minimax_integration_basic_chat() -> None: + """Integration test: basic chat with MiniMax API.""" + client = MiniMaxClient(model="MiniMax-M2.7") + response = await client.get_response([Message(role="user", contents=["Say 'test passed' in exactly those words."])]) + assert response is not None + assert len(response.messages) > 0 + text = response.messages[0].text or "" + assert "test" in text.lower() + + +@skip_if_minimax_integration_tests_disabled +@pytest.mark.integration +@pytest.mark.asyncio +async def test_minimax_integration_streaming() -> None: + """Integration test: streaming response from MiniMax API.""" + client = MiniMaxClient(model="MiniMax-M2.7") + chunks = [] + async for update in await client.get_response([Message(role="user", contents=["Say hello."])], stream=True): + if update.text: + chunks.append(update.text) + assert len(chunks) > 0