From 7aea2ab4c579d5e4aca72b559c8d0c3842ef63ae Mon Sep 17 00:00:00 2001 From: octo-patch <266937838+octo-patch@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:55:06 +0800 Subject: [PATCH] feat(provider): add MiniMax regional support Signed-off-by: octo-patch <266937838+octo-patch@users.noreply.github.com> --- .env.example | 7 +- README.md | 12 +- src/skillspector/cli.py | 3 +- src/skillspector/providers/__init__.py | 7 +- src/skillspector/providers/_agent_cli.py | 1 + .../providers/minimax/__init__.py | 30 +++++ .../providers/minimax/model_registry.yaml | 13 +++ .../providers/minimax/provider.py | 91 +++++++++++++++ src/skillspector/yara_rules/agent_skills.yar | 4 +- tests/unit/test_minimax_provider.py | 107 ++++++++++++++++++ 10 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 src/skillspector/providers/minimax/__init__.py create mode 100644 src/skillspector/providers/minimax/model_registry.yaml create mode 100644 src/skillspector/providers/minimax/provider.py create mode 100644 tests/unit/test_minimax_provider.py diff --git a/.env.example b/.env.example index db03085a0..3b7de5c34 100644 --- a/.env.example +++ b/.env.example @@ -2,7 +2,7 @@ ENV=dev # options: dev|s # Active LLM provider. Selects which provider answers credentials, # metadata, and default-model lookups. Leave unset to default to nv_build. -# Options: openai | anthropic | anthropic_proxy | nv_build +# Options: openai | anthropic | anthropic_proxy | minimax | nv_build SKILLSPECTOR_PROVIDER= # Provider credentials — set the one matching SKILLSPECTOR_PROVIDER (or @@ -24,6 +24,11 @@ SKILLSPECTOR_REASONING_EFFORT= # For SKILLSPECTOR_PROVIDER=anthropic. ANTHROPIC_API_KEY= +# For SKILLSPECTOR_PROVIDER=minimax. MINIMAX_REGION accepts global_en or cn_zh. +MINIMAX_API_KEY= +MINIMAX_REGION=global_en +# MINIMAX_BASE_URL= # optional override + # For SKILLSPECTOR_PROVIDER=anthropic_proxy (Vertex-style raw-predict proxy). # Supports corporate API gateways, GCP Vertex AI, and self-hosted proxies. ANTHROPIC_PROXY_ENDPOINT_URL= diff --git a/README.md b/README.md index 026684453..b144ac853 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,7 @@ inference gateways. | `anthropic` | `ANTHROPIC_API_KEY` | api.anthropic.com | `claude-opus-4-6` | | `anthropic_proxy` | `ANTHROPIC_PROXY_API_KEY` + `ANTHROPIC_PROXY_ENDPOINT_URL` | Any Vertex-style raw-predict proxy | `claude-sonnet-4-6` | | `bedrock` | `AWS_PROFILE` (optional) + `AWS_REGION` — SigV4 via boto3 | AWS Bedrock Runtime | `us.anthropic.claude-sonnet-4-6-20250915-v1:0` | +| `minimax` | `MINIMAX_API_KEY` (+ optional `MINIMAX_REGION`) | Global or China regional endpoint | `MiniMax-M3` | | `nv_build` | `NVIDIA_INFERENCE_KEY` | build.nvidia.com | `deepseek-ai/deepseek-v4-flash` | | `claude_cli` | _(none — uses local CLI auth)_ | local `claude` binary | local Claude runtime fallback, or `SKILLSPECTOR_MODEL` | | `codex_cli` | _(none — uses local CLI auth)_ | local `codex` binary | local Codex runtime fallback, or `SKILLSPECTOR_MODEL` | @@ -273,6 +274,12 @@ export SKILLSPECTOR_PROVIDER=nv_build export NVIDIA_INFERENCE_KEY=nvapi-... skillspector scan ./my-skill/ +# MiniMax global endpoint; use cn_zh for the China endpoint +export SKILLSPECTOR_PROVIDER=minimax +export MINIMAX_API_KEY=your-key +export MINIMAX_REGION=global_en +skillspector scan ./my-skill/ + # Local Claude CLI — no API key; uses your existing `claude auth login` session # Requires: claude CLI installed and authenticated (claude auth login) export SKILLSPECTOR_PROVIDER=claude_cli @@ -577,13 +584,16 @@ Issues (2) | Variable | Description | Required | |----------|-------------|----------| -| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `nv_build`, `claude_cli`, `codex_cli`, or `gemini_cli`. Hosted providers use bundled `model_registry.yaml` defaults; `claude_cli` and `codex_cli` fall back to the local CLI runtime's default model unless `SKILLSPECTOR_MODEL` is set. Defaults to `nv_build`. | Optional | +| `SKILLSPECTOR_PROVIDER` | Active LLM provider: `openai`, `anthropic`, `anthropic_proxy`, `bedrock`, `minimax`, `nv_build`, `claude_cli`, `codex_cli`, or `gemini_cli`. Hosted providers use bundled `model_registry.yaml` defaults; `claude_cli` and `codex_cli` fall back to the local CLI runtime's default model unless `SKILLSPECTOR_MODEL` is set. Defaults to `nv_build`. | Optional | | `NVIDIA_INFERENCE_KEY` | Credential for the `nv_build` provider (build.nvidia.com). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=nv_build` | | `OPENAI_API_KEY` | Credential for the OpenAI provider (`SKILLSPECTOR_PROVIDER=openai`). Also serves as the tier-2 fallback in the credential waterfall when the active provider returns no credentials. | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=openai` | | `OPENAI_BASE_URL` | Override the OpenAI endpoint (e.g. point at Ollama). | Optional | | `SKILLSPECTOR_REASONING_EFFORT` | Optional provider- and model-dependent reasoning-effort setting. Non-empty values are trimmed and passed through unchanged; unset or blank preserves provider-default behavior. | Optional | | `ANTHROPIC_API_KEY` | Credential for the Anthropic provider (`SKILLSPECTOR_PROVIDER=anthropic`). | Required for LLM analysis when `SKILLSPECTOR_PROVIDER=anthropic` | | `ANTHROPIC_BASE_URL` | Override the native Anthropic endpoint (default: `https://api.anthropic.com`). | Optional | +| `MINIMAX_API_KEY` | Credential for the MiniMax provider (`SKILLSPECTOR_PROVIDER=minimax`). | Required when `SKILLSPECTOR_PROVIDER=minimax` | +| `MINIMAX_REGION` | MiniMax regional endpoint: `global_en` (default) or `cn_zh`. | Optional | +| `MINIMAX_BASE_URL` | Override the selected MiniMax regional endpoint. | Optional | | `ANTHROPIC_PROXY_ENDPOINT_URL` | Full endpoint URL for the Anthropic proxy provider (Vertex-style raw-predict). | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` | | `ANTHROPIC_PROXY_API_KEY` | Bearer token for the Anthropic proxy provider. | Required when `SKILLSPECTOR_PROVIDER=anthropic_proxy` | | `ANTHROPIC_PROXY_API_VERSION` | `anthropic_version` value sent in the request body (default: `vertex-2023-10-16`). | Optional | diff --git a/src/skillspector/cli.py b/src/skillspector/cli.py index aa1ed6581..f0f20c8ae 100644 --- a/src/skillspector/cli.py +++ b/src/skillspector/cli.py @@ -276,7 +276,7 @@ def scan( Environment variables: SKILLSPECTOR_PROVIDER Active LLM provider: openai | anthropic | - anthropic_proxy | bedrock | nv_build | + anthropic_proxy | bedrock | minimax | nv_build | nv_inference. Defaults to the NVIDIA path (nv_inference, falling back to nv_build in OSS builds). @@ -288,6 +288,7 @@ def scan( OPENAI_API_KEY [+ OPENAI_BASE_URL] for SKILLSPECTOR_PROVIDER=openai ANTHROPIC_API_KEY for SKILLSPECTOR_PROVIDER=anthropic + MINIMAX_API_KEY for SKILLSPECTOR_PROVIDER=minimax AWS_PROFILE (optional) + AWS_REGION for SKILLSPECTOR_PROVIDER=bedrock (AWS_PROFILE: standard boto3 credential chain when unset; AWS_REGION default: us-west-2) diff --git a/src/skillspector/providers/__init__.py b/src/skillspector/providers/__init__.py index f380fda17..5b8924cf4 100644 --- a/src/skillspector/providers/__init__.py +++ b/src/skillspector/providers/__init__.py @@ -26,6 +26,7 @@ anthropic → AnthropicProvider (api.anthropic.com) anthropic_proxy → AnthropicProxyProvider (Vertex-style raw-predict proxy) bedrock → BedrockProvider (AWS Bedrock Runtime, SigV4) + minimax → MiniMaxProvider (global or China regional endpoint) nv_build → NvBuildProvider (build.nvidia.com) claude_cli → ClaudeCLIProvider (local ``claude`` binary, no API key) codex_cli → CodexCLIProvider (local ``codex`` binary, no API key) @@ -118,6 +119,10 @@ def _select_active_provider() -> LLMProvider: from .bedrock import BedrockProvider return BedrockProvider() + if name == "minimax": + from .minimax import MiniMaxProvider + + return MiniMaxProvider() if name == "nv_build": return NvBuildProvider() if name == "claude_cli": @@ -148,7 +153,7 @@ def _select_active_provider() -> LLMProvider: raise ValueError( f"Unknown SKILLSPECTOR_PROVIDER: {name!r}. " - "Expected one of: openai, anthropic, anthropic_proxy, bedrock, nv_build, " + "Expected one of: openai, anthropic, anthropic_proxy, bedrock, minimax, nv_build, " "claude_cli, codex_cli, gemini_cli, antigravity_cli (or unset)." ) diff --git a/src/skillspector/providers/_agent_cli.py b/src/skillspector/providers/_agent_cli.py index 26ad4e971..a121c6355 100644 --- a/src/skillspector/providers/_agent_cli.py +++ b/src/skillspector/providers/_agent_cli.py @@ -77,6 +77,7 @@ # Includes API keys, cloud creds, SSH agent, and SkillSpector's own keys. _SECRET_ENV_PREFIXES: tuple[str, ...] = ( "ANTHROPIC_API_KEY", + "MINIMAX_API_KEY", "OPENAI_API_KEY", "NVIDIA_INFERENCE_KEY", "NVIDIA_INFERENCE_METADATA_KEY", diff --git a/src/skillspector/providers/minimax/__init__.py b/src/skillspector/providers/minimax/__init__.py new file mode 100644 index 000000000..c363cabac --- /dev/null +++ b/src/skillspector/providers/minimax/__init__.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MiniMax provider package.""" + +from .provider import ( + MINIMAX_CN_BASE_URL, + MINIMAX_GLOBAL_BASE_URL, + REGISTRY_PATH, + MiniMaxProvider, +) + +__all__ = [ + "MINIMAX_CN_BASE_URL", + "MINIMAX_GLOBAL_BASE_URL", + "REGISTRY_PATH", + "MiniMaxProvider", +] diff --git a/src/skillspector/providers/minimax/model_registry.yaml b/src/skillspector/providers/minimax/model_registry.yaml new file mode 100644 index 000000000..1d4b48ff8 --- /dev/null +++ b/src/skillspector/providers/minimax/model_registry.yaml @@ -0,0 +1,13 @@ +# Token-budget metadata for the MiniMax provider. +# +# Format: +# models: +# "": +# context_length: +# max_output_tokens: # optional when no output cap is published + +models: + "MiniMax-M3": + context_length: 1000000 + "MiniMax-M2.7": + context_length: 204800 diff --git a/src/skillspector/providers/minimax/provider.py b/src/skillspector/providers/minimax/provider.py new file mode 100644 index 000000000..78d20fad8 --- /dev/null +++ b/src/skillspector/providers/minimax/provider.py @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""MiniMax provider with global and China regional endpoint selection. + +``MINIMAX_REGION`` accepts ``global_en`` (the default) or ``cn_zh``. +``MINIMAX_BASE_URL`` can override the selected regional endpoint. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +from langchain_core.language_models.chat_models import BaseChatModel + +from skillspector.providers import registry +from skillspector.providers.chat_models import create_openai_compatible_chat_model + +MINIMAX_GLOBAL_BASE_URL = "https://api.minimax.io/v1" +MINIMAX_CN_BASE_URL = "https://api.minimaxi.com/v1" +MINIMAX_BASE_URLS = { + "global_en": MINIMAX_GLOBAL_BASE_URL, + "cn_zh": MINIMAX_CN_BASE_URL, +} + +REGISTRY_PATH = str(Path(__file__).with_name("model_registry.yaml")) + + +def _resolve_base_url() -> str: + override = os.environ.get("MINIMAX_BASE_URL", "").strip() + if override: + return override + + region = os.environ.get("MINIMAX_REGION", "").strip().lower() or "global_en" + try: + return MINIMAX_BASE_URLS[region] + except KeyError as exc: + raise ValueError("MINIMAX_REGION must be 'global_en' or 'cn_zh'") from exc + + +class MiniMaxProvider: + """MiniMax credentials, regional routing, and bundled model metadata.""" + + DEFAULT_MODEL = "MiniMax-M3" + SLOT_DEFAULTS: dict[str, str] = {} + + def resolve_credentials(self) -> tuple[str, str | None] | None: + """Return the MiniMax API key and selected regional base URL.""" + api_key = os.environ.get("MINIMAX_API_KEY", "").strip() + if not api_key: + return None + return api_key, _resolve_base_url() + + def create_chat_model( + self, + model: str, + *, + max_tokens: int, + timeout: float | None = 120, + ) -> BaseChatModel | None: + """Create a chat model for the selected MiniMax endpoint.""" + return create_openai_compatible_chat_model( + model=model, + credentials=self.resolve_credentials(), + max_tokens=max_tokens, + timeout=timeout, + ) + + def get_context_length(self, model: str) -> int | None: + return registry.lookup_context_length(REGISTRY_PATH, model) + + def get_max_output_tokens(self, model: str) -> int | None: + return registry.lookup_max_output_tokens(REGISTRY_PATH, model) + + def resolve_model(self, slot: str = "default") -> str: + """Resolve model from an environment override or the bundled default.""" + user_input = os.environ.get("SKILLSPECTOR_MODEL", "").strip() + return user_input or self.SLOT_DEFAULTS.get(slot, "") or self.DEFAULT_MODEL diff --git a/src/skillspector/yara_rules/agent_skills.yar b/src/skillspector/yara_rules/agent_skills.yar index 6aa11e5ae..bd9f0064f 100644 --- a/src/skillspector/yara_rules/agent_skills.yar +++ b/src/skillspector/yara_rules/agent_skills.yar @@ -27,7 +27,7 @@ rule agent_skill_credential_exfiltration_webhook $secret_env_js = /process\.env(\.|\[|\s|$)/ nocase $secret_dotenv_read = /open\s*\(\s*['"][^'"]*\.env['"]/ nocase $secret_ssh_key = /(\.ssh\/(id_rsa|id_ed25519)|authorized_keys)/ nocase - $secret_cloud_key = /(OPENAI_API_KEY|ANTHROPIC_API_KEY|NVIDIA_INFERENCE_KEY|AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|HF_TOKEN)/ nocase + $secret_cloud_key = /(OPENAI_API_KEY|ANTHROPIC_API_KEY|MINIMAX_API_KEY|NVIDIA_INFERENCE_KEY|AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|HF_TOKEN)/ nocase $send_requests = /(requests|httpx)\.(post|put)\s*\(/ nocase $send_fetch = /(fetch|axios\.post)\s*\(/ nocase @@ -138,4 +138,4 @@ rule agent_skill_destructive_autonomous_actions $autonomy_silent = /(silently|non-interactive|unattended)/ nocase condition: $destructive_rm_root or (any of ($destructive_*) and any of ($autonomy_*)) -} \ No newline at end of file +} diff --git a/tests/unit/test_minimax_provider.py b/tests/unit/test_minimax_provider.py new file mode 100644 index 000000000..f0ea41127 --- /dev/null +++ b/tests/unit/test_minimax_provider.py @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for the MiniMax provider.""" + +from __future__ import annotations + +import pytest +from langchain_openai import ChatOpenAI + +import skillspector.providers as providers_module +from skillspector.providers import get_metadata_provider, registry, resolve_provider_credentials +from skillspector.providers._agent_cli import _scrub_env +from skillspector.providers.minimax import ( + MINIMAX_CN_BASE_URL, + MINIMAX_GLOBAL_BASE_URL, + MiniMaxProvider, +) + + +@pytest.fixture(autouse=True) +def _clean_provider_env(monkeypatch: pytest.MonkeyPatch): + for key in ( + "MINIMAX_API_KEY", + "MINIMAX_BASE_URL", + "MINIMAX_REGION", + "SKILLSPECTOR_MODEL", + "SKILLSPECTOR_MODEL_REGISTRY", + "SKILLSPECTOR_PROVIDER", + ): + monkeypatch.delenv(key, raising=False) + providers_module._INJECTED_PROVIDER.set(None) + registry._load.cache_clear() + yield + providers_module._INJECTED_PROVIDER.set(None) + registry._load.cache_clear() + + +class TestMiniMaxProvider: + def test_returns_none_without_api_key(self) -> None: + assert MiniMaxProvider().resolve_credentials() is None + + def test_uses_global_endpoint_by_default(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + assert MiniMaxProvider().resolve_credentials() == ( + "test-key", + MINIMAX_GLOBAL_BASE_URL, + ) + + def test_selects_china_endpoint(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_REGION", "cn_zh") + assert MiniMaxProvider().resolve_credentials() == ( + "test-key", + MINIMAX_CN_BASE_URL, + ) + + def test_honors_base_url_override(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_BASE_URL", "https://minimax.example.com/v1") + assert MiniMaxProvider().resolve_credentials() == ( + "test-key", + "https://minimax.example.com/v1", + ) + + def test_rejects_unknown_region(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + monkeypatch.setenv("MINIMAX_REGION", "unknown") + with pytest.raises(ValueError, match="global_en.*cn_zh"): + MiniMaxProvider().resolve_credentials() + + def test_creates_chat_model(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + llm = MiniMaxProvider().create_chat_model("MiniMax-M3", max_tokens=123) + assert isinstance(llm, ChatOpenAI) + assert llm.model_name == "MiniMax-M3" + assert llm.max_tokens == 123 + assert str(llm.openai_api_base).rstrip("/") == MINIMAX_GLOBAL_BASE_URL + + def test_bundled_models_and_context_windows(self) -> None: + provider = MiniMaxProvider() + assert provider.resolve_model() == "MiniMax-M3" + assert provider.get_context_length("MiniMax-M3") == 1_000_000 + assert provider.get_context_length("MiniMax-M2.7") == 204_800 + assert provider.get_max_output_tokens("MiniMax-M3") is None + + def test_selector_uses_minimax_provider(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("SKILLSPECTOR_PROVIDER", "minimax") + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + assert resolve_provider_credentials() == ("test-key", MINIMAX_GLOBAL_BASE_URL) + assert isinstance(get_metadata_provider(), MiniMaxProvider) + + def test_api_key_is_removed_from_cli_environment(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("MINIMAX_API_KEY", "test-key") + assert "MINIMAX_API_KEY" not in _scrub_env()