2026-06-22 OWUI Model Selector Cost Descriptions

What I set out to do

Started as a question — what image models are available in Open WebUI, then in OpenRouter — and turned into: “is there a plugin that shows cost in the model selector?” There wasn’t, so the real task became building one: surface input/output cost (and per-image cost for image models) into each model’s description in the OWUI selector.

What I actually did

Investigated the live system instead of guessing. Key findings that shaped the design:

  • OWUI’s selector renders model.info.meta.description, and that field is only populated from OWUI’s DB. Connection (LiteLLM) models get no description, which is why the selector showed only ids.
  • LiteLLM’s /v1/model/info (port 4000) is a clean, authoritative cost source: input_cost_per_token / output_cost_per_token / mode for all 346 proxied models. The data was already there thanks to litellm_openrouter_enrichment.py.
  • No image model exposes a flat generation $/image anywhere — all token-priced. OpenRouter’s pricing.image is the input image cost (it equals the prompt-token price), a red herring.
  • LiteLLM mode is unreliable for detecting image models: only gemini-3.1-flash-image reports image_generation because it’s the one explicitly configured with model_info.mode; every wildcard model reports chat. The reliable signal is OpenRouter’s architecture.output_modalities containing image.

Designed and built (full brainstorm → spec → TDD):

  • owui_model_cost_desc.py — runtime library: pure formatters + injector + cached fetches (LiteLLM cost, OpenRouter blurb/modality), 300s TTL, fail-soft.
  • owui_model_cost_desc_patch.py — boot patcher that appends an EOF wrapper around get_all_models (marker-guarded, drift-safe — no interior anchor).
  • 33 pytest cases, all written before implementation.
  • Wired two read-only mounts + hashInput into open-webui.nix, added the patcher to patch-and-start.sh.

Format: $3 in / $15 out per 1M tok — <blurb>; Gemini image models append (~$0.0039/img) from out_cost × 1290; trailing zeros trimmed; blurb collapsed/clipped to 160 chars; non-destructive (never overwrites an existing description).

Verified live: unit tests green, live smoke against real LiteLLM + OpenRouter produced correct descriptions, just check clean, hm switch recreated the container, patcher logged PATCHED, and inspect.getsource(get_all_models) confirmed the wrapper is bound in the running server. Committed as 7fee85f.

What was striking

  • The per-image estimate is genuinely Gemini-only. The 1290 figure is Google’s documented per-image output-token charge (≤1024px), not a resolution — and OpenAI image models have no comparable documented count, so estimating them would be a guess on a guess.
  • I labeled the estimate ~$0.004 / 1024² image and got correctly called out: 1024² reads as 1,048,576 (pixels), unrelated to the 1290 token count. Two different numbers conflated in one label. Good reminder to keep units honest.
  • The EOF-append wrapper is a much cleaner patch than splicing into a giant async function body — it only depends on a module-level get_all_models existing, which is stable across upstream drift.