Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
7 changes: 6 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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=
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
3 changes: 2 additions & 1 deletion src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,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).
Expand All @@ -289,6 +289,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)
Expand Down
7 changes: 6 additions & 1 deletion src/skillspector/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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)."
)

Expand Down
1 change: 1 addition & 0 deletions src/skillspector/providers/_agent_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions src/skillspector/providers/minimax/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
13 changes: 13 additions & 0 deletions src/skillspector/providers/minimax/model_registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Token-budget metadata for the MiniMax provider.
#
# Format:
# models:
# "<model-label>":
# context_length: <int>
# max_output_tokens: <int> # optional when no output cap is published

models:
"MiniMax-M3":
context_length: 1000000
"MiniMax-M2.7":
context_length: 204800
91 changes: 91 additions & 0 deletions src/skillspector/providers/minimax/provider.py
Original file line number Diff line number Diff line change
@@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Official MiniMax API docs currently list MiniMax-M2.7 / MiniMax-M2.7-highspeed as the supported OpenAI-compatible model IDs, and the /v1/models example does not include MiniMax-M3; this default would make every out-of-box request fail. Use a served model with documented limits (or provide authoritative endpoint evidence plus a live contract test). Source: https://platform.minimax.io/docs/api-reference/api-overview

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
4 changes: 2 additions & 2 deletions src/skillspector/yara_rules/agent_skills.yar
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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_*))
}
}
107 changes: 107 additions & 0 deletions tests/unit/test_minimax_provider.py
Original file line number Diff line number Diff line change
@@ -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()
Loading