Skip to content

Stale LiteLLM spend under a /bin/bash team budget blocks free GLM automations #175

Description

@neubig

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

  1. The original LiteLLM team accumulated approximately $1.57 of spend.

  2. The Keycloak user was deleted without deleting the corresponding OpenHands and LiteLLM records, so LiteLLM could retain that user/team and its spend.

  3. 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).

  4. 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.

  5. 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.
  6. With create_user=True, onboarding deleted and recreated the stale LiteLLM user. The LiteLLM team and its financial state remained separate.

  7. The automation read AUTOMATION_MODEL and loaded the selected profile with workspace.get_llm(). Its run log confirmed litellm_proxy/glm-5.2.

  8. 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.

  9. 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

  1. 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.
  2. In the existing-team conflict branch of _create_team(), re-fetch the team and preserve its financial fields before calling _update_team().
  3. 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.
  4. Update _team_max_budget_check() so a model with trusted server-side zero pricing can run when paid credits are exhausted.
  5. Merge and validate OpenHands#16342.
  6. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions