nexu-io/open-design · error · RuntimeError

{role} must use a Gemini 3.1 preview model. Got: {model}

Error message

{role} must use a Gemini 3.1 preview model. Got: {model}

What it means

Raised by _require_gemini_31_preview, which is invoked from _resolve_model_pins ONLY when provider_name == 'gemini'. Both the planner and rerank model strings must start with 'gemini-3.1-' AND end with '-preview'. Any other model name (including gemini-2.x, gemini-3.0, or non-preview gemini-3.1) is rejected for the gemini provider.

Source

Thrown at design-templates/last30days/scripts/lib/providers.py:350

            rerank_model=rerank_model,
            x_search_backend=_resolve_x_backend(config),
        )
        return runtime, OpenRouterClient(openrouter_key)

    raise RuntimeError(f"Unsupported reasoning provider: {provider_name}")


def _resolve_x_backend(config: dict[str, Any]) -> str | None:
    preferred = (config.get("LAST30DAYS_X_BACKEND") or "").lower()
    if preferred in {"xai", "bird"}:
        return preferred
    return env.get_x_source(config)


def _require_gemini_31_preview(model: str, *, role: str) -> None:
    if model.startswith("gemini-3.1-") and model.endswith("-preview"):
        return
    raise RuntimeError(
        f"{role} must use a Gemini 3.1 preview model. Got: {model}"
    )


def extract_json(text: str) -> dict[str, Any]:
    """Extract the first JSON object from a model response."""
    text = text.strip()
    if not text:
        raise ValueError("Expected JSON response, got empty text")
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        match = re.search(r"\{[\s\S]*\}", text)
        if not match:
            raise
        return json.loads(match.group(0))

View on GitHub (pinned to 5be4028344)

Solutions

  1. Pin both LAST30DAYS_PLANNER_MODEL and LAST30DAYS_RERANK_MODEL to a name matching gemini-3.1-*-preview.
  2. Leave both unset to inherit the defaults from _MODEL_DEFAULTS (which already satisfy the contract).
  3. If you genuinely need a non-3.1-preview Gemini model, switch provider away from 'gemini' or relax _require_gemini_31_preview deliberately.

Example fix

# before
LAST30DAYS_REASONING_PROVIDER=gemini
LAST30DAYS_PLANNER_MODEL=gemini-2.0-flash

# after
LAST30DAYS_REASONING_PROVIDER=gemini
# unset both to use defaults, or:
LAST30DAYS_PLANNER_MODEL=gemini-3.1-flash-preview
Defensive patterns

Strategy: validation

Validate before calling

import re

def validate_gemini_model_pin(model, role):
    if not (model.startswith("gemini-3.1-") and model.endswith("-preview")):
        raise ValueError(
            f"{role} model must match gemini-3.1-*-preview; got {model!r}"
        )
    return model

Type guard

null

Try / catch

null

Prevention

When it happens

Trigger: Setting LAST30DAYS_PLANNER_MODEL or LAST30DAYS_RERANK_MODEL to a non-preview model while LAST30DAYS_REASONING_PROVIDER=gemini. The role in the message identifies which pin failed ('planner' or 'rerank').

Common situations: Pinning an older model like 'gemini-2.0-flash' or 'gemini-pro' that predates the 3.1-preview contract. Copying a model name from another provider's docs. A fork that loosened the contract being reverted.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/acbb1c453ff303c8. Report an issue: GitHub.