Summary
A staging user recreated after a Keycloak-only deletion had this LiteLLM team state:
spend = 1.566280244
max_budget = 0.0
Their automation correctly selected free litellm_proxy/glm-5.2, but LiteLLM rejected its first call because historical team spend exceeded the team budget.
Sequential code path
-
The original LiteLLM team accumulated approximately $1.57 of spend.
-
The Keycloak user was deleted without deleting the corresponding OpenHands and LiteLLM records, so LiteLLM could retain that user/team and its spend.
-
On registration, the auth callback entered the new-user branch in enterprise/server/routes/auth.py. UserStore.create_user() called create_default_settings(), which invoked LiteLlmManager.create_entries(..., create_user=True).
-
LiteLlmManager.create_entries() initialized team_budget from DEFAULT_INITIAL_BUDGET=0.0, then queried /team/info. If it found the existing team, it replaced the default with that team's existing max_budget. Therefore, the current code preserves a positive existing budget on the normal path and does not by itself explain the observed transition to $0.
-
Before the automation ran, the team nevertheless reached spend≈1.57, max_budget=0.0. The transition must be identified from deployment and LiteLLM logs. The relevant possibilities are:
- the surviving team already had
max_budget=0.0 before registration;
- staging ran code predating the existing-budget preservation logic;
/team/info did not return the existing team, after which _create_team() encountered an existing-team conflict and _update_team() applied the $0 default;
- another team-budget writer set the cap to
$0 while preserving spend.
-
With create_user=True, onboarding deleted and recreated the stale LiteLLM user. The LiteLLM team and its financial state remained separate.
-
The automation read AUTOMATION_MODEL and loaded the selected profile with workspace.get_llm(). Its run log confirmed litellm_proxy/glm-5.2.
-
GLM-5.2 has zero input, output, and cache-read cost in staging and production. LiteLLM's _team_max_budget_check() compared historical spend with max_budget without considering the model's zero configured cost and rejected the request.
-
A Canvas conversation created during diagnosis also resolved to gpt-5.5 instead of its intended profile model. OpenHands#16342 fixes that path by resolving the effective profile model and sending it as llm_model.
Fix locations
- Add logging around the
/team/info, /team/new, and /team/update sequence in create_entries() to record the team ID and budget before and after reconciliation.
- In the existing-team conflict branch of
_create_team(), re-fetch the team and preserve its financial fields before calling _update_team().
- Add a combined regression test alongside the separate existing-team budget test and stale-user reset test: start with
max_budget=10, spend=1.57, run create_entries(create_user=True), and assert both values remain unchanged.
- Update
_team_max_budget_check() so a model with trusted server-side zero pricing can run when paid credits are exhausted.
- Merge and validate OpenHands#16342.
- Make staging account reset delete the user's Keycloak, OpenHands, and LiteLLM state together.
This issue was created by an OpenHands AI agent on behalf of Graham Neubig.
Summary
A staging user recreated after a Keycloak-only deletion had this LiteLLM team state:
Their automation correctly selected free
litellm_proxy/glm-5.2, but LiteLLM rejected its first call because historical team spend exceeded the team budget.Sequential code path
The original LiteLLM team accumulated approximately
$1.57of spend.The Keycloak user was deleted without deleting the corresponding OpenHands and LiteLLM records, so LiteLLM could retain that user/team and its spend.
On registration, the auth callback entered the new-user branch in
enterprise/server/routes/auth.py.UserStore.create_user()calledcreate_default_settings(), which invokedLiteLlmManager.create_entries(..., create_user=True).LiteLlmManager.create_entries()initializedteam_budgetfromDEFAULT_INITIAL_BUDGET=0.0, then queried/team/info. If it found the existing team, it replaced the default with that team's existingmax_budget. Therefore, the current code preserves a positive existing budget on the normal path and does not by itself explain the observed transition to$0.Before the automation ran, the team nevertheless reached
spend≈1.57, max_budget=0.0. The transition must be identified from deployment and LiteLLM logs. The relevant possibilities are:max_budget=0.0before registration;/team/infodid not return the existing team, after which_create_team()encountered an existing-team conflict and_update_team()applied the$0default;$0while preserving spend.With
create_user=True, onboarding deleted and recreated the stale LiteLLM user. The LiteLLM team and its financial state remained separate.The automation read
AUTOMATION_MODELand loaded the selected profile withworkspace.get_llm(). Its run log confirmedlitellm_proxy/glm-5.2.GLM-5.2 has zero input, output, and cache-read cost in staging and production. LiteLLM's
_team_max_budget_check()compared historical spend withmax_budgetwithout considering the model's zero configured cost and rejected the request.A Canvas conversation created during diagnosis also resolved to
gpt-5.5instead of its intended profile model. OpenHands#16342 fixes that path by resolving the effective profile model and sending it asllm_model.Fix locations
/team/info,/team/new, and/team/updatesequence increate_entries()to record the team ID and budget before and after reconciliation._create_team(), re-fetch the team and preserve its financial fields before calling_update_team().max_budget=10, spend=1.57, runcreate_entries(create_user=True), and assert both values remain unchanged._team_max_budget_check()so a model with trusted server-side zero pricing can run when paid credits are exhausted.This issue was created by an OpenHands AI agent on behalf of Graham Neubig.