Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ https://github.com/user-attachments/assets/33e0e7b4-9eb2-4a26-8274-f96c2c1c3a48
Experience a sleek, chat-based interface with built-in video playback and intuitive controls. It’s like having a personal assistant for your media.

### 🥣 A mixing bowl of your GenAI APIs
Connect seamlessly with powerful AI tools like LLMs, databases, and GenAI APIs, while VideoDB ensures your video infrastructure is reliable and scalable for cloud storage, indexing and streaming your content effortlessly.
Connect seamlessly with powerful AI tools like LLMs (OpenAI, Anthropic, Google Gemini, MiniMax), databases, and GenAI APIs, while VideoDB ensures your video infrastructure is reliable and scalable for cloud storage, indexing and streaming your content effortlessly.
![Integration-Updated](https://github.com/user-attachments/assets/d06e3b57-1135-4c3b-9f3a-d427d4142b42)

### 🧩 Customizable and Flexible
Expand Down
1 change: 1 addition & 0 deletions backend/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ SQLITE_DB_PATH=
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
GOOGLEAI_API_KEY=
MINIMAX_API_KEY=

# Tools
REPLICATE_API_TOKEN=
Expand Down
2 changes: 2 additions & 0 deletions backend/director/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class LLMType(str, Enum):
OPENAI = "openai"
ANTHROPIC = "anthropic"
GOOGLEAI = "googleai"
MINIMAX = "minimax"
VIDEODB_PROXY = "videodb_proxy"


Expand All @@ -29,5 +30,6 @@ class EnvPrefix(str, Enum):
OPENAI_ = "OPENAI_"
ANTHROPIC_ = "ANTHROPIC_"
GOOGLEAI_ = "GOOGLEAI_"
MINIMAX_ = "MINIMAX_"

DOWNLOADS_PATH="director/downloads"
4 changes: 4 additions & 0 deletions backend/director/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from director.llm.openai import OpenAI
from director.llm.anthropic import AnthropicAI
from director.llm.googleai import GoogleAI
from director.llm.minimax import MiniMax
from director.llm.videodb_proxy import VideoDBProxy


Expand All @@ -14,6 +15,7 @@ def get_default_llm():
openai = True if os.getenv("OPENAI_API_KEY") else False
anthropic = True if os.getenv("ANTHROPIC_API_KEY") else False
googleai = True if os.getenv("GOOGLEAI_API_KEY") else False
minimax = True if os.getenv("MINIMAX_API_KEY") else False

default_llm = os.getenv("DEFAULT_LLM")

Expand All @@ -23,5 +25,7 @@ def get_default_llm():
return AnthropicAI()
elif googleai or default_llm == LLMType.GOOGLEAI:
return GoogleAI()
elif minimax or default_llm == LLMType.MINIMAX:
return MiniMax()
else:
return VideoDBProxy()
200 changes: 200 additions & 0 deletions backend/director/llm/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import json
import re
from enum import Enum

from pydantic import Field, field_validator, FieldValidationInfo
from pydantic_settings import SettingsConfigDict


from director.llm.base import BaseLLM, BaseLLMConfig, LLMResponse, LLMResponseStatus
from director.constants import (
LLMType,
EnvPrefix,
)


class MiniMaxChatModel(str, Enum):
"""Enum for MiniMax Chat models"""

MINIMAX_M2_7 = "MiniMax-M2.7"
MINIMAX_M2_5 = "MiniMax-M2.5"
MINIMAX_M2_5_HIGHSPEED = "MiniMax-M2.5-highspeed"


class MiniMaxConfig(BaseLLMConfig):
"""MiniMax Config"""

model_config = SettingsConfigDict(
env_prefix=EnvPrefix.MINIMAX_,
extra="ignore",
)

llm_type: str = LLMType.MINIMAX
api_key: str = ""
api_base: str = "https://api.minimax.io/v1"
chat_model: str = Field(default=MiniMaxChatModel.MINIMAX_M2_7)
max_tokens: int = 4096
temperature: float = 0.9

@field_validator("api_key")
@classmethod
def validate_non_empty(cls, v, info: FieldValidationInfo):
if not v:
raise ValueError(
f"{info.field_name} must not be empty. Please set {EnvPrefix.MINIMAX_.value}{info.field_name.upper()} environment variable."
)
return v

@field_validator("temperature")
@classmethod
def clamp_temperature(cls, v):
"""Clamp temperature to MiniMax's accepted range [0, 1]."""
return max(0.0, min(1.0, v))


class MiniMax(BaseLLM):
def __init__(self, config: MiniMaxConfig = None):
"""
:param config: MiniMax Config
"""
if config is None:
config = MiniMaxConfig()
super().__init__(config=config)
try:
import openai
except ImportError:
raise ImportError("Please install OpenAI python library.")

self.client = openai.OpenAI(
api_key=self.api_key, base_url=self.api_base
)

def _format_messages(self, messages: list):
"""Format the messages to the format that MiniMax expects via OpenAI-compatible API."""
formatted_messages = []

for message in messages:
if message["role"] == "assistant" and message.get("tool_calls"):
formatted_messages.append(
{
"role": message["role"],
"content": message["content"],
"tool_calls": [
{
"id": tool_call["id"],
"function": {
"name": tool_call["tool"]["name"],
"arguments": json.dumps(
tool_call["tool"]["arguments"]
),
},
"type": tool_call["type"],
}
for tool_call in message["tool_calls"]
],
}
)
else:
formatted_messages.append(message)

return formatted_messages

def _format_tools(self, tools: list):
"""Format the tools to the format that MiniMax expects.

**Example**::

[
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the weather in a given location",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "The city and state"
}
},
"required": ["location"]
}
}
}
]
"""
return [
{
"type": "function",
"function": {
"name": tool.get("name", ""),
"description": tool.get("description", ""),
"parameters": tool.get("parameters", {}),
},
}
for tool in tools
if tool.get("name")
]

@staticmethod
def _strip_think_tags(content: str) -> str:
"""Strip <think>...</think> tags from MiniMax M2.7 reasoning output."""
if not content:
return content
return re.sub(r"<think>.*?</think>\s*", "", content, flags=re.DOTALL).strip()

def chat_completions(
self, messages: list, tools: list = [], stop=None, response_format=None
):
"""Get chat completions using MiniMax.

MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1.
docs: https://platform.minimaxi.com/document/ChatCompletion%20v2
"""
params = {
"model": self.chat_model,
"messages": self._format_messages(messages),
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"timeout": self.timeout,
}

if tools:
params["tools"] = self._format_tools(tools)
params["tool_choice"] = "auto"

if response_format:
params["response_format"] = response_format
Comment on lines +147 to +169
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.

⚠️ Potential issue | 🟡 Minor

Don't silently ignore stop.

chat_completions(..., stop=...) accepts the parameter but never uses it, so MiniMax will ignore caller-supplied stop sequences.

🛑 Suggested fix
         if tools:
             params["tools"] = self._format_tools(tools)
             params["tool_choice"] = "auto"

+        if stop is not None:
+            params["stop"] = stop
+
         if response_format:
             params["response_format"] = response_format
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def chat_completions(
self, messages: list, tools: list = [], stop=None, response_format=None
):
"""Get chat completions using MiniMax.
MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1.
docs: https://platform.minimaxi.com/document/ChatCompletion%20v2
"""
params = {
"model": self.chat_model,
"messages": self._format_messages(messages),
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"timeout": self.timeout,
}
if tools:
params["tools"] = self._format_tools(tools)
params["tool_choice"] = "auto"
if response_format:
params["response_format"] = response_format
def chat_completions(
self, messages: list, tools: list = [], stop=None, response_format=None
):
"""Get chat completions using MiniMax.
MiniMax provides an OpenAI-compatible API at https://api.minimax.io/v1.
docs: https://platform.minimaxi.com/document/ChatCompletion%20v2
"""
params = {
"model": self.chat_model,
"messages": self._format_messages(messages),
"temperature": self.temperature,
"max_tokens": self.max_tokens,
"top_p": self.top_p,
"timeout": self.timeout,
}
if tools:
params["tools"] = self._format_tools(tools)
params["tool_choice"] = "auto"
if stop is not None:
params["stop"] = stop
if response_format:
params["response_format"] = response_format
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 148-148: Do not use mutable data structures for argument defaults

Replace with None; initialize within function

(B006)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/director/llm/minimax.py` around lines 147 - 169, The chat_completions
method currently ignores the stop parameter; update the method (chat_completions
in minimax.py) to include the caller-supplied stop sequences in the request
params when provided (e.g., set params["stop"] = stop or the appropriate MiniMax
key) so MiniMax receives and respects the stop sequences; add this check
alongside the existing response_format/tools handling to only add the stop entry
when stop is not None.


try:
response = self.client.chat.completions.create(**params)
except Exception as e:
print(f"Error: {e}")
return LLMResponse(content=f"Error: {e}")

content = response.choices[0].message.content or ""
content = self._strip_think_tags(content)

return LLMResponse(
content=content,
tool_calls=[
{
"id": tool_call.id,
"tool": {
"name": tool_call.function.name,
"arguments": json.loads(tool_call.function.arguments),
},
"type": tool_call.type,
}
for tool_call in response.choices[0].message.tool_calls
]
if response.choices[0].message.tool_calls
else [],
finish_reason=response.choices[0].finish_reason,
send_tokens=response.usage.prompt_tokens,
recv_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens,
status=LLMResponseStatus.SUCCESS,
Comment on lines +171 to +199
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.

⚠️ Potential issue | 🟠 Major

Keep tool-call parsing inside the guarded error path.

json.loads(tool_call.function.arguments) runs after the API-call try/except, so one malformed tool payload will raise out of chat_completions() instead of returning an LLMResponse. That turns a recoverable provider error into an agent crash.

🧰 Suggested fix
         try:
             response = self.client.chat.completions.create(**params)
+            content = self._strip_think_tags(response.choices[0].message.content or "")
+            tool_calls = [
+                {
+                    "id": tool_call.id,
+                    "tool": {
+                        "name": tool_call.function.name,
+                        "arguments": json.loads(tool_call.function.arguments),
+                    },
+                    "type": tool_call.type,
+                }
+                for tool_call in response.choices[0].message.tool_calls or []
+            ]
         except Exception as e:
             print(f"Error: {e}")
-            return LLMResponse(content=f"Error: {e}")
-
-        content = response.choices[0].message.content or ""
-        content = self._strip_think_tags(content)
+            return LLMResponse(content=f"Error: {e}", status=LLMResponseStatus.ERROR)

         return LLMResponse(
             content=content,
-            tool_calls=[
-                {
-                    "id": tool_call.id,
-                    "tool": {
-                        "name": tool_call.function.name,
-                        "arguments": json.loads(tool_call.function.arguments),
-                    },
-                    "type": tool_call.type,
-                }
-                for tool_call in response.choices[0].message.tool_calls
-            ]
-            if response.choices[0].message.tool_calls
-            else [],
+            tool_calls=tool_calls,
             finish_reason=response.choices[0].finish_reason,
             send_tokens=response.usage.prompt_tokens,
             recv_tokens=response.usage.completion_tokens,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
response = self.client.chat.completions.create(**params)
except Exception as e:
print(f"Error: {e}")
return LLMResponse(content=f"Error: {e}")
content = response.choices[0].message.content or ""
content = self._strip_think_tags(content)
return LLMResponse(
content=content,
tool_calls=[
{
"id": tool_call.id,
"tool": {
"name": tool_call.function.name,
"arguments": json.loads(tool_call.function.arguments),
},
"type": tool_call.type,
}
for tool_call in response.choices[0].message.tool_calls
]
if response.choices[0].message.tool_calls
else [],
finish_reason=response.choices[0].finish_reason,
send_tokens=response.usage.prompt_tokens,
recv_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens,
status=LLMResponseStatus.SUCCESS,
try:
response = self.client.chat.completions.create(**params)
content = self._strip_think_tags(response.choices[0].message.content or "")
tool_calls = [
{
"id": tool_call.id,
"tool": {
"name": tool_call.function.name,
"arguments": json.loads(tool_call.function.arguments),
},
"type": tool_call.type,
}
for tool_call in response.choices[0].message.tool_calls or []
]
except Exception as e:
print(f"Error: {e}")
return LLMResponse(content=f"Error: {e}", status=LLMResponseStatus.ERROR)
return LLMResponse(
content=content,
tool_calls=tool_calls,
finish_reason=response.choices[0].finish_reason,
send_tokens=response.usage.prompt_tokens,
recv_tokens=response.usage.completion_tokens,
total_tokens=response.usage.total_tokens,
status=LLMResponseStatus.SUCCESS,
🧰 Tools
🪛 Ruff (0.15.6)

[warning] 173-173: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/director/llm/minimax.py` around lines 171 - 199, The tool-call JSON
parsing must be moved into the API error-guard so malformed tool payloads don't
bubble up; wrap access to response.choices[0].message.tool_calls and the
json.loads(tool_call.function.arguments) parsing in the same try/except that
surrounds self.client.chat.completions.create (or add an inner try that catches
JSON/ValueError and returns an LLMResponse error), and ensure the method (the
block building the LLMResponse with tool_calls, finish_reason, usage fields)
returns a graceful LLMResponse on parse errors instead of letting exceptions
escape from response or tool_call.function.arguments.

)
Empty file added backend/tests/__init__.py
Empty file.
Loading