Context
The LLM picker (Settings → Agent config → LLM, shipped in feat/llm-model-picker) lets any
admin pick any model from any configured provider, plus arbitrary custom strings via the
"Custom — type a model name…" option. Good for personal / small-team OSS use.
For production / multi-tenant SaaS, an org admin will eventually want to restrict
which models the picker offers. Reasons:
- Cost control — block Opus 4.8 because it's 5× Sonnet at scale.
- Compliance / SOC2 — pin to a list of vetted models (e.g. only EU-hosted Gemini, only
on-prem Ollama). Block external providers entirely.
- Reduce support surface — fewer model variants in flight → fewer "why is my chat
weird?" tickets.
Today
- The curated catalog (
_PROVIDER_CATALOG in apps/core/src/opendevops_core/agent/llm/backend.py)
is convenience, not policy — users can still type any string in the Custom field, and
the backend's PUT validation only checks that the provider has credentials.
- No org / global allowed-list exists.
Proposed shape
- Global allow-list (OSS): new env var
ALLOWED_LLM_MODELS (comma-separated). When
set, the picker only offers models that match; the custom field is hidden; PUT rejects
anything not on the list. Empty/unset = current behavior (everything allowed).
- Per-org allow-list (Product): small admin UI on the org settings page; persisted in
app_config[\"llm_allowed_models_{org_id}\"]. Falls back to the global list when unset.
- Glob support so admins can write `anthropic/` or `-haiku-*` without listing every
model individually.
- Catalog filtering:
available_providers() filters its models[] against the
allowed list before returning; UI doesn't need to know about the policy.
Where it lives in the code
apps/core/src/opendevops_core/agent/llm/backend.py — available_providers() filter +
a is_model_allowed(model: str) -> bool helper.
apps/backend/src/api/routers/settings.py — PUT endpoint rejects with 400 when out of
policy.
- Product repo, mirrored — per-org override read from
app_config.
Out of scope (for the first cut)
- Per-user picks (deferred — not currently supported anywhere).
- Model-by-model cost caps (separate feature; bigger conversation).
Filed from chat discussion 2026-05-30 while merging feat/llm-model-picker.
Context
The LLM picker (Settings → Agent config → LLM, shipped in feat/llm-model-picker) lets any
admin pick any model from any configured provider, plus arbitrary custom strings via the
"Custom — type a model name…" option. Good for personal / small-team OSS use.
For production / multi-tenant SaaS, an org admin will eventually want to restrict
which models the picker offers. Reasons:
on-prem Ollama). Block external providers entirely.
weird?" tickets.
Today
_PROVIDER_CATALOGinapps/core/src/opendevops_core/agent/llm/backend.py)is convenience, not policy — users can still type any string in the Custom field, and
the backend's PUT validation only checks that the provider has credentials.
Proposed shape
ALLOWED_LLM_MODELS(comma-separated). Whenset, the picker only offers models that match; the custom field is hidden; PUT rejects
anything not on the list. Empty/unset = current behavior (everything allowed).
app_config[\"llm_allowed_models_{org_id}\"]. Falls back to the global list when unset.model individually.
available_providers()filters itsmodels[]against theallowed list before returning; UI doesn't need to know about the policy.
Where it lives in the code
apps/core/src/opendevops_core/agent/llm/backend.py—available_providers()filter +a
is_model_allowed(model: str) -> boolhelper.apps/backend/src/api/routers/settings.py— PUT endpoint rejects with 400 when out ofpolicy.
app_config.Out of scope (for the first cut)
Filed from chat discussion 2026-05-30 while merging feat/llm-model-picker.