diff --git a/26.multiple-llm-providers-with-litellm.ipynb b/26.multiple-llm-providers-with-litellm.ipynb
index da9f873..cf9cc6a 100644
--- a/26.multiple-llm-providers-with-litellm.ipynb
+++ b/26.multiple-llm-providers-with-litellm.ipynb
@@ -324,6 +324,20 @@
"print(textwrap.fill(response.choices[0].message.content, 120))"
]
},
+ {
+ "cell_type": "code",
+ "source": "%%time\n\nimport os\n\nresponse = completion(\n model=\"openai/MiniMax-M2.7\",\n messages=[{\"content\": PROMPT, \"role\": \"user\"}],\n api_base=\"https://api.minimax.io/v1\",\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n temperature=0.1,\n)",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
+ {
+ "cell_type": "code",
+ "source": "print(textwrap.fill(response.choices[0].message.content, 120))",
+ "metadata": {},
+ "execution_count": null,
+ "outputs": []
+ },
{
"cell_type": "markdown",
"metadata": {
@@ -819,4 +833,4 @@
},
"nbformat": 4,
"nbformat_minor": 0
-}
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 36837ab..10e8687 100644
--- a/README.md
+++ b/README.md
@@ -74,4 +74,15 @@ Build the future of automation. Design intelligent agents that can reason, plan,
| Teamwork Makes the Dream Work - Build Agentic Workflow | Build an agentic workflow that analyzes Reddit posts and generates a report based on the analysis. All using only local models. | [Read](https://www.mlexpert.io/academy/v1/ai-agents/build-agentic-workflow) | [Watch](https://www.youtube.com/watch?v=dVf1z2BDVtI) |
| Thinking and Acting - Build an AI Agent | Build an AI agent that lets you to talk to your database. Working with a local LLM using LangChain and Ollama. | [Read](https://www.mlexpert.io/academy/v1/ai-agents/build-ai-agent) | [Watch](https://www.youtube.com/watch?v=ay_sYadoxgk) |
| Chat With Your Data - A Local MCP AI Agent | Build a secure, local-first AI agent that can chat with your files. This tutorial uses the Model Context Protocol (MCP), LangGraph, and Streamlit to create a powerful personal knowledge manager. | [Read](https://www.mlexpert.io/academy/v1/ai-agents/build-mcp-agent) | [Watch](https://www.youtube.com/watch?v=ZkMlWwgiFGw) |
-| Agentic RAG - Building an AI Financial Analyst Team | Build a multi-agent system with LangGraph that dynamically plans and retrieves financial data from stock APIs and SEC filings to answer complex questions, moving beyond simple RAG pipelines. | [Read](https://www.mlexpert.io/academy/v1/ai-agents/agentic-rag) | |
\ No newline at end of file
+| Agentic RAG - Building an AI Financial Analyst Team | Build a multi-agent system with LangGraph that dynamically plans and retrieves financial data from stock APIs and SEC filings to answer complex questions, moving beyond simple RAG pipelines. | [Read](https://www.mlexpert.io/academy/v1/ai-agents/agentic-rag) | |
+
+## Model Explorations
+
+Hands-on notebooks for exploring specific LLM providers and models.
+
+| Model | Description | Notebook |
+| ----- | ----------- | -------- |
+| GPT-4o | OpenAI's multimodal model - text generation, vision, streaming, JSON output, and tool calling | [Open](gpt-4o.ipynb) |
+| DeepSeek R1 | DeepSeek's reasoning model via Ollama - lyrics, coding, labeling, summarization, and structured data extraction | [Open](deepseek-r1.ipynb) |
+| MiniMax M2.7 | MiniMax's 204K context model via OpenAI-compatible API - text generation, streaming, structured output, tool calling, and data labelling | [Open](minimax-m2.7.ipynb) |
+| Multiple Providers with LiteLLM | Use OpenAI, Gemini, and MiniMax through a single unified interface with litellm | [Open](26.multiple-llm-providers-with-litellm.ipynb) |
\ No newline at end of file
diff --git a/minimax-m2.7.ipynb b/minimax-m2.7.ipynb
new file mode 100644
index 0000000..d94cb0d
--- /dev/null
+++ b/minimax-m2.7.ipynb
@@ -0,0 +1,624 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!pip install -Uqqq pip --progress-bar off\n",
+ "!pip install -qqq openai==1.82.0 --progress-bar off\n",
+ "!pip install -qqq python-dotenv==1.1.0 --progress-bar off\n",
+ "!pip install -qqq pydantic==2.11.7 --progress-bar off"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import json\n",
+ "import os\n",
+ "import textwrap\n",
+ "from typing import Literal\n",
+ "\n",
+ "from dotenv import load_dotenv\n",
+ "from IPython.display import Markdown, display\n",
+ "from openai import OpenAI\n",
+ "from pydantic import BaseModel\n",
+ "\n",
+ "load_dotenv()\n",
+ "\n",
+ "MODEL_NAME = \"MiniMax-M2.7\"\n",
+ "\n",
+ "client = OpenAI(\n",
+ " api_key=os.environ[\"MINIMAX_API_KEY\"],\n",
+ " base_url=\"https://api.minimax.io/v1\",\n",
+ ")\n",
+ "\n",
+ "\n",
+ "def format_response(response):\n",
+ " response_txt = response.choices[0].message.content\n",
+ " text = \"\"\n",
+ " for chunk in response_txt.split(\"\\n\"):\n",
+ " text += \"\\n\"\n",
+ " if not chunk:\n",
+ " continue\n",
+ " text += (\n",
+ " \"\\n\".join(textwrap.wrap(chunk, 100, break_long_words=False))\n",
+ " ).strip()\n",
+ " return text.strip()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-2",
+ "metadata": {},
+ "source": [
+ "## Text Generation\n",
+ "\n",
+ "[MiniMax M2.7](https://www.minimax.io/) is a large language model with a 204K context window.\n",
+ "It provides an OpenAI-compatible API, so you can use the standard `openai` Python SDK."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "messages = [\n",
+ " {\"role\": \"user\", \"content\": \"Explain what is Deep Learning in one sentence\"},\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.1\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(textwrap.fill(response.choices[0].message.content, 120))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "usage = response.usage\n",
+ "print(\n",
+ " f\"\"\"\n",
+ "Tokens Used\n",
+ "\n",
+ "Prompt: {usage.prompt_tokens}\n",
+ "Completion: {usage.completion_tokens}\n",
+ "Total: {usage.total_tokens}\n",
+ "\"\"\"\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-6",
+ "metadata": {},
+ "source": [
+ "## System Prompt"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "messages = [\n",
+ " {\n",
+ " \"role\": \"system\",\n",
+ " \"content\": \"You are a helpful AI coding assistant. Always provide clear, concise answers with code examples.\",\n",
+ " },\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"How do I read a CSV file in Python using pandas?\",\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.1\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-8",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "display(Markdown(format_response(response)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-9",
+ "metadata": {},
+ "source": [
+ "## Streaming"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-10",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "messages = [\n",
+ " {\"role\": \"user\", \"content\": \"Write a short poem about machine learning\"},\n",
+ "]\n",
+ "\n",
+ "completion = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.7, stream=True\n",
+ ")\n",
+ "\n",
+ "for chunk in completion:\n",
+ " content = chunk.choices[0].delta.content\n",
+ " if content is not None:\n",
+ " print(content, end=\"\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-11",
+ "metadata": {},
+ "source": [
+ "## JSON Response"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-12",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "messages = [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"List the top 3 programming languages for AI development. Return a JSON object with a 'languages' key containing a list of objects with 'name', 'reason', and 'popularity_rank' fields.\",\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME,\n",
+ " messages=messages,\n",
+ " response_format={\"type\": \"json_object\"},\n",
+ " temperature=0.1,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-13",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "result = json.loads(response.choices[0].message.content)\n",
+ "print(json.dumps(result, indent=2))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-14",
+ "metadata": {},
+ "source": [
+ "## Structured Output with Pydantic"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-15",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class SentimentClassification(BaseModel):\n",
+ " sentiment: Literal[\"negative\", \"neutral\", \"positive\"]\n",
+ " reasoning: str\n",
+ "\n",
+ "\n",
+ "PROMPT = \"\"\"\n",
+ "Classify the text sentiment into one of negative, neutral or positive.\n",
+ "Give your reasoning in the `reasoning` field.\n",
+ "\n",
+ "Text:\n",
+ "```\n",
+ "I am very happy to say that AI has taken my job, for good!\n",
+ "```\n",
+ "\n",
+ "Respond with a JSON object matching this schema:\n",
+ "{\"sentiment\": \"negative|neutral|positive\", \"reasoning\": \"your reasoning\"}\n",
+ "\"\"\"\n",
+ "\n",
+ "messages = [{\"role\": \"user\", \"content\": PROMPT}]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME,\n",
+ " messages=messages,\n",
+ " response_format={\"type\": \"json_object\"},\n",
+ " temperature=0.1,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-16",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "json_content = response.choices[0].message.content\n",
+ "result = SentimentClassification.model_validate_json(json_content)\n",
+ "print(f\"Sentiment: {result.sentiment}\")\n",
+ "print(f\"Reasoning: {result.reasoning}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-17",
+ "metadata": {},
+ "source": [
+ "## Simulating a Chat"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "messages = [\n",
+ " {\n",
+ " \"role\": \"system\",\n",
+ " \"content\": \"You are a helpful AI tutor specializing in machine learning.\",\n",
+ " },\n",
+ " {\"role\": \"user\", \"content\": \"What is the difference between supervised and unsupervised learning?\"},\n",
+ " {\n",
+ " \"role\": \"assistant\",\n",
+ " \"content\": \"Supervised learning uses labeled data to train models, while unsupervised learning finds patterns in unlabeled data.\",\n",
+ " },\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": \"Give me an example of each.\",\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.1\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-19",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "display(Markdown(format_response(response)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-20",
+ "metadata": {},
+ "source": [
+ "## Using Tools (Function Calling)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-21",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def get_weather(city: str, unit: str = \"celsius\") -> str:\n",
+ " \"\"\"Get the current weather for a city.\n",
+ "\n",
+ " Parameters\n",
+ " ----------\n",
+ " city : str\n",
+ " The city name\n",
+ " unit : str\n",
+ " Temperature unit (celsius or fahrenheit)\n",
+ "\n",
+ " Returns\n",
+ " -------\n",
+ " str\n",
+ " Weather information as JSON string\n",
+ " \"\"\"\n",
+ " weather_data = {\n",
+ " \"San Francisco\": {\"temp\": 18, \"condition\": \"Foggy\"},\n",
+ " \"New York\": {\"temp\": 25, \"condition\": \"Sunny\"},\n",
+ " \"London\": {\"temp\": 15, \"condition\": \"Rainy\"},\n",
+ " \"Tokyo\": {\"temp\": 28, \"condition\": \"Humid\"},\n",
+ " }\n",
+ " data = weather_data.get(city, {\"temp\": 20, \"condition\": \"Unknown\"})\n",
+ " if unit == \"fahrenheit\":\n",
+ " data[\"temp\"] = round(data[\"temp\"] * 9 / 5 + 32)\n",
+ " data[\"unit\"] = unit\n",
+ " data[\"city\"] = city\n",
+ " return json.dumps(data)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-22",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "tools = [\n",
+ " {\n",
+ " \"type\": \"function\",\n",
+ " \"function\": {\n",
+ " \"name\": \"get_weather\",\n",
+ " \"description\": \"Get the current weather for a city\",\n",
+ " \"parameters\": {\n",
+ " \"type\": \"object\",\n",
+ " \"properties\": {\n",
+ " \"city\": {\n",
+ " \"type\": \"string\",\n",
+ " \"description\": \"The city name\",\n",
+ " },\n",
+ " \"unit\": {\n",
+ " \"type\": \"string\",\n",
+ " \"enum\": [\"celsius\", \"fahrenheit\"],\n",
+ " \"description\": \"Temperature unit\",\n",
+ " },\n",
+ " },\n",
+ " \"required\": [\"city\"],\n",
+ " },\n",
+ " },\n",
+ " }\n",
+ "]\n",
+ "\n",
+ "messages = [\n",
+ " {\"role\": \"user\", \"content\": \"What's the weather like in Tokyo?\"}\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME,\n",
+ " messages=messages,\n",
+ " tools=tools,\n",
+ " tool_choice=\"auto\",\n",
+ " temperature=0.1,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-23",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "response_message = response.choices[0].message\n",
+ "tool_calls = response_message.tool_calls\n",
+ "print(f\"Tool calls: {tool_calls}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-24",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "available_functions = {\"get_weather\": get_weather}\n",
+ "\n",
+ "messages.append(response_message)\n",
+ "\n",
+ "for tool_call in tool_calls:\n",
+ " function_name = tool_call.function.name\n",
+ " function_to_call = available_functions[function_name]\n",
+ " function_args = json.loads(tool_call.function.arguments)\n",
+ " function_response = function_to_call(**function_args)\n",
+ " messages.append(\n",
+ " {\n",
+ " \"tool_call_id\": tool_call.id,\n",
+ " \"role\": \"tool\",\n",
+ " \"name\": function_name,\n",
+ " \"content\": function_response,\n",
+ " }\n",
+ " )"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-25",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "final_response = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.1\n",
+ ")\n",
+ "\n",
+ "print(final_response.choices[0].message.content)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-26",
+ "metadata": {},
+ "source": [
+ "## Text Summarization"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-27",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "article = \"\"\"\n",
+ "Artificial intelligence has made remarkable strides in recent years, particularly in the field of\n",
+ "natural language processing. Large language models, trained on vast amounts of text data, have\n",
+ "demonstrated the ability to generate human-like text, translate languages, write different kinds\n",
+ "of creative content, and answer questions in an informative way. These models use transformer\n",
+ "architectures that process text through attention mechanisms, allowing them to capture long-range\n",
+ "dependencies in language. The implications for industries ranging from healthcare to education\n",
+ "are profound, as these models can assist with everything from medical diagnosis to personalized\n",
+ "tutoring. However, challenges remain around hallucination, bias, and the environmental cost of\n",
+ "training increasingly large models.\n",
+ "\"\"\"\n",
+ "\n",
+ "messages = [\n",
+ " {\n",
+ " \"role\": \"user\",\n",
+ " \"content\": f\"Summarize the following text in 2-3 sentences:\\n\\n{article}\",\n",
+ " },\n",
+ "]\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=MODEL_NAME, messages=messages, temperature=0.1\n",
+ ")\n",
+ "\n",
+ "print(textwrap.fill(response.choices[0].message.content, 120))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-28",
+ "metadata": {},
+ "source": [
+ "## Data Labelling"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-29",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "CLASSIFY_TEXT_PROMPT = \"\"\"\n",
+ "Your task is to analyze the following text and classify it based on multiple criteria.\n",
+ "Provide your analysis as a JSON object. Use only the specified categories:\n",
+ "\n",
+ "1. Target audience:\n",
+ " ['General public', 'Professionals', 'Academics', 'Students']\n",
+ "\n",
+ "2. Tone:\n",
+ " ['Neutral', 'Positive', 'Negative', 'Formal', 'Informal', 'Humorous']\n",
+ "\n",
+ "3. Complexity level:\n",
+ " ['Elementary', 'Intermediate', 'Advanced', 'Technical']\n",
+ "\n",
+ "4. Main topic:\n",
+ " ['Technology', 'Science', 'Health', 'Business', 'Education', 'Entertainment']\n",
+ "\n",
+ "For each classification, choose the most appropriate category.\n",
+ "\n",
+ "\n",
+ "{text}\n",
+ "\n",
+ "\n",
+ "Respond with JSON: {{\"target_audience\": \"...\", \"tone\": \"...\", \"complexity\": \"...\", \"topic\": \"...\"}}\n",
+ "\"\"\"\n",
+ "\n",
+ "texts = [\n",
+ " \"The new iPhone 16 features an improved camera system and A18 chip.\",\n",
+ " \"Deep reinforcement learning combines neural networks with reward-based training.\",\n",
+ " \"Five easy stretches you can do at your desk to reduce back pain!\",\n",
+ "]\n",
+ "\n",
+ "results = []\n",
+ "for text in texts:\n",
+ " response = client.chat.completions.create(\n",
+ " model=MODEL_NAME,\n",
+ " messages=[{\"role\": \"user\", \"content\": CLASSIFY_TEXT_PROMPT.format(text=text)}],\n",
+ " response_format={\"type\": \"json_object\"},\n",
+ " temperature=0.1,\n",
+ " )\n",
+ " result = json.loads(response.choices[0].message.content)\n",
+ " result[\"text\"] = text[:60]\n",
+ " results.append(result)\n",
+ "\n",
+ "for r in results:\n",
+ " print(json.dumps(r, indent=2))\n",
+ " print()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "cell-30",
+ "metadata": {},
+ "source": [
+ "## Using the Highspeed Model\n",
+ "\n",
+ "MiniMax also offers `MiniMax-M2.7-highspeed` for faster inference at a lower cost."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "cell-31",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%%time\n",
+ "\n",
+ "response = client.chat.completions.create(\n",
+ " model=\"MiniMax-M2.7-highspeed\",\n",
+ " messages=[{\"role\": \"user\", \"content\": \"Explain what is Deep Learning in one sentence\"}],\n",
+ " temperature=0.1,\n",
+ ")\n",
+ "\n",
+ "print(textwrap.fill(response.choices[0].message.content, 120))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python",
+ "version": "3.13.0"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
\ No newline at end of file
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_minimax.py b/tests/test_minimax.py
new file mode 100644
index 0000000..ab86a93
--- /dev/null
+++ b/tests/test_minimax.py
@@ -0,0 +1,355 @@
+"""Unit tests for MiniMax M2.7 notebook code patterns."""
+
+import json
+import os
+import textwrap
+from typing import Literal
+from unittest.mock import MagicMock, patch
+
+import pytest
+from pydantic import BaseModel
+
+
+class SentimentClassification(BaseModel):
+ sentiment: Literal["negative", "neutral", "positive"]
+ reasoning: str
+
+
+# --- Unit Tests ---
+
+
+class TestMiniMaxClientSetup:
+ """Test MiniMax client configuration."""
+
+ def test_base_url_is_correct(self):
+ base_url = "https://api.minimax.io/v1"
+ assert base_url.startswith("https://")
+ assert "minimax.io" in base_url
+ assert base_url.endswith("/v1")
+
+ def test_model_names(self):
+ models = ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"]
+ for model in models:
+ assert model.startswith("MiniMax-")
+ assert "M2.7" in model
+
+ def test_temperature_must_be_positive(self):
+ valid_temps = [0.1, 0.5, 0.7, 1.0]
+ invalid_temps = [0.0, -0.1, -1.0]
+ for temp in valid_temps:
+ assert 0 < temp <= 1.0
+ for temp in invalid_temps:
+ assert not (0 < temp <= 1.0)
+
+
+class TestFormatResponse:
+ """Test the format_response helper function."""
+
+ def test_format_simple_text(self):
+ mock_response = MagicMock()
+ mock_response.choices = [MagicMock()]
+ mock_response.choices[0].message.content = "Hello world"
+
+ response_txt = mock_response.choices[0].message.content
+ text = ""
+ for chunk in response_txt.split("\n"):
+ text += "\n"
+ if not chunk:
+ continue
+ text += (
+ "\n".join(textwrap.wrap(chunk, 100, break_long_words=False))
+ ).strip()
+ result = text.strip()
+ assert result == "Hello world"
+
+ def test_format_multiline_text(self):
+ mock_response = MagicMock()
+ mock_response.choices[0].message.content = "Line 1\n\nLine 2\nLine 3"
+
+ response_txt = mock_response.choices[0].message.content
+ text = ""
+ for chunk in response_txt.split("\n"):
+ text += "\n"
+ if not chunk:
+ continue
+ text += (
+ "\n".join(textwrap.wrap(chunk, 100, break_long_words=False))
+ ).strip()
+ result = text.strip()
+ assert "Line 1" in result
+ assert "Line 2" in result
+ assert "Line 3" in result
+
+ def test_format_long_line_wraps(self):
+ long_text = "A " * 100 # 200 chars
+ mock_response = MagicMock()
+ mock_response.choices[0].message.content = long_text.strip()
+
+ response_txt = mock_response.choices[0].message.content
+ text = ""
+ for chunk in response_txt.split("\n"):
+ text += "\n"
+ if not chunk:
+ continue
+ text += (
+ "\n".join(textwrap.wrap(chunk, 100, break_long_words=False))
+ ).strip()
+ result = text.strip()
+ lines = result.split("\n")
+ assert len(lines) > 1
+
+
+class TestStructuredOutput:
+ """Test Pydantic model parsing for structured output."""
+
+ def test_valid_sentiment_positive(self):
+ json_str = '{"sentiment": "positive", "reasoning": "The user is happy"}'
+ result = SentimentClassification.model_validate_json(json_str)
+ assert result.sentiment == "positive"
+ assert result.reasoning == "The user is happy"
+
+ def test_valid_sentiment_negative(self):
+ json_str = '{"sentiment": "negative", "reasoning": "The user is sad"}'
+ result = SentimentClassification.model_validate_json(json_str)
+ assert result.sentiment == "negative"
+
+ def test_valid_sentiment_neutral(self):
+ json_str = '{"sentiment": "neutral", "reasoning": "The text is factual"}'
+ result = SentimentClassification.model_validate_json(json_str)
+ assert result.sentiment == "neutral"
+
+ def test_invalid_sentiment_raises(self):
+ json_str = '{"sentiment": "angry", "reasoning": "Invalid value"}'
+ with pytest.raises(Exception):
+ SentimentClassification.model_validate_json(json_str)
+
+ def test_missing_field_raises(self):
+ json_str = '{"sentiment": "positive"}'
+ with pytest.raises(Exception):
+ SentimentClassification.model_validate_json(json_str)
+
+
+class TestToolCalling:
+ """Test tool/function calling patterns."""
+
+ def test_get_weather_function(self):
+ def get_weather(city: str, unit: str = "celsius") -> str:
+ weather_data = {
+ "San Francisco": {"temp": 18, "condition": "Foggy"},
+ "New York": {"temp": 25, "condition": "Sunny"},
+ "London": {"temp": 15, "condition": "Rainy"},
+ "Tokyo": {"temp": 28, "condition": "Humid"},
+ }
+ data = weather_data.get(city, {"temp": 20, "condition": "Unknown"})
+ if unit == "fahrenheit":
+ data["temp"] = round(data["temp"] * 9 / 5 + 32)
+ data["unit"] = unit
+ data["city"] = city
+ return json.dumps(data)
+
+ result = json.loads(get_weather("Tokyo"))
+ assert result["city"] == "Tokyo"
+ assert result["temp"] == 28
+ assert result["condition"] == "Humid"
+ assert result["unit"] == "celsius"
+
+ def test_get_weather_fahrenheit(self):
+ def get_weather(city: str, unit: str = "celsius") -> str:
+ weather_data = {
+ "San Francisco": {"temp": 18, "condition": "Foggy"},
+ "New York": {"temp": 25, "condition": "Sunny"},
+ }
+ data = weather_data.get(city, {"temp": 20, "condition": "Unknown"})
+ if unit == "fahrenheit":
+ data["temp"] = round(data["temp"] * 9 / 5 + 32)
+ data["unit"] = unit
+ data["city"] = city
+ return json.dumps(data)
+
+ result = json.loads(get_weather("New York", "fahrenheit"))
+ assert result["temp"] == 77 # 25 * 9/5 + 32
+ assert result["unit"] == "fahrenheit"
+
+ def test_get_weather_unknown_city(self):
+ def get_weather(city: str, unit: str = "celsius") -> str:
+ weather_data = {}
+ data = weather_data.get(city, {"temp": 20, "condition": "Unknown"})
+ data["unit"] = unit
+ data["city"] = city
+ return json.dumps(data)
+
+ result = json.loads(get_weather("UnknownCity"))
+ assert result["condition"] == "Unknown"
+ assert result["temp"] == 20
+
+ def test_tool_schema_format(self):
+ tool = {
+ "type": "function",
+ "function": {
+ "name": "get_weather",
+ "description": "Get the current weather for a city",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "city": {"type": "string", "description": "The city name"},
+ "unit": {
+ "type": "string",
+ "enum": ["celsius", "fahrenheit"],
+ },
+ },
+ "required": ["city"],
+ },
+ },
+ }
+ assert tool["type"] == "function"
+ assert tool["function"]["name"] == "get_weather"
+ assert "city" in tool["function"]["parameters"]["required"]
+
+ def test_tool_call_args_parsing(self):
+ args_json = '{"city": "Tokyo", "unit": "celsius"}'
+ args = json.loads(args_json)
+ assert args["city"] == "Tokyo"
+ assert args["unit"] == "celsius"
+
+
+class TestDataLabelling:
+ """Test data labelling prompt patterns."""
+
+ def test_classify_text_prompt_format(self):
+ template = """
+Your task is to analyze the following text and classify it.
+
+{text}
+
+"""
+ text = "The new iPhone features an improved camera."
+ prompt = template.format(text=text)
+ assert text in prompt
+ assert "" in prompt
+ assert "" in prompt
+
+ def test_json_response_parsing(self):
+ response_json = json.dumps(
+ {
+ "target_audience": "General public",
+ "tone": "Neutral",
+ "complexity": "Intermediate",
+ "topic": "Technology",
+ }
+ )
+ result = json.loads(response_json)
+ assert result["topic"] == "Technology"
+ assert result["tone"] == "Neutral"
+
+ def test_multiple_text_classification_results(self):
+ texts = [
+ "Tech news article",
+ "Scientific paper",
+ "Health blog post",
+ ]
+ results = []
+ for text in texts:
+ results.append({"text": text[:60], "topic": "test"})
+ assert len(results) == 3
+ assert all("text" in r for r in results)
+
+
+class TestLiteLLMIntegration:
+ """Test litellm integration patterns for MiniMax."""
+
+ def test_litellm_model_prefix(self):
+ model = "openai/MiniMax-M2.7"
+ assert model.startswith("openai/")
+ provider, model_name = model.split("/", 1)
+ assert provider == "openai"
+ assert model_name == "MiniMax-M2.7"
+
+ def test_litellm_api_base_config(self):
+ api_base = "https://api.minimax.io/v1"
+ assert api_base.startswith("https://")
+ assert "minimax" in api_base
+
+ def test_litellm_completion_kwargs(self):
+ kwargs = {
+ "model": "openai/MiniMax-M2.7",
+ "messages": [{"content": "test", "role": "user"}],
+ "api_base": "https://api.minimax.io/v1",
+ "temperature": 0.1,
+ }
+ assert kwargs["model"] == "openai/MiniMax-M2.7"
+ assert kwargs["api_base"] == "https://api.minimax.io/v1"
+ assert 0 < kwargs["temperature"] <= 1.0
+
+ def test_message_format(self):
+ messages = [
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": "Hello"},
+ ]
+ assert messages[0]["role"] == "system"
+ assert messages[1]["role"] == "user"
+ assert all("role" in m and "content" in m for m in messages)
+
+
+class TestNotebookStructure:
+ """Test that notebook files are valid JSON."""
+
+ def test_minimax_notebook_valid_json(self):
+ notebook_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), "minimax-m2.7.ipynb"
+ )
+ with open(notebook_path) as f:
+ nb = json.load(f)
+ assert nb["nbformat"] == 4
+ assert "cells" in nb
+ assert len(nb["cells"]) > 0
+
+ def test_minimax_notebook_has_required_sections(self):
+ notebook_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), "minimax-m2.7.ipynb"
+ )
+ with open(notebook_path) as f:
+ nb = json.load(f)
+
+ all_source = " ".join(
+ "".join(cell["source"]) for cell in nb["cells"]
+ )
+ assert "MiniMax-M2.7" in all_source
+ assert "api.minimax.io" in all_source
+ assert "MINIMAX_API_KEY" in all_source
+ assert "json_object" in all_source
+ assert "stream=True" in all_source
+ assert "tool_choice" in all_source
+
+ def test_minimax_notebook_cell_types(self):
+ notebook_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), "minimax-m2.7.ipynb"
+ )
+ with open(notebook_path) as f:
+ nb = json.load(f)
+
+ cell_types = [cell["cell_type"] for cell in nb["cells"]]
+ assert "code" in cell_types
+ assert "markdown" in cell_types
+
+ def test_litellm_notebook_has_minimax(self):
+ notebook_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)),
+ "26.multiple-llm-providers-with-litellm.ipynb",
+ )
+ with open(notebook_path) as f:
+ nb = json.load(f)
+
+ all_source = " ".join(
+ "".join(cell["source"]) for cell in nb["cells"]
+ )
+ assert "MiniMax-M2.7" in all_source
+ assert "minimax.io" in all_source
+
+ def test_readme_mentions_minimax(self):
+ readme_path = os.path.join(
+ os.path.dirname(os.path.dirname(__file__)), "README.md"
+ )
+ with open(readme_path) as f:
+ content = f.read()
+ assert "MiniMax" in content
+ assert "minimax-m2.7.ipynb" in content
diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py
new file mode 100644
index 0000000..2230232
--- /dev/null
+++ b/tests/test_minimax_integration.py
@@ -0,0 +1,107 @@
+"""Integration tests for MiniMax M2.7 API.
+
+These tests require a valid MINIMAX_API_KEY environment variable.
+They are skipped automatically if the key is not set.
+"""
+
+import json
+import os
+import textwrap
+
+import pytest
+
+pytestmark = pytest.mark.skipif(
+ not os.environ.get("MINIMAX_API_KEY"),
+ reason="MINIMAX_API_KEY not set",
+)
+
+
+@pytest.fixture
+def client():
+ from openai import OpenAI
+
+ return OpenAI(
+ api_key=os.environ["MINIMAX_API_KEY"],
+ base_url="https://api.minimax.io/v1",
+ )
+
+
+class TestMiniMaxCompletion:
+ """Integration tests for MiniMax chat completions."""
+
+ def test_basic_completion(self, client):
+ response = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[{"role": "user", "content": "Say 'hello' and nothing else."}],
+ temperature=0.1,
+ )
+ assert response.choices[0].message.content is not None
+ assert len(response.choices[0].message.content) > 0
+
+ def test_completion_with_system_prompt(self, client):
+ response = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[
+ {"role": "system", "content": "You always respond in exactly one word."},
+ {"role": "user", "content": "What color is the sky?"},
+ ],
+ temperature=0.1,
+ )
+ assert response.choices[0].message.content is not None
+
+ def test_json_mode(self, client):
+ response = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[
+ {
+ "role": "user",
+ "content": 'Return a JSON object with key "answer" and value 42. Output only JSON, no explanation.',
+ }
+ ],
+ response_format={"type": "json_object"},
+ temperature=0.1,
+ )
+ content = response.choices[0].message.content
+ # Strip thinking tags if present
+ import re
+ content = re.sub(r".*?", "", content, flags=re.DOTALL).strip()
+ # Extract JSON from markdown code block if present
+ json_match = re.search(r"```(?:json)?\s*(.*?)\s*```", content, re.DOTALL)
+ if json_match:
+ content = json_match.group(1)
+ result = json.loads(content)
+ assert "answer" in result
+ assert result["answer"] == 42
+
+ def test_streaming(self, client):
+ stream = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[{"role": "user", "content": "Count from 1 to 3."}],
+ temperature=0.1,
+ stream=True,
+ )
+ chunks = []
+ for chunk in stream:
+ if chunk.choices[0].delta.content is not None:
+ chunks.append(chunk.choices[0].delta.content)
+ full_text = "".join(chunks)
+ assert len(full_text) > 0
+
+ def test_highspeed_model(self, client):
+ response = client.chat.completions.create(
+ model="MiniMax-M2.7-highspeed",
+ messages=[{"role": "user", "content": "Say 'hi' and nothing else."}],
+ temperature=0.1,
+ )
+ assert response.choices[0].message.content is not None
+
+ def test_usage_tracking(self, client):
+ response = client.chat.completions.create(
+ model="MiniMax-M2.7",
+ messages=[{"role": "user", "content": "Hello"}],
+ temperature=0.1,
+ )
+ assert response.usage is not None
+ assert response.usage.prompt_tokens > 0
+ assert response.usage.completion_tokens > 0
+ assert response.usage.total_tokens > 0