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
5 changes: 5 additions & 0 deletions backend/consts/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class ProviderEnum(str, Enum):
MODELENGINE = "modelengine"
DASHSCOPE = "dashscope"
TOKENPONY = "tokenpony"
MINIMAX = "minimax"


# Silicon Flow
Expand All @@ -24,5 +25,9 @@ class ProviderEnum(str, Enum):
TOKENPONY_BASE_URL = "https://api.tokenpony.cn/v1/"
TOKENPONY_GET_URL = "https://api.tokenpony.cn/v1/models"

# MiniMax
MINIMAX_BASE_URL = "https://api.minimax.io/v1/"
MINIMAX_GET_URL = "https://api.minimax.io/v1/models"

# ModelEngine
# Base URL and API key are loaded from environment variables at runtime
3 changes: 3 additions & 0 deletions backend/services/model_management_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
DASHSCOPE_BASE_URL,
DASHSCOPE_REALTIME_BASE_URL,
TOKENPONY_BASE_URL,
MINIMAX_BASE_URL,
)

from database.model_management_db import (
Expand Down Expand Up @@ -193,6 +194,8 @@ async def batch_create_models_for_tenant(user_id: str, tenant_id: str, batch_pay
model_url = DASHSCOPE_REALTIME_BASE_URL if model_type in ("stt", "tts") else DASHSCOPE_BASE_URL
elif provider == ProviderEnum.TOKENPONY.value:
model_url = TOKENPONY_BASE_URL
elif provider == ProviderEnum.MINIMAX.value:
model_url = MINIMAX_BASE_URL
else:
model_url = ""

Expand Down
4 changes: 4 additions & 0 deletions backend/services/model_provider_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from services.providers.tokenpony_provider import TokenPonyModelProvider
from services.providers.dashscope_provider import DashScopeModelProvider
from services.providers.modelengine_provider import ModelEngineProvider, get_model_engine_raw_url, MODEL_ENGINE_NORTH_PREFIX
from services.providers.minimax_provider import MiniMaxModelProvider
from utils.model_name_utils import split_repo_name, add_repo_to_name

logger = logging.getLogger("model_provider")
Expand Down Expand Up @@ -48,6 +49,9 @@ async def get_provider_models(model_data: dict) -> List[dict]:
elif model_data["provider"] == ProviderEnum.TOKENPONY.value:
provider = TokenPonyModelProvider()
model_list = await provider.get_models(model_data)
elif model_data["provider"] == ProviderEnum.MINIMAX.value:
provider = MiniMaxModelProvider()
model_list = await provider.get_models(model_data)

return model_list

Expand Down
2 changes: 2 additions & 0 deletions backend/services/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
from services.providers.base import AbstractModelProvider
from services.providers.silicon_provider import SiliconModelProvider
from services.providers.modelengine_provider import ModelEngineProvider, get_model_engine_raw_url
from services.providers.minimax_provider import MiniMaxModelProvider

__all__ = [
"AbstractModelProvider",
"SiliconModelProvider",
"ModelEngineProvider",
"MiniMaxModelProvider",
"get_model_engine_raw_url",
]
115 changes: 115 additions & 0 deletions backend/services/providers/minimax_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import httpx
from typing import Dict, List

from consts.const import DEFAULT_LLM_MAX_TOKENS
from consts.provider import MINIMAX_GET_URL
from services.providers.base import AbstractModelProvider, _classify_provider_error


# MiniMax known model context windows (tokens)
MINIMAX_MODEL_CONTEXT = {
"MiniMax-M3": 512000,
"MiniMax-M2.7": 192000,
"MiniMax-M2.7-highspeed": 192000,
}


class MiniMaxModelProvider(AbstractModelProvider):
"""Concrete implementation for MiniMax provider."""

async def get_models(self, provider_config: Dict) -> List[Dict]:

Check failure on line 20 in backend/services/providers/minimax_provider.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ6grUle4GsbWVQpNR3C&open=AZ6grUle4GsbWVQpNR3C&pullRequest=2717
"""
Fetch models from MiniMax API, categorize them based on model ID,
and return the requested model type.

Args:
provider_config: Configuration dict containing model_type and api_key

Returns:
List of models with canonical fields. Returns error dict if API call fails.
"""
try:
target_model_type: str = provider_config["model_type"]
model_api_key: str = provider_config["api_key"]
# Default SSL verification to True; can be disabled via provider config.
ssl_verify: bool = provider_config.get("ssl_verify", True)

headers = {"Authorization": f"Bearer {model_api_key}"}
url = MINIMAX_GET_URL

async with httpx.AsyncClient(verify=ssl_verify, timeout=15.0) as client:
response = await client.get(url, headers=headers)
response.raise_for_status()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

安全风险:verify=False 禁用了 SSL 证书验证,生产环境下容易遭受中间人攻击。建议从配置读取或默认为 True

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — fixed in c38ef2c. verify now comes from provider_config.get("ssl_verify", True), so it defaults to True and can only be disabled by explicit operator opt-in (matching the existing ssl_verify config flow elsewhere in the codebase).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

缺少 timeout 配置:httpx.AsyncClient(verify=False) 没有设置 timeout。如果 MiniMax API 响应缓慢或挂起,请求会无限等待。建议添加 timeout=15.0

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in c38ef2c — added timeout=15.0 to httpx.AsyncClient. Also added a regression test (test_get_models_timeout already covered ConnectTimeout, the new test_get_models_ssl_verify_defaults_to_true asserts the timeout kwarg is set).

all_models: List[Dict] = response.json().get("data", [])

# Initialize containers for the main categories
categorized_models = {
"chat": [], # Maps to "llm"
"vlm": [], # Maps to "vlm"
"embedding": [], # Maps to "embedding" / "multi_embedding"
"reranker": [], # Maps to "reranker"
"tts": [], # Maps to "tts"
"stt": [] # Maps to "stt"
}

for model_obj in all_models:
m_id = model_obj.get("id", "")
m_id_lower = m_id.lower()
model_obj.setdefault("object", "model")
model_obj.setdefault("owned_by", "minimax")

# Use known context window or default
max_tokens = MINIMAX_MODEL_CONTEXT.get(m_id, DEFAULT_LLM_MAX_TOKENS)

cleaned_model = {
"id": m_id,
"object": model_obj.get("object"),
"created": model_obj.get("created", 0),
"owned_by": model_obj.get("owned_by"),
"model_tag": "",
"model_type": "",
"max_tokens": max_tokens,
}

# 1. Embedding
if "embo" in m_id_lower or "embedding" in m_id_lower:
cleaned_model.update({"model_tag": "embedding", "model_type": "embedding"})
categorized_models["embedding"].append(cleaned_model)

# 2. TTS (speech models)
elif "speech" in m_id_lower or "tts" in m_id_lower:
cleaned_model.update({"model_tag": "tts", "model_type": "tts"})
categorized_models["tts"].append(cleaned_model)

# 3. STT
elif "stt" in m_id_lower or "whisper" in m_id_lower:
cleaned_model.update({"model_tag": "stt", "model_type": "stt"})
categorized_models["stt"].append(cleaned_model)

# 4. Reranker
elif "rerank" in m_id_lower:
cleaned_model.update({"model_tag": "reranker", "model_type": "reranker"})
categorized_models["reranker"].append(cleaned_model)

# 5. VLM
elif any(kw in m_id_lower for kw in ["-vl", "vl-", "vision"]):
cleaned_model.update({"model_tag": "chat", "model_type": "vlm"})
categorized_models["vlm"].append(cleaned_model)

# 6. Chat / LLM (default fallback)
else:
cleaned_model.update({"model_tag": "chat", "model_type": "llm"})
categorized_models["chat"].append(cleaned_model)

# Return the specific list based on the requested target_model_type
if target_model_type == "llm":
return categorized_models["chat"]
elif target_model_type in ("embedding", "multi_embedding"):
return categorized_models["embedding"]
elif target_model_type in categorized_models:
return categorized_models[target_model_type]
else:
return []

except (httpx.HTTPStatusError, httpx.ConnectTimeout, httpx.ConnectError, Exception) as e:
return _classify_provider_error("MiniMax", exception=e)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import { useSiliconModelList } from "@/hooks/model/useSiliconModelList";
import { useDashscopeModelList } from "@/hooks/model/useDashscopeModelList";
import { useTokenPonyModelList } from "@/hooks/model/useTokenponyModelList";
import { useMinimaxModelList } from "@/hooks/model/useMinimaxModelList";
import log from "@/lib/logger";
import {
ModelChunkSizeSlider,
Expand Down Expand Up @@ -325,6 +326,14 @@
setLoadingModelList,
tenantId,
});
const minimaxHook = useMinimaxModelList({
form,
setModelList,
setSelectedModelIds,
setShowModelList,
setLoadingModelList,
tenantId,
});
let getModelList;
let getProviderSelectedModalList;

Expand All @@ -335,6 +344,8 @@
({ getModelList, getProviderSelectedModalList } = dashscopeHook);
} else if (form.provider === "tokenpony") {
({ getModelList, getProviderSelectedModalList } = tokenponyHook);
} else if (form.provider === "minimax") {
({ getModelList, getProviderSelectedModalList } = minimaxHook);

Check warning on line 348 in frontend/app/[locale]/models/components/model/ModelAddDialog.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this useless assignment to variable "getProviderSelectedModalList".

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ6grUhM4GsbWVQpNR21&open=AZ6grUhM4GsbWVQpNR21&pullRequest=2717
}
// Reset form to default state
const resetForm = useCallback(() => {
Expand Down Expand Up @@ -1082,6 +1093,7 @@
<Option value="silicon">{t("model.provider.silicon")}</Option>
<Option value="dashscope">{t("model.provider.dashscope")}</Option>
<Option value="tokenpony">{t("model.provider.tokenpony")}</Option>
<Option value="minimax">{t("model.provider.minimax")}</Option>
</Select>
{/* ModelEngine URL input (only when provider is ModelEngine) */}
{form.provider === "modelengine" && (
Expand Down Expand Up @@ -1710,7 +1722,7 @@
.split("\n")
.map((line, index) => {
// Parse Markdown-style links: [text](url)
const markdownLinkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;

Check warning on line 1725 in frontend/app/[locale]/models/components/model/ModelAddDialog.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Simplify this regular expression to reduce its runtime, as it has super-linear performance due to backtracking.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ74EHCj4L5Jot4fsYRY&open=AZ74EHCj4L5Jot4fsYRY&pullRequest=2717
const parts: (string | { text: string; url: string })[] = [];
let lastIndex = 0;
let match;
Expand Down
13 changes: 9 additions & 4 deletions frontend/const/modelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const MODEL_SOURCES = {
DASHSCOPE: "dashscope",
TOKENPONY: "tokenpony",
VOLCENGINE: "volcengine",
MINIMAX: "minimax",
} as const;

// Model status constants
Expand All @@ -47,7 +48,8 @@ export const MODEL_PROVIDER_KEYS = [
"aliyuncs",
"tokenpony",
"dashscope",
"volcengine"
"volcengine",
"minimax",
] as const;

export type ModelProviderKey = (typeof MODEL_PROVIDER_KEYS)[number];
Expand All @@ -62,7 +64,8 @@ export const PROVIDER_HINTS: Record<ModelProviderKey, string> = {
aliyuncs: "aliyuncs",
tokenpony: "tokenpony",
dashscope: "dashscope",
volcengine:"bytedance"
volcengine: "bytedance",
minimax: "minimax",
};

// Icon filenames for providers
Expand All @@ -75,7 +78,8 @@ export const PROVIDER_ICON_MAP: Record<ModelProviderKey, string> = {
aliyuncs: "/aliyuncs.png",
dashscope:"/aliyuncs.png",
tokenpony: "/tokenpony.png",
volcengine: "/volcengine.png"
volcengine: "/volcengine.png",
minimax: "/minimax.png",
};

export const OFFICIAL_PROVIDER_ICON = "/modelengine-logo.png";
Expand All @@ -93,7 +97,8 @@ export const PROVIDER_LINKS: Record<string, string> = {
baai: "https://www.baai.ac.cn/",
dashscope: "https://dashscope.aliyun.com/",
tokenpony: "https://www.tokenpony.cn/",
volcengine:"https://www.volcengine.com/"
volcengine: "https://www.volcengine.com/",
minimax: "https://www.minimaxi.com/",
};

// User role constants
Expand Down
Loading