From f5edee72d8e7d44bbc7afa24cd1e437cbdb55dfa Mon Sep 17 00:00:00 2001 From: zaebee Date: Mon, 8 Jun 2026 22:42:22 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(guardian):=20default=20model=20gemini-2?= =?UTF-8?q?.0-flash=20=E2=86=92=20gemini-2.5-flash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2.0-flash drops connections under load; 2.5-flash is stable. Co-Authored-By: Claude Sonnet 4.6 --- scripts/guardian_review.py | 2 +- src/cgis/guardian/providers/gemini.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/guardian_review.py b/scripts/guardian_review.py index 47562f74..cdd85296 100644 --- a/scripts/guardian_review.py +++ b/scripts/guardian_review.py @@ -13,7 +13,7 @@ log = structlog.getLogger(__name__) -_DEFAULT_MODEL = "gemini-2.0-flash" +_DEFAULT_MODEL = "gemini-2.5-flash" async def main() -> None: diff --git a/src/cgis/guardian/providers/gemini.py b/src/cgis/guardian/providers/gemini.py index 57158f84..c9ffb9b5 100644 --- a/src/cgis/guardian/providers/gemini.py +++ b/src/cgis/guardian/providers/gemini.py @@ -6,7 +6,7 @@ class GeminiProvider(BaseProvider): """Google Gemini provider. Requires: uv sync --group guardian""" - def __init__(self, api_key: str, model_name: str = "gemini-2.0-flash") -> None: + def __init__(self, api_key: str, model_name: str = "gemini-2.5-flash") -> None: """Store credentials; google-genai is imported lazily at call time.""" self._api_key = api_key self._model_name = model_name From c36482be5b7771075d15906459157f24de4dd9a9 Mon Sep 17 00:00:00 2001 From: zaebee Date: Mon, 8 Jun 2026 22:45:50 +0000 Subject: [PATCH 2/3] feat(guardian): add MistralProvider and auto-select by env key - MistralProvider uses mistralai SDK (already in guardian dep-group) - guardian_review.py picks Mistral if MISTRAL_API_KEY is set, falls back to Gemini - GUARDIAN_MODEL override works for both providers - Add MISTRAL_API_KEY to guardian.yml workflow env - Add mistralai.* mypy override to silence import-untyped Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/guardian.yml | 1 + pyproject.toml | 4 +++ scripts/guardian_review.py | 28 ++++++++++++++++---- src/cgis/guardian/providers/mistral.py | 36 ++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 src/cgis/guardian/providers/mistral.py diff --git a/.github/workflows/guardian.yml b/.github/workflows/guardian.yml index 405f7454..cdf53350 100644 --- a/.github/workflows/guardian.yml +++ b/.github/workflows/guardian.yml @@ -60,6 +60,7 @@ jobs: - name: Run Guardian Review env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} GUARDIAN_MODEL: ${{ vars.GUARDIAN_MODEL }} run: uv run python scripts/guardian_review.py --output guardian_report.md diff --git a/pyproject.toml b/pyproject.toml index 4aa68c4d..c2931971 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -155,6 +155,10 @@ ignore_missing_imports = true module = "google.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "mistralai.*" +ignore_missing_imports = true + [tool.interrogate] ignore-init-method = true ignore-init-module = true diff --git a/scripts/guardian_review.py b/scripts/guardian_review.py index cdd85296..7f51b767 100644 --- a/scripts/guardian_review.py +++ b/scripts/guardian_review.py @@ -9,11 +9,32 @@ from cgis.guardian.collector import ContextCollector from cgis.guardian.core import GuardianReviewer +from cgis.guardian.providers.base import BaseProvider from cgis.guardian.providers.gemini import GeminiProvider +from cgis.guardian.providers.mistral import MistralProvider log = structlog.getLogger(__name__) -_DEFAULT_MODEL = "gemini-2.5-flash" +_DEFAULT_GEMINI_MODEL = "gemini-2.5-flash" +_DEFAULT_MISTRAL_MODEL = "mistral-medium-latest" + + +def _build_provider() -> tuple[BaseProvider, str]: + """Return (provider, model_name) based on available environment variables.""" + model_override = os.environ.get("GUARDIAN_MODEL") + + mistral_key = os.environ.get("MISTRAL_API_KEY") + if mistral_key: + model = model_override or _DEFAULT_MISTRAL_MODEL + return MistralProvider(api_key=mistral_key, model_name=model), model + + gemini_key = os.environ.get("GEMINI_API_KEY") + if gemini_key: + model = model_override or _DEFAULT_GEMINI_MODEL + return GeminiProvider(api_key=gemini_key, model_name=model), model + + _msg = "Set MISTRAL_API_KEY or GEMINI_API_KEY to run Guardian." + raise OSError(_msg) async def main() -> None: @@ -27,11 +48,8 @@ async def main() -> None: ) args = parser.parse_args() - api_key = os.environ["GEMINI_API_KEY"] - model = os.environ.get("GUARDIAN_MODEL") or _DEFAULT_MODEL + provider, model = _build_provider() project_root = Path(__file__).parent.parent.absolute() - - provider = GeminiProvider(api_key=api_key, model_name=model) collector = ContextCollector(project_root=project_root) reviewer = GuardianReviewer(provider=provider, context_collector=collector) diff --git a/src/cgis/guardian/providers/mistral.py b/src/cgis/guardian/providers/mistral.py new file mode 100644 index 00000000..5265b2d5 --- /dev/null +++ b/src/cgis/guardian/providers/mistral.py @@ -0,0 +1,36 @@ +"""Mistral AI LLM provider for Guardian.""" + +from cgis.guardian.providers.base import BaseProvider + + +class MistralProvider(BaseProvider): + """Mistral AI provider. Requires: uv sync --group guardian""" + + def __init__(self, api_key: str, model_name: str = "mistral-medium-latest") -> None: + """Store credentials; mistralai is imported lazily at call time.""" + self._api_key = api_key + self._model_name = model_name + + async def generate_content(self, system_prompt: str, user_prompt: str) -> str: + """Send prompts to Mistral and return the text response.""" + _install_hint = "mistralai is required. Install with: uv sync --group guardian" + try: + from mistralai import Mistral # noqa: PLC0415 + except ImportError as exc: + raise ImportError(_install_hint) from exc + async with Mistral(api_key=self._api_key) as client: + response = await client.chat.complete_async( + model=self._model_name, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ], + ) + if not response.choices: + _msg = "Mistral returned an empty response" + raise ValueError(_msg) + content = response.choices[0].message.content + if content is None: + _msg = "Mistral returned a null message content" + raise ValueError(_msg) + return str(content) From 2e767fefe341251477968622840cd1be04685a28 Mon Sep 17 00:00:00 2001 From: zaebee Date: Mon, 8 Jun 2026 22:54:55 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(guardian):=20address=20PR=20review=20?= =?UTF-8?q?=E2=80=94=20explicit=20GUARDIAN=5FPROVIDER,=20RuntimeError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GUARDIAN_PROVIDER env var for explicit provider selection (Mistral wins if set, otherwise key-based auto-detection as before) - OSError → RuntimeError for configuration failures (semantic correctness) - Expose GUARDIAN_PROVIDER as GitHub Actions variable Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/guardian.yml | 1 + scripts/guardian_review.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/guardian.yml b/.github/workflows/guardian.yml index cdf53350..99700060 100644 --- a/.github/workflows/guardian.yml +++ b/.github/workflows/guardian.yml @@ -61,6 +61,7 @@ jobs: env: GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + GUARDIAN_PROVIDER: ${{ vars.GUARDIAN_PROVIDER }} GUARDIAN_MODEL: ${{ vars.GUARDIAN_MODEL }} run: uv run python scripts/guardian_review.py --output guardian_report.md diff --git a/scripts/guardian_review.py b/scripts/guardian_review.py index 7f51b767..a19854f4 100644 --- a/scripts/guardian_review.py +++ b/scripts/guardian_review.py @@ -20,11 +20,15 @@ def _build_provider() -> tuple[BaseProvider, str]: - """Return (provider, model_name) based on available environment variables.""" + """Return (provider, model_name) based on GUARDIAN_PROVIDER or available API keys.""" model_override = os.environ.get("GUARDIAN_MODEL") + provider_name = os.environ.get("GUARDIAN_PROVIDER", "").lower() - mistral_key = os.environ.get("MISTRAL_API_KEY") - if mistral_key: + if provider_name == "mistral" or (not provider_name and os.environ.get("MISTRAL_API_KEY")): + mistral_key = os.environ.get("MISTRAL_API_KEY") + if not mistral_key: + _msg = "MISTRAL_API_KEY must be set when GUARDIAN_PROVIDER=mistral" + raise RuntimeError(_msg) model = model_override or _DEFAULT_MISTRAL_MODEL return MistralProvider(api_key=mistral_key, model_name=model), model @@ -34,7 +38,7 @@ def _build_provider() -> tuple[BaseProvider, str]: return GeminiProvider(api_key=gemini_key, model_name=model), model _msg = "Set MISTRAL_API_KEY or GEMINI_API_KEY to run Guardian." - raise OSError(_msg) + raise RuntimeError(_msg) async def main() -> None: