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
- Pin both LAST30DAYS_PLANNER_MODEL and LAST30DAYS_RERANK_MODEL to a name matching gemini-3.1-*-preview.
- Leave both unset to inherit the defaults from _MODEL_DEFAULTS (which already satisfy the contract).
- 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
- For the gemini provider, prefer leaving LAST30DAYS_PLANNER_MODEL / LAST30DAYS_RERANK_MODEL unset to inherit compliant defaults.
- When pinning a custom model, validate it matches gemini-3.1-*-preview before launching a run.
- Treat the contract change (3.1-preview required) as a versioned invariant; document it in the upgrade notes.
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
- Cloudflare API token is required.
- existing config at ${where} is not valid JSON: ${err instanc
- existing config at ${where} is not a JSON object
- no Nano Banana API key — configure it in Settings or set OD_
- Gemini selected but no Google API key is configured.
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/acbb1c453ff303c8.
Report an issue: GitHub.