Commit graph

3 commits

Author SHA1 Message Date
Jason Cook
9167f9725b feat(oauth): refuse /start when STORE_MODEL_IN_DB is not set
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>
2026-04-23 11:48:02 -04:00
Jason Cook
c3932e06d4 fix(oauth): run the OAuth worker on the proxy's main asyncio loop
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>
2026-04-23 10:48:59 -04:00
Jason Cook
98741dff72 feat(chatgpt, github_copilot): OAuth sign-in + token refresh in proxy UI
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>
2026-04-16 20:30:10 -04:00