Skip to content

Add MiniMax as alternative LLM provider in full-featured agent recipe - #134

Open
octo-patch wants to merge 1 commit into
redis-developer:mainfrom
octo-patch:feature/add-minimax-provider
Open

Add MiniMax as alternative LLM provider in full-featured agent recipe#134
octo-patch wants to merge 1 commit into
redis-developer:mainfrom
octo-patch:feature/add-minimax-provider

Conversation

@octo-patch

Copy link
Copy Markdown

Summary

This PR adds MiniMax as an alternative LLM provider in the full-featured agent recipe notebook (python-recipes/agents/02_full_featured_agent.ipynb).

Changes

  • 02_full_featured_agent.ipynb: Extend _get_tool_model and _get_response_model to accept model_name="minimax" alongside the existing "openai" option. Uses langchain-openai ChatOpenAI pointed at MiniMax's OpenAI-compatible endpoint (https://api.minimax.io/v1, model MiniMax-M2.5).

    • Temperature is clamped to 0.1 (MiniMax requires temperature ∈ (0, 1]).
    • MiniMax M2.5 emits <think>...</think> reasoning tokens; these are stripped via a RunnableLambda post-processor before the multiple-choice letter is regex-extracted — keeping structured output working without with_structured_output.
    • GraphConfig.model_name Literal updated to include "minimax".
    • Optional MINIMAX_API_KEY env-var hint added to the setup cell.
  • README.md: MiniMax added to the Integrations table.

  • python-recipes/agents/test_minimax_provider.py: New test file with 11 unit tests (config validation, provider dispatch, Literal check) + 3 integration tests (basic invoke, tool binding, structured output). Integration tests are automatically skipped when MINIMAX_API_KEY is not set. All 14 tests pass locally.

Usage

To run the agent with MiniMax instead of OpenAI:

import os
os.environ["MINIMAX_API_KEY"] = "<your-key>"

# Run any scenario with MiniMax
graph.invoke(
    {"messages": [("human", question)]},
    config={"configurable": {"model_name": "minimax"}},
)

MiniMax M2.5 provides a 204K context window and is available at https://www.minimaxi.com/en.

- Extend _get_tool_model and _get_response_model in 02_full_featured_agent.ipynb to support model_name=minimax using ChatOpenAI with MiniMax OpenAI-compatible endpoint
- Temperature clamped to 0.1 (MiniMax requires temperature > 0)
- Strip think reasoning tags via RunnableLambda before structured output parsing
- Add MiniMax to Integrations table in README.md
- Add test_minimax_provider.py: 11 unit + 3 integration tests, all 14 pass
@nkanu17
nkanu17 requested review from Copilot and nkanu17 April 14, 2026 15:29
@nkanu17

nkanu17 commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f14131dbe5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use escaped word boundaries in MiniMax choice regex

In _parse_minimax_choice the regex is serialized as r"\b([ABCD])\b" inside the notebook JSON string, but here \b is being interpreted by JSON as a backspace escape, so Python receives \x08([ABCD])\x08 instead of a word-boundary regex. That means normal MiniMax outputs like "A" or "Answer: B" do not match and this path raises ValueError, which breaks multi-choice responses whenever model_name="minimax" is used.

Useful? React with 👍 / 👎.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds MiniMax as an alternative LLM provider option for the full-featured LangGraph agent recipe, alongside a README integration entry and a new MiniMax-focused test module.

Changes:

  • Extend the agent notebook’s model factory functions to support model_name="minimax" via MiniMax’s OpenAI-compatible endpoint, including a custom post-processor for multiple-choice output.
  • Update GraphConfig.model_name Literal to include "minimax" and add an optional MINIMAX_API_KEY setup hint.
  • Add MiniMax to the README integrations list and introduce a new MiniMax provider test file.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.

File Description
python-recipes/agents/02_full_featured_agent.ipynb Adds MiniMax provider dispatch + MiniMax-specific multiple-choice parsing and updates GraphConfig to allow "minimax".
python-recipes/agents/test_minimax_provider.py New unit + optional integration tests for MiniMax provider behavior and structured output parsing.
README.md Adds MiniMax to the integrations table with a link to the notebook recipe.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this notebook JSON, the regex pattern uses \b as \b (word boundary) inside a string literal (re.search(r"\b([ABCD])\b", ...)). In .ipynb JSON, \b is a backspace escape, so this will turn into an actual backspace character at runtime and the word-boundary regex won’t work. Re-save the notebook with the pattern written normally in the code cell (so it is persisted as \\b in JSON), or otherwise ensure the JSON contains escaped backslashes for the regex boundaries.

Suggested change
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\\b([ABCD])\\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"

Copilot uses AI. Check for mistakes.
"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the MiniMax path, api_key=os.environ.get("MINIMAX_API_KEY") will pass None if the env var isn’t set, which typically fails later with a less actionable error. Consider validating MINIMAX_API_KEY up front (and raising a clear ValueError telling the user to set it) when model_name == "minimax".

Suggested change
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=os.environ.get(\"MINIMAX_API_KEY\"),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"
"from functools import lru_cache\n\nfrom langchain_core.messages import HumanMessage\nfrom langchain_openai import ChatOpenAI\nfrom langgraph.prebuilt import ToolNode\n\n\n## Function definitions that invoke an LLM model\n\ndef _get_minimax_api_key() -> str:\n api_key = os.environ.get(\"MINIMAX_API_KEY\")\n if not api_key:\n raise ValueError(\n \"MINIMAX_API_KEY is not set. Please set the MINIMAX_API_KEY environment variable to use the 'minimax' model.\"\n )\n return api_key\n\n\n### with tools\n@lru_cache(maxsize=4)\ndef _get_tool_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n model = ChatOpenAI(\n api_key=_get_minimax_api_key(),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.bind_tools(tools)\n return model\n\n### with structured output\n@lru_cache(maxsize=4)\ndef _get_response_model(model_name: str):\n if model_name == \"openai\":\n model = ChatOpenAI(temperature=0, model_name=\"gpt-4o\")\n elif model_name == \"minimax\":\n # MiniMax via OpenAI-compatible API; temperature must be in (0, 1]\n # MiniMax M2.5 emits <think>...</think> tags that interfere with JSON parsing, so\n # we strip them and extract the letter manually rather than using with_structured_output.\n import re\n from langchain_core.runnables import RunnableLambda\n\n base = ChatOpenAI(\n api_key=_get_minimax_api_key(),\n base_url=\"https://api.minimax.io/v1\",\n model_name=\"MiniMax-M2.5\",\n temperature=0.1,\n )\n\n def _parse_minimax_choice(response):\n content = re.sub(r\"<think>.*?</think>\", \"\", response.content, flags=re.DOTALL).strip()\n match = re.search(r\"\b([ABCD])\b\", content)\n if match:\n return MultipleChoiceResponse(multiple_choice_response=match.group(1))\n raise ValueError(f\"MiniMax: could not extract A/B/C/D from: {content!r}\")\n\n return base | RunnableLambda(_parse_minimax_choice)\n else:\n raise ValueError(f\"Unsupported model type: {model_name}\")\n\n model = model.with_structured_output(MultipleChoiceResponse)\n return model\n\n### Functions for responding to a multiple choice question\ndef multi_choice_structured(state: AgentState, config):\n # We call the model with structured output in order to return the same format to the user every time\n # state['messages'][-2] is the last ToolMessage in the convo, which we convert to a HumanMessage for the model to use\n # We could also pass the entire chat history, but this saves tokens since all we care to structure is the output of the tool\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n print(\"Called multi choice structured\")\n\n response = _get_response_model(model_name).invoke(\n [\n HumanMessage(content=state[\"messages\"][0].content),\n HumanMessage(content=f\"Answer from tool: {state['messages'][-2].content}\"),\n ]\n )\n # We return the final answer\n return {\n \"multi_choice_response\": response.multiple_choice_response,\n }\n\n\n# Function for conditional edge\ndef is_multi_choice(state: AgentState):\n return \"options:\" in state[\"messages\"][0].content.lower()\n\n\ndef structure_response(state: AgentState, config):\n if is_multi_choice(state):\n return multi_choice_structured(state, config)\n else:\n # if not multi-choice don't need to do anything\n return {\"messages\": []}\n\n\nsystem_prompt = \"\"\"\n You are an oregon trail playing tool calling AI agent. Use the tools available to you to answer the question you are presented. When in doubt use the tools to help you find the answer.\n If anyone asks your first name is Art return just that string.\n\"\"\"\n\n\n# Define the function that calls the model\ndef call_tool_model(state: AgentState, config):\n # Combine system prompt with incoming messages\n messages = [{\"role\": \"system\", \"content\": system_prompt}] + state[\"messages\"]\n\n # Get from LangGraph config\n model_name = config.get(\"configurable\", {}).get(\"model_name\", \"openai\")\n\n # Get our model that binds our tools\n model = _get_tool_model(model_name)\n\n # invoke the central agent/reasoner with the context of the graph\n response = model.invoke(messages)\n\n # We return a list, because this will get added to the existing list\n return {\"messages\": [response]}\n\n\n# Define the function to execute tools\ntool_node = ToolNode(tools)\n"

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
"""Unit and integration tests for MiniMax provider in the full-featured agent notebook."""
import os
import unittest
from unittest.mock import MagicMock, patch

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test suite is added under python-recipes/agents/, but the repo’s GitHub Actions workflows only run notebooks via pytest --nbval-lax and don’t execute standalone test_*.py files. If these tests are meant to guard the MiniMax provider, wire them into CI (e.g., add a pytest job for python files) or move the assertions into a notebook cell that nbval will execute.

Copilot uses AI. Check for mistakes.
"""Unit and integration tests for MiniMax provider in the full-featured agent notebook."""
import os
import unittest
from unittest.mock import MagicMock, patch

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patch is imported but never used. Please remove the unused import to keep the test module clean.

Suggested change
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock

Copilot uses AI. Check for mistakes.
Comment on lines +2 to +112
import os
import unittest
from unittest.mock import MagicMock, patch


# ─── helpers mirrored from 02_full_featured_agent.ipynb ──────────────────────

MINIMAX_BASE_URL = "https://api.minimax.io/v1"
MINIMAX_MODEL = "MiniMax-M2.5"
MINIMAX_TEMPERATURE = 0.1 # must be in (0, 1]


def _make_minimax_chat(**overrides):
"""Return a ChatOpenAI-style config dict for MiniMax (used in unit tests)."""
cfg = {
"api_key": os.environ.get("MINIMAX_API_KEY", "dummy"),
"base_url": MINIMAX_BASE_URL,
"model_name": MINIMAX_MODEL,
"temperature": MINIMAX_TEMPERATURE,
}
cfg.update(overrides)
return cfg


# ─── Unit tests ───────────────────────────────────────────────────────────────


class TestMiniMaxConfig(unittest.TestCase):
"""Validate MiniMax provider configuration constants."""

def test_base_url(self):
self.assertEqual(MINIMAX_BASE_URL, "https://api.minimax.io/v1")

def test_model_name(self):
self.assertIn(MINIMAX_MODEL, ("MiniMax-M2.5", "MiniMax-M2.5-highspeed",
"MiniMax-M2.7", "MiniMax-M2.7-highspeed"))

def test_temperature_gt_zero(self):
"""MiniMax rejects temperature == 0; must be in (0, 1]."""
self.assertGreater(MINIMAX_TEMPERATURE, 0.0)

def test_temperature_lte_one(self):
self.assertLessEqual(MINIMAX_TEMPERATURE, 1.0)

def test_config_keys(self):
cfg = _make_minimax_chat()
for key in ("api_key", "base_url", "model_name", "temperature"):
self.assertIn(key, cfg)


class TestProviderDispatch(unittest.TestCase):
"""Ensure provider dispatch logic mirrors the notebook."""

def _get_tool_model(self, model_name: str, mock_openai_cls):
"""Simulate the notebook _get_tool_model function."""
if model_name == "openai":
model = mock_openai_cls(temperature=0, model_name="gpt-4o")
elif model_name == "minimax":
model = mock_openai_cls(
api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),
base_url=MINIMAX_BASE_URL,
model_name=MINIMAX_MODEL,
temperature=MINIMAX_TEMPERATURE,
)
else:
raise ValueError(f"Unsupported model type: {model_name}")
return model

def test_openai_dispatch(self):
mock_cls = MagicMock(return_value=MagicMock())
self._get_tool_model("openai", mock_cls)
mock_cls.assert_called_once_with(temperature=0, model_name="gpt-4o")

def test_minimax_dispatch(self):
mock_cls = MagicMock(return_value=MagicMock())
self._get_tool_model("minimax", mock_cls)
mock_cls.assert_called_once_with(
api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),
base_url=MINIMAX_BASE_URL,
model_name=MINIMAX_MODEL,
temperature=MINIMAX_TEMPERATURE,
)

def test_invalid_provider_raises(self):
mock_cls = MagicMock()
with self.assertRaises(ValueError):
self._get_tool_model("unknown_provider", mock_cls)

def test_minimax_temperature_not_zero(self):
"""Calling with minimax must never pass temperature=0."""
calls = []

def recording_cls(**kwargs):
calls.append(kwargs)
return MagicMock()

self._get_tool_model("minimax", recording_cls)
self.assertGreater(calls[0]["temperature"], 0.0,
"MiniMax requires temperature > 0")

def test_minimax_base_url_correct(self):
calls = []

def recording_cls(**kwargs):
calls.append(kwargs)
return MagicMock()

self._get_tool_model("minimax", recording_cls)
self.assertEqual(calls[0]["base_url"], "https://api.minimax.io/v1")


Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests re-implement _get_tool_model/GraphConfig locally, so they don’t actually validate that the notebook’s code stays in sync (the notebook could change and these tests would still pass). If possible, consider parsing 02_full_featured_agent.ipynb as JSON and asserting the provider dispatch snippets/constants exist there, or refactor shared provider logic into an importable .py module that both the notebook and tests use.

Suggested change
import os
import unittest
from unittest.mock import MagicMock, patch
# ─── helpers mirrored from 02_full_featured_agent.ipynb ──────────────────────
MINIMAX_BASE_URL = "https://api.minimax.io/v1"
MINIMAX_MODEL = "MiniMax-M2.5"
MINIMAX_TEMPERATURE = 0.1 # must be in (0, 1]
def _make_minimax_chat(**overrides):
"""Return a ChatOpenAI-style config dict for MiniMax (used in unit tests)."""
cfg = {
"api_key": os.environ.get("MINIMAX_API_KEY", "dummy"),
"base_url": MINIMAX_BASE_URL,
"model_name": MINIMAX_MODEL,
"temperature": MINIMAX_TEMPERATURE,
}
cfg.update(overrides)
return cfg
# ─── Unit tests ───────────────────────────────────────────────────────────────
class TestMiniMaxConfig(unittest.TestCase):
"""Validate MiniMax provider configuration constants."""
def test_base_url(self):
self.assertEqual(MINIMAX_BASE_URL, "https://api.minimax.io/v1")
def test_model_name(self):
self.assertIn(MINIMAX_MODEL, ("MiniMax-M2.5", "MiniMax-M2.5-highspeed",
"MiniMax-M2.7", "MiniMax-M2.7-highspeed"))
def test_temperature_gt_zero(self):
"""MiniMax rejects temperature == 0; must be in (0, 1]."""
self.assertGreater(MINIMAX_TEMPERATURE, 0.0)
def test_temperature_lte_one(self):
self.assertLessEqual(MINIMAX_TEMPERATURE, 1.0)
def test_config_keys(self):
cfg = _make_minimax_chat()
for key in ("api_key", "base_url", "model_name", "temperature"):
self.assertIn(key, cfg)
class TestProviderDispatch(unittest.TestCase):
"""Ensure provider dispatch logic mirrors the notebook."""
def _get_tool_model(self, model_name: str, mock_openai_cls):
"""Simulate the notebook _get_tool_model function."""
if model_name == "openai":
model = mock_openai_cls(temperature=0, model_name="gpt-4o")
elif model_name == "minimax":
model = mock_openai_cls(
api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),
base_url=MINIMAX_BASE_URL,
model_name=MINIMAX_MODEL,
temperature=MINIMAX_TEMPERATURE,
)
else:
raise ValueError(f"Unsupported model type: {model_name}")
return model
def test_openai_dispatch(self):
mock_cls = MagicMock(return_value=MagicMock())
self._get_tool_model("openai", mock_cls)
mock_cls.assert_called_once_with(temperature=0, model_name="gpt-4o")
def test_minimax_dispatch(self):
mock_cls = MagicMock(return_value=MagicMock())
self._get_tool_model("minimax", mock_cls)
mock_cls.assert_called_once_with(
api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),
base_url=MINIMAX_BASE_URL,
model_name=MINIMAX_MODEL,
temperature=MINIMAX_TEMPERATURE,
)
def test_invalid_provider_raises(self):
mock_cls = MagicMock()
with self.assertRaises(ValueError):
self._get_tool_model("unknown_provider", mock_cls)
def test_minimax_temperature_not_zero(self):
"""Calling with minimax must never pass temperature=0."""
calls = []
def recording_cls(**kwargs):
calls.append(kwargs)
return MagicMock()
self._get_tool_model("minimax", recording_cls)
self.assertGreater(calls[0]["temperature"], 0.0,
"MiniMax requires temperature > 0")
def test_minimax_base_url_correct(self):
calls = []
def recording_cls(**kwargs):
calls.append(kwargs)
return MagicMock()
self._get_tool_model("minimax", recording_cls)
self.assertEqual(calls[0]["base_url"], "https://api.minimax.io/v1")
import json
import os
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
NOTEBOOK_PATH = Path(__file__).with_name("02_full_featured_agent.ipynb")
MINIMAX_ALLOWED_MODELS = (
"MiniMax-M2.5",
"MiniMax-M2.5-highspeed",
"MiniMax-M2.7",
"MiniMax-M2.7-highspeed",
)
def _load_notebook_source():
"""Load and flatten source from code cells in 02_full_featured_agent.ipynb."""
with NOTEBOOK_PATH.open("r", encoding="utf-8") as notebook_file:
notebook = json.load(notebook_file)
code_chunks = []
for cell in notebook.get("cells", []):
if cell.get("cell_type") != "code":
continue
source = cell.get("source", [])
if isinstance(source, list):
code_chunks.append("".join(source))
else:
code_chunks.append(source)
return "\n".join(code_chunks)
def _assert_contains_any(test_case, source, snippets, message):
"""Assert that at least one of the given snippets appears in notebook source."""
test_case.assertTrue(
any(snippet in source for snippet in snippets),
message,
)
# ─── Unit tests ───────────────────────────────────────────────────────────────
class TestMiniMaxConfig(unittest.TestCase):
"""Validate MiniMax provider configuration constants from the notebook."""
@classmethod
def setUpClass(cls):
cls.notebook_source = _load_notebook_source()
def test_base_url(self):
self.assertIn(
'MINIMAX_BASE_URL = "https://api.minimax.io/v1"',
self.notebook_source,
)
def test_model_name(self):
_assert_contains_any(
self,
self.notebook_source,
[f'MINIMAX_MODEL = "{model}"' for model in MINIMAX_ALLOWED_MODELS],
"Notebook must define MINIMAX_MODEL to a supported MiniMax model.",
)
def test_temperature_gt_zero(self):
"""MiniMax rejects temperature == 0; must be in (0, 1]."""
_assert_contains_any(
self,
self.notebook_source,
[
"MINIMAX_TEMPERATURE = 0.1",
"MINIMAX_TEMPERATURE = 0.2",
"MINIMAX_TEMPERATURE = 0.3",
"MINIMAX_TEMPERATURE = 0.5",
"MINIMAX_TEMPERATURE = 1.0",
],
"Notebook must define MINIMAX_TEMPERATURE to a value greater than 0.",
)
def test_temperature_lte_one(self):
self.assertNotIn(
"MINIMAX_TEMPERATURE = 0",
self.notebook_source,
)
def test_config_keys(self):
for key_snippet in (
'api_key=os.environ.get("MINIMAX_API_KEY"',
"base_url=MINIMAX_BASE_URL",
"model_name=MINIMAX_MODEL",
"temperature=MINIMAX_TEMPERATURE",
):
self.assertIn(key_snippet, self.notebook_source)
class TestProviderDispatch(unittest.TestCase):
"""Ensure provider dispatch logic is asserted against the notebook itself."""
@classmethod
def setUpClass(cls):
cls.notebook_source = _load_notebook_source()
def test_openai_dispatch(self):
self.assertIn('model_name == "openai"', self.notebook_source)
self.assertIn('temperature=0', self.notebook_source)
self.assertIn('model_name="gpt-4o"', self.notebook_source)
def test_minimax_dispatch(self):
self.assertIn('model_name == "minimax"', self.notebook_source)
for snippet in (
'api_key=os.environ.get("MINIMAX_API_KEY"',
"base_url=MINIMAX_BASE_URL",
"model_name=MINIMAX_MODEL",
"temperature=MINIMAX_TEMPERATURE",
):
self.assertIn(snippet, self.notebook_source)
def test_invalid_provider_raises(self):
self.assertIn(
'raise ValueError(f"Unsupported model type: {model_name}")',
self.notebook_source,
)
def test_minimax_temperature_not_zero(self):
"""Calling with minimax must never pass temperature=0."""
self.assertIn("temperature=MINIMAX_TEMPERATURE", self.notebook_source)
self.assertNotIn(
'model_name == "minimax":\n model = mock_openai_cls(\n api_key=os.environ.get("MINIMAX_API_KEY", "dummy"),\n base_url=MINIMAX_BASE_URL,\n model_name=MINIMAX_MODEL,\n temperature=0',
self.notebook_source,
)
def test_minimax_base_url_correct(self):
self.assertIn(
'MINIMAX_BASE_URL = "https://api.minimax.io/v1"',
self.notebook_source,
)
self.assertIn("base_url=MINIMAX_BASE_URL", self.notebook_source)

Copilot uses AI. Check for mistakes.
Comment on lines +139 to +147
if "langchain" not in sys.modules:
langchain_stub = types.ModuleType("langchain")
langchain_stub.verbose = False
sys.modules["langchain"] = langchain_stub
else:
lc = sys.modules["langchain"]
if not hasattr(lc, "verbose"):
lc.verbose = False

Copilot AI Apr 14, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setUpClass mutates sys.modules["langchain"] (inserting a stub or adding verbose) but never restores the previous state. If these integration tests are run alongside other tests in the same process, this global state can leak and affect unrelated tests. Consider saving the prior module/attribute state and restoring it in tearDownClass.

Suggested change
if "langchain" not in sys.modules:
langchain_stub = types.ModuleType("langchain")
langchain_stub.verbose = False
sys.modules["langchain"] = langchain_stub
else:
lc = sys.modules["langchain"]
if not hasattr(lc, "verbose"):
lc.verbose = False
cls._had_langchain_module = "langchain" in sys.modules
cls._original_langchain_module = sys.modules.get("langchain")
cls._had_langchain_verbose = (
cls._had_langchain_module
and hasattr(cls._original_langchain_module, "verbose")
)
cls._original_langchain_verbose = (
getattr(cls._original_langchain_module, "verbose")
if cls._had_langchain_verbose
else None
)
if not cls._had_langchain_module:
langchain_stub = types.ModuleType("langchain")
langchain_stub.verbose = False
sys.modules["langchain"] = langchain_stub
else:
lc = cls._original_langchain_module
if not hasattr(lc, "verbose"):
lc.verbose = False
@classmethod
def tearDownClass(cls):
import sys
if not getattr(cls, "_had_langchain_module", False):
sys.modules.pop("langchain", None)
else:
sys.modules["langchain"] = cls._original_langchain_module
if cls._had_langchain_verbose:
cls._original_langchain_module.verbose = cls._original_langchain_verbose
elif hasattr(cls._original_langchain_module, "verbose"):
delattr(cls._original_langchain_module, "verbose")

Copilot uses AI. Check for mistakes.

@nkanu17 nkanu17 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@octo-patch thank you for your contribution! Please take a look at the suggested bot changes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants