"""OpenRouter image-model decision tree. The tree and the prices come from the intelligence-radar digest dated 2026-10-08 (``2026-10-08-1527_Image_Generation_with_Generative_AI_Digest``), recorded for callers in ``docs/image-model-selection.md``. Ranks and prices move. This module is the machine-readable copy of that snapshot, not a live leaderboard. Hall of Helix portraits are general generation: square technical illustration, no logos, no readable text, and many of them. Quality still matters, so the hall tier is Efficient, not Frontier and not bulk. """ from __future__ import annotations import re from dataclasses import dataclass from typing import Any from llm_connect.exceptions import LLMConfigurationError # branch, tier -> model, approximate USD, quote USD, per-call cap USD _CHOICES: dict[tuple[str, str], tuple[str, float, float, float]] = { ("vector", "styles"): ("recraft/recraft-v4-styles-vector", 0.05, 0.06, 0.10), ("vector", "default"): ("recraft/recraft-v4.1-vector", 0.08, 0.09, 0.15), ("vector", "pro"): ("recraft/recraft-v4.1-pro-vector", 0.30, 0.32, 0.40), ("editing", "frontier"): ("openai/gpt-image-2.5-sunburst", 0.21, 0.25, 0.30), ("editing", "efficient"): ("microsoft/mai-image-2.6", 0.039, 0.045, 0.08), ("editing", "budget"): ("meta/muse-image", 0.010, 0.012, 0.03), ("text_heavy", "frontier"): ("openai/gpt-image-2.5-sunburst", 0.21, 0.25, 0.30), ("text_heavy", "grounded"): ("google/gemini-nano-banana-2.1", 0.034, 0.04, 0.08), ("brand", "default"): ("recraft/recraft-v4-styles", 0.035, 0.04, 0.08), ("brand", "pro"): ("recraft/recraft-v4-styles-pro", 0.10, 0.12, 0.16), ("general", "frontier"): ("openai/gpt-image-2.5-sunburst", 0.21, 0.25, 0.30), ("general", "frontier_fast"): ("openai/gpt-image-2.5-flare", 0.21, 0.25, 0.30), ("general", "efficient"): ("google/gemini-nano-banana-2.1", 0.034, 0.04, 0.08), ("general", "efficient_fallback"): ("microsoft/mai-image-2.6", 0.039, 0.045, 0.08), ("general", "budget"): ("meta/muse-image", 0.010, 0.012, 0.03), ("general", "bulk"): ("recraft/recraft-v4.1-flash", 0.007, 0.008, 0.02), } _DEFAULT_TIER = { "vector": "default", "editing": "frontier", "text_heavy": "frontier", "brand": "default", "general": "efficient", } # Many hall portraits, and the portrait is what the public post shows. HALL_GENERAL_TIER = "efficient" _SELF_HOST_RE = re.compile(r"\b(self-host(?:ed|ing)?|open weights|on our own hardware)\b", re.I) _VECTOR_RE = re.compile(r"\b(svg|vector output|vector-style|as a vector)\b", re.I) _EDIT_RE = re.compile(r"\b(edit the existing|inpaint|image-to-image|change the attached image)\b", re.I) _BRAND_RE = re.compile(r"\b(brand kit|brand style|style system)\b", re.I) _NEGATED_TEXT_RE = re.compile( r"\b(?:no|not|without|zero)\b(?:\s+\w+){0,5}\s+(?:readable\s+)?(?:text|lettering|glyphs?)\b", re.I, ) _TEXT_RE = re.compile( r"\b(readable text|typography|caption|headline|lettering|signage|the words)\b", re.I, ) @dataclass(frozen=True) class ImageSelection: """One branch of the OpenRouter image tree.""" branch: str tier: str model: str approx_usd: float quote_usd: float max_cost_usd: float reason: str def to_dict(self) -> dict[str, Any]: return { "branch": self.branch, "tier": self.tier, "model": self.model, "approx_usd": self.approx_usd, "quote_usd": self.quote_usd, "max_cost_usd": self.max_cost_usd, "reason": self.reason, } def classify_image_branch(prompt: str) -> str: """Walk the digest tree in order. The first match wins.""" if _SELF_HOST_RE.search(prompt): return "self_host" if _VECTOR_RE.search(prompt): return "vector" if _EDIT_RE.search(prompt): return "editing" without_negations = _NEGATED_TEXT_RE.sub(" ", prompt) # A prompt that forbids readable text is not the text-heavy branch, even # when it mentions words as something the picture must not contain. forbids_text = _NEGATED_TEXT_RE.search(prompt) is not None if _TEXT_RE.search(without_negations) and not forbids_text: return "text_heavy" if _BRAND_RE.search(prompt): return "brand" return "general" def choose_image_model(branch: str, tier: str | None = None) -> ImageSelection: """Return the priced OpenRouter model for one branch and tier.""" if branch == "self_host": raise LLMConfigurationError( "Self-hosted or open-weight image generation is not available through OpenRouter", context={"branch": branch}, ) if branch not in _DEFAULT_TIER: known = ", ".join(sorted(_DEFAULT_TIER)) raise LLMConfigurationError( f"Unknown image branch {branch!r}. Choose from: {known}", context={"branch": branch}, ) selected_tier = tier or _DEFAULT_TIER[branch] choice = _CHOICES.get((branch, selected_tier)) if choice is None: known = ", ".join(sorted(item[1] for item in _CHOICES if item[0] == branch)) raise LLMConfigurationError( f"Unknown tier {selected_tier!r} for branch {branch!r}. Choose from: {known}", context={"branch": branch, "tier": selected_tier}, ) model, approx, quote, cap = choice reason = ( f"{branch}/{selected_tier} from the 2026-10-08 OpenRouter image tree; " f"quote {quote:.3f} USD against about {approx:.3f} USD per 1024 square image" ) return ImageSelection( branch=branch, tier=selected_tier, model=model, approx_usd=approx, quote_usd=quote, max_cost_usd=cap, reason=reason, ) def selection_for_prompt(prompt: str, *, tier: str | None = None) -> ImageSelection: """Classify a prompt, then price it. ``tier`` overrides only the general branch. Specialist branches keep the tree's own default, because a hall portrait that asks for readable text is no longer an Efficient general image. """ branch = classify_image_branch(prompt) if branch == "general": return choose_image_model(branch, tier or HALL_GENERAL_TIER) return choose_image_model(branch, None)