Conversation
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
saurya
added a commit
that referenced
this pull request
Sep 23, 2026
Fixes a regression found while fleet-testing ENABLE_LITELLM=false (#474): saving personal LLM settings for a member provisioned while the flag is off returned 500 "Something went wrong storing settings" for any managed (openhands/*) model. Root cause: with ENABLE_LITELLM off, new-user provisioning (LiteLlmManager.create_entries) correctly skips minting a LiteLLM key, leaving agent_settings.llm.api_key as None. That None flowed into OrgMember.llm_api_key = None, whose setter (unlike its llm_api_key_for_byor sibling) unconditionally encrypted the raw value -- "successfully" JWE-encrypting a null payload into the NOT NULL _llm_api_key column. On the next settings save, decrypting that row produces a Python None wrapped in SecretStr, and pydantic's SecretStr.__len__/__bool__ call len(None) unconditionally, raising TypeError instead of reporting "empty". That TypeError isn't a ValueError, so store_settings' generic `except Exception` handler swallowed it into an opaque 500. - storage/org_member.py: the llm_api_key setter now normalizes None to '' before encrypting, matching the "empty means no key" convention has_real_api_key/llm_api_key_is_set already use. - storage/saas_settings_store.py: _ensure_api_key now checks has_real_api_key() instead of a bare bool()/truthiness check, as defense-in-depth against any already-poisoned rows (e.g. rows written before this fix, or via any other future code path that passes None through the same setter). - tests/unit/test_models.py: new regression test constructing an OrgMember with llm_api_key=None and asserting the round-tripped key is falsy/empty rather than raising. - tests/unit/test_saas_settings_store.py: new regression test calling _ensure_api_key with a SecretStr(None) fallback key and asserting a fresh key is generated instead of raising. Reproduced directly against the ENABLE_LITELLM=false fleet VM (shared-2) via a live Python script inside the running pod, confirmed identical to the reported traceback, then verified all four tests (2 new + existing suite) pass with the fix. Co-authored-by: openhands <openhands@all-hands.dev>
saurya
added a commit
to OpenHands/OpenHands-Cloud
that referenced
this pull request
Sep 23, 2026
Picks up OpenHands/enterprise#474's settings-save 500 fix (sha-9dc7e70), found while fleet-testing ENABLE_LITELLM=true/false. Co-authored-by: openhands <openhands@all-hands.dev>
saurya
added a commit
to OpenHands/OpenHands-Cloud
that referenced
this pull request
Sep 23, 2026
Picks up OpenHands/enterprise#474's settings-save 500 fix (sha-9dc7e70), found while fleet-testing ENABLE_LITELLM=true/false. Co-authored-by: openhands <openhands@all-hands.dev>
saurya
added a commit
to OpenHands/OpenHands-Cloud
that referenced
this pull request
Sep 23, 2026
Picks up OpenHands/enterprise#474's managed-key mint/rotate guard fix (sha-a8024c3). Co-authored-by: openhands <openhands@all-hands.dev>
saurya
added a commit
to OpenHands/OpenHands-Cloud
that referenced
this pull request
Sep 23, 2026
Picks up OpenHands/enterprise#474's managed-key mint/rotate guard fix (sha-a8024c3). Co-authored-by: openhands <openhands@all-hands.dev>
Introduces ENABLE_LITELLM as a database-managed feature flag (env-var
fallback default true, mirroring the ENABLE_BILLING pattern) that gates
every network-touching path in the app on whether this deployment may
contact the LiteLLM gateway.
Backend:
- storage/lite_llm_manager.py: new is_litellm_enabled() runtime resolver;
every existing "LiteLLM not configured" guard (28 call sites, plus the
with_http_client transport wrapper and verify_key) now also checks
the flag, so a disabled deployment never opens a connection to LiteLLM.
create_entries (new-user provisioning) now succeeds without ever
creating a LiteLLM user/team/key when disabled, instead of failing.
- Budgets (server/routes/orgs.py, org_budget_service.py,
run_budget_preflight.py): reads/writes/maintenance/preflight/alerts are
rejected or skipped outright with a clear "enable ENABLE_LITELLM" error,
rather than silently degrading.
- Managed/BYOR LLM keys (server/routes/api_keys.py): refresh/generate
endpoints are rejected when disabled; regular OpenHands application API
keys (create/list/delete, current-key info) are untouched.
- OpenHands/managed model discovery
(server/verified_models/litellm_proxy_model_router.py): returns an empty
managed-model list (BYOK catalogue only) instead of fetching from the
proxy.
- Usage monitoring (org_conversation_service.py): omits stale
budget_monthly_limit/budget_is_disabled fields when LiteLLM is disabled
so per-user usage stats don't imply enforcement that isn't happening;
conversation/usage stats themselves are untouched (OHE's own data).
- Org creation/membership (storage/org_service.py, org_store.py,
org_member_service.py) already routed exclusively through
LiteLlmManager, so they're covered by the manager-level gate with no
further route changes needed.
- New enable_litellm field on WebClientFeatureFlags, threaded through
both config paths (DefaultWebClientConfigInjector + legacy
ServerConfig.get_config), DB-resolved on every request like
enable_billing.
Frontend:
- New FeatureDisabledScreen shared component ("Please enable LiteLLM to
use this feature").
- Budgets and "Your budget" pages render that screen (skipping their
fetches) instead of the full feature UI; their nav items are hidden.
Tests:
- New unit tests asserting the disabled path never constructs an
httpx.AsyncClient across create_entries, verify_key,
get_user_team_info, get_team_members_financial_data,
remove_user_from_team, delete_team, ensure_free_team_models, and
sync_free_model_allowlists.
- New frontend test asserting the Budgets screen shows the placeholder
and skips its data fetch when the flag is off.
Co-authored-by: openhands <openhands@all-hands.dev>
Fixes a regression found while fleet-testing ENABLE_LITELLM=false (#474): saving personal LLM settings for a member provisioned while the flag is off returned 500 "Something went wrong storing settings" for any managed (openhands/*) model. Root cause: with ENABLE_LITELLM off, new-user provisioning (LiteLlmManager.create_entries) correctly skips minting a LiteLLM key, leaving agent_settings.llm.api_key as None. That None flowed into OrgMember.llm_api_key = None, whose setter (unlike its llm_api_key_for_byor sibling) unconditionally encrypted the raw value -- "successfully" JWE-encrypting a null payload into the NOT NULL _llm_api_key column. On the next settings save, decrypting that row produces a Python None wrapped in SecretStr, and pydantic's SecretStr.__len__/__bool__ call len(None) unconditionally, raising TypeError instead of reporting "empty". That TypeError isn't a ValueError, so store_settings' generic `except Exception` handler swallowed it into an opaque 500. - storage/org_member.py: the llm_api_key setter now normalizes None to '' before encrypting, matching the "empty means no key" convention has_real_api_key/llm_api_key_is_set already use. - storage/saas_settings_store.py: _ensure_api_key now checks has_real_api_key() instead of a bare bool()/truthiness check, as defense-in-depth against any already-poisoned rows (e.g. rows written before this fix, or via any other future code path that passes None through the same setter). - tests/unit/test_models.py: new regression test constructing an OrgMember with llm_api_key=None and asserting the round-tripped key is falsy/empty rather than raising. - tests/unit/test_saas_settings_store.py: new regression test calling _ensure_api_key with a SecretStr(None) fallback key and asserting a fresh key is generated instead of raising. Reproduced directly against the ENABLE_LITELLM=false fleet VM (shared-2) via a live Python script inside the running pod, confirmed identical to the reported traceback, then verified all four tests (2 new + existing suite) pass with the fix. Co-authored-by: openhands <openhands@all-hands.dev>
Found via a second live repro against the fleet VM after the first fix (9dc7e70): saving settings for an *existing* user whose default model is openhands/* (virtually every existing user/org) still 500'd with ENABLE_LITELLM off, because ValueError('LiteLLM API configuration not found') propagated straight out of LiteLlmManager.generate_key -- the managed-key ensure/rotate helpers had no flag check of their own; they relied on their LiteLLM-not-configured guard, which only trips when the env vars are unset, not when the flag disables an otherwise fully configured gateway. - storage/saas_settings_store.py (_ensure_api_key): returns immediately when disabled, leaving the existing key field untouched, instead of unconditionally trying to verify/rotate/generate a managed key. - storage/org_store.py (_ensure_managed_llm_key_for_user): same guard for the org-defaults save path. - server/maintenance_task_processor/managed_llm_key_ownership_processor.py: skips the whole batch (reporting all targets as skipped) instead of letting every target fail individually and get logged as a spurious repair error. - tests/unit/test_saas_settings_store.py, tests/unit/test_org_store.py: new regression tests asserting no LiteLlmManager call is made and the personal/org-defaults save paths return successfully when the flag is off. Found by reproducing the settings-save flow end-to-end inside the running shared-2 (ENABLE_LITELLM=false) fleet pod, on the image built from the prior None-key fix (9dc7e70): a synthetic org/user/member with no LiteLLM key on record and an openhands/* model still raised ValueError out of LiteLlmManager.generate_key. Will redeploy the fleet VMs on the image built from this commit to re-verify live. Co-authored-by: openhands <openhands@all-hands.dev>
_set_team_blocked and _block_team landed on main after this branch was cut. with_http_client passes client=None when LiteLLM is disabled and relies on each wrapped function re-checking the flag; these two did not, so they raised AttributeError instead of no-oping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
dylan-openhands
force-pushed
the
enable-litellm-flag
branch
from
September 24, 2026 23:45
a8024c3 to
2c6b768
Compare
2 of 6 tasks
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces
ENABLE_LITELLMas a deployment-wide setting (database-managed feature flag, env-var fallback, defaulttruefor backward compatibility). With the flag off, OHE never contacts the external LiteLLM gateway.This addresses each of the requested behaviors when the flag is off:
LiteLlmManager.create_entriesnow succeeds without creating a LiteLLM user/team or generating a gateway key — the user is left to configure a direct provider (BYOK).LiteLlmManager, so the manager-level gate covers them — OHE records update, LiteLLM sync is skipped.LiteLLMProxyModelServicereturns an empty managed-model list (BYOK catalogue only) instead of querying the proxy.FeatureDisabledScreen("Please enable LiteLLM to use this feature") instead of fetching; the backend budget routes, maintenance job, and preflight job reject/skip outright.budget_monthly_limit/budget_is_disabled; conversation/usage stats themselves (OHE's own tracking) are unaffected.ENABLE_BILLINGflag; LiteLLM-backed reads (e.g.get_credits) degrade gracefully to$0.How it works
storage/lite_llm_manager.pygets a newis_litellm_enabled()runtime resolver, mirroring the existing_is_billing_enabled()pattern (DB-flag-row wins, env var is the fallback,ENABLE_LITELLMdefaults totrue).LiteLlmManager, plus the sharedwith_http_clienttransport wrapper andverify_key, now also check this flag — so a disabled deployment never constructs anhttpx.AsyncClienttargeting LiteLLM, let alone sends a request.WebClientFeatureFlags.enable_litellmis threaded through to the frontend (both the new and legacy config paths), DB-resolved on every request likeenable_billing.FeatureDisabledScreenshared component renders "Please enable LiteLLM to use this feature."; Budgets/Your-Budget wire it in and skip their data fetches; their nav entries are hidden.Testing
tests/unit/test_lite_llm_manager.py— new tests assert the disabled path returns the existing safe fallback (None/False/{}/[]) and never constructs anhttpx.AsyncClient, acrosscreate_entries,verify_key,get_user_team_info,get_team_members_financial_data,remove_user_from_team,delete_team,ensure_free_team_models,sync_free_model_allowlists.getBudgetSettingsis never called when the flag is off.pytest tests/unit): 6045 passed (2 pre-existing, unrelated failures — S3 testcontainers region config and a macOS-specific tmp-dir path assertion — reproduce identically onmain).vitest run): same 151 pre-existing failures onmainand on this branch (verified viagit stash); no new failures introduced.pre-commit run --config ./dev_config/python/.pre-commit-config.yaml(ruff, mypy) andnpm run lint:fix+npm run buildboth pass clean.Manual/exploratory test plan (flag OFF)
LITE_LLM_API_URLrequests are made (check LiteLLM proxy access logs / a network capture) and the user lands with no LLM configured (must add a direct provider key in Settings).skipped: litellm_disabledin logs.POST /api/keys/llm/managed/refreshand/api/keys/llm/byor/refresh; confirm 403; confirm regular API key create/list/delete still works.ENABLE_BILLING=trueandENABLE_LITELLM=false, confirm/api/billing/creditsreturns$0.00rather than erroring.Deployment verification (Replicated fleet)
Plan (not yet executed — pending confirmation before touching shared sandbox infra):
make releaseon this branch inOpenHands/OpenHands-Cloudpublishes to a per-branch Replicated channel named after the branch.OpenHands/infra'sreplicated_fleet.yamlworkflow (clean-installop) against two fleet VMs (e.g.shared-1withENABLE_LITELLM=trueandshared-2withENABLE_LITELLM=falsein that VM's config-values), pointed at theenable-litellm-flagchannel, to compare behavior side-by-side.This PR description was drafted by an AI agent (OpenHands) on behalf of the user.
Enterprise server image for this PR: