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
4 changes: 3 additions & 1 deletion src/pygpt_net/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ def run(**kwargs):
from pygpt_net.provider.llms.perplexity import PerplexityLLM
from pygpt_net.provider.llms.x_ai import xAILLM
from pygpt_net.provider.llms.open_router import OpenRouterLLM
from pygpt_net.provider.llms.minimax import MiniMaxLLM
from pygpt_net.provider.llms.litellm import LiteLLMProvider
from pygpt_net.provider.llms.forge import ForgeLLM
from pygpt_net.provider.llms.edenai import EdenAILLM
Expand Down Expand Up @@ -492,6 +493,7 @@ def run(**kwargs):
launcher.add_llm(PerplexityLLM())
launcher.add_llm(xAILLM())
launcher.add_llm(OpenRouterLLM())
launcher.add_llm(MiniMaxLLM())
launcher.add_llm(ForgeLLM())
launcher.add_llm(EdenAILLM())
launcher.add_llm(LiteLLMProvider())
Expand Down Expand Up @@ -581,4 +583,4 @@ def run(**kwargs):


if __name__ == '__main__':
run()
run()
3 changes: 2 additions & 1 deletion src/pygpt_net/controller/chat/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def check_api_key(
"perplexity": config.get('api_key_perplexity', None),
"deepseek_api": config.get('api_key_deepseek', None),
"mistral_ai": config.get('api_key_mistral', None),
"minimax": config.get('api_key_minimax', None),
}

if model.provider in provider_keys:
Expand Down Expand Up @@ -564,4 +565,4 @@ def show_response_tokens(self, ctx: CtxItem):
self.reset_counter()
self.window.update_status(
f"{trans('status.tokens')}: {short_num(ctx.input_tokens)} + {short_num(ctx.output_tokens)} = {short_num(ctx.total_tokens)}{stats}"
)
)
4 changes: 4 additions & 0 deletions src/pygpt_net/core/models/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,10 @@ def prepare_client_args(
args["api_key"] = cfg.get('api_key_open_router', "")
args["base_url"] = cfg.get('api_endpoint_open_router', "")
self.window.core.debug.info("[api] Using client: OpenRouter API")
elif model.provider == "minimax":
args["api_key"] = cfg.get('api_key_minimax', "")
args["base_url"] = cfg.get('api_endpoint_minimax', "")
self.window.core.debug.info("[api] Using client: MiniMax API")
elif model.provider == "forge":
args["api_key"] = cfg.get('api_key_forge', "") or os.environ.get("FORGE_API_KEY", "")
args["base_url"] = cfg.get('api_endpoint_forge', "") or os.environ.get(
Expand Down
1 change: 1 addition & 0 deletions src/pygpt_net/core/types/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"deepseek_api",
"x_ai",
"open_router",
"minimax",
"forge",
"edenai",
]
Expand Down
2 changes: 2 additions & 0 deletions src/pygpt_net/data/config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"api_endpoint_forge": "https://api.forge.tensorblock.co/v1",
"api_endpoint_google": "https://generativelanguage.googleapis.com/v1beta/openai",
"api_endpoint_hugging_face": "https://router.huggingface.co/v1",
"api_endpoint_minimax": "https://api.minimax.io/v1",
"api_endpoint_mistral": "https://api.mistral.ai/v1",
"api_endpoint_open_router": "https://openrouter.ai/api/v1",
"api_endpoint_perplexity": "https://api.perplexity.ai",
Expand All @@ -87,6 +88,7 @@
"api_key_forge": "",
"api_key_google": "",
"api_key_hugging_face": "",
"api_key_minimax": "",
"api_key_management_xai": "",
"api_key_mistral": "",
"api_key_open_router": "",
Expand Down
42 changes: 42 additions & 0 deletions src/pygpt_net/data/config/models.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,48 @@
"updated_at": "2026-08-19T00:00:00"
},
"items": {
"music-3.0": {
"id": "music-3.0",
"name": "music-3.0",
"mode": ["img"],
"ctx": 0,
"tokens": 0,
"default": false,
"input": ["text"],
"output": ["audio"],
"extra": {},
"imported": false,
"provider": "minimax",
"tool_calls": false
},
"music-2.6": {
"id": "music-2.6",
"name": "music-2.6",
"mode": ["img"],
"ctx": 0,
"tokens": 0,
"default": false,
"input": ["text"],
"output": ["audio"],
"extra": {},
"imported": false,
"provider": "minimax",
"tool_calls": false
},
"music-cover": {
"id": "music-cover",
"name": "music-cover",
"mode": ["img"],
"ctx": 0,
"tokens": 0,
"default": false,
"input": ["audio"],
"output": ["audio"],
"extra": {},
"imported": false,
"provider": "minimax",
"tool_calls": false
},
"SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M": {
"id": "SpeakLeash/bielik-11b-v2.3-instruct:Q4_K_M",
"name": "bielik-11b-v2.3",
Expand Down
33 changes: 33 additions & 0 deletions src/pygpt_net/data/config/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,39 @@
"advanced": false,
"tab": "HuggingFace"
},
"api_key_minimax": {
"section": "api_keys",
"type": "text",
"slider": false,
"label": "settings.api_key.minimax",
"description": "settings.api_key.minimax.desc",
"value": "",
"min": null,
"max": null,
"multiplier": null,
"step": null,
"extra": {"bold": true},
"urls": {"API Keys": "https://platform.minimax.io"},
"secret": true,
"persist": true,
"advanced": false,
"tab": "MiniMax"
},
"api_endpoint_minimax": {
"section": "api_keys",
"type": "text",
"slider": false,
"label": "settings.api_endpoint_minimax",
"description": "settings.api_endpoint_minimax.desc",
"value": "https://api.minimax.io/v1",
"min": null,
"max": null,
"multiplier": null,
"step": null,
"secret": false,
"advanced": false,
"tab": "MiniMax"
},
"api_key_deepseek": {
"section": "api_keys",
"type": "text",
Expand Down
6 changes: 5 additions & 1 deletion src/pygpt_net/provider/api/openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .tools import Tools
from .vision import Vision
from .video import Video
from .music import Music


class ApiOpenAI:
Expand All @@ -66,6 +67,7 @@ def __init__(self, window=None):
self.tools = Tools(window)
self.vision = Vision(window)
self.video = Video(window)
self.music = Music(window)
self.client = None
self.locked = False
self.last_client_args = None # last client args used, for debug purposes
Expand Down Expand Up @@ -203,6 +205,8 @@ def call(
context=context,
extra=extra,
)
elif media_mode == "music" and self.music.is_music_model(model.id if model else ""):
return self.music.generate(context=context, extra=extra)

# vision
elif mode == MODE_VISION:
Expand Down Expand Up @@ -385,4 +389,4 @@ def safe_close(self):
self.client = None
except Exception as e:
self.window.core.debug.log(e)
print("Error closing client:", e)
print("Error closing client:", e)
149 changes: 149 additions & 0 deletions src/pygpt_net/provider/api/openai/music.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""MiniMax music generation support for the media-generation mode."""

import base64
import binascii
import datetime
import os
from typing import Any, Dict, Optional

import requests
from PySide6.QtCore import QObject, QRunnable, Signal, Slot

from pygpt_net.core.bridge.context import BridgeContext
from pygpt_net.core.events import KernelEvent
from pygpt_net.item.ctx import CtxItem


class Music:
"""Generate music with MiniMax's regional music_generation endpoints."""

def __init__(self, window=None):
self.window = window
self.worker: Optional[MusicWorker] = None

@staticmethod
def is_music_model(model_id: str) -> bool:
return str(model_id or "").lower() in {"music-3.0", "music-2.6", "music-cover"}

def generate(self, context: BridgeContext, extra: Optional[Dict[str, Any]] = None,
sync: bool = True) -> bool:
extra = extra or {}
worker = MusicWorker()
worker.window = self.window
worker.ctx = context.ctx or CtxItem()
worker.model = context.model.id if context.model else "music-3.0"
worker.prompt = context.prompt or ""
worker.lyrics = extra.get("lyrics")
worker.output_format = str(extra.get("output_format") or extra.get("format") or "url").lower()
worker.audio_setting = extra.get("audio_setting") or {}
worker.audio_format = str(worker.audio_setting.get("format") or "mp3").lower()
worker.request_options = {
key: extra[key] for key in (
"stream", "lyrics_optimizer", "is_instrumental", "audio_url",
"audio_base64", "cover_feature_id", "aigc_watermark"
) if key in extra
}
worker.inline = bool(extra.get("inline", False))
self.worker = worker
worker.signals.finished.connect(self.window.core.video.handle_finished)
worker.signals.finished_inline.connect(self.window.core.video.handle_finished_inline)
worker.signals.status.connect(self.window.core.video.handle_status)
worker.signals.error.connect(self.window.core.video.handle_error)
if sync or not self.window.controller.kernel.async_allowed(worker.ctx):
worker.run()
else:
self.window.dispatch(KernelEvent(KernelEvent.STATE_BUSY, {"id": "video"}))
self.window.threadpool.start(worker)
return True


class MusicSignals(QObject):
finished = Signal(object, list, str)
finished_inline = Signal(object, list, str)
status = Signal(object)
error = Signal(object)


class MusicWorker(QRunnable):
def __init__(self):
super().__init__()
self.signals = MusicSignals()
self.window = None
self.ctx = None
self.model = "music-3.0"
self.prompt = ""
self.lyrics = None
self.output_format = "url"
self.audio_setting: Dict[str, Any] = {}
self.audio_format = "mp3"
self.request_options: Dict[str, Any] = {}
self.inline = False

@Slot()
def run(self):
try:
cfg = self.window.core.config
token = cfg.get("api_key_minimax") or os.environ.get("MINIMAX_API_KEY") or ""
if not token:
raise RuntimeError("MiniMax API key is not configured.")
endpoint = (cfg.get("api_endpoint_minimax") or "https://api.minimax.io/v1").rstrip("/")
body: Dict[str, Any] = {"model": self.model, "prompt": self.prompt,
"output_format": self.output_format}
if self.lyrics is not None:
body["lyrics"] = self.lyrics
if self.audio_setting:
body["audio_setting"] = self.audio_setting
body.update(self.request_options)
self.signals.status.emit("Generating music...")
response = requests.post(
endpoint + "/music_generation",
headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"},
json=body, timeout=180,
)
response.raise_for_status()
payload = response.json() or {}
if (payload.get("base_resp") or {}).get("status_code") != 0:
raise RuntimeError("MiniMax music generation returned an error.")
data = payload.get("data") or {}
if data.get("status") == 1 and not data.get("audio"):
raise RuntimeError("MiniMax music generation is still in progress.")
audio = data.get("audio")
if not audio:
raise RuntimeError("MiniMax music generation returned no audio.")
raw = self._decode_audio(audio)
path = self._save(raw)
paths = [path] if path else []
if self.inline:
self.signals.finished_inline.emit(self.ctx, paths, self.prompt)
else:
self.signals.finished.emit(self.ctx, paths, self.prompt)
except Exception as exc:
self.signals.error.emit(exc)

@staticmethod
def _decode_audio(audio: Any) -> bytes:
if isinstance(audio, (bytes, bytearray)):
return bytes(audio)
value = str(audio)
if value.startswith(("http://", "https://")):
result = requests.get(value, timeout=120)
result.raise_for_status()
return result.content
try:
return binascii.unhexlify(value)
except (binascii.Error, ValueError):
return base64.b64decode(value)

def _save(self, content: bytes) -> str:
safe = self.window.core.video.make_safe_filename(self.prompt) or "music"
extension = self.audio_format if self.audio_format in {"mp3", "wav", "pcm"} else "mp3"
filename = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S-") + safe + "." + extension
try:
directory = self.window.core.config.get_user_dir("audio")
except Exception:
directory = self.window.core.config.get_user_dir("video")
os.makedirs(directory, exist_ok=True)
path = os.path.join(directory, filename)
with open(path, "wb") as output:
output.write(content)
return path
35 changes: 35 additions & 0 deletions src/pygpt_net/provider/llms/minimax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Optional, Dict, List

from llama_index.core.base.embeddings.base import BaseEmbedding
from llama_index.core.llms.llm import BaseLLM as LlamaBaseLLM

from pygpt_net.core.types import MODE_LLAMA_INDEX
from pygpt_net.provider.llms.base import BaseLLM
from pygpt_net.item.model import ModelItem


class MiniMaxLLM(BaseLLM):
"""MiniMax text provider used alongside media models."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.id = "minimax"
self.name = "MiniMax"
self.type = [MODE_LLAMA_INDEX, "embeddings"]

def llama(self, window, model: ModelItem, stream: bool = False) -> LlamaBaseLLM:
from llama_index.llms.openai_like import OpenAILike
args = self.parse_args(model.llama_index, window)
args.setdefault("model", model.id)
args.setdefault("api_key", window.core.config.get("api_key_minimax", ""))
args.setdefault("api_base", window.core.config.get("api_endpoint_minimax", ""))
args.setdefault("is_chat_model", True)
args.setdefault("is_function_calling_model", model.tool_calls)
return OpenAILike(**self.inject_llamaindex_http_clients(args, window.core.config))

def get_embeddings_model(self, window, config: Optional[List[Dict]] = None) -> BaseEmbedding:
from llama_index.embeddings.openai_like import OpenAILikeEmbedding
args = self.parse_args({"args": config or []}, window)
args.setdefault("api_key", window.core.config.get("api_key_minimax", ""))
args.setdefault("api_base", window.core.config.get("api_endpoint_minimax", ""))
return OpenAILikeEmbedding(**self.inject_llamaindex_http_clients(args, window.core.config))