DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding Automated Email Alerts for Deprecated Groq Models in content‑automation

Adding Automated Email Alerts for Deprecated Groq Models in content‑automation

TL;DR: I added a watchdog that detects when a Groq model is deprecated, sends a formatted email with replacement options and a link to the required GitHub Secrets, and updated the generator to use the new gpt‑oss‑120b. The change fixes the “Unexpected token ‘<’” runtime error and gives the team a clear migration path.


The Problem

During the weekly run of content‑generator.py the pipeline crashed with:

SyntaxError: Unexpected token '<' (at line 42 in generated_content.js)
Enter fullscreen mode Exit fullscreen mode

The error originated from a downstream API call that still referenced the now‑deprecated gpt‑oss‑20b model. Groq announced the deprecation on their status page, but our codebase still tried to instantiate it, causing the API to return an HTML error page (hence the < token) instead of JSON. The failure propagated to the content‑automation step, breaking the daily Dev.to and Bluesky summaries.

In parallel, the team had no visibility that the model was deprecated, so we kept hitting the same error for days. I needed a reliable way to detect model deprecation at runtime and alert the maintainers with actionable information.


What I Tried First

My first attempt was to wrap the model call in a generic try/except block and log the raw response:

try:
    result = client.generate(model="gpt-oss-20b", prompt=prompt)
except Exception as e:
    logger.error(f"Model call failed: {e}")
    raise
Enter fullscreen mode Exit fullscreen mode

While this prevented the crash, it only produced a noisy stack trace in CloudWatch. It didn’t tell anyone why the call failed, nor did it suggest the new model to use. I also tried polling Groq’s /models endpoint daily and writing the list to a JSON file, but the script still needed a manual check to notice a missing model.

Both approaches fell short because they lacked:

  1. Real‑time detection – the error only appeared after the failed call.
  2. Actionable alert – developers had to dig through logs to find the replacement.
  3. Automation – no CI/CD gate to enforce the migration.

The Implementation

1. Model‑list helper

I added a private method _list_available_models in src/content_generator.py that queries Groq’s /models endpoint and returns a list of model IDs. The method already existed but returned an empty list on any exception; I expanded it to raise a custom ModelDiscoveryError when the request fails, so the caller can react appropriately.

# src/content_generator.py
class ModelDiscoveryError(RuntimeError):
    """Raised when the Groq model list cannot be fetched."""

def _list_available_models(self) -> list[str]:
    """Return a list of active model IDs from Groq."""
    try:
        response = requests.get(
            f"{self.base_url}/models",
            headers={"Authorization": f"Bearer {self.api_key}"},
            timeout=5,
        )
        response.raise_for_status()
        data = response.json()
        return [model["id"] for model in data.get("models", [])]
    except Exception as exc:
        raise ModelDiscoveryError("Failed to fetch model list") from exc
Enter fullscreen mode Exit fullscreen mode

2. Deprecation detector

A new method _detect_deprecated_model compares the model we intend to use (self.current_model) against the list from _list_available_models. If the model is missing, it returns a dictionary with the missing model, a list of alternatives, and a URL to the GitHub Secrets page where the new API key must be stored.

def _detect_deprecated_model(self) -> dict | None:
    """Check if the configured model is still available."""
    try:
        available = self._list_available_models()
    except ModelDiscoveryError as e:
        logger.warning(str(e))
        return None  # Fail silently; alert will be sent later

    if self.current_model not in available:
        alternatives = [
            m for m in available if m.startswith("gpt-oss-")
        ]
        return {
            "deprecated": self.current_model,
            "alternatives": alternatives,
            "secrets_url": "https://github.com/yourorg/content-automation/settings/secrets",
        }
    return None
Enter fullscreen mode Exit fullscreen mode

3. Email alert integration

I introduced a lightweight email utility using smtplib. The alert is only sent once per CI run to avoid spamming. The email body lists the deprecated model, suggested replacements, and a direct link to the GitHub Secrets page.

import smtplib
from email.message import EmailMessage

def _send_deprecation_alert(self, info: dict) -> None:
    msg = EmailMessage()
    msg["Subject"] = f"[Alert] Groq model {info['deprecated']} deprecated"
    msg["From"] = "alerts@vibecoding.dev"
    msg["To"] = "dev-team@vibecoding.dev"

    body = f"""\
Hi team,

The Groq model **{info['deprecated']}** you are using in the content‑automation pipeline has been deprecated.

Recommended replacements:
{chr(10).join(f"- {alt}" for alt in info['alternatives'])}

Please update the `GROQ_MODEL` secret and, if needed, add a new API key at:
{info['secrets_url']}

The next run will automatically switch to the first alternative.

— Roberto
"""
    msg.set_content(body)

    with smtplib.SMTP("smtp.sendgrid.net", 587) as smtp:
        smtp.starttls()
        smtp.login("apikey", self.email_api_key)
        smtp.send_message(msg)
Enter fullscreen mode Exit fullscreen mode

4. Wiring it into the generation flow

At the start of generate_content, I call the detector and, if it returns data, trigger the alert and fall back to the first alternative model. This ensures the pipeline continues without manual intervention.

def generate_content(self, prompt: str) -> str:
    # 1️⃣ Detect deprecation
    deprecation = self._detect_deprecated_model()
    if deprecation:
        self._send_deprecation_alert(deprecation)
        # Switch to the first available alternative
        self.current_model = deprecation["alternatives"][0]
        logger.info(
            f"Switched to fallback model {self.current_model} due to deprecation."
        )

    # 2️⃣ Proceed with generation
    try:
        result = self.client.generate(
            model=self.current_model,
            prompt=prompt,
            max_tokens=512,
        )
        return result["choices"][0]["text"]
    except Exception as e:
        logger.error(f"Generation failed with model {self.current_model}: {e}")
        raise
Enter fullscreen mode Exit fullscreen mode

5. Migration to gpt‑oss‑120b

The same commit also updated the default self.current_model constant in src/__init__.py:

# src/__init__.py
DEFAULT_GROQ_MODEL = "gpt-oss-120b"
Enter fullscreen mode Exit fullscreen mode

All references to the old gpt-oss-20b were replaced via a global search, and the bluesky consolidator was adjusted to use the new model’s token limits (increased from 20k to 32k). This prevented the earlier Unexpected token '<' because the API now returns proper JSON.

6. Tests & CI

I added a unit test tests/test_deprecation.py that mocks the /models endpoint, forces a missing model, and asserts that _send_deprecation_alert is called with the correct payload. The CI pipeline now includes a step that fails if the alert email is not sent when a deprecation is simulated.

# .github/workflows/ci.yml
- name: Run deprecation test
  run: pytest -k test_deprecation
Enter fullscreen mode Exit fullscreen mode

Key Takeaway

Never assume external services stay stable; always programmatically verify the resources you depend on and surface failures as actionable alerts. By turning a silent API break into an email with concrete next steps, the team can react instantly and the pipeline stays resilient.


What's Next

  • Dashboard integration: Push deprecation events to our internal

Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/content-automation · 2026-08-04

#playadev #buildinpublic

Top comments (0)