Skip to content

Backfill free-tier team model allowlists when FREE_LLM_MODELS changes - #268

Draft
juanmichelini wants to merge 1 commit into
mainfrom
fix/reconcile-free-tier-models-on-env-change
Draft

Backfill free-tier team model allowlists when FREE_LLM_MODELS changes#268
juanmichelini wants to merge 1 commit into
mainfrom
fix/reconcile-free-tier-models-on-env-change

Conversation

@juanmichelini

@juanmichelini juanmichelini commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Problem

FREE_LLM_MODELS is only applied to a team's LiteLLM models allowlist when the team is (re)provisioned (create_entries / budget updates). Changing the env var — e.g. adding deepseek-v4-flash — therefore does not update existing zero-credit teams. They stay on the stale allowlist and keep hitting 403 team_model_access_denied for the newly-added model until some unrelated provisioning pass happens to touch them.

There was no automatic backfill anywhere: PR #143 is explicitly scoped to provisioning and documents that existing teams are not reconciled; migration 153 only rewrote the stored default model, not the LiteLLM allowlist; and neither the daily maintenance tasks nor the 15-minute budget CronJob backfilled allowlists.

Fix

Add LiteLlmManager.reconcile_free_tier_models() — an idempotent sweep that:

  1. Lists teams via GET /team/list.
  2. For each free-tier team whose allowlist has drifted from the current FREE_LLM_MODELS, re-runs the existing update_team_and_users_budget(team_id, 0.0) backfill (which already re-derives the allowlist from FREE_LLM_MODELS and clears any stale member-level budget).

A team is considered free-tier when either:

  • max_budget is None and models is a non-empty subset of FREE_LLM_MODELS (the exact shape written for max_budget == 0.0), or
  • max_budget == 0.0 with an empty models list (legacy no-credit teams that predate the free-tier restriction).

Paid teams (max_budget > 0), unlimited teams (None budget + no restriction), and teams with custom non-subset allowlists are left untouched. Teams already in sync are skipped, so the proxy is written only on drift.

Wiring — no new infrastructure

The reconcile is invoked best-effort from the existing run_budget_maintenance entrypoint, which the budget-maintenance CronJob already runs on every deploy and every 15 minutes with FREE_LLM_MODELS injected. So:

  • The backfill "happens when the env var changes" (it's a no-op otherwise).
  • No new CronJob, no Helm chart change, no new table, no DB migration.
  • A migration would have been the wrong host: alembic runs in a blocking init container and calling the LiteLLM proxy there could stall deploys. The maintenance runner is non-blocking and already the right place; failures are swallowed so budget maintenance is never broken by a proxy hiccup.

Testing

  • Added TestReconcileFreeTierModels (9 cases): legacy zero-budget team, restricted free-tier missing the new model, already-in-sync skip, paid/unlimited/non-subset skip, only-drifted-teams reconciled, error counting without raising, and no-op without proxy config.
  • Full test_lite_llm_manager.py suite passes (138 tests). No new ruff errors introduced.

This PR was created by an AI agent (OpenHands) on behalf of the user.

@juanmichelini can click here to continue refining the PR


Enterprise server image for this PR:

ghcr.io/openhands/enterprise-server:sha-63870cc

FREE_LLM_MODELS is only applied when a team is (re)provisioned, so adding
a model such as deepseek-v4-flash to the list leaves existing zero-credit
teams on the stale allowlist and they keep getting 403
team_model_access_denied for the new model.

Add LiteLlmManager.reconcile_free_tier_models(), an idempotent sweep that
lists teams and, for each free-tier team whose allowlist has drifted from
the current FREE_LLM_MODELS, re-runs the existing
update_team_and_users_budget(team_id, 0.0) backfill. It is invoked
best-effort from the run_budget_maintenance entrypoint, which the
budget-maintenance CronJob already runs on every deploy and every 15
minutes, so the backfill happens whenever the env var changes without any
new CronJob, chart change, or schema migration. Teams already in sync are
skipped, so the proxy is written only on drift.

Co-authored-by: openhands <openhands@all-hands.dev>
@juanmichelini juanmichelini added python Pull requests that update python code type: fix A bug fix labels Aug 27, 2026 — with OpenHands AI
@juanmichelini juanmichelini added python Pull requests that update python code type: fix A bug fix labels Aug 27, 2026
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  enterprise
  run_budget_maintenance.py 70, 80-84, 88-95
  enterprise/storage
  lite_llm_manager.py 711-721, 737-784, 793-801
Project Total  

This report was generated by python-coverage-comment-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Pull requests that update python code type: fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants