llm-connect/llm_connect/image_selection.py
tegwick e1bab67bd1
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Has been cancelled
Apply the image decision tree to missing hall portraits
A hall visual that stays on the general branch uses the Efficient model
from the 2026-10-08 intelligence-radar tree. render-hall writes only
portraits whose files are still absent.

Assistant: grok
Assistant-Session: 01a11ab6-1534-7d63-8606-662b92a80ed6
2026-10-08 16:44:43 +02:00

157 lines
6.1 KiB
Python

"""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)