diff --git a/.github/workflows/cloud-staging-build.yaml b/.github/workflows/cloud-staging-build.yaml
index 620d809d68..094dd94a77 100644
--- a/.github/workflows/cloud-staging-build.yaml
+++ b/.github/workflows/cloud-staging-build.yaml
@@ -118,8 +118,6 @@ jobs:
- name: Setup extension
run: |
cd tmp_extension
- npm install
- npm run compile:ts
npm install -g @vscode/vsce@3.3.2
npm run build
vsce package
@@ -182,4 +180,4 @@ jobs:
git commit -m "version $VERSION"
git push origin staging
fi
- cd ..
\ No newline at end of file
+ cd ..
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eb0a9e840c..394c3a1c4e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,9 @@
-# (2025-08-18)
+# (2025-08-25)
+
+
+### Reverts
+
+* Revert "Implemented weekend discount" ([734e0c7](https://github.com/Pythagora-io/pythagora-v1/commit/734e0c726b179a45f235a0fd230a6310c77ae740))
diff --git a/README.md b/README.md
index b97572b1ff..8e6e789e80 100644
--- a/README.md
+++ b/README.md
@@ -8,9 +8,9 @@
-[](https://discord.gg/HaqXugmxr9)
+[](https://discord.gg/HaqXugmxr9)
[](https://github.com/Pythagora-io/gpt-pilot)
-[](https://twitter.com/HiPythagora)
+[](https://x.com/PythagoraAI)
@@ -28,17 +28,25 @@
-
+
### GPT Pilot doesn't just generate code, it builds apps!
+
+
+This repo is not being maintained anymore.
+
+# Visit [Pythagora.ai](https://www.pythagora.ai/) for more info
+
+
+
---
-[](https://youtu.be/4g-1cPGK0GA)
+[]([https://youtu.be/4g-1cPGK0GA](https://www.youtube.com/watch?v=o1nEvwjKziw))
-(click to open the video in YouTube) (1:40min)
+(click to open the video in YouTube) (1:04min)
@@ -46,11 +54,11 @@
-

+
-GPT Pilot is the core technology for the [Pythagora VS Code extension](https://bit.ly/3IeZxp6) that aims to provide **the first real AI developer companion**. Not just an autocomplete or a helper for PR messages but rather a real AI developer that can write full features, debug them, talk to you about issues, ask for review, etc.
+GPT Pilot is the core technology for the [Pythagora VS Code extension](https://marketplace.visualstudio.com/items?itemName=PythagoraTechnologies.pythagora-vs-code) that aims to provide **the first real AI developer companion**. Not just an autocomplete or a helper for PR messages but rather a real AI developer that can write full features, debug them, talk to you about issues, ask for review, etc.
---
@@ -112,7 +120,7 @@ After you have Python and (optionally) PostgreSQL installed, follow these steps:
5. `pip install -r requirements.txt` (install the dependencies)
6. `cp example-config.json config.json` (create `config.json` file)
7. Set your key and other settings in `config.json` file:
- - LLM Provider (`openai`, `anthropic` or `groq`) key and endpoints (leave `null` for default) (note that Azure and OpenRouter are suppored via the `openai` setting)
+ - LLM Provider (`openai`, `anthropic`, `groq` or `minimax`) key and endpoints (leave `null` for default) (note that Azure and OpenRouter are suppored via the `openai` setting)
- Your API key (if `null`, will be read from the environment variables)
- database settings: sqlite is used by default, PostgreSQL should also work
- optionally update `fs.ignore_paths` and add files or folders which shouldn't be tracked by GPT Pilot in workspace, useful to ignore folders created by compilers
diff --git a/core/agents/frontend.py b/core/agents/frontend.py
index 60ee86e444..ff76eb505b 100644
--- a/core/agents/frontend.py
+++ b/core/agents/frontend.py
@@ -25,7 +25,7 @@
from core.llm.parser import DescriptiveCodeBlockParser, OptionalCodeBlockParser
from core.log import get_logger
from core.telemetry import telemetry
-from core.ui.base import ProjectStage, UISource
+from core.ui.base import ProjectStage
log = get_logger(__name__)
@@ -168,10 +168,6 @@ async def iterate_frontend(self):
if user_input:
await self.send_message("Errors detected, fixing...")
else:
- await self.ui.send_message(
- "Use code CODE20 and subscribe https://pythagora.ai/pricing",
- source=UISource("Congratulations", "success"),
- )
answer = await self.ask_question(
"Do you want to change anything or report a bug?" if frontend_only else FE_CHANGE_REQ,
buttons={"yes": "I'm done building the UI"} if not frontend_only else None,
diff --git a/core/agents/spec_writer.py b/core/agents/spec_writer.py
index 6d95a9ac5a..2ca25c0d02 100644
--- a/core/agents/spec_writer.py
+++ b/core/agents/spec_writer.py
@@ -11,7 +11,7 @@
from core.log import get_logger
from core.telemetry import telemetry
from core.templates.registry import PROJECT_TEMPLATES
-from core.ui.base import ProjectStage, UISource
+from core.ui.base import ProjectStage
log = get_logger(__name__)
@@ -122,9 +122,6 @@ async def initialize_spec_and_project(self) -> AgentResponse:
await self.ui.send_front_logs_headers("specs_0", ["E1 / T1", "Writing Specification", "working"], "")
- await self.ui.send_message(
- "Use code CODE20 and subscribe https://pythagora.ai/pricing", source=UISource("Congratulations", "success")
- )
await self.send_message(
"## Write specification\n\nPythagora is generating a detailed specification for app based on your input.",
# project_state_id="setup",
diff --git a/core/config/__init__.py b/core/config/__init__.py
index 6ba0503aa5..b679014a7a 100644
--- a/core/config/__init__.py
+++ b/core/config/__init__.py
@@ -78,6 +78,7 @@ class LLMProvider(str, Enum):
GROQ = "groq"
LM_STUDIO = "lm-studio"
AZURE = "azure"
+ MINIMAX = "minimax"
class UIAdapter(str, Enum):
diff --git a/core/llm/base.py b/core/llm/base.py
index ddf92803f9..30cdb87d5f 100644
--- a/core/llm/base.py
+++ b/core/llm/base.py
@@ -423,6 +423,7 @@ def for_provider(provider: LLMProvider) -> type["BaseLLMClient"]:
from .anthropic_client import AnthropicClient
from .azure_client import AzureClient
from .groq_client import GroqClient
+ from .minimax_client import MiniMaxClient
from .openai_client import OpenAIClient
from .relace_client import RelaceClient
@@ -436,6 +437,8 @@ def for_provider(provider: LLMProvider) -> type["BaseLLMClient"]:
return GroqClient
elif provider == LLMProvider.AZURE:
return AzureClient
+ elif provider == LLMProvider.MINIMAX:
+ return MiniMaxClient
else:
raise ValueError(f"Unsupported LLM provider: {provider.value}")
diff --git a/core/llm/minimax_client.py b/core/llm/minimax_client.py
new file mode 100644
index 0000000000..f493af925f
--- /dev/null
+++ b/core/llm/minimax_client.py
@@ -0,0 +1,126 @@
+import datetime
+import os
+import re
+from typing import Optional
+
+import tiktoken
+from httpx import Timeout
+from openai import AsyncOpenAI, RateLimitError
+
+from core.config import LLMProvider
+from core.llm.base import BaseLLMClient
+from core.llm.convo import Convo
+from core.log import get_logger
+
+log = get_logger(__name__)
+tokenizer = tiktoken.get_encoding("cl100k_base")
+
+# MiniMax API base URL (international)
+MINIMAX_BASE_URL = "https://api.minimax.io/v1"
+
+# MiniMax temperature must be in (0.0, 1.0], cannot be 0
+MINIMAX_MIN_TEMPERATURE = 0.01
+
+
+class MiniMaxClient(BaseLLMClient):
+ provider = LLMProvider.MINIMAX
+
+ def _init_client(self):
+ api_key = self.config.api_key or os.environ.get("MINIMAX_API_KEY")
+ self.client = AsyncOpenAI(
+ api_key=api_key,
+ base_url=self.config.base_url or MINIMAX_BASE_URL,
+ timeout=Timeout(
+ max(self.config.connect_timeout, self.config.read_timeout),
+ connect=self.config.connect_timeout,
+ read=self.config.read_timeout,
+ ),
+ )
+
+ async def _make_request(
+ self,
+ convo: Convo,
+ temperature: Optional[float] = None,
+ json_mode: bool = False,
+ ) -> tuple[str, int, int]:
+ temp = self.config.temperature if temperature is None else temperature
+ # MiniMax requires temperature > 0
+ if temp <= 0:
+ temp = MINIMAX_MIN_TEMPERATURE
+
+ completion_kwargs = {
+ "model": self.config.model,
+ "messages": convo.messages,
+ "temperature": temp,
+ "stream": True,
+ }
+
+ # MiniMax does not support response_format; skip json_mode
+
+ stream = await self.client.chat.completions.create(**completion_kwargs)
+ response = []
+ prompt_tokens = 0
+ completion_tokens = 0
+
+ async for chunk in stream:
+ if chunk.usage:
+ prompt_tokens += chunk.usage.prompt_tokens
+ completion_tokens += chunk.usage.completion_tokens
+
+ if not chunk.choices:
+ continue
+
+ content = chunk.choices[0].delta.content
+ if not content:
+ continue
+
+ response.append(content)
+ if self.stream_handler:
+ await self.stream_handler(content)
+
+ response_str = "".join(response)
+
+ # Tell the stream handler we're done
+ if self.stream_handler:
+ await self.stream_handler(None)
+
+ if prompt_tokens == 0 and completion_tokens == 0:
+ prompt_tokens = sum(3 + len(tokenizer.encode(msg["content"])) for msg in convo.messages)
+ completion_tokens = len(tokenizer.encode(response_str))
+ log.warning(
+ "MiniMax response did not include token counts, estimating with tiktoken: "
+ f"{prompt_tokens} input tokens, {completion_tokens} output tokens"
+ )
+
+ return response_str, prompt_tokens, completion_tokens
+
+ def rate_limit_sleep(self, err: RateLimitError) -> Optional[datetime.timedelta]:
+ """
+ Handle MiniMax rate limiting using OpenAI-compatible headers.
+ """
+ headers = err.response.headers
+ if "x-ratelimit-remaining-tokens" not in headers:
+ # Check for retry-after header as fallback
+ if "retry-after" in headers:
+ return datetime.timedelta(seconds=int(headers["retry-after"]))
+ return None
+
+ remaining_tokens = headers["x-ratelimit-remaining-tokens"]
+ time_regex = r"(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?"
+ if remaining_tokens == 0:
+ match = re.search(time_regex, headers.get("x-ratelimit-reset-tokens", ""))
+ else:
+ match = re.search(time_regex, headers.get("x-ratelimit-reset-requests", ""))
+
+ if match:
+ hours = int(match.group(1)) if match.group(1) else 0
+ minutes = int(match.group(2)) if match.group(2) else 0
+ seconds = int(match.group(3)) if match.group(3) else 0
+ total_seconds = hours * 3600 + minutes * 60 + seconds
+ else:
+ total_seconds = 5
+
+ return datetime.timedelta(seconds=total_seconds)
+
+
+__all__ = ["MiniMaxClient"]
diff --git a/example-config.json b/example-config.json
index 1d66c47596..1aa6136cf8 100644
--- a/example-config.json
+++ b/example-config.json
@@ -27,6 +27,14 @@
"azure_deployment": "your-azure-deployment-id",
"api_version": "2024-02-01"
}
+ },
+ // Example config for MiniMax (see https://platform.minimax.io/docs/api-reference/text-openai-api)
+ // Available models: MiniMax-M3 (default), MiniMax-M2.7, MiniMax-M2.7-highspeed
+ "minimax": {
+ "base_url": "https://api.minimax.io/v1",
+ "api_key": "your-minimax-api-key",
+ "connect_timeout": 60.0,
+ "read_timeout": 20.0
}
},
// Each agent can use a different model or configuration. The default, as before, is GPT4 Turbo
diff --git a/pyproject.toml b/pyproject.toml
index ae4420007a..ebdc0039de 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pythagora-core"
-version = "2.0.9"
+version = "2.0.10"
description = "Build complete apps using AI agents"
authors = ["Leon Ostrez "]
license = "FSL-1.1-MIT"
diff --git a/tests/integration/llm/test_minimax.py b/tests/integration/llm/test_minimax.py
new file mode 100644
index 0000000000..7041402320
--- /dev/null
+++ b/tests/integration/llm/test_minimax.py
@@ -0,0 +1,103 @@
+from os import getenv
+from unittest.mock import MagicMock
+
+import pytest
+
+from core.config import LLMConfig, LLMProvider
+from core.llm.base import APIError
+from core.llm.convo import Convo
+from core.llm.minimax_client import MiniMaxClient
+
+run_integration_tests = getenv("INTEGRATION_TESTS", "").lower()
+if run_integration_tests not in ["true", "yes", "1", "on"]:
+ pytest.skip("Skipping integration tests", allow_module_level=True)
+
+if not getenv("MINIMAX_API_KEY"):
+ pytest.skip(
+ "Skipping MiniMax integration tests: MINIMAX_API_KEY is not set",
+ allow_module_level=True,
+ )
+
+
+def _mock_state_manager():
+ sm = MagicMock()
+ sm.get_access_token = MagicMock(return_value=None)
+ return sm
+
+
+@pytest.mark.asyncio
+async def test_incorrect_key():
+ cfg = LLMConfig(
+ provider=LLMProvider.MINIMAX,
+ model="MiniMax-M3",
+ api_key="invalid-key",
+ base_url="https://api.minimax.io/v1",
+ temperature=0.5,
+ )
+
+ llm = MiniMaxClient(cfg, state_manager=_mock_state_manager())
+ convo = Convo("you're a friendly assistant").user("tell me a joke")
+
+ with pytest.raises(APIError):
+ await llm(convo)
+
+
+@pytest.mark.asyncio
+async def test_minimax_success():
+ cfg = LLMConfig(
+ provider=LLMProvider.MINIMAX,
+ model="MiniMax-M3",
+ base_url="https://api.minimax.io/v1",
+ temperature=0.5,
+ )
+
+ streamed_response = []
+
+ async def stream_handler(content: str):
+ if content:
+ streamed_response.append(content)
+
+ llm = MiniMaxClient(cfg, state_manager=_mock_state_manager(), stream_handler=stream_handler)
+ convo = Convo("you're a friendly assistant").user("tell me a joke")
+
+ response, req_log = await llm(convo)
+ assert response == "".join(streamed_response)
+
+ assert req_log.messages == convo.messages
+ assert req_log.prompt_tokens > 0
+ assert req_log.completion_tokens > 0
+
+
+@pytest.mark.asyncio
+async def test_minimax_m27_model():
+ """Verify previous-generation M2.7 model still works."""
+ cfg = LLMConfig(
+ provider=LLMProvider.MINIMAX,
+ model="MiniMax-M2.7",
+ base_url="https://api.minimax.io/v1",
+ temperature=0.5,
+ )
+
+ llm = MiniMaxClient(cfg, state_manager=_mock_state_manager())
+ convo = Convo("you're a friendly assistant").user("say hello")
+
+ response, req_log = await llm(convo)
+ assert len(response) > 0
+ assert req_log.model == "MiniMax-M2.7"
+
+
+@pytest.mark.asyncio
+async def test_minimax_m27_highspeed_model():
+ cfg = LLMConfig(
+ provider=LLMProvider.MINIMAX,
+ model="MiniMax-M2.7-highspeed",
+ base_url="https://api.minimax.io/v1",
+ temperature=0.5,
+ )
+
+ llm = MiniMaxClient(cfg, state_manager=_mock_state_manager())
+ convo = Convo("you're a friendly assistant").user("say hello")
+
+ response, req_log = await llm(convo)
+ assert len(response) > 0
+ assert req_log.model == "MiniMax-M2.7-highspeed"
diff --git a/tests/llm/test_minimax.py b/tests/llm/test_minimax.py
new file mode 100644
index 0000000000..0fb2cc3881
--- /dev/null
+++ b/tests/llm/test_minimax.py
@@ -0,0 +1,215 @@
+from unittest.mock import AsyncMock, MagicMock, call, patch
+
+import pytest
+
+from core.config import LLMConfig, LLMProvider
+from core.llm.base import APIError
+from core.llm.convo import Convo
+from core.llm.minimax_client import MiniMaxClient
+from core.state.state_manager import StateManager
+
+
+async def mock_response_generator(*content):
+ for item in content:
+ chunk = MagicMock()
+ chunk.choices = [MagicMock(delta=MagicMock(content=item))]
+ chunk.usage = None
+ yield chunk
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_calls_model(mock_AsyncOpenAI, mock_state_manager):
+ cfg = LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3", temperature=0.5)
+ convo = Convo("system hello").user("user hello")
+
+ stream = AsyncMock(return_value=mock_response_generator("hello", None, "world"))
+
+ mock_chat = AsyncMock()
+ mock_completions = AsyncMock()
+ mock_completions.create = stream
+ mock_chat.completions = mock_completions
+
+ mock_client = AsyncMock()
+ mock_client.chat = mock_chat
+ mock_AsyncOpenAI.return_value = mock_client
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(cfg, state_manager=sm)
+ response, req_log = await llm(convo)
+ assert response == "helloworld"
+
+ assert req_log.model == "MiniMax-M3"
+ assert req_log.provider == LLMProvider.MINIMAX
+ assert req_log.temperature == 0.5
+ assert req_log.response == response
+ assert req_log.status == "success"
+
+ stream.assert_awaited_once_with(
+ model="MiniMax-M3",
+ messages=[
+ {"role": "system", "content": "system hello"},
+ {"role": "user", "content": "user hello"},
+ ],
+ temperature=0.5,
+ stream=True,
+ )
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_temperature_clamping(mock_AsyncOpenAI, mock_state_manager):
+ """Test that temperature=0 is clamped to MINIMAX_MIN_TEMPERATURE (0.01)."""
+ cfg = LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3", temperature=0.0)
+ convo = Convo("system").user("user")
+
+ stream = AsyncMock(return_value=mock_response_generator("ok"))
+
+ mock_chat = AsyncMock()
+ mock_completions = AsyncMock()
+ mock_completions.create = stream
+ mock_chat.completions = mock_completions
+
+ mock_client = AsyncMock()
+ mock_client.chat = mock_chat
+ mock_AsyncOpenAI.return_value = mock_client
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(cfg, state_manager=sm)
+ await llm(convo)
+
+ # Verify temperature was clamped to 0.01 instead of 0.0
+ stream.assert_awaited_once()
+ call_kwargs = stream.call_args[1] if stream.call_args[1] else {}
+ if not call_kwargs:
+ call_kwargs = dict(zip(["model", "messages", "temperature", "stream"], stream.call_args[0]))
+ assert call_kwargs.get("temperature", stream.call_args.kwargs.get("temperature")) == 0.01
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_no_json_mode(mock_AsyncOpenAI, mock_state_manager):
+ """Test that json_mode=True does NOT add response_format (MiniMax doesn't support it)."""
+ cfg = LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3", temperature=0.5)
+ convo = Convo("system").user("user")
+
+ stream = AsyncMock(return_value=mock_response_generator("ok"))
+
+ mock_chat = AsyncMock()
+ mock_completions = AsyncMock()
+ mock_completions.create = stream
+ mock_chat.completions = mock_completions
+
+ mock_client = AsyncMock()
+ mock_client.chat = mock_chat
+ mock_AsyncOpenAI.return_value = mock_client
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(cfg, state_manager=sm)
+ await llm(convo, json_mode=True)
+
+ # Verify response_format was NOT included in the call
+ call_kwargs = stream.call_args.kwargs
+ assert "response_format" not in call_kwargs
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_stream_handler(mock_AsyncOpenAI, mock_state_manager):
+ cfg = LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3", temperature=0.5)
+ convo = Convo("system hello").user("user hello")
+
+ stream_handler = AsyncMock()
+
+ stream = AsyncMock(return_value=mock_response_generator("hello", None, "world"))
+
+ mock_chat = AsyncMock()
+ mock_completions = AsyncMock()
+ mock_completions.create = stream
+ mock_chat.completions = mock_completions
+
+ mock_client = AsyncMock()
+ mock_client.chat = mock_chat
+ mock_AsyncOpenAI.return_value = mock_client
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(cfg, stream_handler=stream_handler, state_manager=sm)
+ await llm(convo)
+
+ stream_handler.assert_has_awaits([call("hello"), call("world")])
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_default_base_url(mock_AsyncOpenAI, mock_state_manager):
+ """Test that the default base URL is set to MiniMax API endpoint."""
+ cfg = LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3", temperature=0.5)
+
+ sm = StateManager(mock_state_manager)
+ MiniMaxClient(cfg, state_manager=sm)
+
+ mock_AsyncOpenAI.assert_called_once()
+ call_kwargs = mock_AsyncOpenAI.call_args.kwargs
+ assert call_kwargs["base_url"] == "https://api.minimax.io/v1"
+
+
+@pytest.mark.asyncio
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+async def test_minimax_custom_base_url(mock_AsyncOpenAI, mock_state_manager):
+ """Test that a custom base URL overrides the default."""
+ cfg = LLMConfig(
+ provider=LLMProvider.MINIMAX,
+ model="MiniMax-M3",
+ temperature=0.5,
+ base_url="https://api.minimaxi.com/v1",
+ )
+
+ sm = StateManager(mock_state_manager)
+ MiniMaxClient(cfg, state_manager=sm)
+
+ mock_AsyncOpenAI.assert_called_once()
+ call_kwargs = mock_AsyncOpenAI.call_args.kwargs
+ assert call_kwargs["base_url"] == "https://api.minimaxi.com/v1"
+
+
+@pytest.mark.parametrize(
+ ("remaining_tokens", "reset_tokens", "reset_requests", "expected"),
+ [
+ (0, "1h1m1s", "", 3661),
+ (0, "1m", "", 60),
+ (1, "", "1h1m1s", 3661),
+ ],
+)
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+def test_minimax_rate_limit_parser(
+ mock_AsyncOpenAI, mock_state_manager, remaining_tokens, reset_tokens, reset_requests, expected
+):
+ headers = {
+ "x-ratelimit-remaining-tokens": remaining_tokens,
+ "x-ratelimit-reset-tokens": reset_tokens,
+ "x-ratelimit-reset-requests": reset_requests,
+ }
+ err = MagicMock(response=MagicMock(headers=headers))
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3"), state_manager=sm)
+ assert int(llm.rate_limit_sleep(err).total_seconds()) == expected
+
+
+@patch("core.cli.helpers.StateManager")
+@patch("core.llm.minimax_client.AsyncOpenAI")
+def test_minimax_rate_limit_retry_after(mock_AsyncOpenAI, mock_state_manager):
+ """Test rate limiting with retry-after header."""
+ headers = {"retry-after": "30"}
+ err = MagicMock(response=MagicMock(headers=headers))
+
+ sm = StateManager(mock_state_manager)
+ llm = MiniMaxClient(LLMConfig(provider=LLMProvider.MINIMAX, model="MiniMax-M3"), state_manager=sm)
+ assert int(llm.rate_limit_sleep(err).total_seconds()) == 30