From a47e855591b5d2f546f761a46308af60f0b67fc7 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Sun, 15 Mar 2026 23:37:38 +0800 Subject: [PATCH 1/2] Python: Add MiniMax AI connector for chat completion Add a new AI connector for MiniMax, a cloud-based LLM provider offering OpenAI-compatible API. This follows the same pattern as the existing NVIDIA connector, using the AsyncOpenAI client with a custom base URL. New connector includes: - MiniMaxChatCompletion service with streaming support - MiniMaxSettings for configuration via env vars (MINIMAX_API_KEY, etc.) - MiniMaxChatPromptExecutionSettings with MiniMax-specific constraints - Unit tests for settings, prompt execution settings, and chat completion - README with quick-start guide and available models Supported models: MiniMax-M2.5, MiniMax-M2.5-highspeed (204K context) API endpoint: https://api.minimax.io/v1 --- .../semantic_kernel/connectors/ai/README.md | 1 + .../connectors/ai/minimax/README.md | 62 ++++ .../connectors/ai/minimax/__init__.py | 15 + .../prompt_execution_settings/__init__.py | 1 + .../minimax_prompt_execution_settings.py | 46 +++ .../ai/minimax/services/__init__.py | 1 + .../services/minimax_chat_completion.py | 283 ++++++++++++++++++ .../ai/minimax/services/minimax_handler.py | 94 ++++++ .../minimax/services/minimax_model_types.py | 9 + .../ai/minimax/settings/__init__.py | 1 + .../ai/minimax/settings/minimax_settings.py | 32 ++ .../test_minimax_prompt_execution_settings.py | 49 +++ .../services/test_minimax_chat_completion.py | 120 ++++++++ .../minimax/settings/test_minimax_settings.py | 51 ++++ 14 files changed, 765 insertions(+) create mode 100644 python/semantic_kernel/connectors/ai/minimax/README.md create mode 100644 python/semantic_kernel/connectors/ai/minimax/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_prompt_execution_settings.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/minimax_handler.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/services/minimax_model_types.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/settings/__init__.py create mode 100644 python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py create mode 100644 python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py create mode 100644 python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py create mode 100644 python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py diff --git a/python/semantic_kernel/connectors/ai/README.md b/python/semantic_kernel/connectors/ai/README.md index 6fe7510a0697..93ea630595e3 100644 --- a/python/semantic_kernel/connectors/ai/README.md +++ b/python/semantic_kernel/connectors/ai/README.md @@ -42,6 +42,7 @@ All base clients inherit from the [`AIServiceClientBase`](../../services/ai_serv | | [`GoogleAITextEmbedding`](./google/google_ai/services/google_ai_text_embedding.py) | | HuggingFace | [`HuggingFaceTextCompletion`](./hugging_face/services/hf_text_completion.py) | | | [`HuggingFaceTextEmbedding`](./hugging_face/services/hf_text_embedding.py) | +| [MiniMax](./minimax/README.md) | [`MiniMaxChatCompletion`](./minimax/services/minimax_chat_completion.py) | | Mistral AI | [`MistralAIChatCompletion`](./mistral_ai/services/mistral_ai_chat_completion.py) | | | [`MistralAITextEmbedding`](./mistral_ai/services/mistral_ai_text_embedding.py) | | [Nvidia](./nvidia/README.md) | [`NvidiaTextEmbedding`](./nvidia/services/nvidia_text_embedding.py) | diff --git a/python/semantic_kernel/connectors/ai/minimax/README.md b/python/semantic_kernel/connectors/ai/minimax/README.md new file mode 100644 index 000000000000..c678baa2a0d0 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/README.md @@ -0,0 +1,62 @@ +# semantic_kernel.connectors.ai.minimax + +This connector enables integration with MiniMax API for chat completion. It allows you to use MiniMax's models within the Semantic Kernel framework. + +MiniMax provides an OpenAI-compatible API, making integration straightforward. + +## Quick start + +### Initialize the kernel +```python +import semantic_kernel as sk +kernel = sk.Kernel() +``` + +### Add MiniMax chat completion service +You can provide your API key directly or through environment variables. +```python +from semantic_kernel.connectors.ai.minimax import MiniMaxChatCompletion + +chat_service = MiniMaxChatCompletion( + ai_model_id="MiniMax-M2.5", # Default model if not specified + api_key="your-minimax-api-key", # Can also use MINIMAX_API_KEY env variable + service_id="minimax-chat" # Optional service identifier +) +kernel.add_service(chat_service) +``` + +### Basic chat completion +```python +response = await kernel.invoke_prompt("Hello, how are you?") +``` + +### Using with Chat Completion Agent +```python +from semantic_kernel.agents import ChatCompletionAgent +from semantic_kernel.connectors.ai.minimax import MiniMaxChatCompletion + +agent = ChatCompletionAgent( + service=MiniMaxChatCompletion(), + name="SK-Assistant", + instructions="You are a helpful assistant.", +) +response = await agent.get_response(messages="Write a haiku about Semantic Kernel.") +print(response.content) +``` + +## Available Models + +- `MiniMax-M2.5` - Standard model with 204K context window +- `MiniMax-M2.5-highspeed` - High-speed variant with 204K context window + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MINIMAX_API_KEY` | Your MiniMax API key | +| `MINIMAX_BASE_URL` | API endpoint (defaults to `https://api.minimax.io/v1`) | +| `MINIMAX_CHAT_MODEL_ID` | Default chat model ID | + +## Notes + +- MiniMax API requires temperature to be in the range (0.0, 1.0]. A value of exactly 0.0 is not accepted. diff --git a/python/semantic_kernel/connectors/ai/minimax/__init__.py b/python/semantic_kernel/connectors/ai/minimax/__init__.py new file mode 100644 index 000000000000..b5ee00a1a55d --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) Microsoft. All rights reserved. + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_prompt_execution_settings import ( + MiniMaxChatPromptExecutionSettings, + MiniMaxPromptExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.services.minimax_chat_completion import MiniMaxChatCompletion +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings + +__all__ = [ + "MiniMaxChatCompletion", + "MiniMaxChatPromptExecutionSettings", + "MiniMaxPromptExecutionSettings", + "MiniMaxSettings", +] diff --git a/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py new file mode 100644 index 000000000000..2a50eae89411 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_prompt_execution_settings.py b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_prompt_execution_settings.py new file mode 100644 index 000000000000..9b583acfa44b --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/prompt_execution_settings/minimax_prompt_execution_settings.py @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import Annotated, Any, Literal + +from pydantic import BaseModel, Field + +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings + + +class MiniMaxPromptExecutionSettings(PromptExecutionSettings): + """Settings for MiniMax prompt execution.""" + + pass + + +class MiniMaxChatPromptExecutionSettings(MiniMaxPromptExecutionSettings): + """Settings for MiniMax chat prompt execution.""" + + messages: list[dict[str, str]] | None = None + ai_model_id: Annotated[str | None, Field(serialization_alias="model")] = None + temperature: Annotated[float | None, Field(gt=0.0, le=1.0)] = None + top_p: float | None = None + n: int | None = None + stream: bool = False + stop: str | list[str] | None = None + max_tokens: int | None = None + presence_penalty: float | None = None + frequency_penalty: float | None = None + user: str | None = None + tools: list[dict[str, Any]] | None = None + tool_choice: str | dict[str, Any] | None = None + response_format: ( + dict[Literal["type"], Literal["text", "json_object"]] | dict[str, Any] | type[BaseModel] | type | None + ) = None + seed: int | None = None + extra_headers: dict | None = None + extra_body: dict | None = None + timeout: float | None = None + + def prepare_settings_dict(self, **kwargs) -> dict[str, Any]: + """Prepare the settings as a dictionary for the API request.""" + return self.model_dump( + exclude={"service_id", "extension_data", "structured_json_response", "response_format"}, + exclude_none=True, + by_alias=True, + ) diff --git a/python/semantic_kernel/connectors/ai/minimax/services/__init__.py b/python/semantic_kernel/connectors/ai/minimax/services/__init__.py new file mode 100644 index 000000000000..2a50eae89411 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py b/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py new file mode 100644 index 000000000000..939aa46c4d4a --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py @@ -0,0 +1,283 @@ +# Copyright (c) Microsoft. All rights reserved. + +import logging +import sys +from collections.abc import AsyncGenerator +from typing import Any, Literal + +from openai import AsyncOpenAI +from openai.types.chat.chat_completion import ChatCompletion, Choice +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk +from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice +from pydantic import ValidationError + +from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase +from semantic_kernel.connectors.ai.completion_usage import CompletionUsage +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_prompt_execution_settings import ( + MiniMaxChatPromptExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.services.minimax_handler import MiniMaxHandler +from semantic_kernel.connectors.ai.minimax.services.minimax_model_types import MiniMaxModelTypes +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.contents import ( + AuthorRole, + ChatMessageContent, + FinishReason, + FunctionCallContent, + StreamingChatMessageContent, + StreamingTextContent, + TextContent, +) +from semantic_kernel.contents.chat_history import ChatHistory +from semantic_kernel.exceptions.service_exceptions import ServiceInitializationError +from semantic_kernel.utils.feature_stage_decorator import experimental +from semantic_kernel.utils.telemetry.model_diagnostics.decorators import ( + trace_chat_completion, + trace_streaming_chat_completion, +) + +if sys.version_info >= (3, 12): + from typing import override # pragma: no cover +else: + from typing_extensions import override # pragma: no cover + +logger: logging.Logger = logging.getLogger(__name__) + +# Default MiniMax chat model when none is specified +DEFAULT_MINIMAX_CHAT_MODEL = "MiniMax-M2.5" + + +@experimental +class MiniMaxChatCompletion(MiniMaxHandler, ChatCompletionClientBase): + """MiniMax Chat completion class.""" + + def __init__( + self, + ai_model_id: str | None = None, + api_key: str | None = None, + base_url: str | None = None, + service_id: str | None = None, + client: AsyncOpenAI | None = None, + env_file_path: str | None = None, + env_file_encoding: str | None = None, + instruction_role: Literal["system", "user", "assistant", "developer"] | None = None, + ) -> None: + """Initialize a MiniMaxChatCompletion service. + + Args: + ai_model_id (str): MiniMax model name, for example, MiniMax-M2.5. + If not provided, defaults to DEFAULT_MINIMAX_CHAT_MODEL. + service_id (str | None): Service ID tied to the execution settings. + api_key (str | None): The optional API key to use. If provided will override + the env vars or .env file value. + base_url (str | None): Custom API endpoint. Defaults to https://api.minimax.io/v1. (Optional) + client (Optional[AsyncOpenAI]): An existing client to use. (Optional) + env_file_path (str | None): Use the environment settings file as a fallback + to environment variables. (Optional) + env_file_encoding (str | None): The encoding of the environment settings file. (Optional) + instruction_role (Literal["system", "user", "assistant", "developer"] | None): The role to use for + 'instruction' messages. Defaults to "system". (Optional) + """ + try: + minimax_settings = MiniMaxSettings( + api_key=api_key, + base_url=base_url, + chat_model_id=ai_model_id, + env_file_path=env_file_path, + env_file_encoding=env_file_encoding, + ) + except ValidationError as ex: + raise ServiceInitializationError("Failed to create MiniMax settings.", ex) from ex + + if not client and not minimax_settings.api_key: + raise ServiceInitializationError("The MiniMax API key is required.") + if not minimax_settings.chat_model_id: + minimax_settings.chat_model_id = DEFAULT_MINIMAX_CHAT_MODEL + logger.warning(f"Default chat model set as: {minimax_settings.chat_model_id}") + + if not client: + client = AsyncOpenAI( + api_key=minimax_settings.api_key.get_secret_value() if minimax_settings.api_key else None, + base_url=minimax_settings.base_url, + ) + + super().__init__( + ai_model_id=minimax_settings.chat_model_id, + api_key=minimax_settings.api_key.get_secret_value() if minimax_settings.api_key else None, + base_url=minimax_settings.base_url, + service_id=service_id or "", + ai_model_type=MiniMaxModelTypes.CHAT, + client=client, + instruction_role=instruction_role or "system", + ) + + @classmethod + def from_dict(cls: type["MiniMaxChatCompletion"], settings: dict[str, Any]) -> "MiniMaxChatCompletion": + """Initialize a MiniMax service from a dictionary of settings. + + Args: + settings: A dictionary of settings for the service. + """ + return cls( + ai_model_id=settings.get("ai_model_id"), + api_key=settings.get("api_key"), + base_url=settings.get("base_url"), + service_id=settings.get("service_id"), + env_file_path=settings.get("env_file_path"), + ) + + @override + def get_prompt_execution_settings_class(self) -> type["PromptExecutionSettings"]: + return MiniMaxChatPromptExecutionSettings + + @override + @trace_chat_completion("minimax") + async def _inner_get_chat_message_contents( + self, + chat_history: "ChatHistory", + settings: "PromptExecutionSettings", + ) -> list["ChatMessageContent"]: + if not isinstance(settings, MiniMaxChatPromptExecutionSettings): + settings = self.get_prompt_execution_settings_from_settings(settings) + assert isinstance(settings, MiniMaxChatPromptExecutionSettings) # nosec + + settings.stream = False + settings.messages = self._prepare_chat_history_for_request(chat_history) + settings.ai_model_id = settings.ai_model_id or self.ai_model_id + + response = await self._send_request(settings) + assert isinstance(response, ChatCompletion) # nosec + response_metadata = self._get_metadata_from_chat_response(response) + return [self._create_chat_message_content(response, choice, response_metadata) for choice in response.choices] + + @override + @trace_streaming_chat_completion("minimax") + async def _inner_get_streaming_chat_message_contents( + self, + chat_history: "ChatHistory", + settings: "PromptExecutionSettings", + function_invoke_attempt: int = 0, + ) -> AsyncGenerator[list["StreamingChatMessageContent"], Any]: + if not isinstance(settings, MiniMaxChatPromptExecutionSettings): + settings = self.get_prompt_execution_settings_from_settings(settings) + assert isinstance(settings, MiniMaxChatPromptExecutionSettings) # nosec + + settings.stream = True + settings.messages = self._prepare_chat_history_for_request(chat_history) + settings.ai_model_id = settings.ai_model_id or self.ai_model_id + + response = await self._send_request(settings) + assert isinstance(response, AsyncGenerator) # nosec + + async for chunk in response: + if len(chunk.choices) == 0: + continue + chunk_metadata = self._get_metadata_from_chat_response(chunk) + yield [ + self._create_streaming_chat_message_content(chunk, choice, chunk_metadata, function_invoke_attempt) + for choice in chunk.choices + ] + + def _create_chat_message_content( + self, response: ChatCompletion, choice: Choice, response_metadata: dict[str, Any] + ) -> "ChatMessageContent": + """Create a chat message content object from a choice.""" + metadata = self._get_metadata_from_chat_choice(choice) + metadata.update(response_metadata) + + items: list[Any] = self._get_tool_calls_from_chat_choice(choice) + items.extend(self._get_function_call_from_chat_choice(choice)) + if choice.message.content: + items.append(TextContent(text=choice.message.content)) + + return ChatMessageContent( + inner_content=response, + ai_model_id=self.ai_model_id, + metadata=metadata, + role=AuthorRole(choice.message.role), + items=items, + finish_reason=(FinishReason(choice.finish_reason) if choice.finish_reason else None), + ) + + def _create_streaming_chat_message_content( + self, + chunk: ChatCompletionChunk, + choice: ChunkChoice, + chunk_metadata: dict[str, Any], + function_invoke_attempt: int, + ) -> StreamingChatMessageContent: + """Create a streaming chat message content object from a choice.""" + metadata = self._get_metadata_from_chat_choice(choice) + metadata.update(chunk_metadata) + + items: list[Any] = self._get_tool_calls_from_chat_choice(choice) + items.extend(self._get_function_call_from_chat_choice(choice)) + if choice.delta and choice.delta.content is not None: + items.append(StreamingTextContent(choice_index=choice.index, text=choice.delta.content)) + return StreamingChatMessageContent( + choice_index=choice.index, + inner_content=chunk, + ai_model_id=self.ai_model_id, + metadata=metadata, + role=(AuthorRole(choice.delta.role) if choice.delta and choice.delta.role else AuthorRole.ASSISTANT), + finish_reason=(FinishReason(choice.finish_reason) if choice.finish_reason else None), + items=items, + function_invoke_attempt=function_invoke_attempt, + ) + + def _get_metadata_from_chat_response(self, response: ChatCompletion | ChatCompletionChunk) -> dict[str, Any]: + """Get metadata from a chat response.""" + return { + "id": response.id, + "created": response.created, + "system_fingerprint": getattr(response, "system_fingerprint", None), + "usage": CompletionUsage.from_openai(response.usage) if response.usage is not None else None, + } + + def _get_metadata_from_chat_choice(self, choice: Choice | ChunkChoice) -> dict[str, Any]: + """Get metadata from a chat choice.""" + return { + "logprobs": getattr(choice, "logprobs", None), + } + + def _get_tool_calls_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]: + """Get tool calls from a chat choice.""" + content = choice.message if isinstance(choice, Choice) else choice.delta + if content and (tool_calls := getattr(content, "tool_calls", None)) is not None: + return [ + FunctionCallContent( + id=tool.id, + index=getattr(tool, "index", None), + name=tool.function.name, + arguments=tool.function.arguments, + ) + for tool in tool_calls + ] + return [] + + def _get_function_call_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[FunctionCallContent]: + """Get function calls from a chat choice.""" + content = choice.message if isinstance(choice, Choice) else choice.delta + if content and (function_call := getattr(content, "function_call", None)) is not None: + return [ + FunctionCallContent( + id="", + name=function_call.name, + arguments=function_call.arguments, + ) + ] + return [] + + def _prepare_chat_history_for_request( + self, + chat_history: ChatHistory, + role_key: str = "role", + content_key: str = "content", + ) -> list[dict[str, str]]: + """Prepare chat history for request.""" + messages = [] + for message in chat_history.messages: + message_dict = {role_key: message.role.value, content_key: message.content} + messages.append(message_dict) + return messages diff --git a/python/semantic_kernel/connectors/ai/minimax/services/minimax_handler.py b/python/semantic_kernel/connectors/ai/minimax/services/minimax_handler.py new file mode 100644 index 000000000000..fb0f3dcad834 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/minimax_handler.py @@ -0,0 +1,94 @@ +# Copyright (c) Microsoft. All rights reserved. + +import logging +from abc import ABC +from typing import Any, ClassVar, Union + +from openai import AsyncOpenAI, AsyncStream +from openai.types.chat.chat_completion import ChatCompletion +from openai.types.chat.chat_completion_chunk import ChatCompletionChunk +from openai.types.completion import Completion + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_prompt_execution_settings import ( + MiniMaxChatPromptExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.services.minimax_model_types import MiniMaxModelTypes +from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings +from semantic_kernel.const import USER_AGENT +from semantic_kernel.exceptions import ServiceResponseException +from semantic_kernel.kernel_pydantic import KernelBaseModel + +logger: logging.Logger = logging.getLogger(__name__) + +RESPONSE_TYPE = Union[list[Any], ChatCompletion, Completion, AsyncStream[Any]] + + +class MiniMaxHandler(KernelBaseModel, ABC): + """Internal class for calls to MiniMax API's.""" + + MODEL_PROVIDER_NAME: ClassVar[str] = "minimax" + client: AsyncOpenAI + ai_model_type: MiniMaxModelTypes = MiniMaxModelTypes.CHAT + completion_tokens: int = 0 + total_tokens: int = 0 + prompt_tokens: int = 0 + + async def _send_request(self, settings: PromptExecutionSettings) -> RESPONSE_TYPE: + """Send a request to the MiniMax API.""" + if self.ai_model_type == MiniMaxModelTypes.CHAT: + assert isinstance(settings, MiniMaxChatPromptExecutionSettings) # nosec + return await self._send_chat_completion_request(settings) + + raise NotImplementedError(f"Model type {self.ai_model_type} is not supported") + + async def _send_chat_completion_request( + self, settings: MiniMaxChatPromptExecutionSettings + ) -> ChatCompletion | AsyncStream[Any]: + """Send a request to the MiniMax chat completion endpoint.""" + try: + settings_dict = settings.prepare_settings_dict() + response = await self.client.chat.completions.create(**settings_dict) + self.store_usage(response) + return response + except Exception as ex: + raise ServiceResponseException( + f"{type(self)} service failed to complete the chat", + ex, + ) from ex + + def store_usage( + self, + response: ChatCompletion + | Completion + | AsyncStream[ChatCompletionChunk] + | AsyncStream[Completion], + ): + """Store the usage information from the response.""" + if not isinstance(response, AsyncStream) and response.usage: + logger.info(f"MiniMax usage: {response.usage}") + self.prompt_tokens += response.usage.prompt_tokens + self.total_tokens += response.usage.total_tokens + if hasattr(response.usage, "completion_tokens"): + self.completion_tokens += response.usage.completion_tokens + + def to_dict(self) -> dict[str, str]: + """Create a dict of the service settings.""" + client_settings = { + "api_key": self.client.api_key, + "default_headers": {k: v for k, v in self.client.default_headers.items() if k != USER_AGENT}, + } + base = self.model_dump( + exclude={ + "prompt_tokens", + "completion_tokens", + "total_tokens", + "api_type", + "ai_model_type", + "service_id", + "client", + }, + by_alias=True, + exclude_none=True, + ) + base.update(client_settings) + return base diff --git a/python/semantic_kernel/connectors/ai/minimax/services/minimax_model_types.py b/python/semantic_kernel/connectors/ai/minimax/services/minimax_model_types.py new file mode 100644 index 000000000000..e18b0b1aba3f --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/services/minimax_model_types.py @@ -0,0 +1,9 @@ +# Copyright (c) Microsoft. All rights reserved. + +from enum import Enum + + +class MiniMaxModelTypes(Enum): + """MiniMax model types.""" + + CHAT = "chat" diff --git a/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py b/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py new file mode 100644 index 000000000000..2a50eae89411 --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/settings/__init__.py @@ -0,0 +1 @@ +# Copyright (c) Microsoft. All rights reserved. diff --git a/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py new file mode 100644 index 000000000000..6d6569945bac --- /dev/null +++ b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft. All rights reserved. + +from typing import ClassVar + +from pydantic import SecretStr + +from semantic_kernel.kernel_pydantic import KernelBaseSettings + + +class MiniMaxSettings(KernelBaseSettings): + """MiniMax model settings. + + The settings are first loaded from environment variables with the prefix 'MINIMAX_'. If the + environment variables are not found, the settings can be loaded from a .env file with the + encoding 'utf-8'. If the settings are not found in the .env file, the settings are ignored; + however, validation will fail alerting that the settings are missing. + + Optional settings for prefix 'MINIMAX_' are: + - api_key: MiniMax API key + (Env var MINIMAX_API_KEY) + - base_url: str - The url of the MiniMax API endpoint. Defaults to https://api.minimax.io/v1. + (Env var MINIMAX_BASE_URL) + - chat_model_id: str | None - The MiniMax chat model ID to use, for example, MiniMax-M2.5. + (Env var MINIMAX_CHAT_MODEL_ID) + - env_file_path: if provided, the .env settings are read from this file path location + """ + + env_prefix: ClassVar[str] = "MINIMAX_" + + api_key: SecretStr | None = None + base_url: str = "https://api.minimax.io/v1" + chat_model_id: str | None = None diff --git a/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py b/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py new file mode 100644 index 000000000000..8e76f48ca7d7 --- /dev/null +++ b/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py @@ -0,0 +1,49 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_prompt_execution_settings import ( + MiniMaxChatPromptExecutionSettings, +) + + +class TestMiniMaxChatPromptExecutionSettings: + """Test cases for MiniMaxChatPromptExecutionSettings.""" + + def test_default_settings(self): + """Test default settings initialization.""" + settings = MiniMaxChatPromptExecutionSettings() + assert settings.temperature is None + assert settings.top_p is None + assert settings.max_tokens is None + assert settings.stream is False + + def test_settings_with_values(self): + """Test settings with specific values.""" + settings = MiniMaxChatPromptExecutionSettings( + temperature=0.7, + top_p=0.9, + max_tokens=1000, + ) + assert settings.temperature == 0.7 + assert settings.top_p == 0.9 + assert settings.max_tokens == 1000 + + def test_prepare_settings_dict(self): + """Test prepare_settings_dict excludes correct fields.""" + settings = MiniMaxChatPromptExecutionSettings( + temperature=0.7, + max_tokens=100, + service_id="test-service", + ) + result = settings.prepare_settings_dict() + assert "temperature" in result + assert "max_tokens" in result + assert "service_id" not in result + assert "response_format" not in result + + def test_model_alias(self): + """Test that ai_model_id serializes as 'model'.""" + settings = MiniMaxChatPromptExecutionSettings(ai_model_id="MiniMax-M2.5") + result = settings.prepare_settings_dict() + assert "model" in result + assert result["model"] == "MiniMax-M2.5" diff --git a/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py b/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py new file mode 100644 index 000000000000..fb8ea212dc9d --- /dev/null +++ b/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py @@ -0,0 +1,120 @@ +# Copyright (c) Microsoft. All rights reserved. + +from unittest.mock import AsyncMock, patch + +import pytest +from openai.resources.chat.completions import AsyncCompletions +from openai.types.chat import ChatCompletion, ChatCompletionMessage +from openai.types.chat.chat_completion import Choice +from openai.types.completion_usage import CompletionUsage + +from semantic_kernel.connectors.ai.minimax import MiniMaxChatCompletion +from semantic_kernel.connectors.ai.minimax.prompt_execution_settings.minimax_prompt_execution_settings import ( + MiniMaxChatPromptExecutionSettings, +) +from semantic_kernel.connectors.ai.minimax.services.minimax_chat_completion import DEFAULT_MINIMAX_CHAT_MODEL +from semantic_kernel.contents import ChatHistory +from semantic_kernel.exceptions import ServiceInitializationError, ServiceResponseException + + +@pytest.fixture +def minimax_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): + """Fixture to set environment variables for MiniMaxChatCompletion.""" + if exclude_list is None: + exclude_list = [] + + if override_env_param_dict is None: + override_env_param_dict = {} + + env_vars = {"MINIMAX_API_KEY": "test_api_key", "MINIMAX_CHAT_MODEL_ID": "MiniMax-M2.5"} + + env_vars.update(override_env_param_dict) + + for key, value in env_vars.items(): + if key not in exclude_list: + monkeypatch.setenv(key, value) + else: + monkeypatch.delenv(key, raising=False) + + return env_vars + + +def _create_mock_chat_completion(content: str = "Hello!") -> ChatCompletion: + """Helper function to create a mock ChatCompletion response.""" + message = ChatCompletionMessage(role="assistant", content=content) + choice = Choice( + finish_reason="stop", + index=0, + message=message, + ) + usage = CompletionUsage(completion_tokens=20, prompt_tokens=10, total_tokens=30) + return ChatCompletion( + id="test-id", + choices=[choice], + created=1234567890, + model="MiniMax-M2.5", + object="chat.completion", + usage=usage, + ) + + +class TestMiniMaxChatCompletion: + """Test cases for MiniMaxChatCompletion.""" + + def test_init_with_defaults(self, minimax_unit_test_env): + """Test initialization with default values.""" + service = MiniMaxChatCompletion() + assert service.ai_model_id == minimax_unit_test_env["MINIMAX_CHAT_MODEL_ID"] + + def test_get_prompt_execution_settings_class(self, minimax_unit_test_env): + """Test getting the prompt execution settings class.""" + service = MiniMaxChatCompletion() + assert service.get_prompt_execution_settings_class() == MiniMaxChatPromptExecutionSettings + + @pytest.mark.parametrize("exclude_list", [["MINIMAX_API_KEY"]], indirect=True) + def test_init_with_empty_api_key(self, minimax_unit_test_env): + """Test initialization fails with empty API key.""" + with pytest.raises(ServiceInitializationError): + MiniMaxChatCompletion() + + @pytest.mark.parametrize("exclude_list", [["MINIMAX_CHAT_MODEL_ID"]], indirect=True) + def test_init_with_empty_model_id(self, minimax_unit_test_env): + """Test initialization with empty model ID uses default.""" + service = MiniMaxChatCompletion() + assert service.ai_model_id == DEFAULT_MINIMAX_CHAT_MODEL + + def test_init_with_custom_model_id(self, minimax_unit_test_env): + """Test initialization with custom model ID.""" + custom_model = "MiniMax-M2.5-highspeed" + service = MiniMaxChatCompletion(ai_model_id=custom_model) + assert service.ai_model_id == custom_model + + @pytest.mark.asyncio + @patch.object(AsyncCompletions, "create", new_callable=AsyncMock) + async def test_get_chat_message_contents(self, mock_create, minimax_unit_test_env): + """Test basic chat completion.""" + mock_create.return_value = _create_mock_chat_completion("Hello!") + + service = MiniMaxChatCompletion() + chat_history = ChatHistory() + chat_history.add_user_message("Hello") + settings = MiniMaxChatPromptExecutionSettings() + + result = await service.get_chat_message_contents(chat_history, settings) + + assert len(result) == 1 + assert result[0].content == "Hello!" + + @pytest.mark.asyncio + @patch.object(AsyncCompletions, "create", new_callable=AsyncMock) + async def test_error_handling(self, mock_create, minimax_unit_test_env): + """Test error handling.""" + mock_create.side_effect = Exception("API Error") + + service = MiniMaxChatCompletion() + chat_history = ChatHistory() + chat_history.add_user_message("Hello") + settings = MiniMaxChatPromptExecutionSettings() + + with pytest.raises(ServiceResponseException): + await service.get_chat_message_contents(chat_history, settings) diff --git a/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py b/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py new file mode 100644 index 000000000000..132a85726f52 --- /dev/null +++ b/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py @@ -0,0 +1,51 @@ +# Copyright (c) Microsoft. All rights reserved. + + +from semantic_kernel.connectors.ai.minimax.settings.minimax_settings import MiniMaxSettings + + +class TestMiniMaxSettings: + """Test cases for MiniMaxSettings.""" + + def test_init_with_defaults(self): + """Test initialization with default values.""" + settings = MiniMaxSettings() + assert settings.api_key is None + assert settings.base_url == "https://api.minimax.io/v1" + assert settings.chat_model_id is None + + def test_init_with_values(self): + """Test initialization with specific values.""" + settings = MiniMaxSettings( + api_key="test-api-key", + base_url="https://custom.minimax.io/v1", + chat_model_id="MiniMax-M2.5", + ) + + assert settings.api_key.get_secret_value() == "test-api-key" + assert settings.base_url == "https://custom.minimax.io/v1" + assert settings.chat_model_id == "MiniMax-M2.5" + + def test_env_prefix(self): + """Test environment variable prefix.""" + assert MiniMaxSettings.env_prefix == "MINIMAX_" + + def test_api_key_secret_str(self): + """Test that api_key is properly handled as SecretStr.""" + settings = MiniMaxSettings(api_key="secret-key") + + assert hasattr(settings.api_key, "get_secret_value") + assert settings.api_key.get_secret_value() == "secret-key" + + str_repr = str(settings) + assert "secret-key" not in str_repr + + def test_environment_variables(self, monkeypatch): + """Test that environment variables override defaults.""" + monkeypatch.setenv("MINIMAX_API_KEY", "env-key") + monkeypatch.setenv("MINIMAX_CHAT_MODEL_ID", "MiniMax-M2.5") + + settings = MiniMaxSettings() + + assert settings.api_key.get_secret_value() == "env-key" + assert settings.chat_model_id == "MiniMax-M2.5" From 473d8da7167c0b621f3edb2f0269543760155ef4 Mon Sep 17 00:00:00 2001 From: PR Bot Date: Wed, 18 Mar 2026 23:15:48 +0800 Subject: [PATCH 2/2] feat: upgrade MiniMax default model to M2.7 - Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list - Set MiniMax-M2.7 as default model - Keep all previous models as alternatives - Update related tests and documentation --- python/semantic_kernel/connectors/ai/minimax/README.md | 6 ++++-- .../ai/minimax/services/minimax_chat_completion.py | 4 ++-- .../connectors/ai/minimax/settings/minimax_settings.py | 2 +- .../test_minimax_prompt_execution_settings.py | 4 ++-- .../ai/minimax/services/test_minimax_chat_completion.py | 6 +++--- .../ai/minimax/settings/test_minimax_settings.py | 8 ++++---- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/minimax/README.md b/python/semantic_kernel/connectors/ai/minimax/README.md index c678baa2a0d0..b69074ba1185 100644 --- a/python/semantic_kernel/connectors/ai/minimax/README.md +++ b/python/semantic_kernel/connectors/ai/minimax/README.md @@ -18,7 +18,7 @@ You can provide your API key directly or through environment variables. from semantic_kernel.connectors.ai.minimax import MiniMaxChatCompletion chat_service = MiniMaxChatCompletion( - ai_model_id="MiniMax-M2.5", # Default model if not specified + ai_model_id="MiniMax-M2.7", # Default model if not specified api_key="your-minimax-api-key", # Can also use MINIMAX_API_KEY env variable service_id="minimax-chat" # Optional service identifier ) @@ -46,6 +46,8 @@ print(response.content) ## Available Models +- `MiniMax-M2.7` - Latest flagship model with enhanced reasoning and coding (default) +- `MiniMax-M2.7-highspeed` - High-speed version of M2.7 for low-latency scenarios - `MiniMax-M2.5` - Standard model with 204K context window - `MiniMax-M2.5-highspeed` - High-speed variant with 204K context window @@ -59,4 +61,4 @@ print(response.content) ## Notes -- MiniMax API requires temperature to be in the range (0.0, 1.0]. A value of exactly 0.0 is not accepted. +- MiniMax API accepts temperature in the range [0.0, 1.0]. diff --git a/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py b/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py index 939aa46c4d4a..0feb424bed4b 100644 --- a/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/minimax/services/minimax_chat_completion.py @@ -45,7 +45,7 @@ logger: logging.Logger = logging.getLogger(__name__) # Default MiniMax chat model when none is specified -DEFAULT_MINIMAX_CHAT_MODEL = "MiniMax-M2.5" +DEFAULT_MINIMAX_CHAT_MODEL = "MiniMax-M2.7" @experimental @@ -66,7 +66,7 @@ def __init__( """Initialize a MiniMaxChatCompletion service. Args: - ai_model_id (str): MiniMax model name, for example, MiniMax-M2.5. + ai_model_id (str): MiniMax model name, for example, MiniMax-M2.7. If not provided, defaults to DEFAULT_MINIMAX_CHAT_MODEL. service_id (str | None): Service ID tied to the execution settings. api_key (str | None): The optional API key to use. If provided will override diff --git a/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py index 6d6569945bac..11f0ba0016c7 100644 --- a/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py +++ b/python/semantic_kernel/connectors/ai/minimax/settings/minimax_settings.py @@ -20,7 +20,7 @@ class MiniMaxSettings(KernelBaseSettings): (Env var MINIMAX_API_KEY) - base_url: str - The url of the MiniMax API endpoint. Defaults to https://api.minimax.io/v1. (Env var MINIMAX_BASE_URL) - - chat_model_id: str | None - The MiniMax chat model ID to use, for example, MiniMax-M2.5. + - chat_model_id: str | None - The MiniMax chat model ID to use, for example, MiniMax-M2.7. (Env var MINIMAX_CHAT_MODEL_ID) - env_file_path: if provided, the .env settings are read from this file path location """ diff --git a/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py b/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py index 8e76f48ca7d7..87694181d5d0 100644 --- a/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py +++ b/python/tests/unit/connectors/ai/minimax/prompt_execution_settings/test_minimax_prompt_execution_settings.py @@ -43,7 +43,7 @@ def test_prepare_settings_dict(self): def test_model_alias(self): """Test that ai_model_id serializes as 'model'.""" - settings = MiniMaxChatPromptExecutionSettings(ai_model_id="MiniMax-M2.5") + settings = MiniMaxChatPromptExecutionSettings(ai_model_id="MiniMax-M2.7") result = settings.prepare_settings_dict() assert "model" in result - assert result["model"] == "MiniMax-M2.5" + assert result["model"] == "MiniMax-M2.7" diff --git a/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py b/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py index fb8ea212dc9d..a6a456b4aa01 100644 --- a/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py +++ b/python/tests/unit/connectors/ai/minimax/services/test_minimax_chat_completion.py @@ -26,7 +26,7 @@ def minimax_unit_test_env(monkeypatch, exclude_list, override_env_param_dict): if override_env_param_dict is None: override_env_param_dict = {} - env_vars = {"MINIMAX_API_KEY": "test_api_key", "MINIMAX_CHAT_MODEL_ID": "MiniMax-M2.5"} + env_vars = {"MINIMAX_API_KEY": "test_api_key", "MINIMAX_CHAT_MODEL_ID": "MiniMax-M2.7"} env_vars.update(override_env_param_dict) @@ -52,7 +52,7 @@ def _create_mock_chat_completion(content: str = "Hello!") -> ChatCompletion: id="test-id", choices=[choice], created=1234567890, - model="MiniMax-M2.5", + model="MiniMax-M2.7", object="chat.completion", usage=usage, ) @@ -85,7 +85,7 @@ def test_init_with_empty_model_id(self, minimax_unit_test_env): def test_init_with_custom_model_id(self, minimax_unit_test_env): """Test initialization with custom model ID.""" - custom_model = "MiniMax-M2.5-highspeed" + custom_model = "MiniMax-M2.7-highspeed" service = MiniMaxChatCompletion(ai_model_id=custom_model) assert service.ai_model_id == custom_model diff --git a/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py b/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py index 132a85726f52..e6594c83e0a9 100644 --- a/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py +++ b/python/tests/unit/connectors/ai/minimax/settings/test_minimax_settings.py @@ -19,12 +19,12 @@ def test_init_with_values(self): settings = MiniMaxSettings( api_key="test-api-key", base_url="https://custom.minimax.io/v1", - chat_model_id="MiniMax-M2.5", + chat_model_id="MiniMax-M2.7", ) assert settings.api_key.get_secret_value() == "test-api-key" assert settings.base_url == "https://custom.minimax.io/v1" - assert settings.chat_model_id == "MiniMax-M2.5" + assert settings.chat_model_id == "MiniMax-M2.7" def test_env_prefix(self): """Test environment variable prefix.""" @@ -43,9 +43,9 @@ def test_api_key_secret_str(self): def test_environment_variables(self, monkeypatch): """Test that environment variables override defaults.""" monkeypatch.setenv("MINIMAX_API_KEY", "env-key") - monkeypatch.setenv("MINIMAX_CHAT_MODEL_ID", "MiniMax-M2.5") + monkeypatch.setenv("MINIMAX_CHAT_MODEL_ID", "MiniMax-M2.7") settings = MiniMaxSettings() assert settings.api_key.get_secret_value() == "env-key" - assert settings.chat_model_id == "MiniMax-M2.5" + assert settings.chat_model_id == "MiniMax-M2.7"