Without STORE_MODEL_IN_DB=True the proxy still writes OAuth credentials
to LiteLLM_CredentialsTable, but ``proxy_config.get_credentials`` — the
DB → ``litellm.credential_list`` reload run at startup — lives inside
``if store_model_in_db is True:`` in proxy_server.py. Result: silent
data loss on restart and request-time ``api_key: oauth:<name>`` failures
because the name is no longer in the in-memory cache.
Gate the /start endpoints (both ChatGPT and Copilot) behind
``get_secret_bool("STORE_MODEL_IN_DB", False)``. Admins get a clear 400
up front instead of sitting through the 15-minute device-code poll only
to discover the flow silently doesn't persist.
Tests: setenv STORE_MODEL_IN_DB=True in the autouse fixture so the
success-path tests don't each have to opt in, plus one new test per
provider asserting 400 + message when the env var is absent. 167 cases
pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported error during device-code login:
Tokens obtained but DB persist failed: <asyncio.locks.Event object at
0x... [unset]> is bound to a different event loop
Root cause: the background worker was a ``threading.Thread`` that ran
``asyncio.run(persist_credential_to_db(item))``. That creates a fresh
event loop in the worker thread, but ``prisma_client``'s internal
asyncio primitives (locks, futures) are bound to the proxy's main loop.
Awaiting a prisma call from the worker loop hits the cross-loop error.
Fix:
- Refactor the background flow from a thread to an asyncio task
(``asyncio.create_task``) scheduled on the proxy's main loop. Blocking
IO in the flow (device-code poll, token exchange) runs in
``loop.run_in_executor``. The DB persist step (``await
persist_credential_to_db(item)``) now naturally shares a loop with
prisma_client. Same treatment for both ChatGPT and Copilot endpoints.
- For the DBAuthenticator refresh path (called from sync
``validate_environment`` during a request, same cross-loop risk), add
``_register_proxy_main_loop`` + ``_schedule_db_persist`` now prefers
``asyncio.run_coroutine_threadsafe`` onto the registered loop.
``/chatgpt/oauth/start`` and ``/copilot/oauth/start`` register the loop
on invocation (guaranteed to be the proxy's main loop). Falls back to
the old thread + ``asyncio.run`` path only when no loop is registered
(CLI / tests, where prisma_client is ``None`` anyway).
Tests updated: ``TestBackgroundWorker`` now drives the async task
directly via ``await _run_device_code_flow_async(...)``. The
``test_creates_session_and_spawns_worker`` test stubs the task to a
no-op instead of patching ``threading.Thread``.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add "Sign in with ChatGPT" and "Sign in with GitHub Copilot" device-code
OAuth flows to the Add Credential modal. Tokens persist as encrypted JSON
in LiteLLM_CredentialsTable and are picked up at request time when a
model's api_key is set to "oauth:<credential_name>"; a DBAuthenticator
subclass reads from the in-memory credential cache (sync) and writes
refreshed tokens back via a fire-and-forget worker thread.
Per-row Refresh button rotates tokens on demand — ChatGPT via the IdP's
refresh_token grant, Copilot by re-deriving the short-lived API key from
the stored GitHub access token.
Also ships a litellm-chatgpt-login CLI with an optional PKCE+loopback
flow alongside the existing device-code path for local sign-in outside
the UI.
Provider-specific surface lives in new files under
litellm/llms/{chatgpt,github_copilot}/db_authenticator.py and
litellm/proxy/{chatgpt,copilot}_oauth_endpoints/. Shared-file delta is
~275 lines across 10 pre-existing files, the bulk being a pure append
at the end of networking.tsx.
Dispatch convention + operational caveats documented in the new
"ChatGPT / Copilot OAuth Credentials" section of CLAUDE.md.
Security:
- /chatgpt/oauth/* and /copilot/oauth/* endpoints require PROXY_ADMIN
(view-only admins excluded from write paths)
- session_id query params are Query(...) annotated
- session-cap check + slot reservation are atomic; reserved slot is
cleaned up on device-code failure
- PKCE loopback callback html.escape()s the IdP's error_description
before rendering
Tests: 165 cases covering DBAuthenticator read/write, config dispatch,
endpoint admin-only, 429 cap, slot cleanup, background-worker success /
auth-failure / DB-persist-failure paths, PKCE helpers + XSS regression,
CLI broad-exception + KeyboardInterrupt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Unit Tests: Proxy DB Operations / proxy-db (auth-checks, tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py, 20, 8) (push) Waiting to run
Unit Tests: Proxy DB Operations / proxy-db (remaining, tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py, 30, 8) (push) Waiting to run
Brings the date-range branch in line with the non-date-range branch which
already hashes sk- prefixed tokens before querying. Adds coverage for
filter-combination behavior in view_spend_logs.
- Log a warning when dropping callback params that carry os.environ/
references so operators notice the misconfiguration.
- Require absolute paths in oidc/file/ and correct the documented
example to use the leading-slash form.
- Drop the unused return value from _reject_os_environ_references.
- Reject os.environ/ references supplied via /health/test_connection
request params instead of resolving them; config-sourced values are
already resolved before reaching the endpoint.
- Skip os.environ/ references in dynamic callback params loaded from
per-request metadata.
- Constrain oidc/file/ to an allowed credential directory allowlist
(defaults to /var/run/secrets and /run/secrets, overridable via
LITELLM_OIDC_ALLOWED_CREDENTIAL_DIRS).
Budget table entries (team members, end-users) used duration_in_seconds()
for a sliding-window reset, while keys/users/teams used calendar-aligned
get_budget_reset_time(). This made "30d" and "1mo" mean different things
depending on entity type. Now both paths use get_budget_reset_time() for
consistent calendar-aligned resets (e.g. "30d" → 1st of next month).
Fixes#25432
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two code paths in key_management_endpoints.py call hash_token()
unconditionally when invalidating the user_api_key_cache after a key
update. When the caller passes a pre-hashed token ID (not an sk-
prefixed key), hash_token() double-hashes it, producing a cache key
that does not match the actual cached entry. Cache invalidation
silently fails.
This is compounded by update_cache() which writes the stale cached key
object back with a fresh 60s TTL after every successful request,
preventing natural TTL expiry. The stale entry (with outdated fields
like max_budget=None) persists indefinitely under load.
PR #24969 fixed this in update_key_fn but missed two other call sites:
- _process_single_key_update (bulk update path)
- _execute_virtual_key_regeneration (key rotation path)
Fix: replace hash_token() with _hash_token_if_needed() in both
locations, matching the pattern already used elsewhere in the file.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename disable_global_guardrail → disable_global_guardrails to match
the key name used by litellm_pre_call_utils.py, the API endpoints,
and the UI when propagating key/team metadata.
The singular form was introduced in PR #16983 and has never matched
the plural form written by the rest of the codebase, so the feature
silently did nothing.
Re-applies fix originally from #25488. Original commit could not be
merged due to missing signature.
Co-Authored-By: Remi Mabon <remi.mabon@redcare-pharmacy.com>
- Add _verify_org_access to deprecated POST /organization/info endpoint
- Move get_user_object to module-level import in organization_endpoints.py
- Add tests for _verify_team_access 403 denial path
- Introduced a new method in `FileContentStreamingHandler` to resolve streaming request parameters, enhancing the routing logic based on credentials.
- Updated the `should_stream_file_content` method to check against supported providers.
- Cleaned up type hints and imports across multiple files for better organization and clarity.
- Added comprehensive tests to validate the new routing behavior and ensure original data integrity during streaming requests.
* feat(guardrails): optional skip system message in unified guardrail inputs
Made-with: Cursor
* feat(dashboard): skip_system_message_in_guardrail in guardrail UI
Add a tri-state control (inherit / yes / no) when creating or editing
guardrails so admins can set litellm_params.skip_system_message_in_guardrail
without YAML. Table edit merges existing litellm_params before PUT to avoid
wiping content-filter and other provider fields.
Document the dashboard flow in the guardrails quick start with a screenshot.
Made-with: Cursor
* fix(guardrails): type structured_messages as AllMessageValues for mypy
Use AllMessageValues in openai_messages_without_system and cast adapter
request messages so GenericGuardrailAPIInputs matches TypedDict.
Made-with: Cursor
- Changed the import path for `upload_file_to_storage_backend` in test files to reflect the new module structure.
- Ensured consistency in mocking for storage backend service tests.
The raw SQL queries in spend_management_endpoints.py, spend_tracking_utils.py,
and analytics_endpoints.py cast date params to ::timestamptz while comparing
against the plain-timestamp "startTime" column. Postgres resolves the type
mismatch by promoting the column using the DB session timezone, which drifts
the filter window and date_trunc buckets whenever session TZ is not UTC —
silently dropping rows at UTC day boundaries and, for narrow windows, losing
rows entirely.
Wraps every such comparison with `AT TIME ZONE 'UTC'` so the param side
resolves to a plain timestamp matching the column type. Both sides end up
as plain timestamp, Postgres does no implicit conversion, and session TZ
plays no role in the query. The fix is constant-foldable so the existing
startTime index (PR #17504) remains usable.
Also marks strptime-produced datetimes as tz-aware UTC at the call sites
for intent clarity and consistency with parse_date.
Fixes#22529 (Logs page missing recent rows under non-UTC session TZ) and
the Global Usage single-day-returns-two-days symptom reported internally.
- Added validation to ensure credentials include a custom LLM provider before routing.
- Cleaned up type casting for better readability.
- Introduced a new test to verify behavior when a non-OpenAI provider is used, ensuring proper handling of streaming responses.
- Updated imports to include necessary modules for testing.
- Updated `FileContentStreamingHandler` to utilize `custom_llm_provider` from credentials for routing.
- Added error handling for missing `custom_llm_provider` in credentials.
- Introduced new tests to validate streaming behavior with routed providers and non-OpenAI providers.
- Cleaned up imports and ensured proper type casting for improved clarity.
- Static Methods for Streaming Handler Function
- Remove the afile_content_streaming wrapper function. Enabled with a stream boolean in afile_content
- Cleaned up test cases after refactor
- Removed unused imports and streamlined type hints in `litellm/utils.py` and `litellm/files/main.py`.
- Moved `FileContentStreamingResult` to a new `litellm/files/types.py` for better organization.
- Updated `FileContentStreamingResponse` in `litellm/files/streaming.py` to include asynchronous close methods and improved logging capabilities.
- Enhanced tests to ensure proper closure of streaming iterators in `tests/test_litellm/llms/openai/test_openai_file_content_streaming.py` and `tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py`.
- Route multipart forwarding on forward_multipart instead of empty _parsed_body
so litellm_logging_obj no longer forces json= for file uploads.
- Remove custom_body from pass-through endpoint signatures; FastAPI treated it
as a JSON body and rejected multipart before the handler ran. Bedrock passes
JSON via request.state (LITELLM_PASS_THROUGH_CUSTOM_BODY_STATE_KEY).
- Use build_request + send(stream=True) for streaming multipart; httpx 0.28
AsyncClient.request does not accept stream=.
- Add regression test for non-empty _parsed_body multipart path; update Bedrock
custom-body test and query-params test for forward_multipart.
Made-with: Cursor
Non-admins previously skipped RBAC when prisma_client was None but could
still read payloads from custom loggers. Return 403 unless admin view.
Add test_ui_view_request_response_forbids_non_admin_without_db.
Made-with: Cursor
- Drop module-level common_utils import; import team helpers inside callers.
- Inline admin-view role check in _is_admin_view_safe to break import cycle.
- Require non-null row.user before treating spend log as owned by the key
(fixes None==None bypass for service keys).
- Document deferred proxy_server imports in _get_permitted_team_ids_for_spend_logs.
- Update tests (common_utils patches, regression test, ruff cleanups).
Made-with: Cursor