litellm/tests/test_litellm/proxy
Sameer Kankute c792df64d2
feat(mcp): support stateless and stateful clients via session-id routing (#26857)
* feat(mcp): support stateless and stateful clients via session-id routing

- Add session_manager_stateful (stateless=False) alongside stateless
- Route by mcp-session-id: has ID → stateful, initialize (no ID) → stateful, else → stateless
- Peek POST body to detect initialize for routing; replay via wrapped receive
- Handle stale session IDs for both managers
- Add test_mcp_routing_initialize_to_stateful_no_session_to_stateless
- Update test_valid_mcp_session_id_is_preserved, test_concurrent_initialize_session_managers

Made-with: Cursor

* fix(mcp): respect stateful routing and harden initialize detection

Ensure streamable MCP requests are dispatched via the computed target session manager, and guard initialize detection against non-object JSON bodies. Update stale-session test patches to target the stateful manager so routing assertions remain correct.

Made-with: Cursor

* test(mcp): patch stateless/stateful managers in concurrency init test

Update concurrent session-manager initialization test to patch session_manager_stateless and session_manager_stateful directly, matching initialize_session_managers() behavior and preventing NameError from undefined mocks.

Made-with: Cursor

* Fix tests

* Fix tests

* Fix MCP stateful routing edge cases

* Fix stateful MCP auth context refresh

* Fix MCP stateful session cleanup

* fix(mcp): bind stateful sessions to creator and reject hijacks

Stateful mcp-session-id was usable by any authenticated proxy caller. Track
the session creator's hashed API key (or user_id) when a new session is
issued and reject mismatched callers with 403 before _set_or_update_auth_context
overwrites the stored MCPAuthenticatedUser. Also formats nested with-statements
in test_mcp_stale_session.py and fixes a pre-existing AsyncMock mismatch in
test_stale_mcp_session_id_is_stripped.

* fix(mcp): serialize concurrent requests on same stateful session

Bugbot's 'Concurrent requests share context' finding: _update_auth_context
mutates the single MCPAuthenticatedUser stored per session in place on
every request, so two requests sharing one mcp-session-id can overwrite
each other's mcp_servers / auth headers / oauth state / client_ip while
in-flight callbacks are still reading the same object.

Owner-binding alone narrows this to same-principal racing, but the
in-place mutation race remains. Add a per-session asyncio.Lock around
handle_request so concurrent same-session requests run sequentially. The
lock is allocated on demand and torn down with the rest of the session
state on DELETE / idle expiry.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(mcp): include OAuth2 bearer in stateful session owner fingerprint

UserAPIKeyAuth() for OAuth2 passthrough has no api_key/user_id, so every
OAuth caller fingerprinted to "anonymous" and could hijack another OAuth
caller's mcp-session-id. Hash the upstream Authorization header into the
fingerprint as oauth:<sha256>.

* fix(mcp): don't hold stateful session lock for streaming GETs

The per-session lock wraps handle_request, so a long-lived GET (SSE
stream held open for the life of the session) would block every
subsequent POST on the same mcp-session-id. Only POST/DELETE mutate the
shared MCPAuthenticatedUser, so it's sufficient to serialize those —
GETs run lock-free and stream concurrently.

* fix(mcp): allow None user_api_key_auth in MCPAuthenticatedUser

The set_auth_context / _set_or_update_auth_context / _update_auth_context
helpers in server.py all accept Optional[UserAPIKeyAuth] and pass it
straight into MCPAuthenticatedUser, but the dataclass-style constructor
typed user_api_key_auth as required UserAPIKeyAuth. Mypy flagged this on
the stateful-routing branch:

  server.py:3227: error: Incompatible types in assignment (expression
    has type "UserAPIKeyAuth | None", variable has type "UserAPIKeyAuth")
  server.py:3255: error: Argument "user_api_key_auth" to
    "MCPAuthenticatedUser" has incompatible type "UserAPIKeyAuth | None";
    expected "UserAPIKeyAuth"

Widen the parameter type to Optional[UserAPIKeyAuth] to match the call
sites. Runtime behavior is unchanged.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* style: replace with new alias

* fix(mcp): fall back to client_ip in stateful session owner fingerprint

Addresses Greptile review on PR #26857: when no API key, user_id, or
OAuth bearer is available (e.g. unauthenticated/passthrough callers),
the owner fingerprint collapsed to a single 'anonymous' value, allowing
two unrelated callers to drive each other's stateful MCP sessions.

Fold client IP into the fingerprint as a fallback identity signal so
distinct anonymous sources do not share an owner identity.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Fix active stateful MCP session cleanup

* test(mcp): cancel leaked stateful auth-context cleanup task

initialize_session_managers() spawns a real asyncio.create_task running
_cleanup_expired_stateful_session_auth_contexts(). The
test_concurrent_initialize_session_managers test was saving and
restoring the session-manager context-manager globals but did not save,
cancel, or restore _stateful_auth_context_cleanup_task.

Because pyproject.toml sets asyncio_default_fixture_loop_scope=session,
the event loop is shared across tests in the same session, so the
leaked task kept running against module-level dicts for the rest of the
test run. Save and cancel the task in the finally block so the test
fully cleans up after itself.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Fix stateful MCP session fingerprinting

* Hash MCP session user owner fingerprints

* Fix stale MCP session DELETE cleanup

* fix(mcp): harden owner fingerprint hashing for non-str api keys

_owner_fingerprint_for assumed api_key/user_id supported .encode();
MagicMock-based tests (and any non-str truthy values) crashed with
TypeError before routing. Only hash str/bytes secrets; fall through
otherwise so MCP routing and session tests behave correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix MCP stateful cleanup loop resilience

* Fix stateful MCP initialize auth capture

* fix(mcp): drop orphan per-session lock when auth context absent

Defensive cleanup for _stateful_session_locks entries created on
sessions that never enter _stateful_session_auth_contexts. The
periodic cleanup loop only iterates auth_context_last_seen, so such
locks would otherwise live forever. Add a test that reproduces the
leak and verifies the request finalizer pops the lock.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* chore(mcp): trim verbose comment on lock cleanup

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Fix stateful MCP delete failure tracking

* fix test

* fix(mcp): cap routing-peek body size to bound pre-dispatch memory

Authenticated clients that POST without an mcp-session-id forced the proxy
to buffer the entire request body before routing, since the peek loop
drained every body chunk to decide whether the JSON-RPC method was
'initialize'. Cap the peek at 4 KB (more than enough for an initialize
envelope) and let the remainder stream through wrapped_receive into the
downstream handler.

* test: replace dall-e-3 with gpt-image-1 in health check and router tests (#27813)

OpenAI returns 'The model dall-e-3 does not exist' for the test account,
breaking test_openai_img_gen_health_check and test_image_generation.
Switch to gpt-image-1, matching the existing TestOpenAIGPTImage1 pattern.

* fix(tests): drop dall-e-only test classes; route live image tests via gpt-image-1

Second wave of failures from the 2026-05-12 DALL-E shutdown:
- tests/image_gen_tests/test_image_edits.py::TestOpenAIImageEditDallE2
  and tests/image_gen_tests/test_image_generation.py::TestOpenAIDalle3
  are explicitly named for the deprecated models and can't pass; remove.
  gpt-image-1 coverage already exists in sibling classes.
- tests/local_testing/test_router.py image gen tests use dall-e-3 only
  as a routing example; swap to gpt-image-1.
- tests/local_testing/test_custom_callback_input.py image_generation
  success/failure paths swapped to gpt-image-1.

* Fix MCP initialize session active tracking

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix MCP reinitialize session tracking

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Fix MCP reinitialize auth context aliasing

Co-authored-by: Yassin Kortam <yassin@berri.ai>

* Apply black formatting after merge

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Run owner-binding 403 before consuming POST body

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Harden MCP routing peek bound and stateful purge race

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Remove inadvertently committed Next.js build artifacts

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* Run owner check before stale MCP session cleanup

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* fix(mcp): reverse cleanup ordering to terminate transport before clearing owner

Reverses _purge_expired_stateful_session_auth_contexts so the transport
is popped from server_instances and terminated BEFORE owner/auth tracking
is cleared. The previous order left a window where _stateful_session_owners
was already empty but server_instances still served the session, so a
concurrent request would observe expected_owner is None and bypass the
owner-binding check. Addresses Greptile review on PR #26857.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>

* test(mcp): fully reset stateful session tracking in auth-context refresh test

Use _remove_stateful_session_tracking in teardown so the test no longer
leaks _stateful_session_auth_context_last_seen and _stateful_session_locks
between tests, matching the cleanup used by the sibling stateful tests.

* fix(mcp): cap concurrent stateful sessions per caller to bound memory

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
Co-authored-by: Sameerlite <sameerlite@users.noreply.github.com>
Co-authored-by: yuneng-jiang <yuneng@berri.ai>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
Co-authored-by: Claude Babysitter <claude@anthropic.com>
Co-authored-by: mateo-berri <mateo@berri.ai>
2026-05-29 20:30:36 -07:00
..
_experimental/mcp_server feat(mcp): support stateless and stateful clients via session-id routing (#26857) 2026-05-29 20:30:36 -07:00
agent_endpoints style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
anthropic_endpoints test: isolate proxy master_key/prisma_client module globals between tests 2026-04-23 15:31:16 -07:00
auth fix(proxy): enforce tag budgets for key-level tags (#29108) 2026-05-28 11:39:02 -07:00
client fix: scope CLI stored token to base_url to prevent cross-domain credential leakage (#26945) 2026-05-01 12:11:32 -07:00
common_utils fix(proxy): strip LiteLLM policy tracking from OpenAI batch metadata (#28425) 2026-05-26 11:35:42 -07:00
db feat: add componentized proxy deployment with gateway, backend, ui, and migrations (#27557) 2026-05-16 09:25:17 -07:00
discovery_endpoints style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
experimental/mcp_server Litellm fix GitHub action testing (#11163) 2025-05-26 14:41:42 -07:00
google_endpoints Gemini managed agents support (#28270) 2026-05-19 16:02:03 -07:00
guardrails feat(guardrails): wire apply_guardrail into proxy logging callbacks (#28970) 2026-05-28 09:41:02 -07:00
health_endpoints fix(proxy): expose db status on public /health/readiness 2026-05-13 13:02:38 -07:00
hooks fix(proxy): map stripped batch body.model to proxy alias for auth (#29264) 2026-05-29 19:58:04 -07:00
image_endpoints style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
management_endpoints fix(teams): expose keys_count on /v2/team/list and wire UI Resources badge (#28502) 2026-05-29 17:39:07 +05:30
management_helpers decouple S3 audit-log config via s3_audit_callback_params 2026-05-05 13:23:32 -07:00
memory Litellm memory improvements v2 (#26541) 2026-04-25 19:03:43 -07:00
middleware fix(proxy): point /metrics 401 at the opt-out flag 2026-05-08 18:09:14 -07:00
openai_files_endpoint Merge remote-tracking branch 'origin/litellm_internal_staging' into litellm_yj_apr17 2026-04-17 17:36:40 -07:00
pass_through_endpoints fix(proxy): Bedrock Knowledge Base pass-through: preserve SigV4 headers and signed request body (#27526) 2026-05-25 19:21:55 +05:30
policy_engine style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
prompts style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
proxy_server test(proxy): add harness for proxy_server.py behavior-pinning (#28827) 2026-05-25 20:26:44 -07:00
public_endpoints chore(proxy): keep public AI hub unauthenticated 2026-05-01 19:07:21 -07:00
rag_endpoints fix: harden /key/update authorization checks (#27878) 2026-05-14 04:16:04 +00:00
realtime_endpoints test: isolate proxy master_key/prisma_client module globals between tests 2026-04-23 15:31:16 -07:00
response_api_endpoints style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
spend_tracking fix(spend-logs): redact echoed prompts in error_information (LIT-2992) (#27689) 2026-05-13 22:11:24 -07:00
test_configs Litellm fix GitHub action testing (#11163) 2025-05-26 14:41:42 -07:00
types_utils chore(proxy): also scrub guardrail callbacks / module paths from DB overlay 2026-05-14 01:24:51 +00:00
ui_crud_endpoints chore(ci): merge dev branch (#28657) 2026-05-25 13:48:47 -07:00
vector_store_endpoints fix(vector_store): cache use-time embedding-config resolution 2026-05-03 10:27:53 +00:00
__init__.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
conftest.py test: isolate proxy master_key/prisma_client module globals between tests 2026-04-23 15:31:16 -07:00
test_aiohttp_cleanup_closed.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_aiohttp_session_recovery.py fix: use AsyncMock for concurrent test consistency 2026-03-18 00:54:23 +00:00
test_api_key_masking_in_errors.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_audio_speech_prometheus_hooks.py fix req changes 2026-02-28 21:32:57 +05:30
test_batch_expiry.py fix(proxy): strip LiteLLM policy tracking from OpenAI batch metadata (#28425) 2026-05-26 11:35:42 -07:00
test_batch_metadata_none_fix.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_batch_retrieve_bedrock.py fix(proxy/batches): forward model to retrieve_batch for bedrock 2026-04-29 22:48:03 +02:00
test_budget_reservation.py fix(proxy): gate image-gen reservation strictly on model mode 2026-05-09 09:16:27 -07:00
test_caching_routes.py [Bug Fix] Ensure /redis/info works on GCP Redis (#11732) 2025-06-14 15:35:09 -07:00
test_chat_completion_metadata.py fix: propagate JWT auth metadata to OTEL spans (#19627) 2026-01-23 21:21:23 -08:00
test_common_request_processing.py perf: reduce per-request and per-chunk overhead across Anthropic streaming hot paths (#28289) 2026-05-23 12:15:59 -07:00
test_component_allowlists.py feat: add componentized proxy deployment with gateway, backend, ui, and migrations (#27557) 2026-05-16 09:25:17 -07:00
test_cors_config.py refactor: extract _get_cors_config() for testability, fix no-op CORS tests 2026-04-11 22:24:04 +05:30
test_custom_proxy.py fix(ui/): fix routing for custom server root path (#15701) 2025-10-23 13:59:29 -07:00
test_dynamic_mcp_route.py feat(mcp): support MCP access group names in URL-based namespacing (#27726) 2026-05-13 20:20:38 -07:00
test_empty_model_list.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_enforce_user_param.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_fallback_management_endpoints.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_fastapi_offline_routes.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_filter_models_by_team_access_group.py fix: resolve access group names in _filter_models_by_team_id (#25224) 2026-05-01 11:55:36 +05:30
test_health_check_functions.py feat(proxy): skip disable_background_health_check models on GET /health when flag set (#27716) 2026-05-13 09:49:05 -07:00
test_health_check_max_tokens.py Litellm agent oss staging 05 11 2026 (#27733) 2026-05-13 14:09:12 -07:00
test_langfuse_passthrough_security.py chore(callbacks): guard dynamic integration hosts 2026-04-30 14:27:19 -07:00
test_lazy_openapi_snapshot.py [Fix] Tests: Move Misplaced Import in Lazy OpenAPI Snapshot Test 2026-05-01 14:24:53 -07:00
test_litellm_pre_call_utils.py fix(proxy): enforce tag budgets for key-level tags (#29108) 2026-05-28 11:39:02 -07:00
test_max_budget_env_var.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_mcp_asgi_response.py fix(mcp): surface upstream 401 for token-forwarding MCP servers (#27847) 2026-05-13 12:03:36 -07:00
test_model_dump_with_preserved_fields.py Fix_mapped tests part 2 2026-02-26 12:43:39 +05:30
test_model_id_header_propagation.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_model_info_default_limits.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_model_level_guardrails.py fix(proxy): run model-level post_call guardrails on streaming requests (#26922) 2026-05-07 11:53:03 -07:00
test_openapi_schema_validation.py fix(proxy): preserve HTTP operations when injecting WebSocket stubs into OpenAPI schema 2026-05-06 00:28:42 +02:00
test_pricing_field_strip.py fix(proxy): move pricing strip below the litellm_metadata JSON-string parse 2026-05-03 02:04:05 +00:00
test_prometheus_cleanup.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_provider_url_destination_guard.py chore(proxy): move URL-valued model/file_id guard from SDK to proxy 2026-05-02 02:17:46 +00:00
test_proxy_cli.py Add granian as a ASGI compliant web server. Provider better throughput stability, (#26027) 2026-05-21 19:08:37 -07:00
test_proxy_logging_hook_detection.py perf: reduce per-request and per-chunk overhead across Anthropic streaming hot paths (#28289) 2026-05-23 12:15:59 -07:00
test_proxy_server.py chore(admin-ui): regenerate static export with trailingSlash: true (#28112) 2026-05-25 21:06:50 -07:00
test_proxy_types.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_proxy_utils.py feat(otel): OTel-standard attributes on the proxy SERVER span (status code, route/path, preprocessing latency) (#28040) 2026-05-16 13:45:08 -07:00
test_pyroscope.py Implement normalize_nonempty_secret_str function to trim whitespace from secrets and treat empty values as unset. Update proxy_server to use this function for Grafana credentials. Enhance tests to validate the new normalization behavior. 2026-05-04 18:17:31 +00:00
test_redis_auth_cache_flag.py refactor(proxy): replace DualCache with UserApiKeyCache for user API key management 2026-04-28 19:15:03 +00:00
test_response_model_sanitization.py perf: eliminate per-request callback scanning on proxy hot path (#27858) 2026-05-14 09:28:31 -07:00
test_route_a2a_models.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_route_llm_request.py feat(proxy): fix vector store retrieve/list/update/delete without model (#27929) 2026-05-15 04:37:59 -07:00
test_sensitive_route_auth.py chore(proxy): guard sensitive public endpoints 2026-04-30 11:52:47 -07:00
test_shared_health_check.py feat(proxy): skip disable_background_health_check models on GET /health when flag set (#27716) 2026-05-13 09:49:05 -07:00
test_spend_log_cleanup.py fix(proxy): keep spend log cleanup running after batch failures and surface DB errors (#27303) 2026-05-06 18:39:15 +00:00
test_swagger_chat_completions.py fix(proxy): avoid misleading multi-method operation ids 2026-04-30 20:44:14 -07:00
test_team_member_update.py fix mapped tests (#12320) 2025-07-04 10:04:43 -07:00
test_team_org_move.py fix(team_endpoints): auto-add SSO team members to org on move (proxy admin only) (#26377) 2026-04-24 08:36:25 -07:00
test_tools_allowlist_enforcement.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_update_llm_router_resilience.py style: run black formatter on files from main merge 2026-04-17 13:02:59 -07:00
test_utils.py add NO_OPENAPI env var to disable /openapi.json endpoint (#25547) 2026-04-14 23:37:49 +05:30