From 012d9f6c0a3f6bbe8d284d2f74fe9b57bdd26835 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Thu, 11 Jun 2026 10:34:26 -0700 Subject: [PATCH 01/44] feat(rate-limiter): allow opting out of v3 TPM reservation and Redis circuit breaker (#30211) --- litellm/caching/redis_cache.py | 16 ++- litellm/constants.py | 3 + .../hooks/parallel_request_limiter_v3.py | 28 +++-- scripts/health_check/health_check_client.py | 4 +- tests/test_litellm/caching/test_dual_cache.py | 61 ++++++++++ .../hooks/test_parallel_request_limiter_v3.py | 108 ++++++++++++++++++ 6 files changed, 208 insertions(+), 12 deletions(-) diff --git a/litellm/caching/redis_cache.py b/litellm/caching/redis_cache.py index cb9ce475d30..7239bea7853 100644 --- a/litellm/caching/redis_cache.py +++ b/litellm/caching/redis_cache.py @@ -22,6 +22,7 @@ import litellm from litellm._logging import print_verbose, verbose_logger from litellm.constants import ( DEFAULT_REDIS_MAJOR_VERSION, + REDIS_CIRCUIT_BREAKER_ENABLED, REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD, REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT, ) @@ -114,15 +115,23 @@ class RedisCircuitBreaker: OPEN = "open" HALF_OPEN = "half_open" - def __init__(self, failure_threshold: int, recovery_timeout: int) -> None: + def __init__( + self, + failure_threshold: int, + recovery_timeout: int, + enabled: bool = True, + ) -> None: self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout + self.enabled = enabled self._failure_count = 0 self._opened_at: Optional[float] = None self._state = self.CLOSED def is_open(self) -> bool: """Returns True if Redis calls should be skipped.""" + if not self.enabled: + return False if self._state == self.HALF_OPEN: # Probe already in flight — fast-fail all concurrent requests. # Only the one call that caused the OPEN→HALF_OPEN transition @@ -136,6 +145,8 @@ class RedisCircuitBreaker: return False def record_failure(self) -> None: + if not self.enabled: + return self._failure_count += 1 self._opened_at = time.time() if self._failure_count >= self.failure_threshold: @@ -149,6 +160,8 @@ class RedisCircuitBreaker: self._state = self.OPEN def record_success(self) -> None: + if not self.enabled: + return if self._state == self.HALF_OPEN: verbose_logger.info("Redis circuit breaker CLOSED — Redis recovered") self._failure_count = 0 @@ -243,6 +256,7 @@ class RedisCache(BaseCache): self._circuit_breaker = RedisCircuitBreaker( failure_threshold=REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD, recovery_timeout=REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT, + enabled=REDIS_CIRCUIT_BREAKER_ENABLED, ) self._setup_health_pings() diff --git a/litellm/constants.py b/litellm/constants.py index 57f55e6c177..ab8e57d735f 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -398,6 +398,9 @@ REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD = int( REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT = int( os.getenv("REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT", 60) ) +REDIS_CIRCUIT_BREAKER_ENABLED = ( + os.getenv("REDIS_CIRCUIT_BREAKER_ENABLED", "true").lower() == "true" +) # Default Redis major version to assume when version cannot be determined # Using 7 as it's the modern version that supports LPOP with count parameter DEFAULT_REDIS_MAJOR_VERSION = int(os.getenv("DEFAULT_REDIS_MAJOR_VERSION", 7)) diff --git a/litellm/proxy/hooks/parallel_request_limiter_v3.py b/litellm/proxy/hooks/parallel_request_limiter_v3.py index 6b70cea65a3..f45c63d1380 100644 --- a/litellm/proxy/hooks/parallel_request_limiter_v3.py +++ b/litellm/proxy/hooks/parallel_request_limiter_v3.py @@ -313,6 +313,14 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): self.window_size = int(os.getenv("LITELLM_RATE_LIMIT_WINDOW_SIZE", 60)) + # When disabled, TPM is enforced post-call from actual usage (pre-v1.82 + # behavior) instead of reserving an estimated budget upfront, shedding + # the extra per-request Redis Lua round-trip and the global-lock + # in-memory fallback that the reservation path incurs. + self.tpm_reservation_enabled = ( + os.getenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", "true").lower() == "true" + ) + # Batch rate limiter (lazy loaded) self._batch_rate_limiter: Optional[Any] = None @@ -2113,17 +2121,19 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): # Only check rate limits if we have descriptors with actual limits if descriptors: # First pass: RPM and max_parallel_requests sliding-window check. - # `skip_tpm_check=True` tells should_rate_limit to ignore each - # descriptor's tokens_per_unit so its +1-per-key Lua / in-memory - # increment never touches the :tokens counters — those are owned - # exclusively by the atomic reserve_tpm_tokens path below. Without - # this, every concurrent in-flight request would pre-inflate the - # :tokens counter by 1, shrinking the effective TPM budget by N - # and causing false-positive 429s under bursts. + # When reservation is enabled, `skip_tpm_check=True` tells + # should_rate_limit to ignore each descriptor's tokens_per_unit so + # its +1-per-key Lua / in-memory increment never touches the + # :tokens counters — those are owned exclusively by the atomic + # reserve_tpm_tokens path below. Without this, every concurrent + # in-flight request would pre-inflate the :tokens counter by 1, + # shrinking the effective TPM budget by N and causing + # false-positive 429s under bursts. When reservation is disabled, + # this pass enforces TPM directly from the post-call counters. response = await self.should_rate_limit( descriptors=descriptors, parent_otel_span=user_api_key_dict.parent_otel_span, - skip_tpm_check=True, + skip_tpm_check=self.tpm_reservation_enabled, ) if response["overall_code"] == "OVER_LIMIT": @@ -2153,7 +2163,7 @@ class _PROXY_MaxParallelRequestsHandler_v3(CustomLogger): ] has_tpm_limits = bool(configured_tpm_limits) - if has_tpm_limits: + if has_tpm_limits and self.tpm_reservation_enabled: min_configured_tpm_limit = min(configured_tpm_limits) # When the configured TPM cap is small enough to constrain the diff --git a/scripts/health_check/health_check_client.py b/scripts/health_check/health_check_client.py index 497fd6271b4..9ef8b934961 100644 --- a/scripts/health_check/health_check_client.py +++ b/scripts/health_check/health_check_client.py @@ -54,7 +54,7 @@ class LiteLLMHealthCheckClient: timeout: Request timeout in seconds (default: 120, matching Go implementation) completion_prompt: Test prompt for chat/completion models embedding_text: Test text for embedding models - custom_auth_header: Optional custom header name for authentication (e.g., "x-ifood-requester-service"). + custom_auth_header: Optional custom header name for authentication (e.g., "x-requester-service"). If provided, uses this header instead of standard "Authorization" header. """ self.base_url = base_url.rstrip("/") @@ -404,7 +404,7 @@ async def main(): yaml_path = os.environ.get("LITELLM_MODELS_YAML") custom_auth_header = os.environ.get( "LITELLM_CUSTOM_AUTH_HEADER" - ) # e.g., "x-ifood-requester-service" + ) # e.g., "x-requester-service" # Debug: Print custom auth header value if set if custom_auth_header: diff --git a/tests/test_litellm/caching/test_dual_cache.py b/tests/test_litellm/caching/test_dual_cache.py index 64774726201..f4f88def78d 100644 --- a/tests/test_litellm/caching/test_dual_cache.py +++ b/tests/test_litellm/caching/test_dual_cache.py @@ -243,6 +243,67 @@ def test_circuit_breaker_half_open_concurrent_calls_are_fast_failed(): ), "concurrent callers should be fast-failed in HALF_OPEN" +def test_circuit_breaker_disabled_never_opens(): + """When disabled, failures never open the circuit and is_open() stays False.""" + from litellm.caching.redis_cache import RedisCircuitBreaker + + cb = RedisCircuitBreaker(failure_threshold=3, recovery_timeout=60, enabled=False) + + for _ in range(100): + cb.record_failure() + + assert cb._state == "closed" + assert cb.is_open() is False + + +def test_circuit_breaker_disabled_record_success_leaves_state_untouched(): + """ + A disabled breaker must not mutate state in any state-machine method. Force + a non-default (OPEN) state and assert record_success() returns without + resetting it — the same enabled-guard contract as is_open/record_failure. + """ + from litellm.caching.redis_cache import RedisCircuitBreaker + + cb = RedisCircuitBreaker(failure_threshold=3, recovery_timeout=60, enabled=False) + cb._state = "open" + cb._failure_count = 3 + + cb.record_success() + + assert cb._state == "open" + assert cb._failure_count == 3 + + +@pytest.mark.asyncio +async def test_circuit_breaker_disabled_guard_always_calls_method(): + """A disabled breaker lets every guarded call through, even after failures.""" + from litellm.caching.redis_cache import ( + RedisCircuitBreaker, + _redis_circuit_breaker_guard, + ) + + class FakeRedis: + def __init__(self): + self._circuit_breaker = RedisCircuitBreaker( + failure_threshold=1, recovery_timeout=60, enabled=False + ) + self.call_count = 0 + + @_redis_circuit_breaker_guard + async def boom(self): + self.call_count += 1 + raise RuntimeError("redis down") + + fr = FakeRedis() + for _ in range(5): + with pytest.raises(RuntimeError, match="redis down"): + await fr.boom() + + # Every call reached the method body; the breaker never short-circuited. + assert fr.call_count == 5 + assert fr._circuit_breaker.is_open() is False + + @pytest.mark.asyncio async def test_async_increment_cache_returns_none_when_no_in_memory_cache_and_redis_fails(): """ diff --git a/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py b/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py index d10311b9f41..ae699ff8e12 100644 --- a/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py +++ b/tests/test_litellm/proxy/hooks/test_parallel_request_limiter_v3.py @@ -3187,3 +3187,111 @@ def test_get_key_mcp_rpm_limit_precedence(): none_set = UserAPIKeyAuth(api_key=hash_token("sk-mcp-key")) assert get_key_mcp_rpm_limit(none_set) is None assert get_team_mcp_rpm_limit(none_set) is None + + +def test_tpm_reservation_enabled_by_default(monkeypatch): + """Upfront TPM reservation is on unless explicitly disabled via env.""" + monkeypatch.delenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", raising=False) + handler = _PROXY_MaxParallelRequestsHandler( + internal_usage_cache=InternalUsageCache(DualCache()) + ) + assert handler.tpm_reservation_enabled is True + + +@pytest.mark.parametrize("value", ["false", "False", "FALSE"]) +def test_tpm_reservation_disabled_via_env(monkeypatch, value): + monkeypatch.setenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", value) + handler = _PROXY_MaxParallelRequestsHandler( + internal_usage_cache=InternalUsageCache(DualCache()) + ) + assert handler.tpm_reservation_enabled is False + + +@pytest.mark.asyncio +async def test_pre_call_hook_reserves_tpm_when_enabled(monkeypatch): + """ + With reservation enabled, the pre-call hook reserves the estimated token + budget upfront and tells should_rate_limit to skip the :tokens counter so + only the reservation path owns it. + """ + monkeypatch.delenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", raising=False) + handler = _PROXY_MaxParallelRequestsHandler( + internal_usage_cache=InternalUsageCache(DualCache()) + ) + + user_api_key_dict = UserAPIKeyAuth(api_key=hash_token("sk-tpm"), tpm_limit=10_000) + + should_rate_limit_calls: List[Dict[str, Any]] = [] + original_should_rate_limit = handler.should_rate_limit + + async def spy_should_rate_limit(*args, **kwargs): + should_rate_limit_calls.append(kwargs) + return await original_should_rate_limit(*args, **kwargs) + + reserve_calls: List[int] = [] + original_reserve = handler.reserve_tpm_tokens + + async def spy_reserve(*args, **kwargs): + reserve_calls.append(kwargs.get("estimated_tokens")) + return await original_reserve(*args, **kwargs) + + monkeypatch.setattr(handler, "should_rate_limit", spy_should_rate_limit) + monkeypatch.setattr(handler, "reserve_tpm_tokens", spy_reserve) + + await handler.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=handler.internal_usage_cache.dual_cache, + data={"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]}, + call_type="completion", + ) + + assert len(reserve_calls) == 1, "reservation must run when enabled" + assert should_rate_limit_calls[0]["skip_tpm_check"] is True + + +@pytest.mark.asyncio +async def test_pre_call_hook_skips_reservation_when_disabled(monkeypatch): + """ + With reservation disabled, the pre-call hook never calls reserve_tpm_tokens + and enforces TPM directly in should_rate_limit (skip_tpm_check=False), the + pre-v1.82 post-call accounting behavior. + """ + monkeypatch.setenv("LITELLM_TPM_TOKEN_RESERVATION_ENABLED", "false") + handler = _PROXY_MaxParallelRequestsHandler( + internal_usage_cache=InternalUsageCache(DualCache()) + ) + + user_api_key_dict = UserAPIKeyAuth(api_key=hash_token("sk-tpm"), tpm_limit=10_000) + + should_rate_limit_calls: List[Dict[str, Any]] = [] + original_should_rate_limit = handler.should_rate_limit + + async def spy_should_rate_limit(*args, **kwargs): + should_rate_limit_calls.append(kwargs) + return await original_should_rate_limit(*args, **kwargs) + + reserve_calls: List[Any] = [] + + async def spy_reserve(*args, **kwargs): + reserve_calls.append(kwargs) + raise AssertionError("reserve_tpm_tokens must not run when disabled") + + monkeypatch.setattr(handler, "should_rate_limit", spy_should_rate_limit) + monkeypatch.setattr(handler, "reserve_tpm_tokens", spy_reserve) + + data = {"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]} + await handler.async_pre_call_hook( + user_api_key_dict=user_api_key_dict, + cache=handler.internal_usage_cache.dual_cache, + data=data, + call_type="completion", + ) + + assert reserve_calls == [], "reservation must be skipped when disabled" + assert should_rate_limit_calls[0]["skip_tpm_check"] is False + # No reservation stash leaks into the request metadata. + from litellm.proxy.hooks.parallel_request_limiter_v3 import ( + TPM_RESERVED_TOKENS_KEY, + ) + + assert TPM_RESERVED_TOKENS_KEY not in (data.get("metadata") or {}) From a992ed18df4e746c3230c49cfcabcc962988f470 Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Thu, 11 Jun 2026 11:02:42 -0700 Subject: [PATCH 02/44] feat(spend_logs): opt-in native Postgres partitioning for SpendLogs retention (#29466) High-volume deployments see LiteLLM_SpendLogs grow unbounded because retention via DELETE leaves dead tuples that autovacuum cannot reclaim fast enough. With a range-partitioned table, retention drops whole partitions instead: an instant metadata operation that returns disk to the OS immediately. The feature is gated behind general_settings.use_spend_logs_partitioning (default false). With the flag off, the cleanup job never queries the catalog and behaves exactly as today. With it on, the job verifies the table is partitioned, pre-creates upcoming partitions, and drops expired ones; expired rows the drops cannot reach (DEFAULT partition, partitions spanning the cutoff) are still deleted row-wise so retention is never bypassed. If the table is not partitioned it falls back to batched DELETE only. Converting an existing table is a manual, documented operation in db_scripts/partition_spend_logs.sql; db_scripts/unpartition_spend_logs.sql rolls it back. Both scripts rename the old table's indexes aside before recreating them, since a table rename keeps the schema-unique index names and would otherwise silently skip the CREATE INDEX IF NOT EXISTS block. Granularity and pre-create lookahead are tunable via SPEND_LOG_PARTITION_INTERVAL (day/week/month, invalid values fall back to day) and SPEND_LOG_PARTITION_PRECREATE_AHEAD. --- db_scripts/partition_spend_logs.sql | 99 ++++++++ db_scripts/unpartition_spend_logs.sql | 69 ++++++ litellm/constants.py | 4 + litellm/proxy/_types.py | 4 + .../db_transaction_queue/spend_log_cleanup.py | 48 +++- .../spend_logs_partition_manager.py | 208 ++++++++++++++++ .../test_spend_logs_partition_manager.py | 233 ++++++++++++++++++ .../proxy/test_spend_log_cleanup.py | 120 ++++++++- ui/litellm-dashboard/src/lib/http/schema.d.ts | 5 + 9 files changed, 778 insertions(+), 12 deletions(-) create mode 100644 db_scripts/partition_spend_logs.sql create mode 100644 db_scripts/unpartition_spend_logs.sql create mode 100644 litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py create mode 100644 tests/test_litellm/proxy/db/db_transaction_queue/test_spend_logs_partition_manager.py diff --git a/db_scripts/partition_spend_logs.sql b/db_scripts/partition_spend_logs.sql new file mode 100644 index 00000000000..08fcbddb6f8 --- /dev/null +++ b/db_scripts/partition_spend_logs.sql @@ -0,0 +1,99 @@ +-- Converts an existing LiteLLM_SpendLogs table into a native Postgres +-- range-partitioned table keyed on "startTime". +-- +-- Why: at high request volume, retention via DELETE leaves dead tuples that +-- autovacuum cannot reclaim quickly enough, so the table keeps growing on disk +-- (seen at 450GB+ after ~1 month). With partitioning, retention drops whole +-- partitions, which is instant and returns disk to the OS immediately. +-- +-- This is an opt-in, manual operation. The default LiteLLM schema is NOT +-- partitioned, so existing installs are unaffected until you run this. +-- +-- IMPORTANT +-- * Test on a staging copy first and take a backup. +-- * Postgres cannot convert a populated table to partitioned in place, so this +-- renames the old table aside and creates a fresh partitioned table. +-- * The partition key ("startTime") must be part of the primary key, so the +-- PK becomes composite ("request_id", "startTime"). LiteLLM's write path uses +-- INSERT ... ON CONFLICT DO NOTHING, which is compatible with this. +-- * Choose a partition granularity ("day" is the recommended default for +-- high-volume tables) and keep it consistent with SPEND_LOG_PARTITION_INTERVAL. +-- +-- After running this, enable the feature and set a retention period in +-- proxy_config.yaml: +-- general_settings: +-- use_spend_logs_partitioning: true +-- maximum_spend_logs_retention_period: "30d" +-- The spend-log cleanup job then verifies the table is partitioned and reclaims +-- disk by dropping expired partitions instead of deleting rows. It also +-- pre-creates upcoming partitions on each run. To roll back, see +-- db_scripts/unpartition_spend_logs.sql. + +BEGIN; + +ALTER TABLE "LiteLLM_SpendLogs" RENAME TO "LiteLLM_SpendLogs_legacy"; + +-- Renaming a table does NOT rename its indexes, and index names are unique per +-- schema. Move the legacy table's indexes aside so the CREATE INDEX statements +-- below actually create indexes on the new partitioned table instead of being +-- silently skipped by IF NOT EXISTS, and so the new PK keeps the canonical +-- name instead of getting a "_pkey1" suffix. +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_pkey" + RENAME TO "LiteLLM_SpendLogs_legacy_pkey"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_startTime_idx" + RENAME TO "LiteLLM_SpendLogs_legacy_startTime_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_startTime_request_id_idx" + RENAME TO "LiteLLM_SpendLogs_legacy_startTime_request_id_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_end_user_idx" + RENAME TO "LiteLLM_SpendLogs_legacy_end_user_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_session_id_idx" + RENAME TO "LiteLLM_SpendLogs_legacy_session_id_idx"; + +CREATE TABLE "LiteLLM_SpendLogs" ( + LIKE "LiteLLM_SpendLogs_legacy" INCLUDING DEFAULTS INCLUDING GENERATED +) PARTITION BY RANGE ("startTime"); + +ALTER TABLE "LiteLLM_SpendLogs" + ADD PRIMARY KEY ("request_id", "startTime"); + +-- Recreate every index Prisma defines on the table. LIKE ... INCLUDING DEFAULTS +-- INCLUDING GENERATED copies columns and defaults but NOT indexes, so without +-- these the admin-UI cost-reporting queries that filter by end_user/session_id +-- fall back to sequential scans. On a partitioned parent these propagate to +-- every current and future partition automatically. +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_startTime_idx" + ON "LiteLLM_SpendLogs" ("startTime"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_startTime_request_id_idx" + ON "LiteLLM_SpendLogs" ("startTime", "request_id"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_end_user_idx" + ON "LiteLLM_SpendLogs" ("end_user"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_session_id_idx" + ON "LiteLLM_SpendLogs" ("session_id"); + +-- Safety net: any row whose startTime has no explicit partition lands here so +-- writes never fail. The cleanup job never drops the DEFAULT partition. +CREATE TABLE IF NOT EXISTS "LiteLLM_SpendLogs_pdefault" + PARTITION OF "LiteLLM_SpendLogs" DEFAULT; + +COMMIT; + +-- Backfill (optional). Rows route to the correct partition automatically. +-- For large legacy tables, copy in time-bounded batches during a low-traffic +-- window instead of one statement, or simply keep "LiteLLM_SpendLogs_legacy" +-- read-only until its data ages past your retention, then DROP it. +-- +-- Backfilled rows land in the DEFAULT partition until explicit partitions +-- cover their dates. Postgres refuses to create a partition whose range +-- overlaps rows already in DEFAULT, so the cleanup job may log a warning when +-- pre-creating today's partition right after a backfill; it recovers on its +-- own once those dates age out, and future partitions are unaffected because +-- they are always created ahead of writes. +-- +-- INSERT INTO "LiteLLM_SpendLogs" +-- SELECT * FROM "LiteLLM_SpendLogs_legacy" +-- WHERE "startTime" >= now() - interval '30 days'; +-- +-- DROP TABLE "LiteLLM_SpendLogs_legacy"; diff --git a/db_scripts/unpartition_spend_logs.sql b/db_scripts/unpartition_spend_logs.sql new file mode 100644 index 00000000000..0bd82513e4a --- /dev/null +++ b/db_scripts/unpartition_spend_logs.sql @@ -0,0 +1,69 @@ +-- Rolls back db_scripts/partition_spend_logs.sql: converts the native +-- range-partitioned "LiteLLM_SpendLogs" table back into a plain, +-- non-partitioned table matching the default LiteLLM schema. +-- +-- When/why: run this if you want to stop using partition-based retention and +-- return to DELETE-based cleanup, or to restore the original single-column +-- primary key ("request_id") that the partitioned layout had to widen to a +-- composite ("request_id", "startTime"). +-- +-- IMPORTANT +-- * Test on a staging copy first and take a backup. +-- * Postgres cannot convert a partitioned table back in place, so this +-- renames the partitioned table aside and creates a fresh plain table. +-- * The composite PK could in principle hold the same "request_id" in more +-- than one partition, so rows are copied with ON CONFLICT DO NOTHING to +-- restore the single-column PK without failing on such duplicates. +-- * For large tables the INSERT ... SELECT copies every surviving row and may +-- run long; do it during a low-traffic window. +-- * Also remove use_spend_logs_partitioning from proxy_config.yaml (or set it +-- to false) so the cleanup job returns to DELETE-based retention. + +BEGIN; + +ALTER TABLE "LiteLLM_SpendLogs" RENAME TO "LiteLLM_SpendLogs_partitioned"; + +-- Renaming a table does NOT rename its indexes, and index names are unique per +-- schema. Move the partitioned table's indexes aside so the CREATE INDEX +-- statements below actually create indexes on the new plain table instead of +-- being silently skipped by IF NOT EXISTS, and so the new PK keeps the +-- canonical name. +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_pkey" + RENAME TO "LiteLLM_SpendLogs_partitioned_pkey"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_pkey1" + RENAME TO "LiteLLM_SpendLogs_partitioned_pkey1"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_startTime_idx" + RENAME TO "LiteLLM_SpendLogs_partitioned_startTime_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_startTime_request_id_idx" + RENAME TO "LiteLLM_SpendLogs_partitioned_startTime_request_id_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_end_user_idx" + RENAME TO "LiteLLM_SpendLogs_partitioned_end_user_idx"; +ALTER INDEX IF EXISTS "LiteLLM_SpendLogs_session_id_idx" + RENAME TO "LiteLLM_SpendLogs_partitioned_session_id_idx"; + +CREATE TABLE "LiteLLM_SpendLogs" ( + LIKE "LiteLLM_SpendLogs_partitioned" INCLUDING DEFAULTS INCLUDING GENERATED +); + +ALTER TABLE "LiteLLM_SpendLogs" + ADD PRIMARY KEY ("request_id"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_startTime_idx" + ON "LiteLLM_SpendLogs" ("startTime"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_startTime_request_id_idx" + ON "LiteLLM_SpendLogs" ("startTime", "request_id"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_end_user_idx" + ON "LiteLLM_SpendLogs" ("end_user"); + +CREATE INDEX IF NOT EXISTS "LiteLLM_SpendLogs_session_id_idx" + ON "LiteLLM_SpendLogs" ("session_id"); + +INSERT INTO "LiteLLM_SpendLogs" +SELECT * FROM "LiteLLM_SpendLogs_partitioned" +ON CONFLICT ("request_id") DO NOTHING; + +DROP TABLE "LiteLLM_SpendLogs_partitioned"; + +COMMIT; diff --git a/litellm/constants.py b/litellm/constants.py index ab8e57d735f..a5e3926aa8b 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -1497,6 +1497,10 @@ SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES = int( SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS = float( os.getenv("SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS", 0.5) ) +SPEND_LOG_PARTITION_INTERVAL = os.getenv("SPEND_LOG_PARTITION_INTERVAL", "day") +SPEND_LOG_PARTITION_PRECREATE_AHEAD = int( + os.getenv("SPEND_LOG_PARTITION_PRECREATE_AHEAD", 7) +) SPEND_LOG_QUEUE_SIZE_THRESHOLD = int(os.getenv("SPEND_LOG_QUEUE_SIZE_THRESHOLD", 100)) SPEND_LOG_QUEUE_POLL_INTERVAL = float(os.getenv("SPEND_LOG_QUEUE_POLL_INTERVAL", 2.0)) SPEND_COUNTER_RESEED_LOCKS_MAX_SIZE = int( diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 1b594e20d32..35e9e0cd74b 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -2300,6 +2300,10 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): None, description="Maximum retention period for spend logs (e.g., '7d' for 7 days). Logs older than this will be deleted.", ) + use_spend_logs_partitioning: Optional[bool] = Field( + None, + description="If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (db_scripts/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.", + ) mcp_internal_ip_ranges: Optional[List[str]] = Field( None, description="Custom CIDR ranges that define internal/private networks for MCP access control. When set, only these ranges are treated as internal. Defaults to RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8).", diff --git a/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py b/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py index 9475779cfdf..a4c23937b98 100644 --- a/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py +++ b/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py @@ -12,19 +12,31 @@ from litellm.constants import ( SPEND_LOG_RUN_LOOPS, ) from litellm.litellm_core_utils.duration_parser import duration_in_seconds +from litellm.proxy.db.db_transaction_queue.spend_logs_partition_manager import ( + SpendLogsPartitionManager, +) from litellm.proxy.utils import PrismaClient class SpendLogCleanup: """ Handles cleaning up old spend logs based on maximum retention period. - Deletes logs in batches to prevent timeouts. + + When LiteLLM_SpendLogs is range-partitioned, expired data is reclaimed by + dropping whole partitions (instant, frees disk immediately). Otherwise it + falls back to deleting logs in batches. Uses PodLockManager to ensure only one pod runs cleanup in multi-pod deployments. """ - def __init__(self, general_settings=None, redis_cache: Optional[RedisCache] = None): + def __init__( + self, + general_settings=None, + redis_cache: Optional[RedisCache] = None, + partition_manager: Optional[SpendLogsPartitionManager] = None, + ): self.batch_size = SPEND_LOG_CLEANUP_BATCH_SIZE self.retention_seconds: Optional[int] = None + self.partition_manager = partition_manager or SpendLogsPartitionManager() from litellm.proxy.proxy_server import general_settings as default_settings self.general_settings = general_settings or default_settings @@ -89,8 +101,8 @@ class SpendLogCleanup: deleted_result = await prisma_client.db.execute_raw( """ DELETE FROM "LiteLLM_SpendLogs" - WHERE "request_id" IN ( - SELECT "request_id" FROM "LiteLLM_SpendLogs" + WHERE ("request_id", "startTime") IN ( + SELECT "request_id", "startTime" FROM "LiteLLM_SpendLogs" WHERE "startTime" < $1::timestamptz LIMIT $2 ) @@ -195,12 +207,32 @@ class SpendLogCleanup: seconds=float(self.retention_seconds) ) verbose_proxy_logger.info( - f"Deleting logs older than {cutoff_date.isoformat()}" + f"Removing logs older than {cutoff_date.isoformat()}" ) - # Perform the actual deletion - total_deleted = await self._delete_old_logs(prisma_client, cutoff_date) - verbose_proxy_logger.info(f"Deleted {total_deleted} logs") + if self.general_settings.get( + "use_spend_logs_partitioning", False + ) and await self.partition_manager.is_partitioned(prisma_client): + await self.partition_manager.ensure_partitions(prisma_client) + dropped = await self.partition_manager.drop_partitions_older_than( + prisma_client, cutoff_date + ) + verbose_proxy_logger.info( + "Dropped %d expired spend-log partitions: %s", + len(dropped), + dropped, + ) + # DROP only reclaims whole expired partitions. Expired rows can + # still sit in the DEFAULT partition (backfill, coverage gaps) + # or in a partition that spans the cutoff, so retention must + # also delete those stragglers row-wise. + total_deleted = await self._delete_old_logs(prisma_client, cutoff_date) + verbose_proxy_logger.info( + f"Deleted {total_deleted} expired logs not covered by dropped partitions" + ) + else: + total_deleted = await self._delete_old_logs(prisma_client, cutoff_date) + verbose_proxy_logger.info(f"Deleted {total_deleted} logs") except Exception as e: # .exception() captures the traceback; str(e) alone on a Prisma/DB diff --git a/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py new file mode 100644 index 00000000000..eee0f862b4e --- /dev/null +++ b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py @@ -0,0 +1,208 @@ +""" +Manages native Postgres range partitions for the LiteLLM_SpendLogs table. + +At high request volume, retention via batched DELETE leaves dead tuples that +autovacuum cannot reclaim fast enough, so the table keeps growing on disk. When +the table is range-partitioned on startTime, dropping old data becomes a +DROP TABLE on a whole partition: an instant metadata operation that returns disk +to the OS immediately, with no tombstones and no vacuum. + +This manager only acts when use_spend_logs_partitioning is enabled in +general_settings AND the table is already partitioned (set up via the +db_scripts/partition_spend_logs.sql runbook). Without both, the cleanup job +keeps the batched-DELETE path, so existing deployments are untouched. +""" + +import re +from datetime import date, datetime, timedelta, timezone +from typing import List, Optional, Tuple + +from litellm._logging import verbose_proxy_logger +from litellm.constants import ( + SPEND_LOG_PARTITION_INTERVAL, + SPEND_LOG_PARTITION_PRECREATE_AHEAD, +) + +SPEND_LOGS_TABLE = "LiteLLM_SpendLogs" + +PartitionInterval = str # "day" | "week" | "month" + +VALID_PARTITION_INTERVALS = {"day", "week", "month"} + +_BOUND_UPPER_RE = re.compile(r"TO \('([^']+)'\)") + + +def period_start(day: date, interval: PartitionInterval) -> date: + """First day of the partition period that `day` falls into (UTC).""" + if interval == "day": + return day + if interval == "week": + return day - timedelta(days=day.weekday()) + if interval == "month": + return day.replace(day=1) + raise ValueError(f"Unsupported partition interval: {interval}") + + +def next_period_start(start: date, interval: PartitionInterval) -> date: + if interval == "day": + return start + timedelta(days=1) + if interval == "week": + return start + timedelta(days=7) + if interval == "month": + if start.month == 12: + return start.replace(year=start.year + 1, month=1) + return start.replace(month=start.month + 1) + raise ValueError(f"Unsupported partition interval: {interval}") + + +def partition_name(start: date) -> str: + return f"{SPEND_LOGS_TABLE}_p{start.strftime('%Y%m%d')}" + + +def upcoming_partitions( + today: date, interval: PartitionInterval, ahead: int +) -> List[Tuple[str, date, date]]: + """ + Specs (name, lower_inclusive, upper_exclusive) for the current period plus + the next `ahead` periods, so writes always have a partition to land in. + """ + specs: List[Tuple[str, date, date]] = [] + start = period_start(today, interval) + for _ in range(ahead + 1): + upper = next_period_start(start, interval) + specs.append((partition_name(start), start, upper)) + start = upper + return specs + + +def parse_partition_upper_bound(bound_expr: str) -> Optional[datetime]: + """ + Upper bound of a Postgres partition from its `pg_get_expr(relpartbound)` + string, e.g. "FOR VALUES FROM ('2026-06-01 00:00:00') TO ('2026-06-02 00:00:00')". + Returns None for the DEFAULT partition or anything we cannot parse, so such + partitions are never selected for dropping. + """ + if "DEFAULT" in bound_expr.upper(): + return None + match = _BOUND_UPPER_RE.search(bound_expr) + if match is None: + return None + try: + return datetime.fromisoformat(match.group(1)) + except ValueError: + return None + + +def select_partitions_to_drop( + partitions: List[Tuple[str, Optional[datetime]]], cutoff: datetime +) -> List[str]: + """ + Names of partitions whose entire range is older than `cutoff` (upper bound + <= cutoff). `cutoff` and the bounds are UTC-naive. Partitions without a + parseable upper bound (e.g. DEFAULT) are kept. + """ + return [name for name, upper in partitions if upper is not None and upper <= cutoff] + + +class SpendLogsPartitionManager: + def __init__( + self, + interval: PartitionInterval = SPEND_LOG_PARTITION_INTERVAL, + precreate_ahead: int = SPEND_LOG_PARTITION_PRECREATE_AHEAD, + ): + if interval not in VALID_PARTITION_INTERVALS: + verbose_proxy_logger.warning( + "Invalid SPEND_LOG_PARTITION_INTERVAL %r, falling back to 'day'. " + "Supported values: %s", + interval, + sorted(VALID_PARTITION_INTERVALS), + ) + interval = "day" + self.interval = interval + self.precreate_ahead = precreate_ahead + + async def is_partitioned(self, prisma_client) -> bool: + try: + rows = await prisma_client.db.query_raw( + """ + SELECT EXISTS ( + SELECT 1 + FROM pg_partitioned_table pt + JOIN pg_class c ON c.oid = pt.partrelid + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = $1 + AND n.nspname = current_schema() + ) AS partitioned + """, + SPEND_LOGS_TABLE, + ) + except Exception as e: + verbose_proxy_logger.warning( + "Could not determine if %s is partitioned, assuming it is not: %s", + SPEND_LOGS_TABLE, + e, + ) + return False + return bool(rows and rows[0].get("partitioned")) + + async def ensure_partitions(self, prisma_client) -> List[str]: + """ + Ensure the current and upcoming partitions exist, returning the names + now present. CREATE TABLE IF NOT EXISTS is a no-op for partitions that + already exist, so this list is "ensured present", not "newly created". + """ + ensured: List[str] = [] + for name, lower, upper in upcoming_partitions( + datetime.now(timezone.utc).date(), self.interval, self.precreate_ahead + ): + try: + await prisma_client.db.execute_raw( + f'CREATE TABLE IF NOT EXISTS "{name}" ' + f'PARTITION OF "{SPEND_LOGS_TABLE}" ' + f"FOR VALUES FROM ('{lower.isoformat()}') TO ('{upper.isoformat()}')" + ) + ensured.append(name) + except Exception as e: + verbose_proxy_logger.warning( + "Failed to ensure spend-log partition %s: %s", name, e + ) + return ensured + + async def _list_partitions( + self, prisma_client + ) -> List[Tuple[str, Optional[datetime]]]: + rows = await prisma_client.db.query_raw( + """ + SELECT c.relname AS name, + pg_get_expr(c.relpartbound, c.oid) AS bound + FROM pg_inherits i + JOIN pg_class c ON c.oid = i.inhrelid + JOIN pg_class p ON p.oid = i.inhparent + JOIN pg_namespace n ON n.oid = p.relnamespace + WHERE p.relname = $1 + AND n.nspname = current_schema() + """, + SPEND_LOGS_TABLE, + ) + return [ + (row["name"], parse_partition_upper_bound(row.get("bound") or "")) + for row in rows + ] + + async def drop_partitions_older_than( + self, prisma_client, cutoff: datetime + ) -> List[str]: + """DROP every partition whose whole range is older than `cutoff`.""" + cutoff_naive = cutoff.astimezone(timezone.utc).replace(tzinfo=None) + partitions = await self._list_partitions(prisma_client) + to_drop = select_partitions_to_drop(partitions, cutoff_naive) + dropped: List[str] = [] + for name in to_drop: + try: + await prisma_client.db.execute_raw(f'DROP TABLE IF EXISTS "{name}"') + dropped.append(name) + except Exception as e: + verbose_proxy_logger.warning( + "Failed to drop spend-log partition %s: %s", name, e + ) + return dropped diff --git a/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_logs_partition_manager.py b/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_logs_partition_manager.py new file mode 100644 index 00000000000..289de707387 --- /dev/null +++ b/tests/test_litellm/proxy/db/db_transaction_queue/test_spend_logs_partition_manager.py @@ -0,0 +1,233 @@ +""" +Tests for SpendLogsPartitionManager: partition naming/bounds math, retention +selection, the non-partitioned no-op safety path, and the drop/ensure SQL flow. +""" + +from datetime import date, datetime, timezone +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from litellm.proxy.db.db_transaction_queue.spend_logs_partition_manager import ( + SpendLogsPartitionManager, + next_period_start, + parse_partition_upper_bound, + partition_name, + period_start, + select_partitions_to_drop, + upcoming_partitions, +) + + +def test_period_start_per_interval(): + d = date(2026, 6, 3) # a Wednesday + assert period_start(d, "day") == date(2026, 6, 3) + assert period_start(d, "week") == date(2026, 6, 1) # Monday + assert period_start(d, "month") == date(2026, 6, 1) + + +def test_next_period_start_crosses_year_and_month_boundaries(): + assert next_period_start(date(2026, 6, 3), "day") == date(2026, 6, 4) + assert next_period_start(date(2026, 6, 1), "week") == date(2026, 6, 8) + assert next_period_start(date(2026, 12, 1), "month") == date(2027, 1, 1) + + +def test_partition_name_uses_period_start_date(): + assert partition_name(date(2026, 6, 1)) == "LiteLLM_SpendLogs_p20260601" + + +def test_upcoming_partitions_count_and_contiguous_ranges(): + specs = upcoming_partitions(date(2026, 6, 1), "day", ahead=3) + assert len(specs) == 4 # current + 3 ahead + names = [s[0] for s in specs] + assert names == [ + "LiteLLM_SpendLogs_p20260601", + "LiteLLM_SpendLogs_p20260602", + "LiteLLM_SpendLogs_p20260603", + "LiteLLM_SpendLogs_p20260604", + ] + # ranges must be contiguous and half-open: each upper is the next lower + for (_, _, upper), (_, next_lower, _) in zip(specs, specs[1:]): + assert upper == next_lower + + +def test_parse_partition_upper_bound_extracts_to_value(): + bound = "FOR VALUES FROM ('2026-06-01 00:00:00') TO ('2026-06-02 00:00:00')" + assert parse_partition_upper_bound(bound) == datetime(2026, 6, 2, 0, 0, 0) + + +def test_parse_partition_upper_bound_default_is_none(): + assert parse_partition_upper_bound("DEFAULT") is None + assert parse_partition_upper_bound("garbage") is None + + +def test_select_partitions_to_drop_only_fully_expired(): + cutoff = datetime(2026, 6, 10, 0, 0, 0) + partitions = [ + ("p_old", datetime(2026, 6, 9, 0, 0, 0)), # upper < cutoff -> drop + ("p_boundary", datetime(2026, 6, 10, 0, 0, 0)), # upper == cutoff -> drop + ("p_partial", datetime(2026, 6, 11, 0, 0, 0)), # straddles cutoff -> keep + ("p_default", None), # DEFAULT -> keep + ] + assert select_partitions_to_drop(partitions, cutoff) == ["p_old", "p_boundary"] + + +@pytest.mark.asyncio +async def test_is_partitioned_true_and_false(): + mgr = SpendLogsPartitionManager() + + client_true = MagicMock() + client_true.db.query_raw = AsyncMock(return_value=[{"partitioned": True}]) + assert await mgr.is_partitioned(client_true) is True + + client_false = MagicMock() + client_false.db.query_raw = AsyncMock(return_value=[{"partitioned": False}]) + assert await mgr.is_partitioned(client_false) is False + + +@pytest.mark.asyncio +async def test_catalog_queries_are_scoped_to_current_schema(): + """ + Both catalog lookups must filter by current_schema(); otherwise a same-named + table in another schema can flip is_partitioned or return foreign partitions. + """ + mgr = SpendLogsPartitionManager() + client = MagicMock() + client.db.query_raw = AsyncMock(return_value=[]) + + await mgr.is_partitioned(client) + is_partitioned_sql = client.db.query_raw.call_args.args[0] + assert "pg_namespace" in is_partitioned_sql + assert "current_schema()" in is_partitioned_sql + + await mgr._list_partitions(client) + list_sql = client.db.query_raw.call_args.args[0] + assert "pg_namespace" in list_sql + assert "current_schema()" in list_sql + + +@pytest.mark.asyncio +async def test_is_partitioned_swallows_errors_and_returns_false(): + """A catalog query failure must not crash cleanup; fall back to non-partitioned.""" + mgr = SpendLogsPartitionManager() + client = MagicMock() + client.db.query_raw = AsyncMock(side_effect=Exception("db down")) + assert await mgr.is_partitioned(client) is False + + +@pytest.mark.asyncio +async def test_drop_partitions_older_than_drops_expired_only(): + mgr = SpendLogsPartitionManager() + client = MagicMock() + client.db.query_raw = AsyncMock( + return_value=[ + { + "name": "LiteLLM_SpendLogs_p20260601", + "bound": "FOR VALUES FROM ('2026-06-01 00:00:00') TO ('2026-06-02 00:00:00')", + }, + { + "name": "LiteLLM_SpendLogs_p20260609", + "bound": "FOR VALUES FROM ('2026-06-09 00:00:00') TO ('2026-06-10 00:00:00')", + }, + {"name": "LiteLLM_SpendLogs_pdefault", "bound": "DEFAULT"}, + ] + ) + client.db.execute_raw = AsyncMock(return_value=0) + + cutoff = datetime(2026, 6, 5, 0, 0, 0, tzinfo=timezone.utc) + dropped = await mgr.drop_partitions_older_than(client, cutoff) + + assert dropped == ["LiteLLM_SpendLogs_p20260601"] + executed = " ".join(call.args[0] for call in client.db.execute_raw.call_args_list) + assert 'DROP TABLE IF EXISTS "LiteLLM_SpendLogs_p20260601"' in executed + assert "p20260609" not in executed + assert "pdefault" not in executed + + +@pytest.mark.asyncio +async def test_ensure_partitions_issues_create_for_each_period(): + mgr = SpendLogsPartitionManager(interval="day", precreate_ahead=2) + client = MagicMock() + client.db.execute_raw = AsyncMock(return_value=0) + + created = await mgr.ensure_partitions(client) + + assert len(created) == 3 # current + 2 ahead + assert client.db.execute_raw.await_count == 3 + first_sql = client.db.execute_raw.call_args_list[0].args[0] + assert 'PARTITION OF "LiteLLM_SpendLogs"' in first_sql + assert "CREATE TABLE IF NOT EXISTS" in first_sql + + +def test_unsupported_interval_raises(): + with pytest.raises(ValueError): + period_start(date(2026, 6, 1), "year") + with pytest.raises(ValueError): + next_period_start(date(2026, 6, 1), "year") + + +def test_parse_partition_upper_bound_unparseable_to_value_is_none(): + """A TO(...) value that is not a valid timestamp must not raise; return None.""" + assert ( + parse_partition_upper_bound("FOR VALUES FROM ('x') TO ('not-a-date')") is None + ) + + +@pytest.mark.asyncio +async def test_ensure_partitions_continues_when_one_create_fails(): + mgr = SpendLogsPartitionManager(interval="day", precreate_ahead=2) + client = MagicMock() + client.db.execute_raw = AsyncMock(side_effect=[0, Exception("overlap"), 0]) + + created = await mgr.ensure_partitions(client) + + # the failed partition is skipped, the others still created + assert len(created) == 2 + assert client.db.execute_raw.await_count == 3 + + +def test_invalid_interval_falls_back_to_day(): + """ + An invalid interval must not be stored as-is. Otherwise ensure_partitions + raises (via period_start) and aborts the cleanup run before retention drops + old partitions, silently skipping retention. + """ + mgr = SpendLogsPartitionManager(interval="year") + assert mgr.interval == "day" + + +@pytest.mark.asyncio +async def test_invalid_interval_does_not_abort_ensure_partitions(): + """With the fallback, ensure_partitions completes instead of raising ValueError.""" + mgr = SpendLogsPartitionManager(interval="fortnight", precreate_ahead=1) + client = MagicMock() + client.db.execute_raw = AsyncMock(return_value=0) + + created = await mgr.ensure_partitions(client) + + assert len(created) == 2 # current + 1 ahead, day-based fallback + + +@pytest.mark.asyncio +async def test_drop_partitions_continues_when_one_drop_fails(): + mgr = SpendLogsPartitionManager() + client = MagicMock() + client.db.query_raw = AsyncMock( + return_value=[ + { + "name": "LiteLLM_SpendLogs_p20260601", + "bound": "FOR VALUES FROM ('2026-06-01 00:00:00') TO ('2026-06-02 00:00:00')", + }, + { + "name": "LiteLLM_SpendLogs_p20260602", + "bound": "FOR VALUES FROM ('2026-06-02 00:00:00') TO ('2026-06-03 00:00:00')", + }, + ] + ) + client.db.execute_raw = AsyncMock(side_effect=[Exception("locked"), 0]) + + cutoff = datetime(2026, 6, 10, 0, 0, 0, tzinfo=timezone.utc) + dropped = await mgr.drop_partitions_older_than(client, cutoff) + + # both were eligible; the first drop failed so only the second is reported + assert dropped == ["LiteLLM_SpendLogs_p20260602"] diff --git a/tests/test_litellm/proxy/test_spend_log_cleanup.py b/tests/test_litellm/proxy/test_spend_log_cleanup.py index 42bb919295f..a309dd64011 100644 --- a/tests/test_litellm/proxy/test_spend_log_cleanup.py +++ b/tests/test_litellm/proxy/test_spend_log_cleanup.py @@ -183,7 +183,10 @@ async def test_cleanup_old_spend_logs_batch_deletion(): # Check the first call argument call_args_sql = mock_db.execute_raw.call_args_list[0][0][0] assert 'DELETE FROM "LiteLLM_SpendLogs"' in call_args_sql - assert 'WHERE "request_id" IN' in call_args_sql + # must match on the full composite identity: on a partitioned table + # request_id alone is not unique, and deleting by it would let a client + # reusing x-litellm-call-id take out a fresh row alongside the expired one + assert 'WHERE ("request_id", "startTime") IN' in call_args_sql @pytest.mark.asyncio @@ -219,6 +222,109 @@ async def test_cleanup_old_spend_logs_retention_period_cutoff(): ) # Allow 1 second difference for test execution time +@pytest.mark.asyncio +async def test_cleanup_drops_partitions_when_enabled_and_partitioned(): + """ + With use_spend_logs_partitioning enabled and a partitioned table, cleanup + must reclaim disk by dropping partitions AND still delete expired rows the + drops cannot reach (DEFAULT partition, cutoff-spanning partitions), so + retention is never bypassed. + """ + from unittest.mock import AsyncMock, MagicMock + + mock_prisma_client = MagicMock() + mock_prisma_client.db.execute_raw = AsyncMock(return_value=0) + + partition_manager = MagicMock() + partition_manager.is_partitioned = AsyncMock(return_value=True) + partition_manager.ensure_partitions = AsyncMock(return_value=["p1"]) + partition_manager.drop_partitions_older_than = AsyncMock( + return_value=["LiteLLM_SpendLogs_p20260601"] + ) + + cleaner = SpendLogCleanup( + general_settings={ + "maximum_spend_logs_retention_period": "7d", + "use_spend_logs_partitioning": True, + }, + partition_manager=partition_manager, + ) + cleaner.pod_lock_manager = MagicMock() + cleaner.pod_lock_manager.redis_cache = None + + await cleaner.cleanup_old_spend_logs(mock_prisma_client) + + partition_manager.ensure_partitions.assert_awaited_once() + partition_manager.drop_partitions_older_than.assert_awaited_once() + delete_sql = mock_prisma_client.db.execute_raw.call_args_list[0][0][0] + assert 'DELETE FROM "LiteLLM_SpendLogs"' in delete_sql + + +@pytest.mark.asyncio +async def test_cleanup_uses_delete_when_partitioning_not_enabled(): + """ + Even against a partitioned table, the partition path must stay off until + use_spend_logs_partitioning is explicitly enabled, so existing deployments + see zero behavior change. The catalog must not even be queried. + """ + from unittest.mock import AsyncMock, MagicMock + + mock_prisma_client = MagicMock() + mock_prisma_client.db.execute_raw = AsyncMock(side_effect=[10, 0]) + + partition_manager = MagicMock() + partition_manager.is_partitioned = AsyncMock(return_value=True) + partition_manager.ensure_partitions = AsyncMock() + partition_manager.drop_partitions_older_than = AsyncMock() + + cleaner = SpendLogCleanup( + general_settings={"maximum_spend_logs_retention_period": "7d"}, + partition_manager=partition_manager, + ) + cleaner.pod_lock_manager = MagicMock() + cleaner.pod_lock_manager.redis_cache = None + + await cleaner.cleanup_old_spend_logs(mock_prisma_client) + + partition_manager.is_partitioned.assert_not_awaited() + partition_manager.drop_partitions_older_than.assert_not_awaited() + delete_sql = mock_prisma_client.db.execute_raw.call_args_list[0][0][0] + assert 'DELETE FROM "LiteLLM_SpendLogs"' in delete_sql + + +@pytest.mark.asyncio +async def test_cleanup_uses_delete_when_not_partitioned(): + """ + With the feature enabled but the table not actually partitioned (script not + run yet), cleanup must keep using the batched DELETE path. + """ + from unittest.mock import AsyncMock, MagicMock + + mock_prisma_client = MagicMock() + mock_prisma_client.db.execute_raw = AsyncMock(side_effect=[10, 0]) + + partition_manager = MagicMock() + partition_manager.is_partitioned = AsyncMock(return_value=False) + partition_manager.drop_partitions_older_than = AsyncMock() + + cleaner = SpendLogCleanup( + general_settings={ + "maximum_spend_logs_retention_period": "7d", + "use_spend_logs_partitioning": True, + }, + partition_manager=partition_manager, + ) + cleaner.pod_lock_manager = MagicMock() + cleaner.pod_lock_manager.redis_cache = None + + await cleaner.cleanup_old_spend_logs(mock_prisma_client) + + partition_manager.drop_partitions_older_than.assert_not_awaited() + assert mock_prisma_client.db.execute_raw.await_count == 2 + delete_sql = mock_prisma_client.db.execute_raw.call_args_list[0][0][0] + assert 'DELETE FROM "LiteLLM_SpendLogs"' in delete_sql + + @pytest.mark.asyncio async def test_cleanup_old_spend_logs_no_retention_period(): """ @@ -370,7 +476,9 @@ async def test_delete_old_logs_aborts_after_consecutive_failures(monkeypatch): import litellm.proxy.db.db_transaction_queue.spend_log_cleanup as cleanup_module # Lower the threshold so the test is fast and deterministic. - monkeypatch.setattr(cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 3) + monkeypatch.setattr( + cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 3 + ) monkeypatch.setattr( cleanup_module, "SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS", 0.0 ) @@ -400,7 +508,9 @@ async def test_delete_old_logs_resets_consecutive_failures_on_success(monkeypatc intermittent timeouts don't trip the abort threshold.""" import litellm.proxy.db.db_transaction_queue.spend_log_cleanup as cleanup_module - monkeypatch.setattr(cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 3) + monkeypatch.setattr( + cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 3 + ) monkeypatch.setattr( cleanup_module, "SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS", 0.0 ) @@ -471,7 +581,9 @@ async def test_cleanup_releases_lock_after_persistent_batch_failures(monkeypatch must still be released so the next scheduled run isn't permanently blocked.""" import litellm.proxy.db.db_transaction_queue.spend_log_cleanup as cleanup_module - monkeypatch.setattr(cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 2) + monkeypatch.setattr( + cleanup_module, "SPEND_LOG_CLEANUP_MAX_CONSECUTIVE_BATCH_FAILURES", 2 + ) monkeypatch.setattr( cleanup_module, "SPEND_LOG_CLEANUP_BATCH_FAILURE_BACKOFF_SECONDS", 0.0 ) diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 15123bcdbf8..4aeb1cf3e86 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -22175,6 +22175,11 @@ export interface components { * @description decrypt keys with google kms */ use_google_kms?: boolean | null; + /** + * Use Spend Logs Partitioning + * @description If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (db_scripts/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False. + */ + use_spend_logs_partitioning?: boolean | null; /** User Header Mappings */ user_header_mappings?: components["schemas"]["UserHeaderMapping"][] | null; /** From 530c0b2326b80fb0fa2cdf583eab03d2f2979f9f Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 11 Jun 2026 12:07:17 -0700 Subject: [PATCH 03/44] feat(ui): migrate playground to path routing and colocate its files (#30185) * feat(ui): cut playground over to the /ui/playground path route Follows the api-reference recipe: the sidebar and deep links route llm-playground to the path route, ?page=llm-playground redirects, and the legacy switch arm is deleted. The route's page.tsx was already the real implementation, so no view extraction was needed. * refactor(ui): move playground-owned files into its route folder Per the (dashboard) README convention, page-owned code lives in the page's folder: chat_ui/compareUI/complianceUI components, the chat hooks, and the playground-only llm_calls helpers move under (dashboard)/playground/. Modules with non-playground consumers (chat message primitives; fetch_models, chat_completion, responses_api) stay at their lowest common ancestor in src/components/{chat_ui,llm_calls} because legacy pages still import them. eslint-suppressions entries are re-keyed to the new paths so the grandfathered baseline still applies. * test(ui): teach sidebar e2e spec about migrated path routes The sidebar spec asserted ?page= for every item, which the playground cutover correctly broke: the sidebar now links to /ui/playground and the legacy URL redirects there. Drive the expected URL from the migration fixture (now a page-id -> segment map) so future cutovers only add a fixture entry. Also wrap one import line in AgentBuilderView.tsx that the move left unformatted; the changed- files prettier check flagged it. --- .../e2e_tests/fixtures/migratedPages.ts | 21 ++++--- .../tests/navigation/sidebar.spec.ts | 22 +++++-- ui/litellm-dashboard/eslint-suppressions.json | 44 +++++++------- .../src/app/(dashboard)/page.tsx | 3 - .../components}/chat_ui/A2AMetrics.tsx | 0 .../chat_ui/AdditionalModelSettings.test.tsx | 0 .../chat_ui/AdditionalModelSettings.tsx | 0 .../components}/chat_ui/AgentBuilderView.tsx | 18 ++++-- .../chat_ui/AudioRenderer.test.tsx | 2 +- .../components}/chat_ui/AudioRenderer.tsx | 2 +- .../components}/chat_ui/ChatImageRenderer.tsx | 2 +- .../components}/chat_ui/ChatImageUpload.tsx | 0 .../chat_ui/ChatImageUtils.test.tsx | 2 +- .../components}/chat_ui/ChatImageUtils.tsx | 2 +- .../chat_ui/ChatMessageBubble.test.tsx | 10 ++-- .../components}/chat_ui/ChatMessageBubble.tsx | 14 ++--- .../components}/chat_ui/ChatUI.test.tsx | 6 +- .../playground/components}/chat_ui/ChatUI.tsx | 60 +++++++++---------- .../chat_ui/CodeInterpreterOutput.test.tsx | 0 .../chat_ui/CodeInterpreterOutput.tsx | 0 .../chat_ui/CodeInterpreterTool.tsx | 0 .../chat_ui/EndpointSelector.test.tsx | 0 .../components}/chat_ui/EndpointSelector.tsx | 0 .../chat_ui/EndpointUtils.test.tsx | 8 +-- .../components}/chat_ui/EndpointUtils.tsx | 4 +- .../chat_ui/FilePreviewCard.test.tsx | 0 .../components}/chat_ui/FilePreviewCard.tsx | 0 .../chat_ui/RealtimePlayground.tsx | 2 +- .../chat_ui/ResponsesImageRenderer.tsx | 2 +- .../chat_ui/ResponsesImageUpload.tsx | 0 .../chat_ui/ResponsesImageUtils.tsx | 2 +- .../chat_ui/SearchResultsDisplay.tsx | 2 +- .../components}/chat_ui/SessionManagement.tsx | 4 +- .../components}/chat_ui/chatConstants.ts | 2 +- .../components}/compareUI/CompareUI.test.tsx | 6 +- .../components}/compareUI/CompareUI.tsx | 12 ++-- .../components/ComparisonPanel.test.tsx | 6 +- .../compareUI/components/ComparisonPanel.tsx | 6 +- .../components/MessageDisplay.test.tsx | 6 +- .../compareUI/components/MessageDisplay.tsx | 6 +- .../components/MessageInput.test.tsx | 0 .../compareUI/components/MessageInput.tsx | 0 .../components/ModelSelector.test.tsx | 0 .../compareUI/components/ModelSelector.tsx | 0 .../components/UnifiedSelector.test.tsx | 0 .../compareUI/components/UnifiedSelector.tsx | 0 .../compareUI/endpoint_config.test.ts | 2 +- .../components}/compareUI/endpoint_config.ts | 2 +- .../components}/complianceUI/ComplianceUI.tsx | 2 +- .../playground/hooks}/useChatHistory.test.ts | 0 .../playground/hooks}/useChatHistory.ts | 8 +-- .../playground/hooks}/useCodeInterpreter.ts | 4 +- .../playground/llm_calls/a2a_send_message.tsx | 4 +- .../llm_calls/anthropic_messages.tsx | 4 +- .../llm_calls/audio_speech.test.tsx | 0 .../playground/llm_calls/audio_speech.tsx | 2 +- .../llm_calls/audio_transcriptions.test.tsx | 0 .../llm_calls/audio_transcriptions.tsx | 0 .../llm_calls/embeddings_api.test.tsx | 0 .../playground/llm_calls/embeddings_api.tsx | 0 .../playground/llm_calls/fetch_agents.tsx | 2 +- .../playground/llm_calls/image_edits.tsx | 0 .../playground/llm_calls/image_generation.tsx | 0 .../playground/llm_calls/interactions_api.tsx | 0 .../src/app/(dashboard)/playground/page.tsx | 8 +-- .../cost_tracking_settings.test.tsx | 2 +- .../cost_tracking_settings.tsx | 2 +- .../EvaluationSettingsModal.tsx | 2 +- .../MCPSemanticFilterSettings.test.tsx | 2 +- .../MCPSemanticFilterSettings.tsx | 2 +- .../Fallbacks/AddFallbacks.test.tsx | 4 +- .../RouterSettings/Fallbacks/AddFallbacks.tsx | 2 +- .../Fallbacks/Fallbacks.test.tsx | 4 +- .../add_model/ComplexityRouterConfig.tsx | 2 +- .../add_model/RouterConfigBuilder.tsx | 2 +- .../add_model/add_auto_router_tab.tsx | 2 +- .../cache_settings/CacheFieldRenderer.tsx | 2 +- .../src/components/chat/ChatMessages.tsx | 4 +- .../src/components/chat/ChatPage.tsx | 6 +- .../chat_ui/CodeSnippets.test.tsx | 0 .../{playground => }/chat_ui/CodeSnippets.tsx | 2 +- .../chat_ui/MCPEventsDisplay.tsx | 2 +- .../chat_ui/ReasoningContent.tsx | 0 .../chat_ui/ResponseMetrics.tsx | 0 .../chat_ui/mode_endpoint_mapping.tsx | 0 .../{playground => }/chat_ui/types.ts | 0 .../common_components/ModelSelector.tsx | 2 +- .../RouterSettingsAccordion.tsx | 2 +- .../edit_auto_router_modal.tsx | 2 +- .../llm_calls/chat_completion.test.tsx | 0 .../llm_calls/chat_completion.tsx | 2 +- .../llm_calls/code_interpreter_handler.ts | 0 .../llm_calls/fetch_models.tsx | 2 +- .../llm_calls/responses_api.test.tsx | 0 .../llm_calls/responses_api.tsx | 4 +- .../conversation_panel/MessageBubble.tsx | 2 +- .../conversation_panel/types.ts | 2 +- .../conversation_panel/useConversation.ts | 2 +- .../src/components/public_model_hub.tsx | 6 +- .../S3VectorsConfig.test.tsx | 4 +- .../S3VectorsConfig.tsx | 2 +- .../VectorStoreForm.tsx | 2 +- .../src/utils/migratedPages.test.ts | 7 +++ .../src/utils/migratedPages.ts | 1 + 104 files changed, 214 insertions(+), 184 deletions(-) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/A2AMetrics.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/AdditionalModelSettings.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/AdditionalModelSettings.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/AgentBuilderView.tsx (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/AudioRenderer.test.tsx (88%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/AudioRenderer.tsx (90%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatImageRenderer.tsx (95%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatImageUpload.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatImageUtils.test.tsx (99%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatImageUtils.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatMessageBubble.test.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatMessageBubble.tsx (93%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatUI.test.tsx (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ChatUI.tsx (97%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/CodeInterpreterOutput.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/CodeInterpreterOutput.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/CodeInterpreterTool.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/EndpointSelector.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/EndpointSelector.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/EndpointUtils.test.tsx (95%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/EndpointUtils.tsx (82%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/FilePreviewCard.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/FilePreviewCard.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/RealtimePlayground.tsx (99%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ResponsesImageRenderer.tsx (94%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ResponsesImageUpload.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/ResponsesImageUtils.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/SearchResultsDisplay.tsx (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/SessionManagement.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/chat_ui/chatConstants.ts (95%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/CompareUI.test.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/CompareUI.tsx (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/ComparisonPanel.test.tsx (93%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/ComparisonPanel.tsx (97%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/MessageDisplay.test.tsx (94%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/MessageDisplay.tsx (96%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/MessageInput.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/MessageInput.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/ModelSelector.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/ModelSelector.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/UnifiedSelector.test.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/components/UnifiedSelector.tsx (100%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/endpoint_config.test.ts (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/compareUI/endpoint_config.ts (98%) rename ui/litellm-dashboard/src/{components/playground => app/(dashboard)/playground/components}/complianceUI/ComplianceUI.tsx (99%) rename ui/litellm-dashboard/src/{components/playground/chat_ui => app/(dashboard)/playground/hooks}/useChatHistory.test.ts (100%) rename ui/litellm-dashboard/src/{components/playground/chat_ui => app/(dashboard)/playground/hooks}/useChatHistory.ts (98%) rename ui/litellm-dashboard/src/{components/playground/chat_ui => app/(dashboard)/playground/hooks}/useCodeInterpreter.ts (88%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/a2a_send_message.tsx (98%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/anthropic_messages.tsx (96%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/audio_speech.test.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/audio_speech.tsx (96%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/audio_transcriptions.test.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/audio_transcriptions.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/embeddings_api.test.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/embeddings_api.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/fetch_agents.tsx (98%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/image_edits.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/image_generation.tsx (100%) rename ui/litellm-dashboard/src/{components => app/(dashboard)}/playground/llm_calls/interactions_api.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/CodeSnippets.test.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/CodeSnippets.tsx (99%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/MCPEventsDisplay.tsx (99%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/ReasoningContent.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/ResponseMetrics.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/mode_endpoint_mapping.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/chat_ui/types.ts (100%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/chat_completion.test.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/chat_completion.tsx (99%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/code_interpreter_handler.ts (100%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/fetch_models.tsx (94%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/responses_api.test.tsx (100%) rename ui/litellm-dashboard/src/components/{playground => }/llm_calls/responses_api.tsx (98%) diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts index f2ba66147ea..749c3cde179 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts +++ b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts @@ -1,16 +1,23 @@ /** - * Source of truth for the App Router migration smoke (tests/migration/migratedPages.spec.ts). + * Source of truth for the App Router migration E2E suites. * - * Add a route segment here once its migration has MERGED to the branch under test. - * Both suites pick it up automatically: - * - default mount: npm run e2e:migration - * - server-root-path mount: SERVER_ROOT_PATH=/ npm run e2e:migration:root + * Add an entry (legacy sidebar page id -> route segment) once a page's migration + * has MERGED to the branch under test. Consumers pick it up automatically: + * - migration smoke (tests/migration/migratedPages.spec.ts), via MIGRATED_E2E_SEGMENTS: + * default mount: npm run e2e:migration + * server-root-path mount: SERVER_ROOT_PATH=/ npm run e2e:migration:root + * - navigation specs that assert per-page URLs (tests/navigation/sidebar.spec.ts) * * Keep this in lockstep with MIGRATED_PAGES in src/utils/migratedPages.ts. - * Pending (uncomment as each PR lands): playground, and the leaf-pages batch + * Pending (add as each PR lands): the leaf-pages batch * (budgets, caching, cost-tracking, guardrails, guardrails-monitor, logs, * mcp-servers, memory, policies, projects, prompts, search-tools, skills, * tag-management, tool-policies, transform-request, ui-theme, vector-stores, * workflows, access-groups). */ -export const MIGRATED_E2E_SEGMENTS: string[] = ["api-reference"]; +export const MIGRATED_E2E_PAGES: Record = { + api_ref: "api-reference", + "llm-playground": "playground", +}; + +export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))]; diff --git a/ui/litellm-dashboard/e2e_tests/tests/navigation/sidebar.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/navigation/sidebar.spec.ts index b8fb95b764d..7ac2e7df39d 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/navigation/sidebar.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/navigation/sidebar.spec.ts @@ -4,11 +4,23 @@ import { ADMIN_STORAGE_PATH } from "../../constants"; import { Page } from "../../fixtures/pages"; import { menuLabelToPage } from "../../fixtures/menuMappings"; import { navigateToPage } from "../../helpers/navigation"; +import { MIGRATED_E2E_PAGES } from "../../fixtures/migratedPages"; +import type { Page as PlaywrightPage } from "@playwright/test"; const sidebarButtons = { [Role.ProxyAdmin]: ["Virtual Keys", "Playground", "Models", "Usage", "Teams", "Internal Users", "AI Hub"], }; +/** Migrated pages live at a path route; legacy pages keep the ?page= query param. */ +async function expectPageUrl(page: PlaywrightPage, pageKey: string): Promise { + const migratedSegment = MIGRATED_E2E_PAGES[pageKey]; + if (migratedSegment) { + await expect(page).toHaveURL(new RegExp(`/ui/${migratedSegment}/?($|\\?)`)); + } else { + await expect(page).toHaveURL(new RegExp(`[?&]page=${pageKey}(&|$)`)); + } +} + const roles = [{ role: Role.ProxyAdmin, storage: ADMIN_STORAGE_PATH }]; for (const { role, storage } of roles) { @@ -35,8 +47,7 @@ for (const { role, storage } of roles) { await tab.click(); - // Verify URL contains the correct page query parameter - await expect(page).toHaveURL(new RegExp(`[?&]page=${expectedPage}(&|$)`)); + await expectPageUrl(page, expectedPage); } }); @@ -50,13 +61,14 @@ for (const { role, storage } of roles) { // Test direct navigation to verify the helper function works await navigateToPage(page, Page.ApiKeys); - await expect(page).toHaveURL(new RegExp(`[?&]page=${Page.ApiKeys}(&|$)`)); + await expectPageUrl(page, Page.ApiKeys); await navigateToPage(page, Page.Models); - await expect(page).toHaveURL(new RegExp(`[?&]page=${Page.Models}(&|$)`)); + await expectPageUrl(page, Page.Models); + // Migrated page: /ui?page=llm-playground redirects to the path route await navigateToPage(page, Page.LlmPlayground); - await expect(page).toHaveURL(new RegExp(`[?&]page=${Page.LlmPlayground}(&|$)`)); + await expectPageUrl(page, Page.LlmPlayground); }); }); } diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index d3169395b4e..358064c00af 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1533,7 +1533,7 @@ "count": 1 } }, - "src/components/playground/chat_ui/AdditionalModelSettings.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/AdditionalModelSettings.tsx": { "no-restricted-imports": { "count": 1 }, @@ -1541,17 +1541,17 @@ "count": 2 } }, - "src/components/playground/chat_ui/AgentBuilderView.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/AgentBuilderView.tsx": { "react-hooks/set-state-in-effect": { "count": 5 } }, - "src/components/playground/chat_ui/ChatImageUtils.test.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx": { "max-nested-callbacks": { "count": 1 } }, - "src/components/playground/chat_ui/ChatUI.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx": { "no-restricted-imports": { "count": 1 }, @@ -1562,17 +1562,17 @@ "count": 13 } }, - "src/components/playground/chat_ui/CodeInterpreterOutput.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx": { "no-restricted-syntax": { "count": 2 } }, - "src/components/playground/chat_ui/CodeInterpreterTool.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterTool.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/playground/chat_ui/RealtimePlayground.tsx": { + "src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx": { "react-hooks/immutability": { "count": 2 }, @@ -1580,22 +1580,22 @@ "count": 1 } }, - "src/components/playground/compareUI/CompareUI.tsx": { + "src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/playground/compareUI/components/ModelSelector.tsx": { + "src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/playground/complianceUI/ComplianceUI.tsx": { + "src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx": { "react-hooks/preserve-manual-memoization": { "count": 3 } }, - "src/components/playground/llm_calls/a2a_send_message.tsx": { + "src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx": { "max-params": { "count": 2 }, @@ -1603,27 +1603,27 @@ "count": 2 } }, - "src/components/playground/llm_calls/anthropic_messages.tsx": { + "src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/audio_speech.tsx": { + "src/app/(dashboard)/playground/llm_calls/audio_speech.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/audio_transcriptions.tsx": { + "src/app/(dashboard)/playground/llm_calls/audio_transcriptions.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/chat_completion.tsx": { + "src/components/llm_calls/chat_completion.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/embeddings_api.tsx": { + "src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx": { "max-params": { "count": 1 }, @@ -1631,22 +1631,22 @@ "count": 1 } }, - "src/components/playground/llm_calls/fetch_agents.tsx": { + "src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx": { "no-restricted-syntax": { "count": 1 } }, - "src/components/playground/llm_calls/image_edits.tsx": { + "src/app/(dashboard)/playground/llm_calls/image_edits.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/image_generation.tsx": { + "src/app/(dashboard)/playground/llm_calls/image_generation.tsx": { "max-params": { "count": 1 } }, - "src/components/playground/llm_calls/interactions_api.tsx": { + "src/app/(dashboard)/playground/llm_calls/interactions_api.tsx": { "max-params": { "count": 1 }, @@ -1654,7 +1654,7 @@ "count": 1 } }, - "src/components/playground/llm_calls/responses_api.tsx": { + "src/components/llm_calls/responses_api.tsx": { "max-params": { "count": 1 } @@ -2250,4 +2250,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx index 0854b085fae..f26e5dd5ef2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/page.tsx @@ -1,7 +1,6 @@ "use client"; import ModelsAndEndpointsView from "@/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView"; -import PlaygroundPage from "@/app/(dashboard)/playground/page"; import AdminPanel from "@/components/AdminPanel"; import AgentsPanel from "@/components/agents"; import BudgetPanel from "@/components/budgets/budget_panel"; @@ -354,8 +353,6 @@ function CreateKeyPageContent() { premiumUser={premiumUser} teams={teams} /> - ) : page == "llm-playground" ? ( - ) : page == "users" ? ( { it("should render the audio renderer", () => { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/AudioRenderer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AudioRenderer.tsx similarity index 90% rename from ui/litellm-dashboard/src/components/playground/chat_ui/AudioRenderer.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AudioRenderer.tsx index 48a766283e6..4b5a1596c1d 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/AudioRenderer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/AudioRenderer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; interface AudioRendererProps { message: MessageType; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageRenderer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageRenderer.tsx similarity index 95% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageRenderer.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageRenderer.tsx index 49adaaed8c1..f4449bee803 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageRenderer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageRenderer.tsx @@ -1,6 +1,6 @@ import React from "react"; import Image from "next/image"; -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; import { shouldShowChatAttachedImage } from "./ChatImageUtils"; import { FilePdfOutlined } from "@ant-design/icons"; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUpload.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUpload.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUpload.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUpload.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx index ecc4914b0ab..ab6c542d709 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.test.tsx @@ -5,7 +5,7 @@ import { createChatDisplayMessage, shouldShowChatAttachedImage, } from "./ChatImageUtils"; -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; describe("ChatImageUtils", () => { beforeEach(() => { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.tsx index 368ba3825ed..7a6340d9fde 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatImageUtils.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatImageUtils.tsx @@ -1,4 +1,4 @@ -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; export interface ChatMultimodalContent { type: "text" | "image_url"; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx index 7c4c56d5ade..647258b3d48 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import ChatMessageBubble from "./ChatMessageBubble"; -import { EndpointType } from "./mode_endpoint_mapping"; -import { MessageType } from "./types"; +import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; +import { MessageType } from "@/components/chat_ui/types"; // Mock child components to isolate bubble rendering logic vi.mock("react-markdown", () => ({ @@ -17,13 +17,13 @@ vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({ coy: {}, })); -vi.mock("./ReasoningContent", () => ({ +vi.mock("@/components/chat_ui/ReasoningContent", () => ({ default: ({ reasoningContent }: { reasoningContent: string }) => (
{reasoningContent}
), })); -vi.mock("./MCPEventsDisplay", () => ({ +vi.mock("@/components/chat_ui/MCPEventsDisplay", () => ({ default: ({ events }: { events: unknown[] }) =>
{events.length} events
, })); @@ -33,7 +33,7 @@ vi.mock("./SearchResultsDisplay", () => ({ ), })); -vi.mock("./ResponseMetrics", () => ({ +vi.mock("@/components/chat_ui/ResponseMetrics", () => ({ default: ({ timeToFirstToken }: { timeToFirstToken?: number }) => (
TTFT: {timeToFirstToken}
), diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx similarity index 93% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx index b5bff3b70fa..27226757c91 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatMessageBubble.tsx @@ -3,19 +3,19 @@ import React from "react"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; -import { CodeInterpreterResult } from "../llm_calls/code_interpreter_handler"; +import { CodeInterpreterResult } from "@/components/llm_calls/code_interpreter_handler"; import A2AMetrics from "./A2AMetrics"; import AudioRenderer from "./AudioRenderer"; import ChatImageRenderer from "./ChatImageRenderer"; import CodeInterpreterOutput from "./CodeInterpreterOutput"; -import { EndpointType } from "./mode_endpoint_mapping"; -import MCPEventsDisplay from "./MCPEventsDisplay"; -import type { MCPEvent } from "../../mcp_tools/types"; -import ReasoningContent from "./ReasoningContent"; -import ResponseMetrics from "./ResponseMetrics"; +import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; +import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay"; +import type { MCPEvent } from "@/components/mcp_tools/types"; +import ReasoningContent from "@/components/chat_ui/ReasoningContent"; +import ResponseMetrics from "@/components/chat_ui/ResponseMetrics"; import ResponsesImageRenderer from "./ResponsesImageRenderer"; import { SearchResultsDisplay } from "./SearchResultsDisplay"; -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; interface ChatMessageBubbleProps { message: MessageType; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx index 62bda4933ab..9da3e3a4a08 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.test.tsx @@ -1,15 +1,15 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import ChatUI from "./ChatUI"; -import * as fetchModelsModule from "../llm_calls/fetch_models"; +import * as fetchModelsModule from "@/components/llm_calls/fetch_models"; // Mock the fetchAvailableModels function -vi.mock("../llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn(), })); // Mock other networking functions that cause errors -vi.mock("../networking", () => ({ +vi.mock("@/components/networking", () => ({ tagListCall: vi.fn().mockResolvedValue({ data: [] }), vectorStoreListCall: vi.fn().mockResolvedValue({ data: [] }), getGuardrailsList: vi.fn().mockResolvedValue({ data: [] }), diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx index 5ba2df607cc..db46eb30cb8 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ChatUI.tsx @@ -28,28 +28,28 @@ import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; import { v4 as uuidv4 } from "uuid"; -import GuardrailSelector from "../../guardrails/GuardrailSelector"; -import PolicySelector from "../../policies/PolicySelector"; -import MCPToolArgumentsForm, { MCPToolArgumentsFormRef } from "../../mcp_tools/MCPToolArgumentsForm"; -import { MCPServer } from "../../mcp_tools/types"; -import { ByokCredentialModal } from "../../mcp_tools/ByokCredentialModal"; -import NotificationsManager from "../../molecules/notifications_manager"; -import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "../../networking"; -import { MCPToolset } from "../../mcp_tools/types"; -import TagSelector from "../../tag_management/TagSelector"; -import VectorStoreSelector from "../../vector_store_management/VectorStoreSelector"; -import { makeA2ASendMessageRequest } from "../llm_calls/a2a_send_message"; -import { makeAnthropicMessagesRequest } from "../llm_calls/anthropic_messages"; -import { makeOpenAIAudioSpeechRequest } from "../llm_calls/audio_speech"; -import { makeOpenAIAudioTranscriptionRequest } from "../llm_calls/audio_transcriptions"; -import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion"; -import { makeOpenAIEmbeddingsRequest } from "../llm_calls/embeddings_api"; -import { Agent, fetchAvailableAgents } from "../llm_calls/fetch_agents"; -import { fetchAvailableModels, ModelGroup } from "../llm_calls/fetch_models"; -import { makeOpenAIImageEditsRequest } from "../llm_calls/image_edits"; -import { makeOpenAIImageGenerationRequest } from "../llm_calls/image_generation"; -import { makeOpenAIResponsesRequest } from "../llm_calls/responses_api"; -import { makeInteractionsRequest } from "../llm_calls/interactions_api"; +import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; +import PolicySelector from "@/components/policies/PolicySelector"; +import MCPToolArgumentsForm, { MCPToolArgumentsFormRef } from "@/components/mcp_tools/MCPToolArgumentsForm"; +import { MCPServer } from "@/components/mcp_tools/types"; +import { ByokCredentialModal } from "@/components/mcp_tools/ByokCredentialModal"; +import NotificationsManager from "@/components/molecules/notifications_manager"; +import { callMCPTool, fetchMCPServers, fetchMCPToolsets, listMCPTools } from "@/components/networking"; +import { MCPToolset } from "@/components/mcp_tools/types"; +import TagSelector from "@/components/tag_management/TagSelector"; +import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector"; +import { makeA2ASendMessageRequest } from "../../llm_calls/a2a_send_message"; +import { makeAnthropicMessagesRequest } from "../../llm_calls/anthropic_messages"; +import { makeOpenAIAudioSpeechRequest } from "../../llm_calls/audio_speech"; +import { makeOpenAIAudioTranscriptionRequest } from "../../llm_calls/audio_transcriptions"; +import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_completion"; +import { makeOpenAIEmbeddingsRequest } from "../../llm_calls/embeddings_api"; +import { Agent, fetchAvailableAgents } from "../../llm_calls/fetch_agents"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; +import { makeOpenAIImageEditsRequest } from "../../llm_calls/image_edits"; +import { makeOpenAIImageGenerationRequest } from "../../llm_calls/image_generation"; +import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api"; +import { makeInteractionsRequest } from "../../llm_calls/interactions_api"; import A2AMetrics from "./A2AMetrics"; import AdditionalModelSettings from "./AdditionalModelSettings"; import AudioRenderer from "./AudioRenderer"; @@ -59,23 +59,23 @@ import ChatImageUpload from "./ChatImageUpload"; import { createChatDisplayMessage, createChatMultimodalMessage } from "./ChatImageUtils"; import CodeInterpreterOutput from "./CodeInterpreterOutput"; import CodeInterpreterTool from "./CodeInterpreterTool"; -import { generateCodeSnippet } from "./CodeSnippets"; +import { generateCodeSnippet } from "@/components/chat_ui/CodeSnippets"; import EndpointSelector from "./EndpointSelector"; import FilePreviewCard from "./FilePreviewCard"; import ChatMessageBubble from "./ChatMessageBubble"; -import MCPEventsDisplay from "./MCPEventsDisplay"; -import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; -import ReasoningContent from "./ReasoningContent"; -import ResponseMetrics, { TokenUsage } from "./ResponseMetrics"; +import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay"; +import { EndpointType, getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; +import ReasoningContent from "@/components/chat_ui/ReasoningContent"; +import ResponseMetrics, { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; import ResponsesImageRenderer from "./ResponsesImageRenderer"; import ResponsesImageUpload from "./ResponsesImageUpload"; import { createDisplayMessage, createMultimodalMessage } from "./ResponsesImageUtils"; import { SearchResultsDisplay } from "./SearchResultsDisplay"; import SessionManagement from "./SessionManagement"; import RealtimePlayground from "./RealtimePlayground"; -import { A2ATaskMetadata, MessageType } from "./types"; -import { useCodeInterpreter } from "./useCodeInterpreter"; -import { useChatHistory } from "./useChatHistory"; +import { A2ATaskMetadata, MessageType } from "@/components/chat_ui/types"; +import { useCodeInterpreter } from "../../hooks/useCodeInterpreter"; +import { useChatHistory } from "../../hooks/useChatHistory"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; const { TextArea } = Input; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterOutput.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterOutput.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterOutput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterOutput.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterOutput.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterTool.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterTool.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/CodeInterpreterTool.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/CodeInterpreterTool.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/EndpointSelector.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointSelector.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.test.tsx similarity index 95% rename from ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.test.tsx index 6eb481381ac..2bb1395428b 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.test.tsx @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { ModelGroup } from "../llm_calls/fetch_models"; +import type { ModelGroup } from "@/components/llm_calls/fetch_models"; import { determineEndpointType } from "./EndpointUtils"; -import { EndpointType } from "./mode_endpoint_mapping"; +import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; // Mock the getEndpointType function -vi.mock("./mode_endpoint_mapping", () => ({ +vi.mock("@/components/chat_ui/mode_endpoint_mapping", () => ({ EndpointType: { IMAGE: "image", VIDEO: "video", @@ -32,7 +32,7 @@ vi.mock("./mode_endpoint_mapping", () => ({ })); // Import the mocked function -import { getEndpointType } from "./mode_endpoint_mapping"; +import { getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; describe("determineEndpointType", () => { beforeEach(() => { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.tsx similarity index 82% rename from ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.tsx index de337e5638f..84579610a41 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/EndpointUtils.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/EndpointUtils.tsx @@ -1,5 +1,5 @@ -import { ModelGroup } from "../llm_calls/fetch_models"; -import { EndpointType, getEndpointType } from "./mode_endpoint_mapping"; +import { ModelGroup } from "@/components/llm_calls/fetch_models"; +import { EndpointType, getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; /** * Determines the appropriate endpoint type based on the selected model diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/FilePreviewCard.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/FilePreviewCard.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/FilePreviewCard.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/FilePreviewCard.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx index 88c6efd87bd..68a150be8c0 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/RealtimePlayground.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/RealtimePlayground.tsx @@ -3,7 +3,7 @@ import { AudioMutedOutlined, AudioOutlined, CloseCircleOutlined, SendOutlined, SoundOutlined } from "@ant-design/icons"; import { Button, Input, Select, Typography } from "antd"; import React, { useCallback, useEffect, useRef, useState } from "react"; -import { getProxyBaseUrl } from "../../networking"; +import { getProxyBaseUrl } from "@/components/networking"; import { OPEN_AI_VOICE_SELECT_OPTIONS } from "./chatConstants"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageRenderer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageRenderer.tsx similarity index 94% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageRenderer.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageRenderer.tsx index a8707ebd21b..d459d638c0a 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageRenderer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageRenderer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { MessageType } from "./types"; +import { MessageType } from "@/components/chat_ui/types"; import { shouldShowAttachedImage } from "./ResponsesImageUtils"; import { FilePdfOutlined } from "@ant-design/icons"; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUpload.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageUpload.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUpload.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageUpload.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageUtils.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageUtils.tsx index 50dee5c86f6..04dfa39d5c6 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponsesImageUtils.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/ResponsesImageUtils.tsx @@ -1,4 +1,4 @@ -import { MessageType, MultimodalContent } from "./types"; +import { MessageType, MultimodalContent } from "@/components/chat_ui/types"; export const convertImageToBase64 = (file: File): Promise => { return new Promise((resolve, reject) => { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/SearchResultsDisplay.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SearchResultsDisplay.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/chat_ui/SearchResultsDisplay.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SearchResultsDisplay.tsx index 966dbb67fc6..ba84fe95f80 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/SearchResultsDisplay.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SearchResultsDisplay.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { Button } from "antd"; -import { VectorStoreSearchResponse } from "./types"; +import { VectorStoreSearchResponse } from "@/components/chat_ui/types"; import { DatabaseOutlined, FileTextOutlined, DownOutlined, RightOutlined } from "@ant-design/icons"; interface SearchResultsDisplayProps { diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SessionManagement.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SessionManagement.tsx index e782845bfc3..87f9d797643 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/SessionManagement.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/SessionManagement.tsx @@ -1,8 +1,8 @@ import React from "react"; import { Switch, Tooltip } from "antd"; import { InfoCircleOutlined, CopyOutlined } from "@ant-design/icons"; -import { EndpointType } from "./mode_endpoint_mapping"; -import NotificationsManager from "../../molecules/notifications_manager"; +import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; +import NotificationsManager from "@/components/molecules/notifications_manager"; interface SessionManagementProps { endpointType: string; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts similarity index 95% rename from ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts index 9592a521250..2b59fbad2ee 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/chatConstants.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/chat_ui/chatConstants.ts @@ -1,4 +1,4 @@ -import { EndpointType } from "./mode_endpoint_mapping"; +import { EndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; export const OPEN_AI_VOICES = { ALLOY: "alloy", diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.test.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.test.tsx index 8e6da976cbd..4278cb6a0e4 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.test.tsx @@ -2,13 +2,13 @@ import { render, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import CompareUI from "./CompareUI"; -import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion"; +import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_completion"; -vi.mock("../llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn().mockResolvedValue([{ model_group: "gpt-4" }, { model_group: "gpt-3.5-turbo" }]), })); -vi.mock("../llm_calls/chat_completion", () => ({ +vi.mock("@/components/llm_calls/chat_completion", () => ({ makeOpenAIChatCompletionRequest: vi.fn().mockResolvedValue(undefined), })); diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx index ce738e4cd62..e41ee3ba8fa 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/CompareUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/CompareUI.tsx @@ -7,12 +7,12 @@ import { useEffect, useMemo, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import ChatImageUpload from "../chat_ui/ChatImageUpload"; import { createChatDisplayMessage, createChatMultimodalMessage } from "../chat_ui/ChatImageUtils"; -import type { TokenUsage } from "../chat_ui/ResponseMetrics"; -import type { MessageType, VectorStoreSearchResponse } from "../chat_ui/types"; -import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion"; -import { fetchAvailableModels } from "../llm_calls/fetch_models"; -import { Agent, fetchAvailableAgents } from "../llm_calls/fetch_agents"; -import { makeA2AStreamMessageRequest } from "../llm_calls/a2a_send_message"; +import type { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; +import type { MessageType, VectorStoreSearchResponse } from "@/components/chat_ui/types"; +import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_completion"; +import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; +import { Agent, fetchAvailableAgents } from "../../llm_calls/fetch_agents"; +import { makeA2AStreamMessageRequest } from "../../llm_calls/a2a_send_message"; import { ComparisonPanel } from "./components/ComparisonPanel"; import { MessageInput } from "./components/MessageInput"; import { diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.test.tsx similarity index 93% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.test.tsx index 2aef47f71d9..c07ad367606 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.test.tsx @@ -18,15 +18,15 @@ vi.mock("./UnifiedSelector", () => ({ ), })); -vi.mock("../../../tag_management/TagSelector", () => ({ +vi.mock("@/components/tag_management/TagSelector", () => ({ default: () =>
TagSelector
, })); -vi.mock("../../../vector_store_management/VectorStoreSelector", () => ({ +vi.mock("@/components/vector_store_management/VectorStoreSelector", () => ({ default: () =>
VectorStoreSelector
, })); -vi.mock("../../../guardrails/GuardrailSelector", () => ({ +vi.mock("@/components/guardrails/GuardrailSelector", () => ({ default: () =>
GuardrailSelector
, })); diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.tsx index 1074172974f..6ddb5e27947 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/components/ComparisonPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ComparisonPanel.tsx @@ -3,9 +3,9 @@ import { useState } from "react"; import { ComparisonInstance } from "../CompareUI"; import { MessageDisplay } from "./MessageDisplay"; import { UnifiedSelector } from "./UnifiedSelector"; -import TagSelector from "../../../tag_management/TagSelector"; -import VectorStoreSelector from "../../../vector_store_management/VectorStoreSelector"; -import GuardrailSelector from "../../../guardrails/GuardrailSelector"; +import TagSelector from "@/components/tag_management/TagSelector"; +import VectorStoreSelector from "@/components/vector_store_management/VectorStoreSelector"; +import GuardrailSelector from "@/components/guardrails/GuardrailSelector"; import { Checkbox, Divider, Popover, Slider } from "antd"; import { SelectorOption, EndpointConfig, isAgentEndpoint, getComparisonSelection } from "../endpoint_config"; diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.test.tsx similarity index 94% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.test.tsx index c635e9555f6..72a1d41f9fe 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.test.tsx @@ -1,15 +1,15 @@ import { render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; -import type { MessageType } from "../../chat_ui/types"; +import type { MessageType } from "@/components/chat_ui/types"; import { MessageDisplay } from "./MessageDisplay"; -vi.mock("../../chat_ui/ReasoningContent", () => ({ +vi.mock("@/components/chat_ui/ReasoningContent", () => ({ default: ({ reasoningContent }: { reasoningContent: string }) => (
{reasoningContent}
), })); -vi.mock("../../chat_ui/ResponseMetrics", () => ({ +vi.mock("@/components/chat_ui/ResponseMetrics", () => ({ default: () =>
ResponseMetrics
, })); diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.tsx index 088e45b91b3..2e4868d3527 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageDisplay.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageDisplay.tsx @@ -4,10 +4,10 @@ import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; import ChatImageRenderer from "../../chat_ui/ChatImageRenderer"; -import ReasoningContent from "../../chat_ui/ReasoningContent"; -import ResponseMetrics from "../../chat_ui/ResponseMetrics"; +import ReasoningContent from "@/components/chat_ui/ReasoningContent"; +import ResponseMetrics from "@/components/chat_ui/ResponseMetrics"; import { SearchResultsDisplay } from "../../chat_ui/SearchResultsDisplay"; -import type { MessageType } from "../../chat_ui/types"; +import type { MessageType } from "@/components/chat_ui/types"; interface MessageDisplayProps { messages: MessageType[]; diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageInput.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/MessageInput.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/MessageInput.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/MessageInput.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/MessageInput.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/ModelSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/ModelSelector.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/ModelSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/ModelSelector.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/ModelSelector.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/UnifiedSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/UnifiedSelector.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/components/UnifiedSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/compareUI/components/UnifiedSelector.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/components/UnifiedSelector.tsx diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts similarity index 98% rename from ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.test.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts index 67ecf32fc2a..6463b2f9e5e 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.test.ts @@ -12,7 +12,7 @@ import { getComparisonSelection, hasValidSelection, } from "./endpoint_config"; -import { Agent } from "../llm_calls/fetch_agents"; +import { Agent } from "../../llm_calls/fetch_agents"; describe("endpoint_config", () => { it("should export EndpointId constants", () => { diff --git a/ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts similarity index 98% rename from ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts index c8e39f3ad03..2d1ebdaedd6 100644 --- a/ui/litellm-dashboard/src/components/playground/compareUI/endpoint_config.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/compareUI/endpoint_config.ts @@ -3,7 +3,7 @@ * Add new endpoints here to extend the comparison functionality. */ -import { Agent } from "../llm_calls/fetch_agents"; +import { Agent } from "../../llm_calls/fetch_agents"; // Endpoint identifiers export const EndpointId = { diff --git a/ui/litellm-dashboard/src/components/playground/complianceUI/ComplianceUI.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/complianceUI/ComplianceUI.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx index 448ddb5d32b..cf619057c38 100644 --- a/ui/litellm-dashboard/src/components/playground/complianceUI/ComplianceUI.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/complianceUI/ComplianceUI.tsx @@ -9,7 +9,7 @@ import { import { getGuardrailsList, testPoliciesAndGuardrails } from "@/components/networking"; import PolicySelector, { getPolicyOptionEntries } from "@/components/policies/PolicySelector"; import { Policy } from "@/components/policies/types"; -import { makeOpenAIChatCompletionRequest } from "../llm_calls/chat_completion"; +import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_completion"; import { AlertTriangle, BarChart3, diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useChatHistory.test.ts similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.test.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useChatHistory.test.ts diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useChatHistory.ts similarity index 98% rename from ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useChatHistory.ts index d0da281bcc7..6e2a263f599 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useChatHistory.ts @@ -1,8 +1,8 @@ import React, { useState, useEffect } from "react"; -import { MessageType, A2ATaskMetadata } from "./types"; -import { TokenUsage } from "./ResponseMetrics"; -import { MCPEvent } from "../../mcp_tools/types"; -import { truncateString } from "../../../utils/textUtils"; +import { MessageType, A2ATaskMetadata } from "@/components/chat_ui/types"; +import { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; +import { MCPEvent } from "@/components/mcp_tools/types"; +import { truncateString } from "@/utils/textUtils"; export interface UseChatHistoryReturn { // State diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/useCodeInterpreter.ts b/ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useCodeInterpreter.ts similarity index 88% rename from ui/litellm-dashboard/src/components/playground/chat_ui/useCodeInterpreter.ts rename to ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useCodeInterpreter.ts index 430bee33be0..9f32ef072e1 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/useCodeInterpreter.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/hooks/useCodeInterpreter.ts @@ -4,7 +4,7 @@ */ import { useState, useCallback } from "react"; -import { CodeInterpreterResult } from "../llm_calls/code_interpreter_handler"; +import { CodeInterpreterResult } from "@/components/llm_calls/code_interpreter_handler"; export interface UseCodeInterpreterReturn { // State @@ -54,4 +54,4 @@ export function useCodeInterpreter(): UseCodeInterpreterReturn { } // Re-export the type for convenience -export type { CodeInterpreterResult } from "../llm_calls/code_interpreter_handler"; +export type { CodeInterpreterResult } from "@/components/llm_calls/code_interpreter_handler"; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx index 42c1c4cc81f..4e01dce0d9d 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/a2a_send_message.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/a2a_send_message.tsx @@ -2,8 +2,8 @@ // A2A Protocol (JSON-RPC 2.0) implementation for sending messages to agents import { v4 as uuidv4 } from "uuid"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../networking"; -import { A2ATaskMetadata } from "../chat_ui/types"; +import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "@/components/networking"; +import { A2ATaskMetadata } from "@/components/chat_ui/types"; interface A2AMessagePart { kind: "text"; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx index 5570c7408fa..11e7a5e1601 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/anthropic_messages.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/anthropic_messages.tsx @@ -1,6 +1,6 @@ import Anthropic from "@anthropic-ai/sdk"; -import { MessageType } from "../chat_ui/types"; -import { TokenUsage } from "../chat_ui/ResponseMetrics"; +import { MessageType } from "@/components/chat_ui/types"; +import { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; import { getProxyBaseUrl } from "@/components/networking"; import NotificationManager from "@/components/molecules/notifications_manager"; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/audio_speech.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_speech.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/audio_speech.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_speech.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/audio_speech.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_speech.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/playground/llm_calls/audio_speech.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_speech.tsx index c5d4ae4d686..eda5d6ed66a 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/audio_speech.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_speech.tsx @@ -1,7 +1,7 @@ import openai from "openai"; import { getProxyBaseUrl } from "@/components/networking"; import NotificationManager from "@/components/molecules/notifications_manager"; -import type { OpenAIVoice } from "../chat_ui/chatConstants"; +import type { OpenAIVoice } from "../components/chat_ui/chatConstants"; export async function makeOpenAIAudioSpeechRequest( input: string, diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/audio_transcriptions.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_transcriptions.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/audio_transcriptions.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_transcriptions.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/audio_transcriptions.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_transcriptions.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/audio_transcriptions.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/audio_transcriptions.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/embeddings_api.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/embeddings_api.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx index 258adcc93b2..0dc589188f5 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_agents.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/fetch_agents.tsx @@ -1,6 +1,6 @@ // fetch_agents.tsx -import { getProxyBaseUrl, getGlobalLitellmHeaderName, modelInfoCall } from "../../networking"; +import { getProxyBaseUrl, getGlobalLitellmHeaderName, modelInfoCall } from "@/components/networking"; export interface Agent { agent_id: string; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/image_edits.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/image_edits.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/image_edits.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/image_edits.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/image_generation.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/image_generation.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/image_generation.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/image_generation.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/interactions_api.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/interactions_api.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/playground/llm_calls/interactions_api.tsx diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx index abcbe80a382..c9bca86dc23 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/page.tsx @@ -1,10 +1,10 @@ "use client"; import { useState, useEffect } from "react"; -import AgentBuilderView from "@/components/playground/chat_ui/AgentBuilderView"; -import ChatUI from "@/components/playground/chat_ui/ChatUI"; -import CompareUI from "@/components/playground/compareUI/CompareUI"; -import ComplianceUI from "@/components/playground/complianceUI/ComplianceUI"; +import AgentBuilderView from "@/app/(dashboard)/playground/components/chat_ui/AgentBuilderView"; +import ChatUI from "@/app/(dashboard)/playground/components/chat_ui/ChatUI"; +import CompareUI from "@/app/(dashboard)/playground/components/compareUI/CompareUI"; +import ComplianceUI from "@/app/(dashboard)/playground/components/complianceUI/ComplianceUI"; import { TabGroup, TabList, Tab, TabPanels, TabPanel } from "@tremor/react"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { fetchProxySettings } from "@/utils/proxyUtils"; diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.test.tsx b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.test.tsx index b0cf4f8262a..89711a098fe 100644 --- a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.test.tsx +++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.test.tsx @@ -33,7 +33,7 @@ vi.mock("./pricing_calculator/index", () => ({ default: () =>
Pricing Calculator
, })); -vi.mock("../playground/llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn().mockResolvedValue([]), })); diff --git a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx index 9dade715cbc..d9cca4d3c23 100644 --- a/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx +++ b/ui/litellm-dashboard/src/components/CostTrackingSettings/cost_tracking_settings.tsx @@ -24,7 +24,7 @@ import { DocsMenu } from "../HelpLink"; import HowItWorks from "./how_it_works"; import { useDiscountConfig } from "./use_discount_config"; import { useMarginConfig } from "./use_margin_config"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; const DOCS_LINKS = [ { label: "Custom pricing for models", href: "https://docs.litellm.ai/docs/proxy/custom_pricing" }, diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/EvaluationSettingsModal.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/EvaluationSettingsModal.tsx index 90a2c0d597d..0edfa65dfe8 100644 --- a/ui/litellm-dashboard/src/components/GuardrailsMonitor/EvaluationSettingsModal.tsx +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/EvaluationSettingsModal.tsx @@ -1,7 +1,7 @@ import { CloseOutlined, PlayCircleOutlined } from "@ant-design/icons"; import { Button, Modal, Select, Input } from "antd"; import React, { useEffect, useState } from "react"; -import { fetchAvailableModels, type ModelGroup } from "@/components/playground/llm_calls/fetch_models"; +import { fetchAvailableModels, type ModelGroup } from "@/components/llm_calls/fetch_models"; const DEFAULT_PROMPT = `Evaluate whether this guardrail's decision was correct. Analyze the user input, the guardrail action taken, and determine if it was appropriate. diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.test.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.test.tsx index a2288f40039..39542945c45 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.test.tsx @@ -14,7 +14,7 @@ vi.mock("@/app/(dashboard)/hooks/mcpSemanticFilterSettings/useUpdateMCPSemanticF useUpdateMCPSemanticFilterSettings: vi.fn(), })); -vi.mock("@/components/playground/llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn().mockResolvedValue([]), })); diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx index c62e752565c..38b1420f97e 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings.tsx @@ -21,7 +21,7 @@ import { } from "antd"; import { QuestionCircleOutlined, CheckCircleOutlined, SaveOutlined } from "@ant-design/icons"; import { useEffect, useState } from "react"; -import { fetchAvailableModels, ModelGroup } from "@/components/playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import MCPSemanticFilterTestPanel from "./MCPSemanticFilterTestPanel"; import { getCurlCommand, runSemanticFilterTest, TestResult } from "./semanticFilterTestUtils"; diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.test.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.test.tsx index 0e05c141570..1253532f269 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.test.tsx @@ -2,9 +2,9 @@ import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import AddFallbacks, { Fallbacks } from "./AddFallbacks"; -import * as fetchModelsModule from "../../../playground/llm_calls/fetch_models"; +import * as fetchModelsModule from "@/components/llm_calls/fetch_models"; -vi.mock("../../../playground/llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn(), })); diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx index 8b86ff862f9..8147189ddcc 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/AddFallbacks.tsx @@ -9,7 +9,7 @@ import { Button } from "antd"; import React, { useEffect, useState } from "react"; import MessageManager from "@/components/molecules/message_manager"; import NotificationManager from "../../../molecules/notifications_manager"; -import { fetchAvailableModels, ModelGroup } from "../../../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import { AddFallbacksModal } from "./AddFallbacksModal"; import { FallbackGroup } from "./FallbackGroupConfig"; import { FallbackSelectionForm } from "./FallbackSelectionForm"; diff --git a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx index 323b71bd5c4..513abb78590 100644 --- a/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx +++ b/ui/litellm-dashboard/src/components/Settings/RouterSettings/Fallbacks/Fallbacks.test.tsx @@ -3,14 +3,14 @@ import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import Fallbacks from "./Fallbacks"; import * as networkingModule from "../../../networking"; -import * as fetchModelsModule from "../../../playground/llm_calls/fetch_models"; +import * as fetchModelsModule from "@/components/llm_calls/fetch_models"; vi.mock("../../../networking", () => ({ getCallbacksCall: vi.fn(), setCallbacksCall: vi.fn(), })); -vi.mock("../../../playground/llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn(), })); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx index a20cb969e33..d826f3df32c 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined } from "@ant-design/icons"; import { Select as AntdSelect, Card, Divider, Space, Tooltip, Typography } from "antd"; import React from "react"; -import { ModelGroup } from "../playground/llm_calls/fetch_models"; +import { ModelGroup } from "@/components/llm_calls/fetch_models"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx b/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx index 17c962c7e25..08acf993e2c 100644 --- a/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx +++ b/ui/litellm-dashboard/src/components/add_model/RouterConfigBuilder.tsx @@ -14,7 +14,7 @@ import { Typography, } from "antd"; import React, { useEffect, useState } from "react"; -import { ModelGroup } from "../playground/llm_calls/fetch_models"; +import { ModelGroup } from "@/components/llm_calls/fetch_models"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx index 6d01e9bf74e..9e9a097e3fe 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx @@ -6,7 +6,7 @@ import { modelAvailableCall } from "../networking"; import ConnectionErrorDisplay from "./model_connection_test"; import { all_admin_roles } from "@/utils/roles"; import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import RouterConfigBuilder from "./RouterConfigBuilder"; import ComplexityRouterConfig from "./ComplexityRouterConfig"; import NotificationManager from "../molecules/notifications_manager"; diff --git a/ui/litellm-dashboard/src/components/cache_settings/CacheFieldRenderer.tsx b/ui/litellm-dashboard/src/components/cache_settings/CacheFieldRenderer.tsx index eeabda23f9f..6608b09d261 100644 --- a/ui/litellm-dashboard/src/components/cache_settings/CacheFieldRenderer.tsx +++ b/ui/litellm-dashboard/src/components/cache_settings/CacheFieldRenderer.tsx @@ -4,7 +4,7 @@ import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { NumberInput, TextInput } from "@tremor/react"; import { Select } from "antd"; import React, { useEffect, useState } from "react"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import NumericalInput from "../shared/numerical_input"; interface CacheFieldRendererProps { diff --git a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx index 67b55697481..53877be1737 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatMessages.tsx @@ -7,8 +7,8 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; -import ReasoningContent from "../playground/chat_ui/ReasoningContent"; -import MCPEventsDisplay from "../playground/chat_ui/MCPEventsDisplay"; +import ReasoningContent from "@/components/chat_ui/ReasoningContent"; +import MCPEventsDisplay from "@/components/chat_ui/MCPEventsDisplay"; import { ChatMessage } from "./types"; const { Panel } = Collapse; diff --git a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx index 0e729c8ae9d..2a7fdb75eb8 100644 --- a/ui/litellm-dashboard/src/components/chat/ChatPage.tsx +++ b/ui/litellm-dashboard/src/components/chat/ChatPage.tsx @@ -27,9 +27,9 @@ import ChatMessages from "./ChatMessages"; import MCPConnectPicker from "./MCPConnectPicker"; import MCPAppsPanel from "./MCPAppsPanel"; import MCPCredentialsTab from "./MCPCredentialsTab"; -import { fetchAvailableModels } from "../playground/llm_calls/fetch_models"; -import { makeOpenAIChatCompletionRequest } from "../playground/llm_calls/chat_completion"; -import { makeOpenAIResponsesRequest } from "../playground/llm_calls/responses_api"; +import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; +import { makeOpenAIChatCompletionRequest } from "@/components/llm_calls/chat_completion"; +import { makeOpenAIResponsesRequest } from "@/components/llm_calls/responses_api"; import type { MCPEvent } from "./types"; import { getProxyBaseUrl } from "@/components/networking"; import { useUIConfig } from "@/app/(dashboard)/hooks/uiConfig/useUIConfig"; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx b/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.test.tsx rename to ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.tsx b/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.tsx rename to ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx index d38d4dff7a7..576b094b5d3 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/CodeSnippets.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/CodeSnippets.tsx @@ -1,6 +1,6 @@ import { MessageType } from "./types"; import { EndpointType } from "./mode_endpoint_mapping"; -import { MCPServer } from "../../mcp_tools/types"; +import { MCPServer } from "@/components/mcp_tools/types"; interface CodeGenMetadata { tags?: string[]; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx b/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx rename to ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx index cd7ecbf266f..e9319169d20 100644 --- a/ui/litellm-dashboard/src/components/playground/chat_ui/MCPEventsDisplay.tsx +++ b/ui/litellm-dashboard/src/components/chat_ui/MCPEventsDisplay.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Typography, Collapse } from "antd"; -import type { MCPEvent } from "../../mcp_tools/types"; +import type { MCPEvent } from "@/components/mcp_tools/types"; const { Text } = Typography; const { Panel } = Collapse; diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ReasoningContent.tsx b/ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ReasoningContent.tsx rename to ui/litellm-dashboard/src/components/chat_ui/ReasoningContent.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ResponseMetrics.tsx b/ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/ResponseMetrics.tsx rename to ui/litellm-dashboard/src/components/chat_ui/ResponseMetrics.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx b/ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/mode_endpoint_mapping.tsx rename to ui/litellm-dashboard/src/components/chat_ui/mode_endpoint_mapping.tsx diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/types.ts b/ui/litellm-dashboard/src/components/chat_ui/types.ts similarity index 100% rename from ui/litellm-dashboard/src/components/playground/chat_ui/types.ts rename to ui/litellm-dashboard/src/components/chat_ui/types.ts diff --git a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx index 24d6696dec6..060a8f0f111 100644 --- a/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx +++ b/ui/litellm-dashboard/src/components/common_components/ModelSelector.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import { TextInput, Text } from "@tremor/react"; import { Select } from "antd"; import { RobotOutlined } from "@ant-design/icons"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; interface ModelSelectorProps { accessToken: string; diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx index 74b968b5b03..0aa274b5749 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx @@ -5,7 +5,7 @@ import RouterSettingsForm, { RouterSettingsFormValue } from "../router_settings/ import { Fallbacks } from "../Settings/RouterSettings/Fallbacks/AddFallbacks"; import { FallbackSelectionForm } from "../Settings/RouterSettings/Fallbacks/FallbackSelectionForm"; import { FallbackGroup } from "../Settings/RouterSettings/Fallbacks/FallbackGroupConfig"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; export interface RouterSettingsAccordionValue { router_settings: { diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx index e4e6ebd1da9..caa061147d8 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import { Modal, Form, Button, Select as AntdSelect } from "antd"; import { Text, TextInput } from "@tremor/react"; import { modelAvailableCall, modelPatchUpdateCall } from "../networking"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import RouterConfigBuilder from "../add_model/RouterConfigBuilder"; import NotificationsManager from "../molecules/notifications_manager"; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx rename to ui/litellm-dashboard/src/components/llm_calls/chat_completion.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx rename to ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx index a45c3036f59..54d9273c463 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx +++ b/ui/litellm-dashboard/src/components/llm_calls/chat_completion.tsx @@ -3,7 +3,7 @@ import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; import { TokenUsage } from "../chat_ui/ResponseMetrics"; import { VectorStoreSearchResponse } from "../chat_ui/types"; import { getProxyBaseUrl } from "@/components/networking"; -import { MCPServer, MCPToolset, type MCPEvent } from "../../mcp_tools/types"; +import { MCPServer, MCPToolset, type MCPEvent } from "@/components/mcp_tools/types"; export async function makeOpenAIChatCompletionRequest( chatHistory: { role: string; content: string | any[] }[], diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/code_interpreter_handler.ts b/ui/litellm-dashboard/src/components/llm_calls/code_interpreter_handler.ts similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/code_interpreter_handler.ts rename to ui/litellm-dashboard/src/components/llm_calls/code_interpreter_handler.ts diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_models.tsx b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx similarity index 94% rename from ui/litellm-dashboard/src/components/playground/llm_calls/fetch_models.tsx rename to ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx index 14ba8c2381b..6be6b65502a 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/fetch_models.tsx +++ b/ui/litellm-dashboard/src/components/llm_calls/fetch_models.tsx @@ -1,6 +1,6 @@ // fetch_models.ts -import { modelHubCall } from "../../networking"; +import { modelHubCall } from "@/components/networking"; export interface ModelGroup { model_group: string; diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx b/ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.test.tsx rename to ui/litellm-dashboard/src/components/llm_calls/responses_api.test.tsx diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx rename to ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx index 4e88a356cf3..d3dd866b36b 100644 --- a/ui/litellm-dashboard/src/components/playground/llm_calls/responses_api.tsx +++ b/ui/litellm-dashboard/src/components/llm_calls/responses_api.tsx @@ -3,8 +3,8 @@ import { MessageType } from "../chat_ui/types"; import { TokenUsage } from "../chat_ui/ResponseMetrics"; import { getProxyBaseUrl } from "@/components/networking"; import NotificationManager from "@/components/molecules/notifications_manager"; -import type { MCPEvent } from "../../mcp_tools/types"; -import { MCPServer, MCPToolset } from "../../mcp_tools/types"; +import type { MCPEvent } from "@/components/mcp_tools/types"; +import { MCPServer, MCPToolset } from "@/components/mcp_tools/types"; import { CodeInterpreterResult, CodeInterpreterState, diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx index 12189f2b2c3..7d58465751e 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx @@ -3,7 +3,7 @@ import { RobotOutlined, UserOutlined } from "@ant-design/icons"; import ReactMarkdown from "react-markdown"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { coy } from "react-syntax-highlighter/dist/esm/styles/prism"; -import ResponseMetrics from "../../../playground/chat_ui/ResponseMetrics"; +import ResponseMetrics from "@/components/chat_ui/ResponseMetrics"; import { Message } from "./types"; interface MessageBubbleProps { diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/types.ts b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/types.ts index b56f7b2f582..33d6f2dc7cc 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/types.ts +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/types.ts @@ -1,4 +1,4 @@ -import { TokenUsage } from "../../../playground/chat_ui/ResponseMetrics"; +import { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; export interface Message { role: string; diff --git a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts index 7b595c777c1..e55d8bdeadf 100644 --- a/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts +++ b/ui/litellm-dashboard/src/components/prompts/prompt_editor_view/conversation_panel/useConversation.ts @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from "react"; import NotificationsManager from "../../../molecules/notifications_manager"; -import { TokenUsage } from "../../../playground/chat_ui/ResponseMetrics"; +import { TokenUsage } from "@/components/chat_ui/ResponseMetrics"; import { Message } from "./types"; import { convertToDotPrompt, extractVariables } from "../utils"; import { getProxyBaseUrl, getGlobalLitellmHeaderName } from "../../../networking"; diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx index 6ad8db19d8d..5299ec0fce8 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx @@ -18,9 +18,9 @@ import { } from "./networking"; import { Plugin } from "./claude_code_plugins/types"; import SkillHubDashboard from "./AIHub/SkillHubDashboard"; -import { generateCodeSnippet } from "./playground/chat_ui/CodeSnippets"; -import { getEndpointType } from "./playground/chat_ui/mode_endpoint_mapping"; -import { MessageType } from "./playground/chat_ui/types"; +import { generateCodeSnippet } from "@/components/chat_ui/CodeSnippets"; +import { getEndpointType } from "@/components/chat_ui/mode_endpoint_mapping"; +import { MessageType } from "@/components/chat_ui/types"; import { getProviderLogoAndName } from "./provider_info_helpers"; const { TabPane } = Tabs; diff --git a/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.test.tsx b/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.test.tsx index 459f7ed5dd5..b43c74d71a0 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.test.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.test.tsx @@ -1,10 +1,10 @@ import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import S3VectorsConfig from "./S3VectorsConfig"; -import * as fetchModels from "../playground/llm_calls/fetch_models"; +import * as fetchModels from "@/components/llm_calls/fetch_models"; // Mock fetchAvailableModels -vi.mock("../playground/llm_calls/fetch_models", () => ({ +vi.mock("@/components/llm_calls/fetch_models", () => ({ fetchAvailableModels: vi.fn(), })); diff --git a/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.tsx b/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.tsx index 0568f982bab..9f2c8f5e45e 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/S3VectorsConfig.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import { Alert, Form, Input, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; interface S3VectorsConfigProps { accessToken: string | null; diff --git a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreForm.tsx b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreForm.tsx index 538292a2437..4f0433025b7 100644 --- a/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreForm.tsx +++ b/ui/litellm-dashboard/src/components/vector_store_management/VectorStoreForm.tsx @@ -10,7 +10,7 @@ import { getProviderSpecificFields, VectorStoreFieldConfig, } from "../vector_store_providers"; -import { fetchAvailableModels, ModelGroup } from "../playground/llm_calls/fetch_models"; +import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import NotificationsManager from "../molecules/notifications_manager"; interface VectorStoreFormProps { diff --git a/ui/litellm-dashboard/src/utils/migratedPages.test.ts b/ui/litellm-dashboard/src/utils/migratedPages.test.ts index bd4ad7af5b8..58958f7b649 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.test.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.test.ts @@ -40,6 +40,13 @@ describe("migratedHref / legacyPageHref", () => { expect(MIGRATED_PAGES.api_ref).toBe("api-reference"); expect(MIGRATED_PAGES["api-reference"]).toBe("api-reference"); }); + + it("maps the llm-playground sidebar id to the playground route", async () => { + vi.doMock("@/components/networking", () => ({ serverRootPath: "/" })); + const { MIGRATED_PAGES } = await import("./migratedPages"); + + expect(MIGRATED_PAGES["llm-playground"]).toBe("playground"); + }); }); describe("dev server (NODE_ENV=development)", () => { diff --git a/ui/litellm-dashboard/src/utils/migratedPages.ts b/ui/litellm-dashboard/src/utils/migratedPages.ts index d6f1e7d6f1d..d911bf0566a 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.ts @@ -12,6 +12,7 @@ export const MIGRATED_PAGES: Record = { api_ref: "api-reference", // Legacy alias: older bookmarks used the hyphenated ?page=api-reference form. "api-reference": "api-reference", + "llm-playground": "playground", }; function uiBase(): string { From a2c916fb45e238020f48e665beb43a8a360f2641 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 11 Jun 2026 13:20:21 -0700 Subject: [PATCH 04/44] feat(ui): migrate projects and access-groups to path routes (#30226) * feat(ui): cut projects and access-groups over to path routes Same recipe as playground (#30185): MIGRATED_PAGES entries route the sidebar and redirect the legacy ?page= URLs, the switch arms are deleted, and the e2e fixture grows two entries. Both components were already zero-prop and self-fetching via React Query hooks, so the route wrappers are trivial. * refactor(ui): move Projects and AccessGroups components into their route folders Both folders were imported only by the legacy switch, so they colocate wholesale under (dashboard)/{projects,access-groups}/components. Their React Query hooks stay in the shared (dashboard)/hooks layer. eslint suppressions are re-keyed to the new paths. * test(ui): enable enable_projects_ui in e2e global setup The projects migration smoke clicks the Projects sidebar link, which only renders when the enterprise-gated enable_projects_ui setting is on; the seeded e2e database starts with it off, so the locator timed out in both e2e_ui_testing jobs. CI already launches the proxy with LITELLM_LICENSE for premium UI coverage, so flip the setting in globalSetup via the same /update/ui_settings call the admin UI toggle makes, failing loudly if the PATCH is rejected. * test(ui): use Playwright request context instead of raw fetch in global setup The frontend lint bans raw fetch() outside src/lib/http/; the e2e convention for proxy API calls is Playwright's APIRequestContext, as in routerSettings.spec.ts. --- .../e2e_tests/fixtures/migratedPages.ts | 6 ++++-- ui/litellm-dashboard/e2e_tests/globalSetup.ts | 17 ++++++++++++++++- ui/litellm-dashboard/eslint-suppressions.json | 8 ++++---- .../AccessGroupsDetailsPage.test.tsx | 2 +- .../components}/AccessGroupsDetailsPage.tsx | 2 +- .../AccessGroupsModal/AccessGroupBaseForm.tsx | 0 .../AccessGroupCreateModal.tsx | 0 .../AccessGroupsModal/AccessGroupEditModal.tsx | 0 .../components}/AccessGroupsPage.test.tsx | 2 +- .../components}/AccessGroupsPage.tsx | 6 +++--- .../access-groups/components}/types.ts | 0 .../src/app/(dashboard)/access-groups/page.tsx | 9 +++++++++ .../src/app/(dashboard)/page.tsx | 6 ------ .../components}/ProjectDetailsPage.test.tsx | 2 +- .../projects/components}/ProjectDetailsPage.tsx | 2 +- .../components}/ProjectKeysSection.test.tsx | 2 +- .../projects/components}/ProjectKeysSection.tsx | 0 .../components}/ProjectKeysTable.test.tsx | 2 +- .../projects/components}/ProjectKeysTable.tsx | 2 +- .../ProjectModals/CreateProjectModal.test.tsx | 2 +- .../ProjectModals/CreateProjectModal.tsx | 0 .../ProjectModals/EditProjectModal.test.tsx | 2 +- .../ProjectModals/EditProjectModal.tsx | 0 .../ProjectModals/ProjectBaseForm.test.tsx | 2 +- .../ProjectModals/ProjectBaseForm.tsx | 6 +++--- .../ProjectModals/projectFormUtils.test.ts | 0 .../ProjectModals/projectFormUtils.ts | 0 .../projects/components}/ProjectsPage.test.tsx | 2 +- .../projects/components}/ProjectsPage.tsx | 0 .../src/app/(dashboard)/projects/page.tsx | 9 +++++++++ .../src/utils/migratedPages.test.ts | 8 ++++++++ ui/litellm-dashboard/src/utils/migratedPages.ts | 2 ++ ui/litellm-dashboard/tsconfig.tsbuildinfo | 2 +- 33 files changed, 71 insertions(+), 32 deletions(-) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsDetailsPage.test.tsx (99%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsDetailsPage.tsx (99%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsModal/AccessGroupBaseForm.tsx (100%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsModal/AccessGroupCreateModal.tsx (100%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsModal/AccessGroupEditModal.tsx (100%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsPage.test.tsx (99%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/AccessGroupsPage.tsx (97%) rename ui/litellm-dashboard/src/{components/AccessGroups => app/(dashboard)/access-groups/components}/types.ts (100%) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/access-groups/page.tsx rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectDetailsPage.test.tsx (98%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectDetailsPage.tsx (99%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectKeysSection.test.tsx (97%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectKeysSection.tsx (100%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectKeysTable.test.tsx (98%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectKeysTable.tsx (94%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/CreateProjectModal.test.tsx (96%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/CreateProjectModal.tsx (100%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/EditProjectModal.test.tsx (97%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/EditProjectModal.tsx (100%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/ProjectBaseForm.test.tsx (99%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/ProjectBaseForm.tsx (98%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/projectFormUtils.test.ts (100%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectModals/projectFormUtils.ts (100%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectsPage.test.tsx (98%) rename ui/litellm-dashboard/src/{components/Projects => app/(dashboard)/projects/components}/ProjectsPage.tsx (100%) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/projects/page.tsx diff --git a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts index 749c3cde179..38b9a875828 100644 --- a/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts +++ b/ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts @@ -11,13 +11,15 @@ * Keep this in lockstep with MIGRATED_PAGES in src/utils/migratedPages.ts. * Pending (add as each PR lands): the leaf-pages batch * (budgets, caching, cost-tracking, guardrails, guardrails-monitor, logs, - * mcp-servers, memory, policies, projects, prompts, search-tools, skills, + * mcp-servers, memory, policies, prompts, search-tools, skills, * tag-management, tool-policies, transform-request, ui-theme, vector-stores, - * workflows, access-groups). + * workflows). */ export const MIGRATED_E2E_PAGES: Record = { api_ref: "api-reference", "llm-playground": "playground", + projects: "projects", + "access-groups": "access-groups", }; export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))]; diff --git a/ui/litellm-dashboard/e2e_tests/globalSetup.ts b/ui/litellm-dashboard/e2e_tests/globalSetup.ts index 0b3fa7e8807..661155b761f 100644 --- a/ui/litellm-dashboard/e2e_tests/globalSetup.ts +++ b/ui/litellm-dashboard/e2e_tests/globalSetup.ts @@ -1,4 +1,4 @@ -import { chromium, expect } from "@playwright/test"; +import { chromium, expect, request } from "@playwright/test"; import { users, Role, STORAGE_PATHS } from "./fixtures/users"; import * as fs from "fs"; @@ -6,6 +6,21 @@ async function globalSetup() { const browser = await chromium.launch(); const rootPath = process.env.SERVER_ROOT_PATH ?? ""; + // The Projects sidebar item is hidden unless the enterprise-gated + // enable_projects_ui setting is on, and the seeded DB starts with it off. + // The proxy runs with LITELLM_LICENSE in CI, so enable it the same way + // the admin UI toggle does; the projects migration smoke needs the link. + const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234"; + const api = await request.newContext(); + const settingsRes = await api.patch(`http://localhost:4000${rootPath}/update/ui_settings`, { + headers: { Authorization: `Bearer ${masterKey}` }, + data: { enable_projects_ui: true }, + }); + if (!settingsRes.ok()) { + throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`); + } + await api.dispose(); + for (const role of Object.values(Role)) { const { email, password } = users[role]; const storagePath = STORAGE_PATHS[role]; diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 358064c00af..8f865915e41 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -472,22 +472,22 @@ "count": 4 } }, - "src/components/Projects/ProjectDetailsPage.tsx": { + "src/app/(dashboard)/projects/components/ProjectDetailsPage.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/Projects/ProjectKeysSection.tsx": { + "src/app/(dashboard)/projects/components/ProjectKeysSection.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/Projects/ProjectModals/ProjectBaseForm.tsx": { + "src/app/(dashboard)/projects/components/ProjectModals/ProjectBaseForm.tsx": { "react-hooks/set-state-in-effect": { "count": 2 } }, - "src/components/Projects/ProjectsPage.tsx": { + "src/app/(dashboard)/projects/components/ProjectsPage.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.test.tsx index ee8a8d0ffc5..cf41f623fd6 100644 --- a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.test.tsx @@ -3,7 +3,7 @@ import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAcc import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { renderWithProviders } from "../../../tests/test-utils"; +import { renderWithProviders } from "../../../../../tests/test-utils"; import { AccessGroupDetail } from "./AccessGroupsDetailsPage"; vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails"); diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.tsx index ae0cd8cd61b..72a89093bdb 100644 --- a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsDetailsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.tsx @@ -17,7 +17,7 @@ import { } from "antd"; import { ArrowLeftIcon, BotIcon, EditIcon, KeyIcon, LayersIcon, ServerIcon, UsersIcon } from "lucide-react"; import { useState } from "react"; -import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag"; +import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag"; import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal"; const { Title, Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupBaseForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupBaseForm.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupBaseForm.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupBaseForm.tsx diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupCreateModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupCreateModal.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupCreateModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupCreateModal.tsx diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupEditModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupEditModal.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsModal/AccessGroupEditModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsModal/AccessGroupEditModal.tsx diff --git a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.test.tsx index d50811949f5..7c8aaa2b785 100644 --- a/ui/litellm-dashboard/src/components/AccessGroups/AccessGroupsPage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsPage.test.tsx @@ -65,7 +65,7 @@ vi.mock("./AccessGroupsModal/AccessGroupCreateModal", () => ({ ) : null, })); -vi.mock("../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({ +vi.mock("@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({ default: ({ variant, tooltipText, onClick }: { variant: string; tooltipText: string; onClick: () => void }) => (