From e33c0d7ac9ec1bc53a83ae9b85dd158dd71a095c Mon Sep 17 00:00:00 2001 From: Bipin Rimal Date: Mon, 6 Apr 2026 14:20:59 +0545 Subject: [PATCH] docs: Fix provider/deployer framing in EU AI Act compliance guide --- .circleci/config.yml | 956 ++---------- .claude/settings.json | 36 - .github/ISSUE_TEMPLATE/config.yml | 2 +- .../helm-oci-chart-releaser/action.yml | 36 +- .github/codeql/codeql-config.yml | 19 +- .github/dependabot.yaml | 3 + .github/workflows/_test-unit-base.yml | 96 ++ .../workflows/_test-unit-services-base.yml | 164 +++ .../auto_update_price_and_context_window.yml | 14 +- .github/workflows/check-schema-sync.yml | 58 + .github/workflows/check_duplicate_issues.yml | 7 +- .github/workflows/codeql.yml | 13 +- .github/workflows/codspeed.yml | 8 +- .../workflows/create_daily_staging_branch.yml | 16 +- .github/workflows/ghcr_deploy.yml | 444 ------ .github/workflows/ghcr_helm_deploy.yml | 67 - .github/workflows/helm_unit_test.yml | 24 +- .github/workflows/interpret_load_test.py | 139 -- .github/workflows/issue-keyword-labeler.yml | 13 +- .github/workflows/label-component.yml | 2 +- .github/workflows/llm-translation-testing.yml | 58 +- .github/workflows/load_test.yml | 59 - .github/workflows/locustfile.py | 28 - .github/workflows/main.yml | 34 - .github/workflows/publish-migrations.yml | 207 --- .github/workflows/publish_enterprise.yml | 94 -- .github/workflows/publish_proxy_extras.yml | 74 - .github/workflows/publish_to_pypi.yml | 136 ++ .github/workflows/read_pyproject_version.yml | 17 +- .github/workflows/redeploy_proxy.py | 20 - .github/workflows/regenerate-poetry-lock.yml | 80 - .github/workflows/reset_stable.yml | 39 - .github/workflows/run_observatory_tests.yml | 16 +- .github/workflows/scan_duplicate_issues.yml | 7 +- .github/workflows/scorecard.yml | 47 + .github/workflows/simple_pypi_publish.yml | 67 - .github/workflows/stale.yml | 15 +- .github/workflows/sync-schema.yml | 73 + .github/workflows/test-linting.yml | 158 +- .github/workflows/test-litellm-matrix.yml | 22 +- .github/workflows/test-litellm-ui-build.yml | 6 +- .github/workflows/test-litellm.yml | 63 +- .github/workflows/test-mcp.yml | 65 +- .github/workflows/test-model-map.yaml | 9 +- .../test-proxy-e2e-azure-batches.yml | 19 +- .github/workflows/test-unit-caching-redis.yml | 38 + .github/workflows/test-unit-core-utils.yml | 20 + .github/workflows/test-unit-documentation.yml | 67 + .../test-unit-enterprise-routing.yml | 24 + .github/workflows/test-unit-integrations.yml | 20 + .github/workflows/test-unit-llm-providers.yml | 29 + .github/workflows/test-unit-misc.yml | 31 + .github/workflows/test-unit-proxy-auth.yml | 20 + .github/workflows/test-unit-proxy-db.yml | 45 + .../workflows/test-unit-proxy-endpoints.yml | 35 + .github/workflows/test-unit-proxy-infra.yml | 28 + .github/workflows/test-unit-proxy-legacy.yml | 96 ++ .../test-unit-responses-caching-types.yml | 20 + .github/workflows/test-unit-security.yml | 28 + .github/workflows/test_server_root_path.yml | 8 +- .github/workflows/zizmor.yml | 31 + .pre-commit-config.yaml | 12 +- .../rules/security/no-claude-directory.yml | 18 + CLAUDE.md | 5 + README.md | 4 +- ci_cd/security_scans.sh | 15 +- cookbook/benchmark/readme.md | 2 +- cookbook/codellama-server/README.MD | 1 - cookbook/litellm_proxy_server/readme.md | 1 - .../index.md | 17 +- .../index.md | 15 +- docs/my-website/blog/authors.yml | 24 + .../blog/ci_cd_v2_improvements/index.md | 55 + .../blog/claude_code_beta_headers/index.md | 17 +- docs/my-website/blog/claude_opus_4_6/index.md | 17 +- .../blog/claude_sonnet_4_6/index.md | 12 +- .../fastapi_middleware_performance/index.mdx | 15 +- docs/my-website/blog/gemin_3.1/index.md | 20 +- docs/my-website/blog/gemini_3/index.md | 26 +- .../blog/gemini_3_1_flash_lite/index.md | 21 +- docs/my-website/blog/gemini_3_flash/index.md | 20 +- .../gemini_embedding_2_multimodal/index.md | 7 +- docs/my-website/blog/gpt_5_3_codex/index.md | 17 +- docs/my-website/blog/gpt_5_4/index.md | 17 +- .../blog/gpt_5_4_mini_nano/index.md | 106 ++ .../index.md | 78 + .../httpx_cache_eviction_incident/index.md | 16 +- .../blog/litellm_observatory/index.md | 18 +- docs/my-website/blog/minimax_m2_5/index.md | 17 +- .../blog/model_cost_map_incident/index.md | 5 +- .../realtime_webrtc_http_endpoints/index.md | 18 +- .../index.md | 15 +- .../blog/security_townhall_updates/index.md | 190 +++ .../shared_ci_cd_environment.png | Bin 0 -> 45892 bytes .../blog/security_update_march_2026/index.md | 786 ++++++++++ .../my-website/blog/server_root_path/index.md | 14 +- .../sub_millisecond_proxy_overhead/index.md | 17 +- .../vanta_compliance_recertification/index.md | 18 + .../blog/video_characters_litellm/index.md | 19 +- .../blog/vllm_embeddings_incident/index.md | 15 +- .../docs/anthropic_unified/index.md | 9 +- .../docs/completion/prompt_caching.md | 206 ++- docs/my-website/docs/data_security.md | 2 - .../docs/debugging/local_debugging.md | 2 +- docs/my-website/docs/enterprise.md | 6 +- docs/my-website/docs/fine_tuning.md | 2 +- docs/my-website/docs/guides/index.md | 78 + docs/my-website/docs/index.md | 1051 ++++++------- docs/my-website/docs/integrations/index.md | 340 ++++- docs/my-website/docs/integrations/letta.md | 12 +- .../docs/integrations/observability_index.md | 28 + .../integrations/websearch_interception.md | 2 +- .../docs/learn/gateway_quickstart.md | 174 +++ docs/my-website/docs/learn/index.md | 117 ++ docs/my-website/docs/learn/sdk_quickstart.md | 174 +++ docs/my-website/docs/load_test_advanced.md | 4 +- docs/my-website/docs/migration.md | 3 +- .../docs/observability/arize_integration.md | 1 - .../observability/gcs_bucket_integration.md | 3 +- .../observability/langfuse_integration.md | 1 - .../langfuse_otel_integration.md | 6 + .../observability/langsmith_integration.md | 1 - .../docs/observability/logfire_integration.md | 1 - .../docs/observability/lunary_integration.md | 1 - .../docs/observability/opik_integration.md | 1 - .../docs/observability/phoenix_integration.md | 1 - .../observability/promptlayer_integration.md | 1 - .../docs/observability/slack_integration.md | 1 - .../observability/sumologic_integration.md | 1 - .../observability/supabase_integration.md | 1 - .../docs/observability/wandb_integration.md | 1 - docs/my-website/docs/prompt_management.md | 48 + docs/my-website/docs/providers/azure/azure.md | 2 +- .../docs/providers/azure/azure_anthropic.md | 5 +- docs/my-website/docs/providers/gemini.md | 109 +- .../my-website/docs/providers/gemini/music.md | 28 + docs/my-website/docs/providers/openai.md | 86 +- docs/my-website/docs/proxy/call_hooks.md | 2 +- docs/my-website/docs/proxy/config_settings.md | 46 +- docs/my-website/docs/proxy/configs.md | 16 + docs/my-website/docs/proxy/cost_tracking.md | 2 +- .../docs/proxy/docker_quick_start.md | 307 +++- docs/my-website/docs/proxy/email.md | 2 +- .../docs/proxy/endpoint_activity.md | 2 +- docs/my-website/docs/proxy/enterprise.md | 2 +- docs/my-website/docs/proxy/guardrails/akto.md | 139 ++ .../docs/proxy/guardrails/aporia_api.md | 2 +- .../docs/proxy/guardrails/custom_guardrail.md | 14 +- .../docs/proxy/guardrails/guardrails_ai.md | 2 +- docs/my-website/docs/proxy/health.md | 83 ++ .../proxy/high_availability_control_plane.md | 190 +++ docs/my-website/docs/proxy/ip_address.md | 2 +- .../docs/proxy/keys_teams_router_settings.md | 4 +- docs/my-website/docs/proxy/load_balancing.md | 59 +- docs/my-website/docs/proxy/logging.md | 6 +- .../my-website/docs/proxy/model_compare_ui.md | 4 +- docs/my-website/docs/proxy/multiple_admins.md | 36 +- docs/my-website/docs/proxy/oauth2.md | 2 +- docs/my-website/docs/proxy/pass_through.md | 1 - docs/my-website/docs/proxy/prod.md | 2 +- .../docs/proxy/prompt_management.md | 19 +- docs/my-website/docs/proxy/public_routes.md | 2 +- .../docs/proxy/sync_anthropic_beta_headers.md | 2 +- docs/my-website/docs/proxy/tag_routing.md | 2 +- docs/my-website/docs/proxy/team_logging.md | 4 +- docs/my-website/docs/proxy/team_model_add.md | 2 +- docs/my-website/docs/proxy/token_auth.md | 2 +- .../docs/proxy/ui_store_model_db_setting.md | 4 +- docs/my-website/docs/proxy/user_onboarding.md | 2 +- docs/my-website/docs/proxy_server.md | 1 - docs/my-website/docs/reasoning_content.md | 88 +- docs/my-website/docs/response_api.md | 89 +- docs/my-website/docs/routing.md | 15 +- docs/my-website/docs/secret.md | 2 +- .../docs/secret_managers/aws_kms.md | 2 +- .../secret_managers/aws_secret_manager.md | 2 +- .../docs/secret_managers/azure_key_vault.md | 2 +- .../docs/secret_managers/cyberark.md | 2 +- .../docs/secret_managers/google_kms.md | 2 +- .../secret_managers/google_secret_manager.md | 2 +- .../docs/secret_managers/hashicorp_vault.md | 2 +- .../docs/secret_managers/overview.md | 2 +- docs/my-website/docs/troubleshoot.md | 1 - .../claude_code_plugin_marketplace.md | 18 +- .../my-website/docs/tutorials/compare_llms.md | 2 +- .../tutorials/file_search_responses_api.md | 241 +++ docs/my-website/docs/tutorials/index.md | 98 ++ .../my-website/docs/tutorials/installation.md | 8 +- .../docs/tutorials/opencode_integration.md | 27 +- .../docs/tutorials/vertex_ai_pay_go.md | 151 ++ .../docs/vertex_batch_passthrough.md | 6 +- docs/my-website/docusaurus.config.js | 144 +- docs/my-website/img/ci_cd_architecture.png | Bin 0 -> 162042 bytes docs/my-website/img/hero.png | Bin 0 -> 6505890 bytes .../img/isolated_ci_cd_environments.png | Bin 0 -> 16046 bytes .../my-website/img/mcp_zero_trust_gateway.png | Bin 0 -> 301348 bytes .../proxy_version.png | Bin 0 -> 405422 bytes .../img/shared_ci_cd_environment.png | Bin 0 -> 53721 bytes docs/my-website/package.json | 2 +- docs/my-website/release_notes/index.md | 52 + .../release_notes/v1.63.14/index.md | 8 +- .../release_notes/v1.63.2-stable/index.md | 8 +- .../release_notes/v1.80.15/index.md | 3 +- .../my-website/release_notes/v1.81.0/index.md | 4 +- .../{v1.81.12.md => v1.81.12/index.md} | 4 +- .../{v1.81.14.md => v1.81.14/index.md} | 8 +- .../{v1.81.6.md => v1.81.6/index.md} | 0 .../{v1.81.9.md => v1.81.9/index.md} | 4 +- .../{v1.82.0.md => v1.82.0/index.md} | 0 .../{v1.82.3.md => v1.82.3/index.md} | 174 ++- docs/my-website/sidebars-release-notes.js | 14 + docs/my-website/sidebars.js | 626 +++++--- .../ControlPlaneArchitecture.tsx | 95 ++ .../ControlPlaneArchitecture/index.tsx | 1 + .../styles.module.css | 517 +++++++ .../src/components/NavigationCards/index.js | 44 + .../NavigationCards/styles.module.css | 82 ++ .../VersionVerificationTable/index.tsx | 84 ++ .../styles.module.css | 106 ++ docs/my-website/src/css/custom.css | 757 +++++++++- docs/my-website/src/theme/DocSidebar/index.js | 29 + .../src/theme/DocSidebar/styles.module.css | 30 + .../src/theme/Navbar/Content/index.js | 71 + docs/my-website/src/theme/TOC/index.js | 36 + .../src/theme/TOC/styles.module.css | 74 + .../img/blog/vanta_soc2_recertification.png | Bin 0 -> 40144 bytes .../static/img/vertex_cost_tracking_flow.svg | 63 + enterprise/LICENSE.md | 2 +- enterprise/README.md | 2 +- .../pagerduty/pagerduty.py | 2 + .../send_emails/base_email.py | 2 +- .../proxy/common_utils/check_batch_cost.py | 51 +- .../proxy/hooks/managed_files.py | 183 ++- enterprise/pyproject.toml | 4 +- litellm-js/spend-logs/package.json | 2 +- ...tellm_proxy_extras-0.4.58-py3-none-any.whl | Bin 0 -> 76589 bytes .../dist/litellm_proxy_extras-0.4.58.tar.gz | Bin 0 -> 32038 bytes ...tellm_proxy_extras-0.4.60-py3-none-any.whl | Bin 0 -> 76605 bytes .../dist/litellm_proxy_extras-0.4.60.tar.gz | Bin 0 -> 32034 bytes .../20260311180521_schema_sync/migration.sql | 11 - .../migration.sql | 9 + .../litellm_proxy_extras/schema.prisma | 18 +- litellm-proxy-extras/pyproject.toml | 4 +- litellm/__init__.py | 13 +- litellm/_logging.py | 21 +- litellm/_redis.py | 63 +- litellm/batches/main.py | 33 +- litellm/caching/dual_cache.py | 21 +- litellm/caching/redis_cache.py | 113 +- .../transformation.py | 132 +- litellm/constants.py | 10 + litellm/cost_calculator.py | 10 +- .../anthropic_cache_control_hook.py | 9 +- litellm/integrations/custom_logger.py | 5 + litellm/integrations/datadog/datadog.py | 12 +- .../integrations/datadog/datadog_handler.py | 11 +- .../integrations/datadog/datadog_llm_obs.py | 4 +- .../focus/destinations/factory.py | 7 +- .../langfuse/langfuse_prompt_management.py | 6 +- litellm/integrations/langsmith.py | 186 ++- litellm/integrations/opentelemetry.py | 21 +- litellm/integrations/prometheus.py | 402 ++++- litellm/integrations/s3_v2.py | 34 +- .../integrations/vantage/vantage_logger.py | 8 +- .../websearch_interception/handler.py | 108 ++ litellm/litellm_core_utils/core_helpers.py | 1 + .../litellm_core_utils/default_encoding.py | 5 +- .../get_llm_provider_logic.py | 16 +- litellm/litellm_core_utils/litellm_logging.py | 366 +++-- .../prompt_templates/common_utils.py | 144 +- .../prompt_templates/factory.py | 75 +- .../litellm_core_utils/streaming_handler.py | 132 +- .../a2a/chat/guardrail_translation/handler.py | 105 +- .../llms/anthropic/batches/transformation.py | 7 +- .../chat/guardrail_translation/handler.py | 174 ++- litellm/llms/anthropic/chat/handler.py | 97 +- litellm/llms/anthropic/chat/transformation.py | 231 ++- litellm/llms/anthropic/common_utils.py | 59 +- .../adapters/handler.py | 31 +- .../adapters/transformation.py | 306 +++- .../messages/handler.py | 84 +- .../messages/transformation.py | 15 +- .../responses_adapters/transformation.py | 14 +- .../experimental_pass_through/utils.py | 11 + litellm/llms/anthropic/files/handler.py | 10 +- .../llms/anthropic/files/transformation.py | 8 +- .../llms/anthropic/skills/transformation.py | 15 +- .../llms/azure/chat/gpt_5_transformation.py | 11 +- litellm/llms/azure/fine_tuning/handler.py | 197 ++- litellm/llms/azure_ai/agents/handler.py | 106 +- .../guardrail_translation/base_translation.py | 2 + .../llms/base_llm/responses/transformation.py | 8 + .../llms/base_llm/videos/transformation.py | 16 +- .../bedrock/chat/converse_transformation.py | 68 +- litellm/llms/bedrock/count_tokens/handler.py | 9 +- .../bedrock/count_tokens/transformation.py | 14 +- .../rerank/guardrail_translation/handler.py | 1 + litellm/llms/custom_httpx/llm_http_handler.py | 20 +- .../llms/fireworks_ai/chat/transformation.py | 13 +- litellm/llms/gemini/files/transformation.py | 59 +- .../gemini/image_generation/transformation.py | 11 +- litellm/llms/gemini/videos/transformation.py | 31 +- .../audio_transcription/transformation.py | 7 + .../ocr/guardrail_translation/handler.py | 20 +- litellm/llms/mistral/ocr/transformation.py | 4 + litellm/llms/moonshot/chat/transformation.py | 28 +- .../llms/openai/chat/gpt_5_transformation.py | 45 +- .../llms/openai/chat/gpt_transformation.py | 5 +- .../chat/guardrail_translation/handler.py | 43 +- .../guardrail_translation/handler.py | 21 +- .../guardrail_translation/handler.py | 1 + litellm/llms/openai/fine_tuning/handler.py | 63 +- .../guardrail_translation/handler.py | 1 + .../guardrail_translation/handler.py | 28 +- .../llms/openai/responses/transformation.py | 3 + .../speech/guardrail_translation/handler.py | 1 + .../guardrail_translation/handler.py | 21 +- litellm/llms/ovhcloud/chat/transformation.py | 10 +- .../guardrail_translation/handler.py | 29 +- .../llms/runwayml/videos/transformation.py | 39 +- litellm/llms/sagemaker/chat/transformation.py | 4 +- litellm/llms/vertex_ai/batches/handler.py | 145 ++ .../llms/vertex_ai/batches/transformation.py | 4 +- .../vertex_ai_context_caching.py | 11 +- .../llms/vertex_ai/gemini/transformation.py | 42 + .../vertex_and_google_ai_studio_gemini.py | 164 ++- .../batch_embed_content_transformation.py | 4 + .../count_tokens/handler.py | 23 +- .../llms/vertex_ai/videos/transformation.py | 39 +- litellm/main.py | 27 +- ...odel_prices_and_context_window_backup.json | 824 ++++++++--- .../mcp_server/discoverable_endpoints.py | 111 +- .../guardrail_translation/handler.py | 1 + .../mcp_server/mcp_server_manager.py | 72 +- .../mcp_server/rest_endpoints.py | 12 +- .../proxy/_experimental/out/404/index.html | 2 +- .../_experimental/out/__next.__PAGE__.txt | 43 +- .../proxy/_experimental/out/__next._full.txt | 99 +- .../proxy/_experimental/out/__next._head.txt | 2 +- .../proxy/_experimental/out/__next._index.txt | 8 +- .../proxy/_experimental/out/__next._tree.txt | 4 +- .../_buildManifest.js | 0 .../_clientMiddlewareManifest.json | 0 .../_ssgManifest.js | 0 ...2e3b7dd6499c245.js => 02158aed2f4518e2.js} | 2 +- .../_next/static/chunks/04a7af91517db55b.js | 1 + .../_next/static/chunks/056b4991f668b494.js | 1 - .../_next/static/chunks/065cbe2de8230973.js | 1 + .../_next/static/chunks/06ebe9b0e9cdf241.js | 50 - .../_next/static/chunks/0713a1954ae8db53.js | 3 + .../_next/static/chunks/08d5ac6e0b6220c0.js | 1 + .../_next/static/chunks/0a80887cd471a6cc.js | 8 + .../_next/static/chunks/0b8ec8bf90ea9721.js | 72 + ...dd55e1f36a7225c.js => 0be054dbc84bd8be.js} | 2 +- .../_next/static/chunks/0dda11815be4f78b.js | 105 -- .../_next/static/chunks/0ea9112947894f26.js | 1 - .../_next/static/chunks/112ad77f3dd2e3cd.js | 1 + .../_next/static/chunks/11362340846735c3.js | 420 ------ .../_next/static/chunks/117fd0772eee5df6.js | 1 - .../_next/static/chunks/123bb7375879d789.js | 3 - .../_next/static/chunks/17741b7a77c20f1b.js | 1 - .../_next/static/chunks/179425128d293da9.js | 7 - .../_next/static/chunks/1b424ce64213980f.js | 1 - .../_next/static/chunks/1d6119b4214ab712.js | 50 + .../_next/static/chunks/1da362a651d209bd.js | 1 + .../_next/static/chunks/1eb2ed6e2dd204b7.js | 50 - .../_next/static/chunks/1eccde2dab0b3311.js | 1 - .../_next/static/chunks/1f58814a2409d571.js | 1 - .../_next/static/chunks/1f6df7977860dc7b.js | 1 - .../_next/static/chunks/1fcff413509b2e1f.js | 7 - .../_next/static/chunks/203dde2108f3f1ac.js | 1 + .../_next/static/chunks/22970a12064ba16b.js | 231 --- .../_next/static/chunks/22e715061d511345.js | 8 - .../_next/static/chunks/23bf955e8672ce98.js | 1 - .../_next/static/chunks/23e34a8c920ebd31.js | 1 + .../_next/static/chunks/262c0742212bf6d1.js | 1 + ...6ef9d81cc17cfa8.js => 26542a70b9512f71.js} | 8 +- .../_next/static/chunks/26fda1c4c6936e38.js | 1 - .../_next/static/chunks/2793ac912badcf02.js | 420 ++++++ .../_next/static/chunks/29f944b40b65da0a.js | 420 ++++++ .../_next/static/chunks/2bacff998dbae5da.js | 7 - .../_next/static/chunks/2c21eeb7a235384a.js | 1 - .../_next/static/chunks/2d313397aa3e57de.js | 38 + .../_next/static/chunks/2d44417ec0ed6970.js | 1 + .../_next/static/chunks/2e7ede393477220f.js | 1 + .../_next/static/chunks/2faf62c238d105eb.js | 1 + .../_next/static/chunks/305a1cf07cfab07b.js | 8 + .../_next/static/chunks/31e02a31dea7d5d2.js | 8 - .../_next/static/chunks/338e84191fe615bf.js | 231 +++ .../_next/static/chunks/348b31083769a7c4.js | 21 - .../_next/static/chunks/354ca537c6c0601c.js | 8 + .../_next/static/chunks/3569f12d1e9d5e0d.js | 1 - .../_next/static/chunks/3675074b1d85e268.js | 10 - .../_next/static/chunks/38976546132cd527.js | 105 -- .../_next/static/chunks/39768ec0eebd2554.js | 1 - .../_next/static/chunks/39bdd72c165f9ec0.js | 8 + .../_next/static/chunks/3b19a8bdc8d26868.js | 1 + .../_next/static/chunks/3da2633a10defd79.js | 1 - .../_next/static/chunks/3de1b6df2372e93b.js | 1 + .../_next/static/chunks/3f320784d80bed94.js | 1 + .../_next/static/chunks/40f766ecc87dbf9a.js | 1 - .../_next/static/chunks/4242033bd0f32638.js | 1 - .../_next/static/chunks/42c127841d8c1bd3.js | 1 + .../_next/static/chunks/43404a268a45c17a.js | 14 + .../_next/static/chunks/4348e537165edb3b.js | 1 - .../_next/static/chunks/440d96637d3ff94d.js | 17 + .../_next/static/chunks/442ccb8d620e1fa6.js | 1 - .../_next/static/chunks/4472ece1be7379b3.js | 8 - .../_next/static/chunks/46b252adc34d9549.js | 7 + .../_next/static/chunks/491d92760452057a.js | 1 + ...e9cf43b8c0c76aa.js => 49e9dce7df902771.js} | 6 +- .../_next/static/chunks/4b3c0ae9e54d843c.js | 1 - .../_next/static/chunks/4c4469911e2f315e.js | 1 - .../_next/static/chunks/4cc2a4292409c9b3.js | 12 + .../_next/static/chunks/4e0ee3124dcdc85b.js | 8 + .../_next/static/chunks/4e5da3c236abd875.js | 8 + .../_next/static/chunks/5282ed7355826608.js | 1 - .../_next/static/chunks/528456b9ec2e4413.js | 1 + .../_next/static/chunks/53218dce8acb3bff.js | 1 - .../_next/static/chunks/53a3a23605a87ee1.js | 1 + .../_next/static/chunks/53a707a5829899ed.js | 1 + ...a04d31843c96649.js => 5400ee883dfa8c43.js} | 2 +- .../_next/static/chunks/5595eb6378e90997.js | 1 - .../_next/static/chunks/55c8ff5e9c6d1e1d.js | 1 - .../_next/static/chunks/56a8bf43ce752d47.js | 1 + .../_next/static/chunks/575cc1c8ef6c4319.js | 1 - ...f6546cd8a44d3b3.js => 58461a445becf104.js} | 8 +- .../_next/static/chunks/5855ff7033bd4d2e.js | 1 + .../_next/static/chunks/591e3b6fbe6e4d4a.js | 1 - .../_next/static/chunks/5929da573d876909.js | 1 + ...b3c0b070b14da06.js => 5963ae3163ecd9b6.js} | 4 +- .../_next/static/chunks/59945beef3825b62.js | 1 - .../_next/static/chunks/5ab3a0c9cca409f3.js | 1 + .../_next/static/chunks/5c0770ecd9172a56.js | 1 + .../_next/static/chunks/5c0ed5c66b49ddbe.js | 1 + .../_next/static/chunks/5c823f037243a06f.js | 1 - .../_next/static/chunks/5f4170980a69ffa3.js | 10 + .../_next/static/chunks/62261c4511c6ef17.js | 7 + .../_next/static/chunks/62a03e24dd5227b9.js | 7 - .../_next/static/chunks/635dd51f7caede88.js | 17 - .../_next/static/chunks/65f709264734a9bf.js | 46 + ...0e37187792c3754.js => 673d847ad9c91666.js} | 4 +- .../_next/static/chunks/67ae4f6900d6d2b5.js | 1 - .../_next/static/chunks/68066e020262ced9.js | 7 - .../_next/static/chunks/6a6f476ca1e20bb3.js | 1 - .../_next/static/chunks/6b13d13478bbc3d8.js | 1 - .../_next/static/chunks/6b2bc4046c4cbfc8.js | 1 + .../_next/static/chunks/6f6d3e604e986144.js | 1 + .../_next/static/chunks/702ac50fd26100ab.js | 1 - .../_next/static/chunks/715057b8e12f1cd9.js | 7 - .../_next/static/chunks/7174130ddef406dd.js | 8 - ...f9e9c54ac262de2.js => 726579f2940c2a2f.js} | 2 +- .../_next/static/chunks/72c3e48f096ce28f.js | 1 + .../_next/static/chunks/74ce31aa0fb2adc9.js | 14 - .../_next/static/chunks/76dacbb0a43f577b.js | 1 - .../_next/static/chunks/77bf62fbc704d017.js | 1 + .../_next/static/chunks/7a2dc852f68481ea.js | 50 + .../_next/static/chunks/7b9ef931d44e410f.js | 1 - .../_next/static/chunks/7bcc54a58176051b.js | 8 + .../_next/static/chunks/7c797521435cb59c.js | 1 + .../_next/static/chunks/7d82a1cebfdb679c.js | 1 - .../_next/static/chunks/7e3f5ce4b2a613d4.js | 1 - .../_next/static/chunks/7f59802b710501d5.js | 1 + .../_next/static/chunks/80079c810f42a5e5.js | 427 ------ .../_next/static/chunks/80899acb7e1a7640.js | 12 - .../_next/static/chunks/82426ffeda186236.js | 1 + ...dfde809dc4ad794.js => 82bc4bb51160556f.js} | 8 +- .../_next/static/chunks/836c30941dbab57e.js | 1 + .../_next/static/chunks/8454375d75f636e8.js | 1 - ...84161a27f806cd4.js => 84dd260c7412819c.js} | 2 +- .../_next/static/chunks/877101abed503ab2.js | 1 + .../_next/static/chunks/89274859d3d9d1de.js | 1 + .../_next/static/chunks/8a6de9a16d49b44f.js | 1 + .../_next/static/chunks/8a76c69fc7bff9fe.js | 1 + .../_next/static/chunks/8ae157c8a223fdc3.js | 1 - .../_next/static/chunks/8cc98e6cf29063c4.js | 1 - .../_next/static/chunks/8dc3b559a2e76f88.css | 1 - .../_next/static/chunks/8dda507c226082ca.js | 17 - .../_next/static/chunks/900e393d6a9d7b12.js | 1 + .../_next/static/chunks/908828a91f602d8b.js | 86 ++ .../_next/static/chunks/90c332d66ef5954b.js | 8 - ...c8a270fee94ced6.js => 92c3c06057498511.js} | 2 +- .../_next/static/chunks/92e50c28acb1e29e.js | 7 + .../_next/static/chunks/9474f2e878525bf7.js | 1 + .../_next/static/chunks/94b1900e63940a2b.js | 8 - .../_next/static/chunks/9606513e20bc3d4f.js | 1 + .../_next/static/chunks/96616c4e8f4c2b15.js | 1 - ...7ddb5107368a659.js => 99109c78121231a0.js} | 4 +- .../_next/static/chunks/994a6506f0e0b01f.js | 1 + .../_next/static/chunks/9984a74a61012f00.js | 1 + .../_next/static/chunks/99d715502d5069f4.js | 1 + ...a0f22bd4b3393bd.js => 9d3522e82d255059.js} | 4 +- .../_next/static/chunks/a02911bccf9acc36.js | 10 - .../_next/static/chunks/a09d5d7fd3464016.js | 1 + .../_next/static/chunks/a0b814e0f184a60a.js | 7 + .../_next/static/chunks/a3bf706d78352fd9.js | 3 - .../_next/static/chunks/a6c7f80b3968f639.js | 420 ------ .../_next/static/chunks/a6effb44cc0c9028.js | 1 - .../_next/static/chunks/a7f104aa2cc7f3f0.js | 1 - .../_next/static/chunks/a85adee4198d5478.js | 86 -- .../_next/static/chunks/a89452659b6e1d90.js | 139 -- .../_next/static/chunks/ac9e96d21c200b48.js | 8 - .../_next/static/chunks/aca9c2b0aa46b0d7.js | 1 + .../_next/static/chunks/acbeac1b0fde1fdf.js | 1 - .../_next/static/chunks/ad08830c666dfc68.js | 7 + .../_next/static/chunks/ad682fd0bc31a0da.js | 420 ++++++ .../_next/static/chunks/ada57dab6523afc4.js | 1 + .../_next/static/chunks/ae615fbed4c01ba7.js | 1 - .../_next/static/chunks/af8668386d7005fe.js | 1 + .../_next/static/chunks/b0286888a3293fd9.js | 1 + .../_next/static/chunks/b02d6062e7602700.js | 1 - .../_next/static/chunks/b4bd164f5553a31d.js | 17 + .../_next/static/chunks/ba42d2587315d00e.js | 1 - .../_next/static/chunks/bdcb8f26948ea49f.js | 7 - .../_next/static/chunks/be342ee9c36c54df.js | 1 - .../_next/static/chunks/c2bda16ec35d1a65.js | 8 + .../_next/static/chunks/c53c9c7afec96700.js | 14 + .../_next/static/chunks/c599cfb1e6aec71d.js | 1 + .../_next/static/chunks/c74f3813068add76.js | 17 + .../_next/static/chunks/c7c5a941c9e13136.js | 8 + .../_next/static/chunks/c8eee6971ca36303.js | 17 - .../_next/static/chunks/cab8d46a8c32ec36.css | 1 + .../_next/static/chunks/cac89fc12fb6ef7e.js | 1 - ...a167cef4b09b496.js => caf98722823e1b40.js} | 4 +- .../_next/static/chunks/cb86c3ef30e0cf21.js | 8 - .../_next/static/chunks/cc1429f96b037302.js | 98 ++ .../_next/static/chunks/cd9e9161805efaa3.js | 1 + .../_next/static/chunks/cdf98a03da656604.js | 1 - .../_next/static/chunks/ce9cf9f407f4b359.js | 98 ++ .../_next/static/chunks/cecdaabafa264083.js | 1 + .../_next/static/chunks/d069df5baead6d90.js | 1 - .../_next/static/chunks/d104f25e5302e120.js | 1 + .../_next/static/chunks/d1ddfd3f3d5b2449.js | 10 + .../_next/static/chunks/d223c00dadf4b924.js | 1 - .../_next/static/chunks/d44e73d8ebac5747.js | 420 ------ .../_next/static/chunks/d512ca3b7169bef6.js | 1 + .../_next/static/chunks/d63044bdf28324dd.js | 38 - .../_next/static/chunks/d63f055c4b72844e.js | 1 - .../_next/static/chunks/d64d74932cb225a3.js | 1 - .../_next/static/chunks/d9b0d7b22cad03c6.js | 8 - .../_next/static/chunks/dad8b43751822f79.js | 420 ++++++ .../_next/static/chunks/db928c0f158d84b3.js | 8 + ...ae216e2208b329b.js => dc23b2a2258ffad1.js} | 2 +- .../_next/static/chunks/e1da6931dfaabba1.js | 1 + .../_next/static/chunks/e1e3f652dbc5be03.js | 1 + .../_next/static/chunks/e775bbab37491d9c.js | 1 - .../_next/static/chunks/e884277804d6854d.js | 1 + .../_next/static/chunks/e8ed72789c2b42ff.js | 39 - .../_next/static/chunks/e9de3f8db541361f.js | 1 + .../_next/static/chunks/ea80fa81416a4ac8.js | 3 + .../_next/static/chunks/eb659e9b99d203f2.js | 14 + .../_next/static/chunks/ecc42934cfd4bef0.js | 1 - .../_next/static/chunks/ed079ecd9e95349e.js | 7 - .../_next/static/chunks/ed4c3592cb02914b.js | 1 + .../_next/static/chunks/ee3a30e704bf7c47.js | 1 + ...d471965761a22ff.js => ee5f9a39a526e423.js} | 2 +- .../_next/static/chunks/ee7baaa6c1518142.js | 1 - .../_next/static/chunks/ee80c765f82c1de2.js | 1 + .../_next/static/chunks/ee9b8424e31e26a3.js | 1 - .../_next/static/chunks/f059e45298abbf27.js | 1 + ...4e29148cb2f2582.js => f3e0cbc0e84e0a5d.js} | 2 +- .../_next/static/chunks/f4eadf7003875fab.js | 1 + .../_next/static/chunks/f683569e573c506e.js | 1 - .../_next/static/chunks/f6cd2dbfa2452bc1.js | 8 - .../_next/static/chunks/f9133c1eea037690.js | 1 - .../_next/static/chunks/f9560c32394a893f.js | 1 + .../_next/static/chunks/f9b9defe307eeda9.js | 7 + ...d6e5aad99b19216.js => f9c24d6e7ec43046.js} | 2 +- .../_next/static/chunks/f9c75b7b331b5bb7.js | 86 -- .../_next/static/chunks/fc4d54eb6afe7984.js | 1 - .../_next/static/chunks/fc873acd3d409c53.js | 1 + .../_next/static/chunks/fce4815a81e5c63d.js | 14 - .../_next/static/chunks/fe4472f1d94e88f2.js | 1 - ...f6751.js => turbopack-d1b22f5e0bd58c57.js} | 2 +- .../proxy/_experimental/out/_not-found.txt | 8 +- .../out/_not-found/__next._full.txt | 8 +- .../out/_not-found/__next._head.txt | 2 +- .../out/_not-found/__next._index.txt | 8 +- .../_not-found/__next._not-found.__PAGE__.txt | 2 +- .../out/_not-found/__next._not-found.txt | 2 +- .../out/_not-found/__next._tree.txt | 4 +- .../_experimental/out/_not-found/index.html | 2 +- .../proxy/_experimental/out/api-reference.txt | 14 +- ...KGRhc2hib2FyZCk.api-reference.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.api-reference.txt | 2 +- .../api-reference/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/api-reference/__next._full.txt | 14 +- .../out/api-reference/__next._head.txt | 2 +- .../out/api-reference/__next._index.txt | 8 +- .../out/api-reference/__next._tree.txt | 4 +- .../out/api-reference/index.html | 2 +- .../_experimental/out/assets/logos/akto.svg | 10 + litellm/proxy/_experimental/out/chat.html | 1 - litellm/proxy/_experimental/out/chat.txt | 10 +- .../_experimental/out/chat/__next._full.txt | 10 +- .../_experimental/out/chat/__next._head.txt | 2 +- .../_experimental/out/chat/__next._index.txt | 8 +- .../_experimental/out/chat/__next._tree.txt | 4 +- .../out/chat/__next.chat.__PAGE__.txt | 4 +- .../_experimental/out/chat/__next.chat.txt | 2 +- .../proxy/_experimental/out/chat/index.html | 1 + .../out/experimental/api-playground.txt | 45 +- ...k.experimental.api-playground.__PAGE__.txt | 4 +- ...2hib2FyZCk.experimental.api-playground.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../api-playground/__next._full.txt | 45 +- .../api-playground/__next._head.txt | 2 +- .../api-playground/__next._index.txt | 8 +- .../api-playground/__next._tree.txt | 4 +- .../experimental/api-playground/index.html | 2 +- .../out/experimental/budgets.txt | 45 +- ...ib2FyZCk.experimental.budgets.__PAGE__.txt | 4 +- ....!KGRhc2hib2FyZCk.experimental.budgets.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../budgets/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/experimental/budgets/__next._full.txt | 45 +- .../out/experimental/budgets/__next._head.txt | 2 +- .../experimental/budgets/__next._index.txt | 8 +- .../out/experimental/budgets/__next._tree.txt | 4 +- .../out/experimental/budgets/index.html | 2 +- .../out/experimental/caching.txt | 45 +- ...ib2FyZCk.experimental.caching.__PAGE__.txt | 4 +- ....!KGRhc2hib2FyZCk.experimental.caching.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../caching/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/experimental/caching/__next._full.txt | 45 +- .../out/experimental/caching/__next._head.txt | 2 +- .../experimental/caching/__next._index.txt | 8 +- .../out/experimental/caching/__next._tree.txt | 4 +- .../out/experimental/caching/index.html | 2 +- .../out/experimental/claude-code-plugins.txt | 45 +- ...erimental.claude-code-plugins.__PAGE__.txt | 4 +- ...FyZCk.experimental.claude-code-plugins.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../claude-code-plugins/__next._full.txt | 45 +- .../claude-code-plugins/__next._head.txt | 2 +- .../claude-code-plugins/__next._index.txt | 8 +- .../claude-code-plugins/__next._tree.txt | 4 +- .../claude-code-plugins/index.html | 2 +- .../out/experimental/old-usage.txt | 45 +- ...2FyZCk.experimental.old-usage.__PAGE__.txt | 4 +- ...KGRhc2hib2FyZCk.experimental.old-usage.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../old-usage/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../experimental/old-usage/__next._full.txt | 45 +- .../experimental/old-usage/__next._head.txt | 2 +- .../experimental/old-usage/__next._index.txt | 8 +- .../experimental/old-usage/__next._tree.txt | 4 +- .../out/experimental/old-usage/index.html | 2 +- .../out/experimental/prompts.txt | 45 +- ...ib2FyZCk.experimental.prompts.__PAGE__.txt | 4 +- ....!KGRhc2hib2FyZCk.experimental.prompts.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../prompts/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/experimental/prompts/__next._full.txt | 45 +- .../out/experimental/prompts/__next._head.txt | 2 +- .../experimental/prompts/__next._index.txt | 8 +- .../out/experimental/prompts/__next._tree.txt | 4 +- .../out/experimental/prompts/index.html | 2 +- .../out/experimental/tag-management.txt | 45 +- ...k.experimental.tag-management.__PAGE__.txt | 4 +- ...2hib2FyZCk.experimental.tag-management.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.experimental.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../tag-management/__next._full.txt | 45 +- .../tag-management/__next._head.txt | 2 +- .../tag-management/__next._index.txt | 8 +- .../tag-management/__next._tree.txt | 4 +- .../experimental/tag-management/index.html | 2 +- .../proxy/_experimental/out/guardrails.txt | 14 +- ...t.!KGRhc2hib2FyZCk.guardrails.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.guardrails.txt | 2 +- .../guardrails/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/guardrails/__next._full.txt | 14 +- .../out/guardrails/__next._head.txt | 2 +- .../out/guardrails/__next._index.txt | 8 +- .../out/guardrails/__next._tree.txt | 4 +- .../_experimental/out/guardrails/index.html | 2 +- litellm/proxy/_experimental/out/index.html | 2 +- litellm/proxy/_experimental/out/index.txt | 99 +- litellm/proxy/_experimental/out/login.txt | 10 +- .../_experimental/out/login/__next._full.txt | 10 +- .../_experimental/out/login/__next._head.txt | 2 +- .../_experimental/out/login/__next._index.txt | 8 +- .../_experimental/out/login/__next._tree.txt | 4 +- .../out/login/__next.login.__PAGE__.txt | 4 +- .../_experimental/out/login/__next.login.txt | 2 +- .../proxy/_experimental/out/login/index.html | 2 +- litellm/proxy/_experimental/out/logs.txt | 16 +- .../__next.!KGRhc2hib2FyZCk.logs.__PAGE__.txt | 4 +- .../out/logs/__next.!KGRhc2hib2FyZCk.logs.txt | 2 +- .../out/logs/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../_experimental/out/logs/__next._full.txt | 16 +- .../_experimental/out/logs/__next._head.txt | 2 +- .../_experimental/out/logs/__next._index.txt | 8 +- .../_experimental/out/logs/__next._tree.txt | 4 +- .../proxy/_experimental/out/logs/index.html | 2 +- .../_experimental/out/mcp/oauth/callback.txt | 10 +- .../out/mcp/oauth/callback/__next._full.txt | 10 +- .../out/mcp/oauth/callback/__next._head.txt | 2 +- .../out/mcp/oauth/callback/__next._index.txt | 8 +- .../out/mcp/oauth/callback/__next._tree.txt | 4 +- .../__next.mcp.oauth.callback.__PAGE__.txt | 4 +- .../callback/__next.mcp.oauth.callback.txt | 2 +- .../mcp/oauth/callback/__next.mcp.oauth.txt | 2 +- .../out/mcp/oauth/callback/__next.mcp.txt | 2 +- .../out/mcp/oauth/callback/index.html | 2 +- litellm/proxy/_experimental/out/model-hub.txt | 14 +- ...xt.!KGRhc2hib2FyZCk.model-hub.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.model-hub.txt | 2 +- .../out/model-hub/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/model-hub/__next._full.txt | 14 +- .../out/model-hub/__next._head.txt | 2 +- .../out/model-hub/__next._index.txt | 8 +- .../out/model-hub/__next._tree.txt | 4 +- .../_experimental/out/model-hub/index.html | 2 +- litellm/proxy/_experimental/out/model_hub.txt | 30 +- .../out/model_hub/__next._full.txt | 30 +- .../out/model_hub/__next._head.txt | 2 +- .../out/model_hub/__next._index.txt | 8 +- .../out/model_hub/__next._tree.txt | 4 +- .../model_hub/__next.model_hub.__PAGE__.txt | 4 +- .../out/model_hub/__next.model_hub.txt | 2 +- .../_experimental/out/model_hub/index.html | 2 +- .../_experimental/out/model_hub_table.txt | 40 +- .../out/model_hub_table/__next._full.txt | 40 +- .../out/model_hub_table/__next._head.txt | 2 +- .../out/model_hub_table/__next._index.txt | 8 +- .../out/model_hub_table/__next._tree.txt | 4 +- .../__next.model_hub_table.__PAGE__.txt | 4 +- .../__next.model_hub_table.txt | 2 +- .../out/model_hub_table/index.html | 2 +- .../out/models-and-endpoints.txt | 45 +- ...ib2FyZCk.models-and-endpoints.__PAGE__.txt | 4 +- ....!KGRhc2hib2FyZCk.models-and-endpoints.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/models-and-endpoints/__next._full.txt | 45 +- .../out/models-and-endpoints/__next._head.txt | 2 +- .../models-and-endpoints/__next._index.txt | 8 +- .../out/models-and-endpoints/__next._tree.txt | 4 +- .../out/models-and-endpoints/index.html | 2 +- .../proxy/_experimental/out/onboarding.txt | 10 +- .../out/onboarding/__next._full.txt | 10 +- .../out/onboarding/__next._head.txt | 2 +- .../out/onboarding/__next._index.txt | 8 +- .../out/onboarding/__next._tree.txt | 4 +- .../onboarding/__next.onboarding.__PAGE__.txt | 4 +- .../out/onboarding/__next.onboarding.txt | 2 +- .../_experimental/out/onboarding/index.html | 2 +- .../proxy/_experimental/out/organizations.txt | 14 +- ...KGRhc2hib2FyZCk.organizations.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.organizations.txt | 2 +- .../organizations/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/organizations/__next._full.txt | 14 +- .../out/organizations/__next._head.txt | 2 +- .../out/organizations/__next._index.txt | 8 +- .../out/organizations/__next._tree.txt | 4 +- .../out/organizations/index.html | 2 +- .../proxy/_experimental/out/playground.txt | 14 +- ...t.!KGRhc2hib2FyZCk.playground.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.playground.txt | 2 +- .../playground/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/playground/__next._full.txt | 14 +- .../out/playground/__next._head.txt | 2 +- .../out/playground/__next._index.txt | 8 +- .../out/playground/__next._tree.txt | 4 +- .../_experimental/out/playground/index.html | 2 +- litellm/proxy/_experimental/out/policies.txt | 14 +- ...ext.!KGRhc2hib2FyZCk.policies.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.policies.txt | 2 +- .../out/policies/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/policies/__next._full.txt | 14 +- .../out/policies/__next._head.txt | 2 +- .../out/policies/__next._index.txt | 8 +- .../out/policies/__next._tree.txt | 4 +- .../_experimental/out/policies/index.html | 2 +- .../out/settings/admin-settings.txt | 45 +- ...FyZCk.settings.admin-settings.__PAGE__.txt | 4 +- ...GRhc2hib2FyZCk.settings.admin-settings.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.settings.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../settings/admin-settings/__next._full.txt | 45 +- .../settings/admin-settings/__next._head.txt | 2 +- .../settings/admin-settings/__next._index.txt | 8 +- .../settings/admin-settings/__next._tree.txt | 4 +- .../out/settings/admin-settings/index.html | 2 +- .../out/settings/logging-and-alerts.txt | 45 +- ...k.settings.logging-and-alerts.__PAGE__.txt | 4 +- ...2hib2FyZCk.settings.logging-and-alerts.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.settings.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../logging-and-alerts/__next._full.txt | 45 +- .../logging-and-alerts/__next._head.txt | 2 +- .../logging-and-alerts/__next._index.txt | 8 +- .../logging-and-alerts/__next._tree.txt | 4 +- .../settings/logging-and-alerts/index.html | 2 +- .../out/settings/router-settings.txt | 45 +- ...yZCk.settings.router-settings.__PAGE__.txt | 4 +- ...Rhc2hib2FyZCk.settings.router-settings.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.settings.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.txt | 4 +- .../settings/router-settings/__next._full.txt | 45 +- .../settings/router-settings/__next._head.txt | 2 +- .../router-settings/__next._index.txt | 8 +- .../settings/router-settings/__next._tree.txt | 4 +- .../out/settings/router-settings/index.html | 2 +- .../_experimental/out/settings/ui-theme.txt | 45 +- .../__next.!KGRhc2hib2FyZCk.settings.txt | 2 +- ...c2hib2FyZCk.settings.ui-theme.__PAGE__.txt | 4 +- ...ext.!KGRhc2hib2FyZCk.settings.ui-theme.txt | 2 +- .../ui-theme/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/settings/ui-theme/__next._full.txt | 45 +- .../out/settings/ui-theme/__next._head.txt | 2 +- .../out/settings/ui-theme/__next._index.txt | 8 +- .../out/settings/ui-theme/__next._tree.txt | 4 +- .../out/settings/ui-theme/index.html | 2 +- litellm/proxy/_experimental/out/teams.txt | 16 +- ...__next.!KGRhc2hib2FyZCk.teams.__PAGE__.txt | 4 +- .../teams/__next.!KGRhc2hib2FyZCk.teams.txt | 2 +- .../out/teams/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../_experimental/out/teams/__next._full.txt | 16 +- .../_experimental/out/teams/__next._head.txt | 2 +- .../_experimental/out/teams/__next._index.txt | 8 +- .../_experimental/out/teams/__next._tree.txt | 4 +- .../proxy/_experimental/out/teams/index.html | 2 +- litellm/proxy/_experimental/out/test-key.txt | 14 +- ...ext.!KGRhc2hib2FyZCk.test-key.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.test-key.txt | 2 +- .../out/test-key/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/test-key/__next._full.txt | 14 +- .../out/test-key/__next._head.txt | 2 +- .../out/test-key/__next._index.txt | 8 +- .../out/test-key/__next._tree.txt | 4 +- .../_experimental/out/test-key/index.html | 2 +- .../_experimental/out/tools/mcp-servers.txt | 45 +- ...c2hib2FyZCk.tools.mcp-servers.__PAGE__.txt | 4 +- ...ext.!KGRhc2hib2FyZCk.tools.mcp-servers.txt | 2 +- .../__next.!KGRhc2hib2FyZCk.tools.txt | 2 +- .../mcp-servers/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/tools/mcp-servers/__next._full.txt | 45 +- .../out/tools/mcp-servers/__next._head.txt | 2 +- .../out/tools/mcp-servers/__next._index.txt | 8 +- .../out/tools/mcp-servers/__next._tree.txt | 4 +- .../out/tools/mcp-servers/index.html | 2 +- .../_experimental/out/tools/vector-stores.txt | 45 +- .../__next.!KGRhc2hib2FyZCk.tools.txt | 2 +- ...hib2FyZCk.tools.vector-stores.__PAGE__.txt | 4 +- ...t.!KGRhc2hib2FyZCk.tools.vector-stores.txt | 2 +- .../vector-stores/__next.!KGRhc2hib2FyZCk.txt | 4 +- .../out/tools/vector-stores/__next._full.txt | 45 +- .../out/tools/vector-stores/__next._head.txt | 2 +- .../out/tools/vector-stores/__next._index.txt | 8 +- .../out/tools/vector-stores/__next._tree.txt | 4 +- .../out/tools/vector-stores/index.html | 2 +- litellm/proxy/_experimental/out/usage.txt | 16 +- .../out/usage/__next.!KGRhc2hib2FyZCk.txt | 4 +- ...__next.!KGRhc2hib2FyZCk.usage.__PAGE__.txt | 4 +- .../usage/__next.!KGRhc2hib2FyZCk.usage.txt | 2 +- .../_experimental/out/usage/__next._full.txt | 16 +- .../_experimental/out/usage/__next._head.txt | 2 +- .../_experimental/out/usage/__next._index.txt | 8 +- .../_experimental/out/usage/__next._tree.txt | 4 +- .../proxy/_experimental/out/usage/index.html | 2 +- litellm/proxy/_experimental/out/users.txt | 16 +- .../out/users/__next.!KGRhc2hib2FyZCk.txt | 4 +- ...__next.!KGRhc2hib2FyZCk.users.__PAGE__.txt | 4 +- .../users/__next.!KGRhc2hib2FyZCk.users.txt | 2 +- .../_experimental/out/users/__next._full.txt | 16 +- .../_experimental/out/users/__next._head.txt | 2 +- .../_experimental/out/users/__next._index.txt | 8 +- .../_experimental/out/users/__next._tree.txt | 4 +- .../proxy/_experimental/out/users/index.html | 2 +- .../proxy/_experimental/out/virtual-keys.txt | 14 +- .../virtual-keys/__next.!KGRhc2hib2FyZCk.txt | 4 +- ...!KGRhc2hib2FyZCk.virtual-keys.__PAGE__.txt | 4 +- .../__next.!KGRhc2hib2FyZCk.virtual-keys.txt | 2 +- .../out/virtual-keys/__next._full.txt | 14 +- .../out/virtual-keys/__next._head.txt | 2 +- .../out/virtual-keys/__next._index.txt | 8 +- .../out/virtual-keys/__next._tree.txt | 4 +- .../_experimental/out/virtual-keys/index.html | 2 +- litellm/proxy/_new_secret_config.yaml | 63 +- litellm/proxy/_types.py | 11 +- .../claude_code_marketplace.py | 37 +- litellm/proxy/auth/auth_checks.py | 299 ++-- litellm/proxy/auth/auth_utils.py | 70 +- litellm/proxy/auth/handle_jwt.py | 32 +- litellm/proxy/auth/login_utils.py | 30 +- litellm/proxy/auth/route_checks.py | 1 + litellm/proxy/auth/user_api_key_auth.py | 297 ++-- litellm/proxy/batches_endpoints/endpoints.py | 18 +- litellm/proxy/common_request_processing.py | 535 +++++-- .../common_utils/openai_endpoint_utils.py | 11 +- .../proxy/common_utils/reset_budget_job.py | 86 +- litellm/proxy/db/create_views.py | 16 +- litellm/proxy/db/prisma_client.py | 44 + .../ui_discovery_endpoints.py | 7 + .../example_config_yaml/otel_test_config.yaml | 14 +- .../guardrail_hooks/akto/__init__.py | 39 + .../guardrails/guardrail_hooks/akto/akto.py | 492 +++++++ .../mcp_jwt_signer/__init__.py | 84 ++ .../mcp_jwt_signer/mcp_jwt_signer.py | 891 +++++++++++ .../guardrail_hooks/openai/moderations.py | 91 +- .../unified_guardrail/unified_guardrail.py | 3 + litellm/proxy/health_check.py | 58 +- .../proxy/hooks/parallel_request_limiter.py | 36 +- .../hooks/parallel_request_limiter_v3.py | 8 +- .../proxy/hooks/proxy_track_cost_callback.py | 45 +- litellm/proxy/litellm_pre_call_utils.py | 61 +- .../budget_management_endpoints.py | 26 +- .../internal_user_endpoints.py | 220 ++- .../key_management_endpoints.py | 138 +- .../mcp_management_endpoints.py | 29 +- .../model_management_endpoints.py | 171 ++- .../management_endpoints/team_endpoints.py | 229 ++- litellm/proxy/management_endpoints/ui_sso.py | 201 ++- .../proxy/management_helpers/audit_logs.py | 128 +- .../openai_files_endpoints/common_utils.py | 5 +- .../llm_passthrough_endpoints.py | 10 +- .../anthropic_passthrough_logging_handler.py | 16 +- .../pass_through_endpoints.py | 237 +-- litellm/proxy/proxy_cli.py | 18 +- litellm/proxy/proxy_server.py | 454 +++++- .../proxy/response_api_endpoints/endpoints.py | 33 +- .../response_polling/background_streaming.py | 14 +- litellm/proxy/schema.prisma | 4 + .../spend_management_endpoints.py | 15 +- .../spend_tracking/spend_tracking_utils.py | 1 + .../proxy/spend_tracking/vantage_endpoints.py | 31 +- litellm/proxy/utils.py | 136 +- litellm/proxy/video_endpoints/utils.py | 4 +- litellm/responses/file_search/__init__.py | 0 .../responses/file_search/emulated_handler.py | 592 ++++++++ .../streaming_iterator.py | 60 +- .../transformation.py | 58 + litellm/responses/main.py | 353 ++++- litellm/responses/streaming_iterator.py | 43 +- litellm/router.py | 370 ++++- litellm/router_utils/health_state_cache.py | 100 ++ .../deployment_affinity_check.py | 105 +- litellm/setup_wizard.py | 668 +++++++++ litellm/types/guardrails.py | 8 +- .../anthropic_cache_control_hook.py | 11 +- litellm/types/integrations/prometheus.py | 90 ++ litellm/types/llms/openai.py | 40 +- litellm/types/llms/vertex_ai.py | 17 +- litellm/types/proxy/claude_code_endpoints.py | 3 +- .../types/proxy/control_plane_endpoints.py | 14 + .../ui_discovery_endpoints.py | 6 +- .../proxy/guardrails/guardrail_hooks/akto.py | 55 + .../management_endpoints/team_endpoints.py | 8 +- litellm/types/proxy/vantage_endpoints.py | 3 +- litellm/types/responses/main.py | 36 + litellm/types/router.py | 6 + litellm/types/utils.py | 36 + litellm/types/videos/utils.py | 4 +- litellm/utils.py | 133 +- litellm/videos/main.py | 34 +- model_prices_and_context_window.json | 824 ++++++++--- package.json | 2 +- poetry.lock | 35 +- pyproject.toml | 9 +- requirements.txt | 4 +- schema.prisma | 13 + scripts/install.sh | 143 ++ tests/agent_tests/test_a2a_agent.py | 82 +- tests/audio_tests/azure_speech.mp3 | Bin 22464 -> 55680 bytes tests/audio_tests/test_audio_speech.py | 112 +- tests/batches_tests/test_fine_tuning_api.py | 118 +- .../test_openai_batches_and_files.py | 52 +- .../test_router_settings.py | 12 +- .../test_prometheus_logging_callbacks.py | 20 +- .../test_prometheus_unit_tests.py | 6 +- .../guardrails_tests/test_akto_guardrails.py | 550 +++++++ tests/image_gen_tests/test_image_edit.png | Bin 2176454 -> 468229 bytes tests/image_gen_tests/test_image_edits.py | 255 ++-- .../image_gen_tests/test_image_generation.py | 45 +- tests/image_gen_tests/vertex_key.json | 8 +- .../test_litellm_proxy_extras_utils.py | 21 + .../vertex_ai/test_gemini_batch_embeddings.py | 39 + .../test_azure_ai_anthropic_token_counter.py | 14 +- .../test_bedrock_token_counter.py | 64 + tests/litellm_utils_tests/test_hashicorp.py | 17 +- .../litellm_utils_tests/test_health_check.py | 56 +- .../test_litellm_overhead.py | 2 +- .../test_secret_manager.py | 88 +- tests/litellm_utils_tests/vertex_key.json | 8 +- .../test_azure_responses_api.py | 65 +- ...t_base_responses_api_streaming_iterator.py | 156 +- .../test_manus_files_all_methods.py | 71 - .../test_manus_responses_api.py | 127 -- .../test_anthropic_completion.py | 85 ++ tests/llm_translation/test_azure_agents.py | 279 +++- tests/llm_translation/test_azure_ai.py | 151 +- tests/llm_translation/test_azure_o_series.py | 40 +- tests/llm_translation/test_azure_openai.py | 76 +- .../test_bedrock_completion.py | 76 +- .../test_clarifai_completion.py | 109 -- tests/llm_translation/test_cloudflare.py | 173 ++- .../test_fireworks_ai_translation.py | 34 +- tests/llm_translation/test_gemini.py | 6 +- .../test_gemini_image_usage.py | 55 + tests/llm_translation/test_prompt_factory.py | 222 +++ tests/llm_translation/test_rerank.py | 42 - .../test_router_llm_translation_tests.py | 16 +- tests/llm_translation/test_snowflake.py | 202 +-- tests/llm_translation/test_watsonx.py | 10 + tests/load_tests/vertex_key.json | 8 +- .../adroit-crow-413218-bc47f303efc9.json | 13 - .../example_config_yaml/azure_config.yaml | 4 +- tests/local_testing/test_acooldowns_router.py | 2 +- tests/local_testing/test_alangfuse.py | 263 ---- .../test_amazing_vertex_completion.py | 185 +-- tests/local_testing/test_arize_ai.py | 4 +- tests/local_testing/test_assistants.py | 11 +- tests/local_testing/test_azure_openai.py | 10 +- tests/local_testing/test_azure_perf.py | 8 +- tests/local_testing/test_caching.py | 153 +- tests/local_testing/test_caching_ssl.py | 12 +- tests/local_testing/test_class.py | 8 +- tests/local_testing/test_completion.py | 371 ++--- tests/local_testing/test_config.py | 21 +- .../test_configs/test_bad_config.yaml | 8 +- ...st_cloudflare_azure_with_cache_config.yaml | 2 +- .../test_configs/test_config_no_auth.yaml | 14 +- .../test_configs/test_custom_logger.yaml | 8 +- tests/local_testing/test_embedding.py | 146 +- tests/local_testing/test_exceptions.py | 107 +- tests/local_testing/test_gcs_bucket.py | 821 +++-------- tests/local_testing/test_loadtest_router.py | 4 +- .../test_prompt_injection_detection.py | 4 +- tests/local_testing/test_router.py | 145 +- .../test_router_budget_limiter.py | 5 +- tests/local_testing/test_router_caching.py | 4 +- .../local_testing/test_router_client_init.py | 25 +- .../test_router_cooldown_handlers.py | 4 +- tests/local_testing/test_router_debug_logs.py | 4 +- tests/local_testing/test_router_fallbacks.py | 78 +- tests/local_testing/test_router_init.py | 704 --------- tests/local_testing/test_router_timeout.py | 4 +- tests/local_testing/test_router_utils.py | 29 +- tests/local_testing/test_simple_shuffle.py | 53 - tests/local_testing/test_streaming.py | 212 +-- tests/local_testing/test_timeout.py | 8 +- .../local_testing/test_tpm_rpm_routing_v2.py | 16 +- tests/local_testing/vertex_key.json | 8 +- .../gcs_pub_sub_body/spend_logs_payload.json | 2 +- tests/logging_callback_tests/test_alerting.py | 59 +- .../test_amazing_s3_logs.py | 168 +-- .../test_azure_blob_storage.py | 45 - .../test_custom_callback_router.py | 48 +- tests/logging_callback_tests/test_datadog.py | 18 +- .../test_gcs_pub_sub.py | 12 +- .../test_langfuse_e2e_test.py | 66 +- tests/ocr_tests/vertex_key.json | 8 +- tests/old_proxy_tests/tests/load_test_q.py | 4 +- .../tests/test_vtx_sdk_embedding.py | 2 +- tests/pass_through_tests/test_assembly_ai.py | 112 +- tests/pass_through_tests/test_local_vertex.js | 2 +- tests/pass_through_tests/test_vertex.test.js | 4 +- tests/pass_through_tests/test_vertex_ai.py | 6 +- .../test_vertex_with_spend.test.js | 4 +- tests/pass_through_tests/vertex_key.json | 8 +- .../test_azure_anthropic_structured_output.py | 4 +- .../test_claude_code_marketplace.py | 46 + tests/proxy_admin_ui_tests/package.json | 2 +- .../ui_unit_tests/package.json | 2 +- .../adroit-crow-413218-bc47f303efc9.json | 13 - .../example_config_yaml/azure_config.yaml | 4 +- .../test_configs/test_bad_config.yaml | 8 +- ...st_cloudflare_azure_with_cache_config.yaml | 2 +- .../test_configs/test_config_no_auth.yaml | 14 +- tests/proxy_unit_tests/test_jwt.py | 3 + .../proxy_unit_tests/test_jwt_key_mapping.py | 75 + .../test_proxy_pass_user_config.py | 9 +- tests/proxy_unit_tests/test_proxy_server.py | 189 +-- tests/proxy_unit_tests/test_proxy_utils.py | 252 ++++ .../test_response_polling_pre_call_checks.py | 182 +++ tests/proxy_unit_tests/vertex_key.json | 8 +- .../test_get_model_list_alias_optimization.py | 8 +- .../test_router_helper_utils.py | 85 ++ .../test_router_index_management.py | 22 + .../test_spend_accuracy_tests.py | 13 +- .../test_adding_passthrough_model.py | 83 +- tests/test_litellm/caching/test_dual_cache.py | 99 ++ ...responses_transformation_transformation.py | 636 ++++++-- .../azure_cancel_expected_output.json | 20 + .../azure_cancel_raw_response.json | 18 + .../azure_cancel_request.json | 3 + .../azure_create_expected_output.json | 20 + .../azure_create_raw_response.json | 18 + .../azure_create_request.json | 8 + .../azure_list_raw_response.json | 20 + .../azure_list_request.json | 4 + .../datadog/test_datadog_tags_regression.py | 2 +- .../integrations/test_langsmith_init.py | 178 ++- .../integrations/test_opentelemetry.py | 131 +- .../test_prometheus_spend_logs_metadata.py | 110 ++ .../test_prometheus_user_team_metrics.py | 192 ++- .../test_websearch_short_circuit.py | 384 +++++ .../interactions/test_openapi_compliance.py | 24 +- ...llm_core_utils_prompt_templates_factory.py | 225 +++ .../litellm_core_utils/test_core_helpers.py | 3 + .../test_litellm_logging.py | 382 ++++- .../test_streaming_handler.py | 262 ++++ .../chat/test_anthropic_chat_handler.py | 484 +++++- .../test_anthropic_chat_transformation.py | 940 ++++++------ ...est_code_interpreter_results_extraction.py | 268 ++++ ...al_pass_through_adapters_transformation.py | 127 ++ ...erimental_pass_through_messages_handler.py | 325 +++- .../test_responses_adapters_transformation.py | 86 +- .../anthropic/test_anthropic_common_utils.py | 442 +++++- .../test_anthropic_files_and_batches.py | 2 +- .../chat/test_azure_gpt5_transformation.py | 9 +- .../llms/azure/test_azure_common_utils.py | 103 +- .../llms/azure/test_azure_fine_tuning_api.py | 150 ++ ...e_anthropic_count_tokens_transformation.py | 3 +- .../chat/test_converse_transformation.py | 1309 +++++++---------- .../test_fireworks_ai_chat_transformation.py | 29 + .../files/test_gemini_files_transformation.py | 71 +- .../test_gemini_realtime_transformation.py | 15 + tests/test_litellm/llms/mistral/__init__.py | 0 ...tral_audio_transcription_transformation.py | 44 + .../test_litellm/llms/mistral/ocr/__init__.py | 0 .../ocr/test_mistral_ocr_transformation.py | 81 + .../test_moonshot_chat_transformation.py | 70 +- .../chat/test_openai_gpt_transformation.py | 39 + .../test_openai_responses_transformation.py | 26 +- .../llms/openai/test_gpt5_transformation.py | 292 +++- .../test_openrouter_provider_routing.py | 14 +- .../test_snowflake_chat_transformation.py | 181 ++- .../llms/test_file_search_responses.py | 892 +++++++++++ .../test_vertex_ai_context_caching.py | 39 +- .../gemini/test_context_circulation.py | 234 +++ ...emini_streaming_tool_call_finish_reason.py | 72 + ...test_vertex_and_google_ai_studio_gemini.py | 36 + .../test_vertex_ai_batch_transformation.py | 127 ++ .../count_tokens/__init__.py | 0 .../test_count_tokens_location.py | 164 +++ .../llms/xai/test_xai_cost_calculator.py | 52 +- .../mcp_server/test_discoverable_endpoints.py | 138 ++ .../mcp_server/test_mcp_hook_extra_headers.py | 707 +++++++++ .../proxy/agent_endpoints/test_endpoints.py | 4 + .../test_claude_code_marketplace.py | 222 +++ .../proxy/auth/test_auth_checks.py | 151 ++ .../proxy/auth/test_auth_utils.py | 236 ++- .../proxy/auth/test_handle_jwt.py | 667 ++++++--- .../proxy/auth/test_password_hashing.py | 69 + .../proxy/auth/test_route_checks.py | 75 + .../proxy/auth/test_user_api_key_auth.py | 342 +++-- .../proxy/db/test_prisma_client.py | 88 +- .../proxy/db/test_prisma_self_heal.py | 35 + .../test_ui_discovery_endpoints.py | 57 +- .../openai/test_moderations.py | 327 ++++ .../test_openai_moderation_streaming.py | 103 ++ .../test_unified_guardrail.py | 41 +- .../test_deferred_guardrail_logging.py | 955 ++++++++++++ .../proxy/guardrails/test_mcp_jwt_signer.py | 1103 ++++++++++++++ .../test_internal_user_endpoints.py | 177 ++- .../test_key_management_endpoints.py | 614 +++++--- .../test_mcp_management_endpoints.py | 52 + .../test_model_management_endpoints.py | 612 +++++++- .../test_team_endpoints.py | 386 ++++- .../proxy/management_endpoints/test_ui_sso.py | 257 +++- .../test_audit_log_callbacks.py | 345 +++++ .../test_files_endpoint.py | 339 +++-- .../test_spend_management_endpoints.py | 102 +- .../proxy/test_common_request_processing.py | 77 + .../proxy/test_max_budget_env_var.py | 49 + .../proxy/test_model_info_default_limits.py | 167 +++ tests/test_litellm/proxy/test_proxy_cli.py | 41 + tests/test_litellm/proxy/test_proxy_server.py | 878 ++++++++--- .../proxy/test_response_model_sanitization.py | 58 + .../test_litellm_completion_responses.py | 94 ++ .../test_responses_prompt_management.py | 383 +++++ .../test_deployment_affinity_check.py | 281 ++++ .../router_utils/test_health_state_cache.py | 113 ++ .../test_router_health_check_routing.py | 197 +++ .../test_claude_opus_4_6_config.py | 98 +- tests/test_litellm/test_constants.py | 90 +- tests/test_litellm/test_main.py | 34 + .../test_project_alias_tracking.py | 134 ++ tests/test_litellm/test_redis.py | 122 +- tests/test_litellm/test_router.py | 84 +- .../test_router_order_fallback.py | 331 +++++ tests/test_litellm/test_secret_redaction.py | 49 + tests/test_litellm/test_setup_wizard.py | 188 +++ tests/test_litellm/test_utils.py | 248 +++- .../types/llms/test_types_llms_openai.py | 69 +- tests/test_models.py | 3 + tests/test_team_logging.py | 134 -- tests/unified_google_tests/vertex_key.json | 8 +- .../test_azure_ai_vector_store.py | 6 +- .../tests/navigation/sidebar.spec.ts | 1 - ui/litellm-dashboard/package.json | 2 +- .../public/assets/logos/akto.svg | 10 + .../app/(dashboard)/api-reference/page.tsx | 10 +- .../app/(dashboard)/components/Sidebar2.tsx | 38 +- .../(dashboard)/hooks/budgets/useBudgets.ts | 70 + .../hooks/common/queryKeysFactory.test.ts | 34 + .../app/(dashboard)/hooks/login/useLogin.ts | 4 +- .../hooks/proxySettings/useProxySettings.ts | 21 + .../app/(dashboard)/hooks/teams/useTeams.ts | 39 +- .../hooks/uiConfig/useUIConfig.test.ts | 4 + .../src/app/(dashboard)/layout.tsx | 34 +- .../src/app/(dashboard)/playground/page.tsx | 66 - .../teams/components/TeamsHeaderTabs.test.tsx | 54 + .../components/TeamsTable/TeamsTable.test.tsx | 129 ++ .../src/app/login/LoginPage.test.tsx | 15 +- .../src/app/login/LoginPage.tsx | 127 +- .../app/onboarding/OnboardingForm.test.tsx | 95 ++ ui/litellm-dashboard/src/app/page.tsx | 33 +- .../AIHub/AgentHubTableColumns.test.tsx | 146 ++ .../components/AIHub/AgentHubTableColumns.tsx | 1 - .../AccessGroupCreateModal.tsx | 5 +- .../AccessGroupEditModal.tsx | 5 +- .../src/components/BulkEditUsers.tsx | 4 +- .../CloudZeroCreateModal.tsx | 9 +- .../CloudZeroIntegrationSettings.tsx | 15 +- .../CloudZeroUpdateModal.tsx | 9 +- .../src/components/CreateUserButton.tsx | 6 +- .../components/DebugWarningBanner.test.tsx | 41 + .../ExportFormatSelector.test.tsx | 20 + .../EntityUsageExport/ExportSummary.test.tsx | 59 + .../ExportTypeSelector.test.tsx | 46 + .../UsageExportHeader.test.tsx | 73 + .../GuardrailConfig.test.tsx | 98 ++ .../GuardrailsMonitor/MetricCard.test.tsx | 49 + .../src/components/HelpLink.test.tsx | 38 + .../WorkerDropdown/WorkerDropdown.test.tsx | 134 ++ .../Navbar/WorkerDropdown/WorkerDropdown.tsx | 38 + .../src/components/OldTeams.test.tsx | 66 +- .../src/components/OldTeams.tsx | 1039 ++++++------- .../ProjectModals/CreateProjectModal.tsx | 7 +- .../ProjectModals/EditProjectModal.tsx | 7 +- .../SearchTools/SearchToolTester.tsx | 5 +- .../RouterSettings/Fallbacks/AddFallbacks.tsx | 5 +- .../Fallbacks/FallbackSelectionForm.tsx | 5 +- .../src/components/TeamSSOSettings.tsx | 4 + .../src/components/ToolDetail.tsx | 1 - .../ToolPolicies/PolicySelect.test.tsx | 84 ++ .../add_model/AddModelForm.test.tsx | 15 + .../src/components/add_model/AddModelForm.tsx | 3 +- .../add_model/ComplexityRouterConfig.test.tsx | 82 ++ .../agent_management/AgentSelector.test.tsx | 144 ++ .../src/components/agents/add_agent_form.tsx | 16 +- .../agents/agent_card_grid.test.tsx | 90 ++ .../src/components/agents/agent_info.tsx | 9 +- .../src/components/budgets/budget_modal.tsx | 21 +- .../components/budgets/budget_panel.test.tsx | 178 ++- .../src/components/budgets/budget_panel.tsx | 48 +- .../components/budgets/edit_budget_modal.tsx | 41 +- .../src/components/chat/ChatPage.tsx | 5 +- .../src/components/chat/MCPAppsPanel.tsx | 6 +- .../src/components/chat/MCPConnectPicker.tsx | 7 +- .../src/components/chat/MCPCredentialsTab.tsx | 5 +- .../add_plugin_form.test.tsx | 131 ++ .../claude_code_plugins/add_plugin_form.tsx | 64 +- .../claude_code_plugins/helpers.test.ts | 329 +++++ .../TableIconActionButton.tsx | 2 + .../OrganizationDropdown.tsx | 12 +- .../RateLimitTypeFormItem.test.tsx | 61 + .../common_components/team_dropdown.tsx | 121 +- .../guardrails/guardrail_garden_configs.ts | 6 + .../guardrails/guardrail_garden_data.ts | 8 + .../guardrails/guardrail_info_helpers.tsx | 1 + .../key_team_helpers/filter_helpers.test.ts | 90 ++ .../transform_key_info.test.ts | 62 + .../src/components/leftnav.tsx | 47 +- .../mcp_tools/ByokCredentialModal.tsx | 9 +- .../molecules/message_manager.test.ts | 107 ++ .../components/molecules/message_manager.tsx | 38 + .../src/components/navbar.tsx | 56 +- .../src/components/networking.tsx | 135 +- .../organisms/create_key_button.test.tsx | 51 +- .../organisms/create_key_button.tsx | 28 +- .../src/components/page_metadata.ts | 2 +- .../chat_ui/ChatMessageBubble.test.tsx | 296 ++++ .../playground/chat_ui/ChatMessageBubble.tsx | 214 +++ .../components/playground/chat_ui/ChatUI.tsx | 602 +------- .../chat_ui/CodeInterpreterTool.tsx | 5 +- .../chat_ui/FilePreviewCard.test.tsx | 70 + .../playground/chat_ui/FilePreviewCard.tsx | 45 + .../playground/chat_ui/useChatHistory.test.ts | 591 ++++++++ .../playground/chat_ui/useChatHistory.ts | 392 +++++ .../src/components/policies/index.tsx | 29 +- .../policies/pipeline_flow_builder.tsx | 9 +- .../components/policies/policy_templates.tsx | 5 +- .../src/components/route_preview.tsx | 8 +- .../shared/CreatedKeyDisplay.test.tsx | 14 +- .../components/shared/CreatedKeyDisplay.tsx | 5 +- .../src/components/team/TeamInfo.tsx | 7 +- .../components/ui/AntDLoadingSpinner.test.tsx | 61 + .../src/components/ui/AntDLoadingSpinner.tsx | 12 + .../CreateVectorStore.tsx | 19 +- .../DocumentsTable.tsx | 5 +- .../VectorStoreTester.tsx | 5 +- .../GuardrailViewer/GuardrailViewer.test.tsx | 33 + .../GuardrailViewer/GuardrailViewer.tsx | 60 +- .../GuardrailViewer/__tests__/fixtures.ts | 2 +- .../CollapsibleMessage.test.tsx | 54 + .../LogDetailsDrawer/HistoryTree.test.tsx | 50 + .../view_logs/LogDetailsDrawer/InputCard.tsx | 4 +- .../view_logs/LogDetailsDrawer/OutputCard.tsx | 5 +- .../SimpleMessageBlock.test.tsx | 55 + .../SimpleToolCallBlock.test.tsx | 51 + .../LogDetailsDrawer/useKeyboardNavigation.ts | 8 +- .../src/components/view_users/columns.tsx | 22 +- .../src/contexts/AntdGlobalProvider.tsx | 14 +- ui/litellm-dashboard/src/hooks/useWorker.ts | 65 + ui/litellm-dashboard/tests/setupTests.ts | 5 + 1313 files changed, 55291 insertions(+), 19640 deletions(-) delete mode 100644 .claude/settings.json create mode 100644 .github/workflows/_test-unit-base.yml create mode 100644 .github/workflows/_test-unit-services-base.yml create mode 100644 .github/workflows/check-schema-sync.yml delete mode 100644 .github/workflows/ghcr_deploy.yml delete mode 100644 .github/workflows/ghcr_helm_deploy.yml delete mode 100644 .github/workflows/interpret_load_test.py delete mode 100644 .github/workflows/load_test.yml delete mode 100644 .github/workflows/locustfile.py delete mode 100644 .github/workflows/main.yml delete mode 100644 .github/workflows/publish-migrations.yml delete mode 100644 .github/workflows/publish_enterprise.yml delete mode 100644 .github/workflows/publish_proxy_extras.yml create mode 100644 .github/workflows/publish_to_pypi.yml delete mode 100644 .github/workflows/redeploy_proxy.py delete mode 100644 .github/workflows/regenerate-poetry-lock.yml delete mode 100644 .github/workflows/reset_stable.yml create mode 100644 .github/workflows/scorecard.yml delete mode 100644 .github/workflows/simple_pypi_publish.yml create mode 100644 .github/workflows/sync-schema.yml create mode 100644 .github/workflows/test-unit-caching-redis.yml create mode 100644 .github/workflows/test-unit-core-utils.yml create mode 100644 .github/workflows/test-unit-documentation.yml create mode 100644 .github/workflows/test-unit-enterprise-routing.yml create mode 100644 .github/workflows/test-unit-integrations.yml create mode 100644 .github/workflows/test-unit-llm-providers.yml create mode 100644 .github/workflows/test-unit-misc.yml create mode 100644 .github/workflows/test-unit-proxy-auth.yml create mode 100644 .github/workflows/test-unit-proxy-db.yml create mode 100644 .github/workflows/test-unit-proxy-endpoints.yml create mode 100644 .github/workflows/test-unit-proxy-infra.yml create mode 100644 .github/workflows/test-unit-proxy-legacy.yml create mode 100644 .github/workflows/test-unit-responses-caching-types.yml create mode 100644 .github/workflows/test-unit-security.yml create mode 100644 .github/workflows/zizmor.yml create mode 100644 .semgrep/rules/security/no-claude-directory.yml create mode 100644 docs/my-website/blog/ci_cd_v2_improvements/index.md create mode 100644 docs/my-website/blog/gpt_5_4_mini_nano/index.md create mode 100644 docs/my-website/blog/guardrail_logging_secret_exposure_incident/index.md create mode 100644 docs/my-website/blog/security_townhall_updates/index.md create mode 100644 docs/my-website/blog/security_townhall_updates/shared_ci_cd_environment.png create mode 100644 docs/my-website/blog/security_update_march_2026/index.md create mode 100644 docs/my-website/blog/vanta_compliance_recertification/index.md create mode 100644 docs/my-website/docs/guides/index.md create mode 100644 docs/my-website/docs/integrations/observability_index.md create mode 100644 docs/my-website/docs/learn/gateway_quickstart.md create mode 100644 docs/my-website/docs/learn/index.md create mode 100644 docs/my-website/docs/learn/sdk_quickstart.md create mode 100644 docs/my-website/docs/prompt_management.md create mode 100644 docs/my-website/docs/providers/gemini/music.md create mode 100644 docs/my-website/docs/proxy/guardrails/akto.md create mode 100644 docs/my-website/docs/proxy/high_availability_control_plane.md create mode 100644 docs/my-website/docs/tutorials/file_search_responses_api.md create mode 100644 docs/my-website/docs/tutorials/index.md create mode 100644 docs/my-website/docs/tutorials/vertex_ai_pay_go.md create mode 100644 docs/my-website/img/ci_cd_architecture.png create mode 100644 docs/my-website/img/hero.png create mode 100644 docs/my-website/img/isolated_ci_cd_environments.png create mode 100644 docs/my-website/img/mcp_zero_trust_gateway.png create mode 100644 docs/my-website/img/security_update_march_2026/proxy_version.png create mode 100644 docs/my-website/img/shared_ci_cd_environment.png create mode 100644 docs/my-website/release_notes/index.md rename docs/my-website/release_notes/{v1.81.12.md => v1.81.12/index.md} (99%) rename docs/my-website/release_notes/{v1.81.14.md => v1.81.14/index.md} (99%) rename docs/my-website/release_notes/{v1.81.6.md => v1.81.6/index.md} (100%) rename docs/my-website/release_notes/{v1.81.9.md => v1.81.9/index.md} (99%) rename docs/my-website/release_notes/{v1.82.0.md => v1.82.0/index.md} (100%) rename docs/my-website/release_notes/{v1.82.3.md => v1.82.3/index.md} (58%) create mode 100644 docs/my-website/sidebars-release-notes.js create mode 100644 docs/my-website/src/components/ControlPlaneArchitecture/ControlPlaneArchitecture.tsx create mode 100644 docs/my-website/src/components/ControlPlaneArchitecture/index.tsx create mode 100644 docs/my-website/src/components/ControlPlaneArchitecture/styles.module.css create mode 100644 docs/my-website/src/components/NavigationCards/index.js create mode 100644 docs/my-website/src/components/NavigationCards/styles.module.css create mode 100644 docs/my-website/src/components/VersionVerificationTable/index.tsx create mode 100644 docs/my-website/src/components/VersionVerificationTable/styles.module.css create mode 100644 docs/my-website/src/theme/DocSidebar/index.js create mode 100644 docs/my-website/src/theme/DocSidebar/styles.module.css create mode 100644 docs/my-website/src/theme/Navbar/Content/index.js create mode 100644 docs/my-website/src/theme/TOC/index.js create mode 100644 docs/my-website/src/theme/TOC/styles.module.css create mode 100644 docs/my-website/static/img/blog/vanta_soc2_recertification.png create mode 100644 docs/my-website/static/img/vertex_cost_tracking_flow.svg create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.58.tar.gz create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.60-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.60.tar.gz delete mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260311180521_schema_sync/migration.sql create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260318140652_add_index_to_team_table/migration.sql create mode 100644 litellm/llms/anthropic/experimental_pass_through/utils.py rename litellm/proxy/_experimental/out/_next/static/{aKKihXXKRJWLQThZgi8Rq => Hp-LQxDEAEt-JSJFExm-i}/_buildManifest.js (100%) rename litellm/proxy/_experimental/out/_next/static/{aKKihXXKRJWLQThZgi8Rq => Hp-LQxDEAEt-JSJFExm-i}/_clientMiddlewareManifest.json (100%) rename litellm/proxy/_experimental/out/_next/static/{aKKihXXKRJWLQThZgi8Rq => Hp-LQxDEAEt-JSJFExm-i}/_ssgManifest.js (100%) rename litellm/proxy/_experimental/out/_next/static/chunks/{d2e3b7dd6499c245.js => 02158aed2f4518e2.js} (95%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/04a7af91517db55b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/056b4991f668b494.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/065cbe2de8230973.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/06ebe9b0e9cdf241.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/0713a1954ae8db53.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/08d5ac6e0b6220c0.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/0a80887cd471a6cc.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/0b8ec8bf90ea9721.js rename litellm/proxy/_experimental/out/_next/static/chunks/{9dd55e1f36a7225c.js => 0be054dbc84bd8be.js} (95%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/0dda11815be4f78b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/0ea9112947894f26.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/112ad77f3dd2e3cd.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/11362340846735c3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/117fd0772eee5df6.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/123bb7375879d789.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/17741b7a77c20f1b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/179425128d293da9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1b424ce64213980f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1d6119b4214ab712.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1da362a651d209bd.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1eb2ed6e2dd204b7.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1eccde2dab0b3311.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1f58814a2409d571.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1f6df7977860dc7b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/1fcff413509b2e1f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/203dde2108f3f1ac.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/22970a12064ba16b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/22e715061d511345.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/23bf955e8672ce98.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/23e34a8c920ebd31.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/262c0742212bf6d1.js rename litellm/proxy/_experimental/out/_next/static/chunks/{66ef9d81cc17cfa8.js => 26542a70b9512f71.js} (80%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/26fda1c4c6936e38.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2793ac912badcf02.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/29f944b40b65da0a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2bacff998dbae5da.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2c21eeb7a235384a.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2d313397aa3e57de.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2d44417ec0ed6970.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2e7ede393477220f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/2faf62c238d105eb.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/305a1cf07cfab07b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/31e02a31dea7d5d2.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/338e84191fe615bf.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/348b31083769a7c4.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/354ca537c6c0601c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3569f12d1e9d5e0d.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3675074b1d85e268.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/38976546132cd527.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/39768ec0eebd2554.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/39bdd72c165f9ec0.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3b19a8bdc8d26868.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3da2633a10defd79.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3de1b6df2372e93b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/3f320784d80bed94.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/40f766ecc87dbf9a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4242033bd0f32638.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/42c127841d8c1bd3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/43404a268a45c17a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4348e537165edb3b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/440d96637d3ff94d.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/442ccb8d620e1fa6.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4472ece1be7379b3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/46b252adc34d9549.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/491d92760452057a.js rename litellm/proxy/_experimental/out/_next/static/chunks/{ae9cf43b8c0c76aa.js => 49e9dce7df902771.js} (99%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4b3c0ae9e54d843c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4c4469911e2f315e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4cc2a4292409c9b3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4e0ee3124dcdc85b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/4e5da3c236abd875.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5282ed7355826608.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/528456b9ec2e4413.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/53218dce8acb3bff.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/53a3a23605a87ee1.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/53a707a5829899ed.js rename litellm/proxy/_experimental/out/_next/static/chunks/{1a04d31843c96649.js => 5400ee883dfa8c43.js} (97%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5595eb6378e90997.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/55c8ff5e9c6d1e1d.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/56a8bf43ce752d47.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/575cc1c8ef6c4319.js rename litellm/proxy/_experimental/out/_next/static/chunks/{df6546cd8a44d3b3.js => 58461a445becf104.js} (70%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5855ff7033bd4d2e.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/591e3b6fbe6e4d4a.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5929da573d876909.js rename litellm/proxy/_experimental/out/_next/static/chunks/{3b3c0b070b14da06.js => 5963ae3163ecd9b6.js} (92%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/59945beef3825b62.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5ab3a0c9cca409f3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5c0770ecd9172a56.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5c0ed5c66b49ddbe.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5c823f037243a06f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/5f4170980a69ffa3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/62261c4511c6ef17.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/62a03e24dd5227b9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/635dd51f7caede88.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/65f709264734a9bf.js rename litellm/proxy/_experimental/out/_next/static/chunks/{e0e37187792c3754.js => 673d847ad9c91666.js} (88%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/67ae4f6900d6d2b5.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/68066e020262ced9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6a6f476ca1e20bb3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6b13d13478bbc3d8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6b2bc4046c4cbfc8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/6f6d3e604e986144.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/702ac50fd26100ab.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/715057b8e12f1cd9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7174130ddef406dd.js rename litellm/proxy/_experimental/out/_next/static/chunks/{7f9e9c54ac262de2.js => 726579f2940c2a2f.js} (99%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/72c3e48f096ce28f.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/74ce31aa0fb2adc9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/76dacbb0a43f577b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/77bf62fbc704d017.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7a2dc852f68481ea.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7b9ef931d44e410f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7bcc54a58176051b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7c797521435cb59c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7d82a1cebfdb679c.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7e3f5ce4b2a613d4.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/7f59802b710501d5.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/80079c810f42a5e5.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/80899acb7e1a7640.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/82426ffeda186236.js rename litellm/proxy/_experimental/out/_next/static/chunks/{8dfde809dc4ad794.js => 82bc4bb51160556f.js} (70%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/836c30941dbab57e.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8454375d75f636e8.js rename litellm/proxy/_experimental/out/_next/static/chunks/{184161a27f806cd4.js => 84dd260c7412819c.js} (70%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/877101abed503ab2.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/89274859d3d9d1de.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8a6de9a16d49b44f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8a76c69fc7bff9fe.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8ae157c8a223fdc3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8cc98e6cf29063c4.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8dc3b559a2e76f88.css delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/8dda507c226082ca.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/900e393d6a9d7b12.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/908828a91f602d8b.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/90c332d66ef5954b.js rename litellm/proxy/_experimental/out/_next/static/chunks/{dc8a270fee94ced6.js => 92c3c06057498511.js} (97%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/92e50c28acb1e29e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9474f2e878525bf7.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/94b1900e63940a2b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9606513e20bc3d4f.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/96616c4e8f4c2b15.js rename litellm/proxy/_experimental/out/_next/static/chunks/{67ddb5107368a659.js => 99109c78121231a0.js} (74%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/994a6506f0e0b01f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/9984a74a61012f00.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/99d715502d5069f4.js rename litellm/proxy/_experimental/out/_next/static/chunks/{ea0f22bd4b3393bd.js => 9d3522e82d255059.js} (71%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a02911bccf9acc36.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a09d5d7fd3464016.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a0b814e0f184a60a.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a3bf706d78352fd9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a6c7f80b3968f639.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a6effb44cc0c9028.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a7f104aa2cc7f3f0.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a85adee4198d5478.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/a89452659b6e1d90.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ac9e96d21c200b48.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/aca9c2b0aa46b0d7.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/acbeac1b0fde1fdf.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ad08830c666dfc68.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ad682fd0bc31a0da.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ada57dab6523afc4.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ae615fbed4c01ba7.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/af8668386d7005fe.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/b0286888a3293fd9.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/b02d6062e7602700.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/b4bd164f5553a31d.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ba42d2587315d00e.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/bdcb8f26948ea49f.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/be342ee9c36c54df.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c2bda16ec35d1a65.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c53c9c7afec96700.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c599cfb1e6aec71d.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c74f3813068add76.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c7c5a941c9e13136.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/c8eee6971ca36303.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cab8d46a8c32ec36.css delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cac89fc12fb6ef7e.js rename litellm/proxy/_experimental/out/_next/static/chunks/{6a167cef4b09b496.js => caf98722823e1b40.js} (72%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cb86c3ef30e0cf21.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cc1429f96b037302.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cd9e9161805efaa3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cdf98a03da656604.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ce9cf9f407f4b359.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/cecdaabafa264083.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d069df5baead6d90.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d104f25e5302e120.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d1ddfd3f3d5b2449.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d223c00dadf4b924.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d44e73d8ebac5747.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d512ca3b7169bef6.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d63044bdf28324dd.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d63f055c4b72844e.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d64d74932cb225a3.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/d9b0d7b22cad03c6.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/dad8b43751822f79.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/db928c0f158d84b3.js rename litellm/proxy/_experimental/out/_next/static/chunks/{1ae216e2208b329b.js => dc23b2a2258ffad1.js} (65%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e1da6931dfaabba1.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e1e3f652dbc5be03.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e775bbab37491d9c.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e884277804d6854d.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e8ed72789c2b42ff.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/e9de3f8db541361f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ea80fa81416a4ac8.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/eb659e9b99d203f2.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ecc42934cfd4bef0.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ed079ecd9e95349e.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ed4c3592cb02914b.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ee3a30e704bf7c47.js rename litellm/proxy/_experimental/out/_next/static/chunks/{2d471965761a22ff.js => ee5f9a39a526e423.js} (97%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ee7baaa6c1518142.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ee80c765f82c1de2.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/ee9b8424e31e26a3.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f059e45298abbf27.js rename litellm/proxy/_experimental/out/_next/static/chunks/{54e29148cb2f2582.js => f3e0cbc0e84e0a5d.js} (97%) create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f4eadf7003875fab.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f683569e573c506e.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f6cd2dbfa2452bc1.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f9133c1eea037690.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f9560c32394a893f.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f9b9defe307eeda9.js rename litellm/proxy/_experimental/out/_next/static/chunks/{9d6e5aad99b19216.js => f9c24d6e7ec43046.js} (98%) delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/f9c75b7b331b5bb7.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/fc4d54eb6afe7984.js create mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/fc873acd3d409c53.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/fce4815a81e5c63d.js delete mode 100644 litellm/proxy/_experimental/out/_next/static/chunks/fe4472f1d94e88f2.js rename litellm/proxy/_experimental/out/_next/static/chunks/{turbopack-901b35f89c1f6751.js => turbopack-d1b22f5e0bd58c57.js} (98%) create mode 100644 litellm/proxy/_experimental/out/assets/logos/akto.svg delete mode 100644 litellm/proxy/_experimental/out/chat.html create mode 100644 litellm/proxy/_experimental/out/chat/index.html create mode 100644 litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py create mode 100644 litellm/proxy/guardrails/guardrail_hooks/akto/akto.py create mode 100644 litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/__init__.py create mode 100644 litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py create mode 100644 litellm/responses/file_search/__init__.py create mode 100644 litellm/responses/file_search/emulated_handler.py create mode 100644 litellm/router_utils/health_state_cache.py create mode 100644 litellm/setup_wizard.py create mode 100644 litellm/types/proxy/control_plane_endpoints.py create mode 100644 litellm/types/proxy/guardrails/guardrail_hooks/akto.py create mode 100755 scripts/install.sh create mode 100644 tests/guardrails_tests/test_akto_guardrails.py delete mode 100644 tests/llm_responses_api_testing/test_manus_files_all_methods.py delete mode 100644 tests/llm_responses_api_testing/test_manus_responses_api.py delete mode 100644 tests/llm_translation/test_clarifai_completion.py delete mode 100644 tests/local_testing/adroit-crow-413218-bc47f303efc9.json delete mode 100644 tests/local_testing/test_router_init.py delete mode 100644 tests/local_testing/test_simple_shuffle.py delete mode 100644 tests/logging_callback_tests/test_azure_blob_storage.py delete mode 100644 tests/proxy_unit_tests/adroit-crow-413218-bc47f303efc9.json create mode 100644 tests/proxy_unit_tests/test_response_polling_pre_call_checks.py create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_cancel_expected_output.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_cancel_raw_response.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_cancel_request.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_create_expected_output.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_create_raw_response.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_create_request.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_list_raw_response.json create mode 100644 tests/test_litellm/expected_fine_tuning_api/azure_list_request.json create mode 100644 tests/test_litellm/integrations/test_prometheus_spend_logs_metadata.py create mode 100644 tests/test_litellm/integrations/websearch_interception/test_websearch_short_circuit.py create mode 100644 tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py create mode 100644 tests/test_litellm/llms/azure/test_azure_fine_tuning_api.py create mode 100644 tests/test_litellm/llms/mistral/__init__.py create mode 100644 tests/test_litellm/llms/mistral/ocr/__init__.py create mode 100644 tests/test_litellm/llms/mistral/ocr/test_mistral_ocr_transformation.py create mode 100644 tests/test_litellm/llms/test_file_search_responses.py create mode 100644 tests/test_litellm/llms/vertex_ai/gemini/test_context_circulation.py create mode 100644 tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/__init__.py create mode 100644 tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/test_count_tokens_location.py create mode 100644 tests/test_litellm/proxy/_experimental/mcp_server/test_mcp_hook_extra_headers.py create mode 100644 tests/test_litellm/proxy/anthropic_endpoints/test_claude_code_marketplace.py create mode 100644 tests/test_litellm/proxy/auth/test_password_hashing.py create mode 100644 tests/test_litellm/proxy/guardrails/test_deferred_guardrail_logging.py create mode 100644 tests/test_litellm/proxy/guardrails/test_mcp_jwt_signer.py create mode 100644 tests/test_litellm/proxy/management_helpers/test_audit_log_callbacks.py create mode 100644 tests/test_litellm/proxy/test_max_budget_env_var.py create mode 100644 tests/test_litellm/proxy/test_model_info_default_limits.py create mode 100644 tests/test_litellm/responses/test_responses_prompt_management.py create mode 100644 tests/test_litellm/router_utils/test_health_state_cache.py create mode 100644 tests/test_litellm/router_utils/test_router_health_check_routing.py create mode 100644 tests/test_litellm/test_project_alias_tracking.py create mode 100644 tests/test_litellm/test_router_order_fallback.py create mode 100644 tests/test_litellm/test_setup_wizard.py create mode 100644 ui/litellm-dashboard/public/assets/logos/akto.svg create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/budgets/useBudgets.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/common/queryKeysFactory.test.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/proxySettings/useProxySettings.ts create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsHeaderTabs.test.tsx create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/teams/components/TeamsTable/TeamsTable.test.tsx create mode 100644 ui/litellm-dashboard/src/app/onboarding/OnboardingForm.test.tsx create mode 100644 ui/litellm-dashboard/src/components/AIHub/AgentHubTableColumns.test.tsx create mode 100644 ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/EntityUsageExport/UsageExportHeader.test.tsx create mode 100644 ui/litellm-dashboard/src/components/GuardrailsMonitor/GuardrailConfig.test.tsx create mode 100644 ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Navbar/WorkerDropdown/WorkerDropdown.test.tsx create mode 100644 ui/litellm-dashboard/src/components/Navbar/WorkerDropdown/WorkerDropdown.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx create mode 100644 ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx create mode 100644 ui/litellm-dashboard/src/components/agent_management/AgentSelector.test.tsx create mode 100644 ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx create mode 100644 ui/litellm-dashboard/src/components/claude_code_plugins/add_plugin_form.test.tsx create mode 100644 ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts create mode 100644 ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/filter_helpers.test.ts create mode 100644 ui/litellm-dashboard/src/components/key_team_helpers/transform_key_info.test.ts create mode 100644 ui/litellm-dashboard/src/components/molecules/message_manager.test.ts create mode 100644 ui/litellm-dashboard/src/components/molecules/message_manager.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.test.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/ChatMessageBubble.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.test.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/FilePreviewCard.tsx create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.test.ts create mode 100644 ui/litellm-dashboard/src/components/playground/chat_ui/useChatHistory.ts create mode 100644 ui/litellm-dashboard/src/components/ui/AntDLoadingSpinner.test.tsx create mode 100644 ui/litellm-dashboard/src/components/ui/AntDLoadingSpinner.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/CollapsibleMessage.test.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/HistoryTree.test.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleMessageBlock.test.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SimpleToolCallBlock.test.tsx create mode 100644 ui/litellm-dashboard/src/hooks/useWorker.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 12e3cb1f6b6..d4943b59c9a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,7 +42,7 @@ commands: "pydantic==2.11.0" "mcp==1.25.0" "requests-mock>=1.12.1" \ "responses==0.25.7" "pytest-xdist==3.6.1" "pytest-timeout==2.2.0" \ "pytest-cov==5.0.0" "semantic_router==0.1.10" "fastapi-offline==1.7.3" \ - "a2a" + "a2a" "parameterized>=0.9.0" - setup_litellm_enterprise_pip - save_cache: paths: @@ -406,119 +406,6 @@ jobs: # Store test results - store_test_results: path: test-results - caching_unit_tests: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - resource_class: large - working_directory: ~/project - parallelism: 2 - - steps: - - checkout - - setup_google_dns - - run: - name: DNS lookup for Redis host - command: | - sudo apt-get update - sudo apt-get install -y dnsutils - dig redis-19899.c239.us-east-1-2.ec2.redns.redis-cloud.com +short - - run: - name: Show git commit hash - command: | - echo "Git commit hash: $CIRCLE_SHA1" - - - restore_cache: - keys: - - v2-caching-deps-{{ checksum ".circleci/requirements.txt" }} - - v2-caching-deps- - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip - python -m pip install -r .circleci/requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-asyncio==0.21.1" - pip install "pytest-cov==5.0.0" - pip install "mypy==1.18.2" - pip install "google-generativeai==0.3.2" - pip install "google-cloud-aiplatform==1.43.0" - pip install pyarrow - pip install "boto3==1.36.0" - pip install "aioboto3==13.4.0" - pip install langchain - pip install lunary==0.2.5 - pip install "azure-identity==1.16.1" - pip install "langfuse==2.59.7" - pip install "logfire==0.29.0" - pip install numpydoc - pip install traceloop-sdk==0.21.1 - pip install opentelemetry-api==1.25.0 - pip install opentelemetry-sdk==1.25.0 - pip install opentelemetry-exporter-otlp==1.25.0 - pip install openai==1.100.1 - pip install prisma==0.11.0 - pip install "detect_secrets==1.5.0" - pip install "httpx==0.24.1" - pip install "respx==0.22.0" - pip install fastapi - pip install "gunicorn==21.2.0" - pip install "anyio==4.2.0" - pip install "aiodynamo==23.10.1" - pip install "asyncio==3.4.3" - pip install "apscheduler==3.10.4" - pip install "PyGithub==1.59.1" - pip install argon2-cffi - pip install "pytest-mock==3.12.0" - pip install python-multipart - pip install google-cloud-aiplatform - pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.2" - pip install "diskcache==5.6.1" - pip install "Pillow==10.3.0" - pip install "jsonschema==4.22.0" - pip install "websockets==13.1.0" - pip install "pytest-xdist==3.6.1" - - setup_litellm_enterprise_pip - - save_cache: - paths: - - /home/circleci/.pyenv/versions - - /home/circleci/.local - key: v2-caching-deps-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Run prisma ./docker/entrypoint.sh - command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - - # Run pytest and generate JUnit XML report - - run: - name: Run tests - command: | - pwd - ls - mkdir -p test-results - - TEST_FILES=$(circleci tests glob "tests/local_testing/**/test_*.py") - - echo "$TEST_FILES" | circleci tests run \ - --split-by=timings \ - --verbose \ - --command="xargs python -m pytest \ - -v \ - --junitxml=test-results/junit.xml \ - --durations=5 \ - -k 'caching or cache'" - no_output_timeout: 15m - - # Store test results - - store_test_results: - path: test-results auth_ui_unit_tests: docker: - image: cimg/python:3.11 @@ -664,376 +551,6 @@ jobs: # Store test results - store_test_results: path: test-results - litellm_security_tests: - docker: - - image: cimg/python:3.13 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - - image: cimg/postgres:14.0 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: circle_test - resource_class: xlarge - working_directory: ~/project - environment: - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/circle_test" - steps: - - checkout - - setup_google_dns - - run: - name: Show git commit hash - command: | - echo "Git commit hash: $CIRCLE_SHA1" - - setup_remote_docker: - docker_layer_caching: true - - restore_cache: - keys: - - v3-litellm-uv-deps-{{ checksum "requirements.txt" }}-{{ checksum ".circleci/config.yml" }} - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip uv - uv pip install --system -r requirements.txt - pip install "pytest==7.3.1" "pytest-retry==1.6.3" "pytest-mock==3.12.0" \ - "pytest-asyncio==0.21.1" "pytest-cov==5.0.0" - - save_cache: - paths: - - ~/.local/lib - - ~/.local/bin - - ~/.cache/uv - key: v3-litellm-uv-deps-{{ checksum "requirements.txt" }}-{{ checksum ".circleci/config.yml" }} - - run: - name: Install dockerize - command: | - wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz - sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz - rm dockerize-linux-amd64-v0.6.1.tar.gz - - run: - name: Wait for PostgreSQL to be ready - command: dockerize -wait tcp://localhost:5432 -timeout 1m - - run: - name: Run Security Scans - command: | - chmod +x ci_cd/security_scans.sh - ./ci_cd/security_scans.sh - - run: - name: Run prisma ./docker/entrypoint.sh - command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - # Run pytest and generate JUnit XML report - - run: - name: Run tests - command: | - python -m pytest tests/proxy_security_tests -v -x --junitxml=test-results/junit.xml --durations=5 - no_output_timeout: 15m - # Store test results - - store_test_results: - path: test-results - # Split proxy unit tests into 3 jobs for faster execution and better debugging - # test_key_generate_prisma runs separately without parallel execution to avoid event loop issues with logging worker - litellm_proxy_unit_testing_key_generation: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: medium - steps: - - checkout - - setup_google_dns - - run: - name: Show git commit hash - command: | - echo "Git commit hash: $CIRCLE_SHA1" - - run: - name: Install PostgreSQL - command: | - sudo apt-get update - sudo apt-get install -y postgresql-14 postgresql-contrib-14 - - restore_cache: - keys: - - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip - python -m pip install -r .circleci/requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-asyncio==0.21.1" - pip install "pytest-cov==5.0.0" - pip install "pytest-timeout==2.2.0" - pip install "pytest-forked==1.6.0" - pip install "mypy==1.18.2" - pip install "google-generativeai==0.3.2" - pip install "google-cloud-aiplatform==1.43.0" - pip install "google-genai==1.22.0" - pip install pyarrow - pip install "boto3==1.36.0" - pip install "aioboto3==13.4.0" - pip install langchain - pip install lunary==0.2.5 - pip install "azure-identity==1.16.1" - pip install "langfuse==2.59.7" - pip install "logfire==0.29.0" - pip install numpydoc - pip install traceloop-sdk==0.21.1 - pip install opentelemetry-api==1.25.0 - pip install opentelemetry-sdk==1.25.0 - pip install opentelemetry-exporter-otlp==1.25.0 - pip install openai==1.100.1 - pip install prisma==0.11.0 - pip install "detect_secrets==1.5.0" - pip install "httpx==0.24.1" - pip install "respx==0.22.0" - pip install fastapi - pip install "gunicorn==21.2.0" - pip install "anyio==4.2.0" - pip install "aiodynamo==23.10.1" - pip install "asyncio==3.4.3" - pip install "apscheduler==3.10.4" - pip install "PyGithub==1.59.1" - pip install argon2-cffi - pip install "pytest-mock==3.12.0" - pip install python-multipart - pip install google-cloud-aiplatform - pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.2" - pip install "diskcache==5.6.1" - pip install "Pillow==10.3.0" - pip install "jsonschema==4.22.0" - pip install "pytest-postgresql==7.0.1" - pip install "fakeredis==2.28.1" - - setup_litellm_enterprise_pip - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Run prisma ./docker/entrypoint.sh - command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - - run: - name: Run key generation tests (no parallel execution to avoid event loop issues) - command: | - pwd - ls - # Run without -n flag to avoid pytest-xdist event loop conflicts with logging worker - python -m pytest tests/proxy_unit_tests/test_key_generate_prisma.py --cov=litellm --cov-report=xml --junitxml=test-results/junit-key-generation.xml --durations=10 --timeout=300 -vv --log-cli-level=INFO - no_output_timeout: 15m - - run: - name: Rename the coverage files - command: | - mv coverage.xml litellm_proxy_unit_tests_key_generation_coverage.xml - mv .coverage litellm_proxy_unit_tests_key_generation_coverage - - store_test_results: - path: test-results - - persist_to_workspace: - root: . - paths: - - litellm_proxy_unit_tests_key_generation_coverage.xml - - litellm_proxy_unit_tests_key_generation_coverage - litellm_proxy_unit_testing_part1: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: xlarge - steps: - - checkout - - setup_google_dns - - run: - name: Show git commit hash - command: | - echo "Git commit hash: $CIRCLE_SHA1" - - run: - name: Install PostgreSQL - command: | - sudo apt-get update - sudo apt-get install -y postgresql-14 postgresql-contrib-14 - - restore_cache: - keys: - - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip - python -m pip install -r .circleci/requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-asyncio==0.21.1" - pip install "pytest-cov==5.0.0" - pip install "pytest-timeout==2.2.0" - pip install "pytest-forked==1.6.0" - pip install "mypy==1.18.2" - pip install "google-generativeai==0.3.2" - pip install "google-cloud-aiplatform==1.43.0" - pip install "google-genai==1.22.0" - pip install pyarrow - pip install "boto3==1.36.0" - pip install "aioboto3==13.4.0" - pip install langchain - pip install lunary==0.2.5 - pip install "azure-identity==1.16.1" - pip install "langfuse==2.59.7" - pip install "logfire==0.29.0" - pip install numpydoc - pip install traceloop-sdk==0.21.1 - pip install opentelemetry-api==1.25.0 - pip install opentelemetry-sdk==1.25.0 - pip install opentelemetry-exporter-otlp==1.25.0 - pip install openai==1.100.1 - pip install prisma==0.11.0 - pip install "detect_secrets==1.5.0" - pip install "httpx==0.24.1" - pip install "respx==0.22.0" - pip install fastapi - pip install "gunicorn==21.2.0" - pip install "anyio==4.2.0" - pip install "aiodynamo==23.10.1" - pip install "asyncio==3.4.3" - pip install "apscheduler==3.10.4" - pip install "PyGithub==1.59.1" - pip install argon2-cffi - pip install "pytest-mock==3.12.0" - pip install python-multipart - pip install google-cloud-aiplatform - pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.2" - pip install "diskcache==5.6.1" - pip install "Pillow==10.3.0" - pip install "jsonschema==4.22.0" - pip install "pytest-postgresql==7.0.1" - pip install "fakeredis==2.28.1" - pip install "pytest-xdist==3.6.1" - - setup_litellm_enterprise_pip - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Run prisma ./docker/entrypoint.sh - command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - - run: - name: Run proxy unit tests (part 1 - auth checks) - command: | - pwd - ls - python -m pytest tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py --junitxml=test-results/junit-part1.xml --durations=10 -n 8 --timeout=300 -v - no_output_timeout: 15m - - store_test_results: - path: test-results - litellm_proxy_unit_testing_part2: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: xlarge - steps: - - checkout - - setup_google_dns - - run: - name: Show git commit hash - command: | - echo "Git commit hash: $CIRCLE_SHA1" - - run: - name: Install PostgreSQL - command: | - sudo apt-get update - sudo apt-get install -y postgresql-14 postgresql-contrib-14 - - restore_cache: - keys: - - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Install Dependencies - command: | - python -m pip install --upgrade pip - python -m pip install -r .circleci/requirements.txt - pip install "pytest==7.3.1" - pip install "pytest-retry==1.6.3" - pip install "pytest-asyncio==0.21.1" - pip install "pytest-cov==5.0.0" - pip install "pytest-timeout==2.2.0" - pip install "pytest-forked==1.6.0" - pip install "mypy==1.18.2" - pip install "google-generativeai==0.3.2" - pip install "google-cloud-aiplatform==1.43.0" - pip install "google-genai==1.22.0" - pip install pyarrow - pip install "boto3==1.36.0" - pip install "aioboto3==13.4.0" - pip install langchain - pip install lunary==0.2.5 - pip install "azure-identity==1.16.1" - pip install "langfuse==2.59.7" - pip install "logfire==0.29.0" - pip install numpydoc - pip install traceloop-sdk==0.21.1 - pip install opentelemetry-api==1.25.0 - pip install opentelemetry-sdk==1.25.0 - pip install opentelemetry-exporter-otlp==1.25.0 - pip install openai==1.100.1 - pip install prisma==0.11.0 - pip install "detect_secrets==1.5.0" - pip install "httpx==0.24.1" - pip install "respx==0.22.0" - pip install fastapi - pip install "gunicorn==21.2.0" - pip install "anyio==4.2.0" - pip install "aiodynamo==23.10.1" - pip install "asyncio==3.4.3" - pip install "apscheduler==3.10.4" - pip install "PyGithub==1.59.1" - pip install argon2-cffi - pip install "pytest-mock==3.12.0" - pip install python-multipart - pip install google-cloud-aiplatform - pip install prometheus-client==0.20.0 - pip install "pydantic==2.10.2" - pip install "diskcache==5.6.1" - pip install "Pillow==10.3.0" - pip install "jsonschema==4.22.0" - pip install "pytest-postgresql==7.0.1" - pip install "fakeredis==2.28.1" - pip install "pytest-xdist==3.6.1" - - setup_litellm_enterprise_pip - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - run: - name: Run prisma ./docker/entrypoint.sh - command: | - set +e - chmod +x docker/entrypoint.sh - ./docker/entrypoint.sh - set -e - - run: - name: Run proxy unit tests (part 2 - remaining tests) - command: | - pwd - ls - python -m pytest tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py --junitxml=test-results/junit-part2.xml --durations=10 -n 8 --timeout=300 -v - no_output_timeout: 15m - - store_test_results: - path: test-results litellm_assistants_api_testing: # Runs all tests with the "assistants" keyword docker: - image: cimg/python:3.13.1 @@ -1115,7 +632,7 @@ jobs: for dir in "${IGNORE_DIRS[@]}"; do IGNORE_ARGS="$IGNORE_ARGS --ignore=$dir" done - python -m pytest -v tests/llm_translation $IGNORE_ARGS --junitxml=test-results/junit.xml --durations=20 -n 8 --timeout=120 --timeout_method=thread + python -m pytest -v tests/llm_translation $IGNORE_ARGS --junitxml=test-results/junit.xml --durations=20 -n 8 --timeout=120 --timeout_method=thread --retries 2 --retry-delay 5 no_output_timeout: 15m # Store test results @@ -1331,7 +848,7 @@ jobs: command: | pwd ls - python -m pytest -vv tests/unified_google_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 + python -m pytest -vv tests/unified_google_tests --cov=litellm --cov-report=xml -x -s -v --junitxml=test-results/junit.xml --durations=5 --retries 3 --retry-delay 5 no_output_timeout: 15m - run: name: Rename the coverage files @@ -1507,101 +1024,6 @@ jobs: no_output_timeout: 15m - store_test_results: path: test-results - litellm_mapped_tests_llms: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: large - steps: - - setup_litellm_test_deps - - run: - name: Run LLM provider tests - command: | - python -m pytest tests/test_litellm/llms --junitxml=test-results/junit-llms.xml --durations=10 -n 4 --maxfail=5 --timeout=300 -vv --log-cli-level=WARNING - no_output_timeout: 15m - - store_test_results: - path: test-results - litellm_mapped_tests_core: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: large - steps: - - setup_litellm_test_deps - - run: - name: Run core tests - command: | - python -m pytest tests/test_litellm --ignore=tests/test_litellm/proxy --ignore=tests/test_litellm/llms --ignore=tests/test_litellm/integrations --ignore=tests/test_litellm/litellm_core_utils --ignore=tests/test_litellm/experimental_mcp_client --junitxml=test-results/junit-core.xml --durations=10 -n 4 --maxfail=5 --timeout=300 -vv --log-cli-level=WARNING - no_output_timeout: 15m - - store_test_results: - path: test-results - litellm_mapped_tests_litellm_core_utils: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: large - steps: - - setup_litellm_test_deps - - run: - name: Run litellm_core_utils tests - command: | - python -m pytest tests/test_litellm/litellm_core_utils --junitxml=test-results/junit-litellm-core-utils.xml --durations=10 -n 4 --maxfail=5 --timeout=300 -vv --log-cli-level=WARNING - no_output_timeout: 15m - - store_test_results: - path: test-results - litellm_mapped_tests_mcps: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: medium - steps: - - setup_litellm_test_deps - - run: - name: Run MCP client tests - command: | - python -m pytest tests/test_litellm/experimental_mcp_client --cov=litellm --cov-report=xml --junitxml=test-results/junit-mcps.xml --durations=10 -n 2 --maxfail=5 --timeout=300 -vv --log-cli-level=WARNING - no_output_timeout: 15m - - run: - name: Rename the coverage files - command: | - mv coverage.xml litellm_mcps_tests_coverage.xml - mv .coverage litellm_mcps_tests_coverage - - store_test_results: - path: test-results - - persist_to_workspace: - root: . - paths: - - litellm_mcps_tests_coverage.xml - - litellm_mcps_tests_coverage - litellm_mapped_tests_integrations: - docker: - - image: cimg/python:3.11 - auth: - username: ${DOCKERHUB_USERNAME} - password: ${DOCKERHUB_PASSWORD} - working_directory: ~/project - resource_class: large - steps: - - setup_litellm_test_deps - - run: - name: Run integrations tests - command: | - python -m pytest tests/test_litellm/integrations --junitxml=test-results/junit-integrations.xml --durations=10 -n 4 --maxfail=5 --timeout=300 -vv --log-cli-level=WARNING - no_output_timeout: 15m - - store_test_results: - path: test-results litellm_mapped_enterprise_tests: docker: - image: cimg/python:3.11 @@ -2137,6 +1559,25 @@ jobs: pip install "pytest-asyncio==0.21.1" pip install aiohttp pip install apscheduler + - run: + name: Install dockerize + command: | + sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz + sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz + sudo rm dockerize-linux-amd64-v0.6.1.tar.gz + - run: + name: Start PostgreSQL Database + command: | + docker run -d \ + --name postgres-db \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=litellm_test \ + -p 5432:5432 \ + postgres:14 + - run: + name: Wait for PostgreSQL to be ready + command: dockerize -wait tcp://localhost:5432 -timeout 1m - attach_workspace: at: ~/project - run: @@ -2145,29 +1586,41 @@ jobs: zstd -d litellm-docker-database.tar.zst --stdout | docker load docker images | grep litellm-docker-database - run: - name: Run Docker container + name: Seed database with real schema + command: | + docker run -d \ + -p 4001:4000 \ + -e DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/litellm_test" \ + -e LITELLM_MASTER_KEY="sk-1234" \ + --name schema-seed \ + --add-host=host.docker.internal:host-gateway \ + -v $(pwd)/litellm/proxy/example_config_yaml/simple_config.yaml:/app/config.yaml \ + litellm-docker-database:ci \ + --config /app/config.yaml \ + --port 4000 \ + --use_prisma_db_push + - run: + name: Wait for schema seed to complete + command: dockerize -wait http://localhost:4001 -timeout 5m + - run: + name: Stop schema seed container + command: docker stop schema-seed && docker rm schema-seed + - run: + name: Run Docker container with bad schema and disabled updates command: | docker run -d \ -p 4000:4000 \ - -e DATABASE_URL=$PROXY_DATABASE_URL \ + -e DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/litellm_test" \ -e DEFAULT_NUM_WORKERS_LITELLM_PROXY=1 \ -e DISABLE_SCHEMA_UPDATE="True" \ + --name my-app \ + --add-host=host.docker.internal:host-gateway \ -v $(pwd)/litellm/proxy/example_config_yaml/bad_schema.prisma:/app/schema.prisma \ -v $(pwd)/litellm/proxy/example_config_yaml/bad_schema.prisma:/app/litellm/proxy/schema.prisma \ -v $(pwd)/litellm/proxy/example_config_yaml/disable_schema_update.yaml:/app/config.yaml \ - --name my-app \ litellm-docker-database:ci \ --config /app/config.yaml \ --port 4000 - - run: - name: Install curl and dockerize - command: | - sudo apt-get update - sudo apt-get install -y curl - sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz - sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz - sudo rm dockerize-linux-amd64-v0.6.1.tar.gz - - run: name: Wait for container to be ready command: dockerize -wait http://localhost:4000 -timeout 1m @@ -2575,9 +2028,6 @@ jobs: -e OPENAI_API_KEY=$OPENAI_API_KEY \ -e LITELLM_LICENSE=$LITELLM_LICENSE \ -e OTEL_EXPORTER="in_memory" \ - -e APORIA_API_BASE_2=$APORIA_API_BASE_2 \ - -e APORIA_API_KEY_2=$APORIA_API_KEY_2 \ - -e APORIA_API_BASE_1=$APORIA_API_BASE_1 \ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ -e DEFAULT_NUM_WORKERS_LITELLM_PROXY=1 \ @@ -2585,7 +2035,6 @@ jobs: -e DD_API_KEY=$DD_API_KEY \ -e DD_SITE=$DD_SITE \ -e AWS_REGION_NAME=$AWS_REGION_NAME \ - -e APORIA_API_KEY_1=$APORIA_API_KEY_1 \ -e COHERE_API_KEY=$COHERE_API_KEY \ -e GCS_FLUSH_INTERVAL="1" \ --add-host host.docker.internal:host-gateway \ @@ -3061,6 +2510,20 @@ jobs: name: Build Docker image command: | docker build -t my-app:latest -f docker/build_from_pip/Dockerfile.build_from_pip . + - run: + name: Start PostgreSQL Database + command: | + docker run -d \ + --name postgres-db \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=circle_test \ + -p 5432:5432 \ + postgres:14 + - run: + name: Wait for PostgreSQL to be ready + command: | + timeout 60s bash -c 'until docker exec postgres-db pg_isready -U postgres -d circle_test; do sleep 2; done' - run: name: Run Docker container # intentionally give bad redis credentials here @@ -3068,7 +2531,7 @@ jobs: command: | docker run -d \ -p 4000:4000 \ - -e DATABASE_URL=$PROXY_DATABASE_URL \ + -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/circle_test \ -e REDIS_HOST=$REDIS_HOST \ -e REDIS_PASSWORD=$REDIS_PASSWORD \ -e REDIS_PORT=$REDIS_PORT \ @@ -3076,18 +2539,15 @@ jobs: -e OPENAI_API_KEY=$OPENAI_API_KEY \ -e LITELLM_LICENSE=$LITELLM_LICENSE \ -e OTEL_EXPORTER="in_memory" \ - -e APORIA_API_BASE_2=$APORIA_API_BASE_2 \ - -e APORIA_API_KEY_2=$APORIA_API_KEY_2 \ - -e APORIA_API_BASE_1=$APORIA_API_BASE_1 \ -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID \ -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY \ -e AWS_REGION_NAME=$AWS_REGION_NAME \ - -e APORIA_API_KEY_1=$APORIA_API_KEY_1 \ -e COHERE_API_KEY=$COHERE_API_KEY \ -e USE_DDTRACE=True \ -e DD_API_KEY=$DD_API_KEY \ -e DD_SITE=$DD_SITE \ -e GCS_FLUSH_INTERVAL="1" \ + --add-host host.docker.internal:host-gateway \ --name my-app \ -v $(pwd)/docker/build_from_pip/litellm_config.yaml:/app/config.yaml \ my-app:latest \ @@ -3118,8 +2578,11 @@ jobs: - run: name: Stop and remove first container command: | - docker stop my-app - docker rm my-app + docker stop my-app || true + docker rm my-app || true + docker stop postgres-db || true + docker rm postgres-db || true + when: always proxy_pass_through_endpoint_tests: machine: image: ubuntu-2204:2023.10.1 @@ -3543,93 +3006,6 @@ jobs: - codecov/upload: file: ./coverage.xml - publish_to_pypi: - docker: - - image: cimg/python:3.8 - working_directory: ~/project - - environment: - TWINE_USERNAME: __token__ - - steps: - - checkout - - - run: - name: Copy model_prices_and_context_window File to model_prices_and_context_window_backup - command: | - cp model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json - - - run: - name: Checkout code - command: git checkout $CIRCLE_SHA1 - - # Check if setup.py is modified and publish to PyPI - - run: - name: PyPI publish - command: | - echo "Install TOML package." - python -m pip install toml - VERSION=$(python -c "import toml; print(toml.load('pyproject.toml')['tool']['poetry']['version'])") - PACKAGE_NAME=$(python -c "import toml; print(toml.load('pyproject.toml')['tool']['poetry']['name'])") - if ! pip show -v $PACKAGE_NAME | grep -q "Version: ${VERSION}"; then - echo "pyproject.toml modified" - echo -e "[pypi]\nusername = $PYPI_PUBLISH_USERNAME\npassword = $PYPI_PUBLISH_PASSWORD" > ~/.pypirc - python -m pip install --upgrade pip - pip install build - pip install wheel - pip install --upgrade twine setuptools - rm -rf build dist - - echo "Building package" - python -m build - - echo "Twine upload to dist" - echo "Contents of dist directory:" - ls dist/ - twine upload --verbose dist/* - else - echo "Version ${VERSION} of package is already published on PyPI." - - # Check if corresponding Docker nightly image exists - NIGHTLY_TAG="v${VERSION}-nightly" - echo "Checking for Docker nightly image: litellm/litellm:${NIGHTLY_TAG}" - - # Check Docker Hub for the nightly image - if curl -s "https://hub.docker.com/v2/repositories/litellm/litellm/tags/${NIGHTLY_TAG}" | grep -q "name"; then - echo "Docker nightly image ${NIGHTLY_TAG} exists. This release was already completed successfully." - echo "Skipping PyPI publish and continuing to ensure Docker images are up to date." - circleci step halt - else - echo "ERROR: PyPI package ${VERSION} exists but Docker nightly image ${NIGHTLY_TAG} does not exist!" - echo "This indicates an incomplete release. Please investigate." - exit 1 - fi - fi - - run: - name: Trigger Github Action for new Docker Container + Trigger Load Testing - command: | - echo "Install TOML package." - python3 -m pip install toml - VERSION=$(python3 -c "import toml; print(toml.load('pyproject.toml')['tool']['poetry']['version'])") - echo "LiteLLM Version ${VERSION}" - - # Determine which branch to use for Docker build - if [[ "$CIRCLE_BRANCH" =~ ^litellm_release_day_.* ]]; then - BUILD_BRANCH="$CIRCLE_BRANCH" - echo "Using release branch: $BUILD_BRANCH" - else - BUILD_BRANCH="main" - echo "Using default branch: $BUILD_BRANCH" - fi - - curl -X POST \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: Bearer $GITHUB_TOKEN" \ - "https://api.github.com/repos/BerriAI/litellm/actions/workflows/ghcr_deploy.yml/dispatches" \ - -d "{\"ref\":\"${BUILD_BRANCH}\", \"inputs\":{\"tag\":\"v${VERSION}-nightly\", \"commit_hash\":\"$CIRCLE_SHA1\"}}" - echo "triggering load testing server for version ${VERSION} and commit ${CIRCLE_SHA1}" - curl -X POST "https://proxyloadtester-production.up.railway.app/start/load/test?version=${VERSION}&commit_hash=${CIRCLE_SHA1}&release_type=nightly" - publish_proxy_extras: docker: - image: cimg/python:3.8 @@ -3942,37 +3318,39 @@ jobs: - setup_google_dns - attach_workspace: at: ~/project + - run: + name: Install dockerize + command: | + sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz + sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz + sudo rm dockerize-linux-amd64-v0.6.1.tar.gz + - run: + name: Start PostgreSQL Database + command: | + docker run -d \ + --name postgres-db \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=litellm_schema_sync \ + -p 5432:5432 \ + postgres:14 + - run: + name: Wait for PostgreSQL to be ready + command: dockerize -wait tcp://localhost:5432 -timeout 1m - run: name: Load Docker Database Image command: | zstd -d litellm-docker-database.tar.zst --stdout | docker load docker images | grep litellm-docker-database - run: - name: Install Neon CLI + name: Run schema sync via prisma db push command: | - npm i -g neonctl - - run: - name: Install curl and dockerize - command: | - sudo apt-get update - sudo apt-get install -y curl - sudo wget https://github.com/jwilder/dockerize/releases/download/v0.6.1/dockerize-linux-amd64-v0.6.1.tar.gz - sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.1.tar.gz - sudo rm dockerize-linux-amd64-v0.6.1.tar.gz - - run: - name: Sync schema on base e2e database - command: | - BASE_DATABASE_URL=$(neon connection-string \ - --project-id $NEON_PROJECT_ID \ - --api-key $NEON_API_KEY \ - --branch br-fancy-paper-ad1olsb3 \ - --database-name yuneng-trial-db \ - --role neondb_owner) docker run -d \ -p 4000:4000 \ - -e DATABASE_URL=$BASE_DATABASE_URL \ + -e DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/litellm_schema_sync" \ -e LITELLM_MASTER_KEY="sk-1234" \ --name schema-sync \ + --add-host=host.docker.internal:host-gateway \ -v $(pwd)/litellm/proxy/example_config_yaml/simple_config.yaml:/app/config.yaml \ litellm-docker-database:ci \ --config /app/config.yaml \ @@ -4089,34 +3467,14 @@ workflows: only: - main - /litellm_.*/ - - caching_unit_tests: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_proxy_unit_testing_key_generation: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_proxy_unit_testing_part1: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_proxy_unit_testing_part2: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_security_tests: - filters: - branches: - only: - main - /litellm_.*/ - litellm_assistants_api_testing: @@ -4170,7 +3528,6 @@ workflows: - main - /litellm_.*/ - prisma_schema_sync: - context: e2e_ui_tests requires: - build_docker_database_image filters: @@ -4178,32 +3535,32 @@ workflows: only: - main - /litellm_.*/ - - e2e_ui_testing: - name: e2e_ui_testing_chromium - browser: chromium - context: e2e_ui_tests - requires: - - ui_build - - build_docker_database_image - - prisma_schema_sync - filters: - branches: - only: - - main - - /litellm_.*/ - - e2e_ui_testing: - name: e2e_ui_testing_firefox - browser: firefox - context: e2e_ui_tests - requires: - - ui_build - - build_docker_database_image - - prisma_schema_sync - filters: - branches: - only: - - main - - /litellm_.*/ + # - e2e_ui_testing: # migrate to dynamic db - currently requires neon cli + # name: e2e_ui_testing_chromium + # browser: chromium + # context: e2e_ui_tests + # requires: + # - ui_build + # - build_docker_database_image + # - prisma_schema_sync + # filters: + # branches: + # only: + # - main + # - /litellm_.*/ + # - e2e_ui_testing: + # name: e2e_ui_testing_firefox + # browser: firefox + # context: e2e_ui_tests + # requires: + # - ui_build + # - build_docker_database_image + # - prisma_schema_sync + # filters: + # branches: + # only: + # - main + # - /litellm_.*/ - build_and_test: requires: - build_docker_database_image @@ -4352,34 +3709,14 @@ workflows: only: - main - /litellm_.*/ - - litellm_mapped_tests_llms: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_mapped_tests_core: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_mapped_tests_mcps: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_mapped_tests_integrations: - filters: - branches: - only: - main - /litellm_.*/ - - litellm_mapped_tests_litellm_core_utils: - filters: - branches: - only: - main - /litellm_.*/ - batches_testing: @@ -4429,11 +3766,6 @@ workflows: - search_testing - litellm_mapped_tests_proxy_part1 - litellm_mapped_tests_proxy_part2 - - litellm_mapped_tests_llms - - litellm_mapped_tests_core - - litellm_mapped_tests_mcps - - litellm_mapped_tests_integrations - - litellm_mapped_tests_litellm_core_utils - litellm_mapped_enterprise_tests - batches_testing - litellm_utils_testing @@ -4441,8 +3773,6 @@ workflows: - image_gen_testing - logging_testing - audio_testing - - caching_unit_tests - - litellm_proxy_unit_testing_key_generation - langfuse_logging_unit_tests - local_testing_part1 - local_testing_part2 @@ -4489,59 +3819,3 @@ workflows: only: - main - /litellm_release_day_.*/ - - publish_to_pypi: - requires: - - mypy_linting - - semgrep - - local_testing_part1 - - local_testing_part2 - - build_and_test - - e2e_openai_endpoints - - test_bad_database_url - - llm_translation_testing - - realtime_translation_testing - - mcp_testing - - agent_testing - - google_generate_content_endpoint_testing - - llm_responses_api_testing - - ocr_testing - - search_testing - - litellm_mapped_tests_proxy_part1 - - litellm_mapped_tests_proxy_part2 - - litellm_mapped_tests_llms - - litellm_mapped_tests_core - - litellm_mapped_tests_mcps - - litellm_mapped_tests_integrations - - litellm_mapped_tests_litellm_core_utils - - litellm_mapped_enterprise_tests - - batches_testing - - litellm_utils_testing - - pass_through_unit_testing - - image_gen_testing - - logging_testing - - audio_testing - - litellm_router_testing - - litellm_router_unit_testing - - caching_unit_tests - - langfuse_logging_unit_tests - - litellm_assistants_api_testing - - auth_ui_unit_tests - - ui_unit_tests - - db_migration_disable_update_check - - e2e_ui_testing_chromium - - e2e_ui_testing_firefox - - litellm_proxy_unit_testing_key_generation - - litellm_proxy_unit_testing_part1 - - litellm_proxy_unit_testing_part2 - - litellm_security_tests - - installing_litellm_on_python - - installing_litellm_on_python_3_13 - - proxy_logging_guardrails_model_info_tests - - proxy_spend_accuracy_tests - - proxy_multi_instance_tests - - proxy_store_model_in_db_tests - - proxy_build_from_pip_tests - - proxy_pass_through_endpoint_tests - - check_code_and_doc_quality - - publish_proxy_extras - - guardrails_testing diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 8c1d85f96e0..00000000000 --- a/.claude/settings.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(git show:*)", - "Bash(git worktree add:*)", - "Read(//Users/krrishdholakia/Documents/litellm/**)", - "Read(//Users/krrishdholakia/Documents/litellm-claude-code-guardrails/litellm/types/**)", - "Read(//Users/krrishdholakia/Documents/litellm-claude-code-guardrails/**)", - "Read(//Users/krrishdholakia/Documents/litellm-claude-code-guardrails/litellm/**)", - "Bash(python:*)", - "Bash(python -c \"\nimport sys; sys.path.insert\\(0, ''.''\\)\nfrom litellm.proxy.guardrails.guardrail_hooks.claude_code.guardrail import ClaudeCodeGuardrail, HOSTED_TOOL_PREFIXES\nprint\\(''HOSTED_TOOL_PREFIXES:'', HOSTED_TOOL_PREFIXES\\)\nprint\\(''ClaudeCodeGuardrail imported OK''\\)\n\")", - "Read(//Users/krrishdholakia/Documents/litellm-mcp-jwt-groups/litellm/proxy/**)", - "Read(//Users/krrishdholakia/Documents/litellm-mcp-jwt-groups/**)", - "Bash(poetry run pytest:*)", - "Bash(git add:*)", - "Bash(git commit:*)", - "Bash(poetry run python:*)", - "Bash(poetry run pip:*)", - "Bash(git reset:*)", - "Bash(git cherry-pick:*)", - "Bash(git checkout:*)", - "Read(//Users/krrishdholakia/Documents/litellm/litellm/proxy/guardrails/guardrail_hooks/**)", - "Read(//Users/krrishdholakia/Documents/**)", - "Bash(git -C /Users/krrishdholakia/Documents/litellm-mcp-user-permissions worktree list)", - "Bash(ls:*)" - ], - "additionalDirectories": [ - "/Users/krrishdholakia/Documents/litellm-mcp-group-plan/plan", - "/Users/krrishdholakia/Documents/litellm-claude-code-guardrails/litellm/proxy/guardrails/guardrail_hooks/claude_code", - "/Users/krrishdholakia/Documents/litellm-claude-code-guardrails/litellm/types", - "/Users/krrishdholakia/Documents/litellm-claude-code-guardrails", - "/Users/krrishdholakia/Documents/litellm-mcp-jwt-groups/litellm/proxy", - "/Users/krrishdholakia/Documents/litellm-mcp-jwt-groups/tests/test_litellm/proxy/auth" - ] - } -} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4744ab048c7..cbf380bac01 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ blank_issues_enabled: true contact_links: - name: Schedule Demo - url: https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions + url: https://enterprise.litellm.ai/demo about: Speak directly with Krrish and Ishaan, the founders, to discuss issues, share feedback, or explore improvements for LiteLLM - name: Discord url: https://discord.com/invite/wuPM9dRgDw diff --git a/.github/actions/helm-oci-chart-releaser/action.yml b/.github/actions/helm-oci-chart-releaser/action.yml index 1823e262832..454c591d436 100644 --- a/.github/actions/helm-oci-chart-releaser/action.yml +++ b/.github/actions/helm-oci-chart-releaser/action.yml @@ -41,32 +41,54 @@ runs: using: composite steps: - name: Helm | Setup - uses: azure/setup-helm@v4 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: v3.20.0 - name: Helm | Login shell: bash - run: echo ${{ inputs.registry_password }} | helm registry login -u ${{ inputs.registry_username }} --password-stdin ${{ inputs.registry }} + env: + REGISTRY_PASSWORD: ${{ inputs.registry_password }} + REGISTRY_USERNAME: ${{ inputs.registry_username }} + REGISTRY: ${{ inputs.registry }} + run: echo "$REGISTRY_PASSWORD" | helm registry login -u "$REGISTRY_USERNAME" --password-stdin "$REGISTRY" - name: Helm | Dependency if: inputs.update_dependencies == 'true' shell: bash - run: helm dependency update ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }} + env: + CHART_PATH: ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }} + run: helm dependency update "$CHART_PATH" - name: Helm | Package shell: bash - run: helm package ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }} --version ${{ inputs.tag }} --app-version ${{ inputs.app_version }} + env: + CHART_PATH: ${{ inputs.path == null && format('{0}/{1}', 'charts', inputs.name) || inputs.path }} + TAG: ${{ inputs.tag }} + APP_VERSION: ${{ inputs.app_version }} + run: helm package "$CHART_PATH" --version "$TAG" --app-version "$APP_VERSION" - name: Helm | Push shell: bash - run: helm push ${{ inputs.name }}-${{ inputs.tag }}.tgz oci://${{ inputs.registry }}/${{ inputs.repository }} + env: + NAME: ${{ inputs.name }} + TAG: ${{ inputs.tag }} + REGISTRY: ${{ inputs.registry }} + REPOSITORY: ${{ inputs.repository }} + run: helm push "${NAME}-${TAG}.tgz" "oci://${REGISTRY}/${REPOSITORY}" - name: Helm | Logout shell: bash - run: helm registry logout ${{ inputs.registry }} + env: + REGISTRY: ${{ inputs.registry }} + run: helm registry logout "$REGISTRY" - name: Helm | Output id: output shell: bash - run: echo "image=${{ inputs.registry }}/${{ inputs.repository }}/${{ inputs.name }}:${{ inputs.tag }}" >> $GITHUB_OUTPUT + env: + REGISTRY: ${{ inputs.registry }} + REPOSITORY: ${{ inputs.repository }} + NAME: ${{ inputs.name }} + TAG: ${{ inputs.tag }} + run: echo "image=${REGISTRY}/${REPOSITORY}/${NAME}:${TAG}" >> $GITHUB_OUTPUT diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 20807685e12..36d70c1d746 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -1,22 +1,21 @@ name: "LiteLLM CodeQL config" -# Use security-extended suite instead of security-and-quality to avoid -# result sets > 2 GiB on this codebase that cause fatal OOM failures. queries: - - uses: security-extended + - uses: security-and-quality -# These two queries are security queries included in security-extended that -# individually produce result sets > 2 GiB on this codebase, causing fatal -# OOM failures. Exclude them as a safety net until CI confirms they no longer -# OOM; drop these exclusions in a follow-up once verified. +# Known OOM queries on large Python codebases: +# CodeQL builds a full data flow graph in memory. These two queries trace +# sensitive data through every log call / regex pattern, causing combinatorial +# path explosion on codebases with extensive logging like LiteLLM (>2 GiB +# result sets). This is a known CodeQL scaling limitation, not a code issue. +# Re-test periodically as CodeQL improves or the codebase refactors logging. query-filters: - exclude: - id: py/clear-text-logging-sensitive-data # CWE-312 — > 2 GiB result set + id: py/clear-text-logging-sensitive-data # CWE-312 - exclude: - id: py/polynomial-redos # CWE-730 — > 2 GiB result set + id: py/polynomial-redos # CWE-730 paths-ignore: - tests - docs - "**/*.md" - - litellm/proxy/_experimental/out diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 58e7cfe10da..c49882a8d62 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -4,6 +4,9 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 + semver-major-days: 14 groups: github-actions: patterns: diff --git a/.github/workflows/_test-unit-base.yml b/.github/workflows/_test-unit-base.yml new file mode 100644 index 00000000000..f1ae30e67d7 --- /dev/null +++ b/.github/workflows/_test-unit-base.yml @@ -0,0 +1,96 @@ +name: _Unit Test Base (Reusable) + +on: + workflow_call: + inputs: + test-path: + description: "Pytest path(s) to run" + required: true + type: string + workers: + description: "Number of pytest-xdist workers" + required: false + type: number + default: 2 + reruns: + description: "Number of reruns for flaky tests" + required: false + type: number + default: 2 + timeout-minutes: + description: "Job timeout in minutes" + required: false + type: number + default: 20 + max-failures: + description: "Stop after this many failures" + required: false + type: number + default: 10 + +permissions: + contents: read + +jobs: + run: + name: Run tests + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout-minutes }} + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install Poetry + run: pip install 'poetry==2.3.2' + + - name: Cache Poetry dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pypoetry + ~/.cache/pip + .venv + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install google-genai==1.22.0 \ + google-cloud-aiplatform==1.115.0 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core==0.23.0 + + - name: Setup litellm-enterprise + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ + + - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache + run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 + poetry run prisma generate --schema litellm/proxy/schema.prisma + + - name: Run tests + env: + TEST_PATH: ${{ inputs.test-path }} + MAX_FAILURES: ${{ inputs.max-failures }} + WORKERS: ${{ inputs.workers }} + RERUNS: ${{ inputs.reruns }} + run: | + poetry run pytest ${TEST_PATH:?} \ + --tb=short -vv \ + --maxfail="${MAX_FAILURES}" \ + -n "${WORKERS}" \ + --reruns "${RERUNS}" \ + --reruns-delay 1 \ + --dist=loadscope \ + --durations=20 diff --git a/.github/workflows/_test-unit-services-base.yml b/.github/workflows/_test-unit-services-base.yml new file mode 100644 index 00000000000..d53a9e8822a --- /dev/null +++ b/.github/workflows/_test-unit-services-base.yml @@ -0,0 +1,164 @@ +name: _Unit Test Services Base (Reusable) + +on: + workflow_call: + inputs: + test-path: + description: "Pytest path(s) to run" + required: true + type: string + workers: + description: "Number of pytest-xdist workers (0 = no parallelism)" + required: false + type: number + default: 2 + reruns: + description: "Number of reruns for flaky tests" + required: false + type: number + default: 2 + timeout-minutes: + description: "Job timeout in minutes" + required: false + type: number + default: 20 + max-failures: + description: "Stop after this many failures" + required: false + type: number + default: 10 + enable-redis: + description: "Pass Redis Cloud credentials to tests via REDIS_HOST/PORT/PASSWORD env vars" + required: false + type: boolean + default: false + enable-postgres: + description: "Start a local Postgres service container and run Prisma migrations" + required: false + type: boolean + default: false + secrets: + REDIS_HOST: + required: false + REDIS_PORT: + required: false + REDIS_PASSWORD: + required: false + DATABASE_URL: + required: false + POSTGRES_USER: + required: false + POSTGRES_PASSWORD: + required: false + +permissions: + contents: read + +jobs: + run: + name: Run tests + runs-on: ubuntu-latest + timeout-minutes: ${{ inputs.timeout-minutes }} + # Environment is derived from the enable-* flags, not caller-controllable. + # This prevents callers from passing arbitrary environment names to bypass secret scoping. + # Note: Postgres service container always starts (GHA limitation), so any Redis job + # also needs Postgres secrets → uses integration-redis-postgres, not integration-redis. + environment: >- + ${{ + inputs.enable-redis && 'integration-redis-postgres' || + inputs.enable-postgres && 'integration-postgres' || + '' + }} + + services: + postgres: + image: postgres@sha256:705a5d5b5836f3fcba0d02c4d281e6a7dd9ed2dd4078640f08a1e1e9896e097d # postgres:14 + env: + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_DB: litellm_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install Poetry + run: pip install 'poetry==2.3.2' + + - name: Cache Poetry dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pypoetry + ~/.cache/pip + .venv + key: ${{ runner.os }}-poetry-services-${{ hashFiles('poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry-services- + + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install google-genai==1.22.0 \ + google-cloud-aiplatform==1.115.0 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core==0.23.0 + + - name: Setup litellm-enterprise + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ + + - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache + run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 + poetry run prisma generate --schema litellm/proxy/schema.prisma + + - name: Run Prisma migrations + if: ${{ inputs.enable-postgres }} + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + run: | + poetry run prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss + + - name: Run tests + env: + TEST_PATH: ${{ inputs.test-path }} + MAX_FAILURES: ${{ inputs.max-failures }} + WORKERS: ${{ inputs.workers }} + RERUNS: ${{ inputs.reruns }} + DATABASE_URL: ${{ inputs.enable-postgres && secrets.DATABASE_URL || '' }} + REDIS_HOST: ${{ inputs.enable-redis && secrets.REDIS_HOST || '' }} + REDIS_PORT: ${{ inputs.enable-redis && secrets.REDIS_PORT || '' }} + REDIS_PASSWORD: ${{ inputs.enable-redis && secrets.REDIS_PASSWORD || '' }} + run: | + if [ "${WORKERS}" = "0" ]; then + poetry run pytest ${TEST_PATH:?} \ + --tb=short -vv \ + --maxfail="${MAX_FAILURES}" \ + --reruns "${RERUNS}" \ + --reruns-delay 1 \ + --durations=20 + else + poetry run pytest ${TEST_PATH:?} \ + --tb=short -vv \ + --maxfail="${MAX_FAILURES}" \ + -n "${WORKERS}" \ + --reruns "${RERUNS}" \ + --reruns-delay 1 \ + --dist=loadscope \ + --durations=20 + fi diff --git a/.github/workflows/auto_update_price_and_context_window.yml b/.github/workflows/auto_update_price_and_context_window.yml index 98b9d868e68..60e89936219 100644 --- a/.github/workflows/auto_update_price_and_context_window.yml +++ b/.github/workflows/auto_update_price_and_context_window.yml @@ -2,18 +2,24 @@ name: Updates model_prices_and_context_window.json and Create Pull Request on: schedule: - - cron: "0 0 * * 0" # Run every Sundays at midnight + - cron: "0 0 * * 0" # Run every Sundays at midnight #- cron: "0 0 * * *" # Run daily at midnight +permissions: + contents: write + pull-requests: write + jobs: auto_update_price_and_context_window: if: github.repository == 'BerriAI/litellm' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Install Dependencies run: | - pip install aiohttp + pip install 'aiohttp==3.13.3' - name: Update JSON Data run: | python ".github/workflows/auto_update_price_and_context_window_file.py" @@ -26,4 +32,4 @@ jobs: --head auto-update-price-and-context-window-$(date +'%Y-%m-%d') \ --base main env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/check-schema-sync.yml b/.github/workflows/check-schema-sync.yml new file mode 100644 index 00000000000..0e5e2804e60 --- /dev/null +++ b/.github/workflows/check-schema-sync.yml @@ -0,0 +1,58 @@ +name: Check Schema Sync + +on: + pull_request: + paths: + - 'schema.prisma' + - 'litellm/proxy/schema.prisma' + - 'litellm-proxy-extras/litellm_proxy_extras/schema.prisma' + +permissions: + contents: read + +jobs: + check-sync: + name: Verify schema.prisma copies match root + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout PR + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Reject symlinked schema files + run: | + for f in schema.prisma litellm/proxy/schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma; do + if [ -L "$f" ]; then + echo "::error file=$f::$f is a symlink, which is not allowed" + exit 1 + fi + done + + - name: Check all schemas match root + run: | + EXIT=0 + + diff schema.prisma litellm/proxy/schema.prisma || { + echo "::error file=litellm/proxy/schema.prisma::litellm/proxy/schema.prisma differs from root schema.prisma" + EXIT=1 + } + + diff schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma || { + echo "::error file=litellm-proxy-extras/litellm_proxy_extras/schema.prisma::litellm-proxy-extras/litellm_proxy_extras/schema.prisma differs from root schema.prisma" + EXIT=1 + } + + if [ "$EXIT" -ne 0 ]; then + echo "" + echo "Schema files are out of sync." + echo "The root schema.prisma is the source of truth." + echo "" + echo "To fix, run from the repo root:" + echo " cp schema.prisma litellm/proxy/schema.prisma" + echo " cp schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma" + exit 1 + fi + + echo "All schema copies are in sync with root." diff --git a/.github/workflows/check_duplicate_issues.yml b/.github/workflows/check_duplicate_issues.yml index 6d11ce573eb..289d78880ad 100644 --- a/.github/workflows/check_duplicate_issues.yml +++ b/.github/workflows/check_duplicate_issues.yml @@ -12,7 +12,7 @@ jobs: contents: read steps: - name: Check for potential duplicates - uses: wow-actions/potential-duplicates@v1 + uses: wow-actions/potential-duplicates@4d4ea0352e0383859279938e255179dd1dbb67b5 # v1.1.0 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} label: potential-duplicate @@ -30,13 +30,14 @@ jobs: - name: Checkout close script if: github.event.action == 'opened' - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: sparse-checkout: .github/scripts + persist-credentials: false - name: Set up Python if: github.event.action == 'opened' - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.11" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b7cce2e4be..e86fca17c7a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,8 +6,8 @@ on: pull_request: branches: [main] schedule: - # Run weekly on Sundays at 04:00 UTC - - cron: "0 4 * * 0" + # Run daily at 04:00 UTC + - cron: "0 4 * * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,6 +15,7 @@ concurrency: jobs: analyze: + if: github.event_name != 'schedule' || github.repository == 'BerriAI/litellm' name: Analyze (${{ matrix.language }}) runs-on: ubuntu-latest timeout-minutes: 30 @@ -37,16 +38,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@ebcb5b36ded6beda4ceefea6a8bc4cc885255bb3 # v3 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/codspeed.yml b/.github/workflows/codspeed.yml index 385b95fdaf5..52d64addea9 100644 --- a/.github/workflows/codspeed.yml +++ b/.github/workflows/codspeed.yml @@ -25,10 +25,12 @@ jobs: timeout-minutes: 15 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" @@ -38,7 +40,7 @@ jobs: pip install pytest pytest-codspeed==4.3.0 - name: Run benchmarks - uses: CodSpeedHQ/action@v4 + uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4.12.1 with: mode: simulation run: pytest tests/benchmarks/ --codspeed diff --git a/.github/workflows/create_daily_staging_branch.yml b/.github/workflows/create_daily_staging_branch.yml index 08aebd7d04c..424d8de0a41 100644 --- a/.github/workflows/create_daily_staging_branch.yml +++ b/.github/workflows/create_daily_staging_branch.yml @@ -2,18 +2,22 @@ name: Create Daily Staging Branch on: schedule: - - cron: '0 0,12 * * *' # Runs every 12 hours at midnight and noon UTC - workflow_dispatch: # Allow manual trigger + - cron: "0 0,12 * * *" # Runs every 12 hours at midnight and noon UTC + workflow_dispatch: # Allow manual trigger jobs: create-staging-branch: + if: github.repository == 'BerriAI/litellm' runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: fetch-depth: 0 + persist-credentials: false - name: Create daily staging branch env: @@ -43,13 +47,17 @@ jobs: fi create-internal-dev-branch: + if: github.repository == 'BerriAI/litellm' runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: fetch-depth: 0 + persist-credentials: false - name: Create internal dev branch env: diff --git a/.github/workflows/ghcr_deploy.yml b/.github/workflows/ghcr_deploy.yml deleted file mode 100644 index 344b0ec48ee..00000000000 --- a/.github/workflows/ghcr_deploy.yml +++ /dev/null @@ -1,444 +0,0 @@ -# this workflow is triggered by an API call when there is a new PyPI release of LiteLLM -name: Build, Publish LiteLLM Docker Image. New Release -on: - workflow_dispatch: - inputs: - tag: - description: "The tag version you want to build" - required: true - release_type: - description: "The release type you want to build. Can be 'latest', 'stable', 'dev', 'rc'" - type: string - default: "latest" - commit_hash: - description: "Commit hash" - required: true - -# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds. -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - CHART_NAME: litellm-helm - -# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. -jobs: - # print commit hash, tag, and release type - print: - runs-on: ubuntu-latest - steps: - - run: | - echo "Commit hash: ${{ github.event.inputs.commit_hash }}" - echo "Tag: ${{ github.event.inputs.tag }}" - echo "Release type: ${{ github.event.inputs.release_type }}" - docker-hub-deploy: - if: github.repository == 'BerriAI/litellm' - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: litellm/litellm:${{ github.event.inputs.tag || 'latest' }} - - - name: Build and push litellm-database image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: ./docker/Dockerfile.database - tags: litellm/litellm-database:${{ github.event.inputs.tag || 'latest' }} - - - name: Build and push litellm-spend-logs image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: ./litellm-js/spend-logs/Dockerfile - tags: litellm/litellm-spend_logs:${{ github.event.inputs.tag || 'latest' }} - - - name: Build and push litellm-non_root image - uses: docker/build-push-action@v5 - with: - context: . - push: true - file: ./docker/Dockerfile.non_root - tags: litellm/litellm-non_root:${{ github.event.inputs.tag || 'latest' }} - build-and-push-image: - runs-on: ubuntu-latest - # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - # Configure multi platform Docker builds - - name: Set up QEMU - uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345 - # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. - # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. - # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. - - name: Build and push Docker image - uses: docker/build-push-action@4976231911ebf5f32aad765192d35f942aa48cb8 - with: - context: . - push: true - tags: | - ${{ steps.meta.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }}, - ${{ steps.meta.outputs.tags }}-${{ github.event.inputs.release_type }} - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - ${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm:main-stable', env.REGISTRY) || '' }}, - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm:{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - labels: ${{ steps.meta.outputs.labels }} - platforms: local,linux/amd64,linux/arm64,linux/arm64/v8 - - build-and-push-image-ee: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for EE Dockerfile - id: meta-ee - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee - # Configure multi platform Docker builds - - name: Set up QEMU - uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345 - - - name: Build and push EE Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - file: Dockerfile - push: true - tags: | - ${{ steps.meta-ee.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }}, - ${{ steps.meta-ee.outputs.tags }}-${{ github.event.inputs.release_type }} - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-ee:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - ${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-ee:main-stable', env.REGISTRY) || '' }} - labels: ${{ steps.meta-ee.outputs.labels }} - platforms: local,linux/amd64,linux/arm64,linux/arm64/v8 - - build-and-push-image-database: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for database Dockerfile - id: meta-database - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-database - # Configure multi platform Docker builds - - name: Set up QEMU - uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345 - - - name: Build and push Database Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - file: ./docker/Dockerfile.database - push: true - tags: | - ${{ steps.meta-database.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }}, - ${{ steps.meta-database.outputs.tags }}-${{ github.event.inputs.release_type }} - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-database:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - ${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-database:main-stable', env.REGISTRY) || '' }} - labels: ${{ steps.meta-database.outputs.labels }} - platforms: local,linux/amd64,linux/arm64,linux/arm64/v8 - - build-and-push-image-non_root: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for non_root Dockerfile - id: meta-non_root - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-non_root - # Configure multi platform Docker builds - - name: Set up QEMU - uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345 - - - name: Build and push non_root Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - file: ./docker/Dockerfile.non_root - push: true - tags: | - ${{ steps.meta-non_root.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }}, - ${{ steps.meta-non_root.outputs.tags }}-${{ github.event.inputs.release_type }} - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-non_root:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - ${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-non_root:main-stable', env.REGISTRY) || '' }} - labels: ${{ steps.meta-non_root.outputs.labels }} - platforms: local,linux/amd64,linux/arm64,linux/arm64/v8 - - build-and-push-image-spend-logs: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.commit_hash }} - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for spend-logs Dockerfile - id: meta-spend-logs - uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-spend_logs - # Configure multi platform Docker builds - - name: Set up QEMU - uses: docker/setup-qemu-action@e0e4588fad221d38ee467c0bffd91115366dc0c5 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@edfb0fe6204400c56fbfd3feba3fe9ad1adfa345 - - - name: Build and push Database Docker image - uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 - with: - context: . - file: ./litellm-js/spend-logs/Dockerfile - push: true - tags: | - ${{ steps.meta-spend-logs.outputs.tags }}-${{ github.event.inputs.tag || 'latest' }}, - ${{ steps.meta-spend-logs.outputs.tags }}-${{ github.event.inputs.release_type }} - ${{ (github.event.inputs.release_type == 'stable' || github.event.inputs.release_type == 'rc') && format('{0}/berriai/litellm-spend_logs:main-{1}', env.REGISTRY, github.event.inputs.tag) || '' }}, - ${{ github.event.inputs.release_type == 'stable' && format('{0}/berriai/litellm-spend_logs:main-stable', env.REGISTRY) || '' }} - platforms: local,linux/amd64,linux/arm64,linux/arm64/v8 - - run-observatory-tests: - if: github.event.inputs.release_type == 'rc' || github.event.inputs.release_type == 'stable' - needs: [docker-hub-deploy] - uses: ./.github/workflows/run_observatory_tests.yml - with: - tag: ${{ github.event.inputs.tag }} - commit_hash: ${{ github.event.inputs.commit_hash }} - secrets: inherit - - build-and-push-helm-chart: - if: github.event.inputs.release_type != 'dev' - needs: [docker-hub-deploy, build-and-push-image, build-and-push-image-database] - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: lowercase github.repository_owner - run: | - echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV} - - # Sync Helm chart version with LiteLLM release version (1-1 versioning) - # This allows users to easily map Helm chart versions to LiteLLM versions - # See: https://codefresh.io/docs/docs/ci-cd-guides/helm-best-practices/ - - name: Calculate chart and app versions - id: chart_version - shell: bash - run: | - INPUT_TAG="${{ github.event.inputs.tag }}" - RELEASE_TYPE="${{ github.event.inputs.release_type }}" - - # Chart version = LiteLLM version without 'v' prefix (Helm semver convention) - # v1.81.0 -> 1.81.0, v1.81.0.rc.1 -> 1.81.0.rc.1 - CHART_VERSION="${INPUT_TAG#v}" - - # Add suffix for 'latest' releases (rc already has suffix in tag) - if [ "$RELEASE_TYPE" = "latest" ]; then - CHART_VERSION="${CHART_VERSION}-latest" - fi - - # App version = Docker tag (keeps 'v' prefix to match Docker image tags) - APP_VERSION="${INPUT_TAG}" - - echo "version=${CHART_VERSION}" | tee -a $GITHUB_OUTPUT - echo "app_version=${APP_VERSION}" | tee -a $GITHUB_OUTPUT - - - uses: ./.github/actions/helm-oci-chart-releaser - with: - name: ${{ env.CHART_NAME }} - repository: ${{ env.REPO_OWNER }} - tag: ${{ steps.chart_version.outputs.version }} - app_version: ${{ steps.chart_version.outputs.app_version }} - path: deploy/charts/${{ env.CHART_NAME }} - registry: ${{ env.REGISTRY }} - registry_username: ${{ github.actor }} - registry_password: ${{ secrets.GITHUB_TOKEN }} - update_dependencies: true - - release: - name: "New LiteLLM Release" - needs: [docker-hub-deploy, build-and-push-image, build-and-push-image-database] - permissions: - contents: write - runs-on: "ubuntu-latest" - - steps: - - name: Display version - run: echo "Current version is ${{ github.event.inputs.tag }}" - - name: "Set Release Tag" - run: echo "RELEASE_TAG=${{ github.event.inputs.tag }}" >> $GITHUB_ENV - - name: Display release tag - run: echo "RELEASE_TAG is $RELEASE_TAG" - - name: "Create release" - uses: "actions/github-script@v6" - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - script: | - const commitHash = "${{ github.event.inputs.commit_hash}}"; - console.log("Commit Hash:", commitHash); // Add this line for debugging - try { - const response = await github.rest.repos.createRelease({ - draft: false, - generate_release_notes: true, - target_commitish: commitHash, - name: process.env.RELEASE_TAG, - owner: context.repo.owner, - prerelease: false, - repo: context.repo.repo, - tag_name: process.env.RELEASE_TAG, - }); - - core.exportVariable('RELEASE_ID', response.data.id); - core.exportVariable('RELEASE_UPLOAD_URL', response.data.upload_url); - } catch (error) { - core.setFailed(error.message); - } - - name: Fetch Release Notes - id: release-notes - uses: actions/github-script@v6 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - script: | - try { - const response = await github.rest.repos.getRelease({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: process.env.RELEASE_ID, - }); - const formattedBody = JSON.stringify(response.data.body).slice(1, -1); - return formattedBody; - } catch (error) { - core.setFailed(error.message); - } - env: - RELEASE_ID: ${{ env.RELEASE_ID }} - - name: Github Releases To Discord - env: - WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} - REALEASE_TAG: ${{ env.RELEASE_TAG }} - RELEASE_NOTES: ${{ steps.release-notes.outputs.result }} - run: | - curl -H "Content-Type: application/json" -X POST -d '{ - "content": "New LiteLLM release '"${RELEASE_TAG}"'", - "username": "Release Changelog", - "avatar_url": "https://cdn.discordapp.com/avatars/487431320314576937/bd64361e4ba6313d561d54e78c9e7171.png", - "embeds": [ - { - "title": "Changelog for LiteLLM '"${RELEASE_TAG}"'", - "description": "'"${RELEASE_NOTES}"'", - "color": 2105893 - } - ] - }' $WEBHOOK_URL - diff --git a/.github/workflows/ghcr_helm_deploy.yml b/.github/workflows/ghcr_helm_deploy.yml deleted file mode 100644 index 21b2eaafe19..00000000000 --- a/.github/workflows/ghcr_helm_deploy.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Standalone workflow to publish LiteLLM Helm Chart -# Note: The main ghcr_deploy.yml workflow also publishes the Helm chart as part of a full release -name: Build, Publish LiteLLM Helm Chart. New Release -on: - workflow_dispatch: - inputs: - tag: - description: "LiteLLM version tag (e.g., v1.81.0)" - required: true - -# Defines two custom environment variables for the workflow. Used for the Container registry domain, and a name for the Docker image that this workflow builds. -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - REPO_OWNER: ${{github.repository_owner}} - -# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. -jobs: - build-and-push-helm-chart: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: lowercase github.repository_owner - run: | - echo "REPO_OWNER=`echo ${{github.repository_owner}} | tr '[:upper:]' '[:lower:]'`" >>${GITHUB_ENV} - - # Sync Helm chart version with LiteLLM release version (1-1 versioning) - - name: Calculate chart and app versions - id: chart_version - shell: bash - run: | - INPUT_TAG="${{ github.event.inputs.tag }}" - - # Chart version = LiteLLM version without 'v' prefix - # v1.81.0 -> 1.81.0 - CHART_VERSION="${INPUT_TAG#v}" - - # App version = Docker tag (keeps 'v' prefix) - APP_VERSION="${INPUT_TAG}" - - echo "version=${CHART_VERSION}" | tee -a $GITHUB_OUTPUT - echo "app_version=${APP_VERSION}" | tee -a $GITHUB_OUTPUT - - - name: Lint helm chart - run: helm lint deploy/charts/litellm-helm - - - uses: ./.github/actions/helm-oci-chart-releaser - with: - name: litellm-helm - repository: ${{ env.REPO_OWNER }} - tag: ${{ steps.chart_version.outputs.version }} - app_version: ${{ steps.chart_version.outputs.app_version }} - path: deploy/charts/litellm-helm - registry: ${{ env.REGISTRY }} - registry_username: ${{ github.actor }} - registry_password: ${{ secrets.GITHUB_TOKEN }} - update_dependencies: true - diff --git a/.github/workflows/helm_unit_test.yml b/.github/workflows/helm_unit_test.yml index c4b83af70a1..06836b1d1cd 100644 --- a/.github/workflows/helm_unit_test.yml +++ b/.github/workflows/helm_unit_test.yml @@ -6,22 +6,36 @@ on: branches: - main +permissions: + contents: read + jobs: unit-test: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up Helm 3.11.1 - uses: azure/setup-helm@v1 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: - version: '3.11.1' + version: "3.11.1" - name: Install Helm Unit Test Plugin run: | helm plugin install https://github.com/helm-unittest/helm-unittest --version v0.4.4 + - name: Verify Helm Unit Test Plugin integrity + run: | + EXPECTED_SHA="e251ba198448629678ff2168e1a469249d998155" + PLUGIN_DIR="$(helm env HELM_PLUGINS)/helm-unittest" + ACTUAL_SHA="$(git -C "$PLUGIN_DIR" rev-parse HEAD)" + if [ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]; then + echo "::error::Helm unittest plugin checksum mismatch! Expected $EXPECTED_SHA but got $ACTUAL_SHA" + exit 1 + fi + echo "Helm unittest plugin integrity verified: $ACTUAL_SHA" - name: Run unit tests - run: - helm unittest -f 'tests/*.yaml' deploy/charts/litellm-helm \ No newline at end of file + run: helm unittest -f 'tests/*.yaml' deploy/charts/litellm-helm diff --git a/.github/workflows/interpret_load_test.py b/.github/workflows/interpret_load_test.py deleted file mode 100644 index 348ff300fff..00000000000 --- a/.github/workflows/interpret_load_test.py +++ /dev/null @@ -1,139 +0,0 @@ -import csv -import os -from github import Github - - -def interpret_results(csv_file): - with open(csv_file, newline="") as csvfile: - csvreader = csv.DictReader(csvfile) - rows = list(csvreader) - """ - in this csv reader - - Create 1 new column "Status" - - if a row has a median response time < 300 and an average response time < 300, Status = "Passed ✅" - - if a row has a median response time >= 300 or an average response time >= 300, Status = "Failed ❌" - - Order the table in this order Name, Status, Median Response Time, Average Response Time, Requests/s,Failures/s, Min Response Time, Max Response Time, all other columns - """ - - # Add a new column "Status" - for row in rows: - median_response_time = float( - row["Median Response Time"].strip().rstrip("ms") - ) - average_response_time = float( - row["Average Response Time"].strip().rstrip("s") - ) - - request_count = int(row["Request Count"]) - failure_count = int(row["Failure Count"]) - - failure_percent = round((failure_count / request_count) * 100, 2) - - # Determine status based on conditions - if ( - median_response_time < 300 - and average_response_time < 300 - and failure_percent < 5 - ): - row["Status"] = "Passed ✅" - else: - row["Status"] = "Failed ❌" - - # Construct Markdown table header - markdown_table = "| Name | Status | Median Response Time (ms) | Average Response Time (ms) | Requests/s | Failures/s | Request Count | Failure Count | Min Response Time (ms) | Max Response Time (ms) |" - markdown_table += ( - "\n| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |" - ) - - # Construct Markdown table rows - for row in rows: - markdown_table += f"\n| {row['Name']} | {row['Status']} | {row['Median Response Time']} | {row['Average Response Time']} | {row['Requests/s']} | {row['Failures/s']} | {row['Request Count']} | {row['Failure Count']} | {row['Min Response Time']} | {row['Max Response Time']} |" - print("markdown table: ", markdown_table) - return markdown_table - - -def _get_docker_run_command_stable_release(release_version): - return f""" -\n\n -## Docker Run LiteLLM Proxy - -``` -docker run \\ --e STORE_MODEL_IN_DB=True \\ --p 4000:4000 \\ -ghcr.io/berriai/litellm:litellm_stable_release_branch-{release_version} -``` - """ - - -def _get_docker_run_command(release_version): - return f""" -\n\n -## Docker Run LiteLLM Proxy - -``` -docker run \\ --e STORE_MODEL_IN_DB=True \\ --p 4000:4000 \\ -ghcr.io/berriai/litellm:main-{release_version} -``` - """ - - -def get_docker_run_command(release_version): - if "stable" in release_version: - return _get_docker_run_command_stable_release(release_version) - else: - return _get_docker_run_command(release_version) - - -if __name__ == "__main__": - return - csv_file = "load_test_stats.csv" # Change this to the path of your CSV file - markdown_table = interpret_results(csv_file) - - # Update release body with interpreted results - github_token = os.getenv("GITHUB_TOKEN") - g = Github(github_token) - repo = g.get_repo( - "BerriAI/litellm" - ) # Replace with your repository's username and name - latest_release = repo.get_latest_release() - print("got latest release: ", latest_release) - print(latest_release.title) - print(latest_release.tag_name) - - release_version = latest_release.title - - print("latest release body: ", latest_release.body) - print("markdown table: ", markdown_table) - - # check if "Load Test LiteLLM Proxy Results" exists - existing_release_body = latest_release.body - if "Load Test LiteLLM Proxy Results" in latest_release.body: - # find the "Load Test LiteLLM Proxy Results" section and delete it - start_index = latest_release.body.find("Load Test LiteLLM Proxy Results") - existing_release_body = latest_release.body[:start_index] - - docker_run_command = get_docker_run_command(release_version) - print("docker run command: ", docker_run_command) - - new_release_body = ( - existing_release_body - + docker_run_command - + "\n\n" - + "### Don't want to maintain your internal proxy? get in touch 🎉" - + "\nHosted Proxy Alpha: https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions" - + "\n\n" - + "## Load Test LiteLLM Proxy Results" - + "\n\n" - + markdown_table - ) - print("new release body: ", new_release_body) - try: - latest_release.update_release( - name=latest_release.tag_name, - message=new_release_body, - ) - except Exception as e: - print(e) diff --git a/.github/workflows/issue-keyword-labeler.yml b/.github/workflows/issue-keyword-labeler.yml index 936f90f747f..7e2693209b6 100644 --- a/.github/workflows/issue-keyword-labeler.yml +++ b/.github/workflows/issue-keyword-labeler.yml @@ -2,8 +2,8 @@ name: Issue Keyword Labeler on: issues: - types: - - opened + types: + - opened jobs: scan-and-label: @@ -13,7 +13,9 @@ jobs: contents: read steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Scan for provider keywords id: scan @@ -24,7 +26,7 @@ jobs: - name: Ensure label exists if: steps.scan.outputs.found == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -51,7 +53,7 @@ jobs: - name: Add label to the issue if: steps.scan.outputs.found == 'true' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -61,4 +63,3 @@ jobs: issue_number: context.issue.number, labels: ['llm translation'] }); - diff --git a/.github/workflows/label-component.yml b/.github/workflows/label-component.yml index fd079fce6c1..e0c2fa94d8c 100644 --- a/.github/workflows/label-component.yml +++ b/.github/workflows/label-component.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Add component labels - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/llm-translation-testing.yml b/.github/workflows/llm-translation-testing.yml index 7fda37a66dc..922013c4b54 100644 --- a/.github/workflows/llm-translation-testing.yml +++ b/.github/workflows/llm-translation-testing.yml @@ -4,38 +4,41 @@ on: workflow_dispatch: inputs: release_candidate_tag: - description: 'Release candidate tag/version' + description: "Release candidate tag/version" required: true type: string push: tags: - - 'v*-rc*' # Triggers on release candidate tags like v1.0.0-rc1 - + - "v*-rc*" # Triggers on release candidate tags like v1.0.0-rc1 + +permissions: + contents: read + jobs: run-llm-translation-tests: runs-on: ubuntu-latest timeout-minutes: 90 - + steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: + persist-credentials: false ref: ${{ github.event.inputs.release_candidate_tag || github.ref }} - + - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: '3.11' - + python-version: "3.11" + - name: Install Poetry - uses: snok/install-poetry@v1 - with: - version: latest - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Cache Poetry dependencies - uses: actions/cache@v3 + run: | + pip install 'poetry==2.3.2' + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + + - name: Restore Poetry dependencies cache + uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.0 with: path: | ~/.cache/pypoetry @@ -43,15 +46,15 @@ jobs: key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} restore-keys: | ${{ runner.os }}-poetry- - + - name: Install dependencies run: | poetry install --with dev - poetry run pip install pytest-xdist pytest-timeout - + poetry run pip install 'pytest-xdist==3.8.0' 'pytest-timeout==2.4.0' + - name: Create test results directory run: mkdir -p test-results - + - name: Run LLM Translation Tests env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} @@ -61,13 +64,14 @@ jobs: AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_API_BASE: ${{ secrets.AZURE_API_BASE }} AZURE_API_VERSION: ${{ secrets.AZURE_API_VERSION }} - # Add other API keys as needed + RC_TAG: ${{ github.event.inputs.release_candidate_tag || github.ref_name }} + COMMIT_SHA: ${{ github.sha }} run: | python .github/workflows/run_llm_translation_tests.py \ - --tag "${{ github.event.inputs.release_candidate_tag || github.ref_name }}" \ - --commit "${{ github.sha }}" \ + --tag "$RC_TAG" \ + --commit "$COMMIT_SHA" \ || true # Continue even if tests fail - + - name: Display test summary if: always() run: | @@ -79,9 +83,9 @@ jobs: else echo "Warning: Test report was not generated" fi - + - name: Upload test artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: LLM-Translation-Artifact-${{ github.event.inputs.release_candidate_tag || github.ref_name }} diff --git a/.github/workflows/load_test.yml b/.github/workflows/load_test.yml deleted file mode 100644 index cdaffa328c9..00000000000 --- a/.github/workflows/load_test.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Test Locust Load Test - -on: - workflow_run: - workflows: ["Build, Publish LiteLLM Docker Image. New Release"] - types: - - completed - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Setup Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install PyGithub - - name: re-deploy proxy - run: | - echo "Current working directory: $PWD" - ls - python ".github/workflows/redeploy_proxy.py" - env: - LOAD_TEST_REDEPLOY_URL1: ${{ secrets.LOAD_TEST_REDEPLOY_URL1 }} - LOAD_TEST_REDEPLOY_URL2: ${{ secrets.LOAD_TEST_REDEPLOY_URL2 }} - working-directory: ${{ github.workspace }} - - name: Run Load Test - id: locust_run - uses: BerriAI/locust-github-action@master - with: - LOCUSTFILE: ".github/workflows/locustfile.py" - URL: "https://post-release-load-test-proxy.onrender.com/" - USERS: "20" - RATE: "20" - RUNTIME: "300s" - - name: Process Load Test Stats - run: | - echo "Current working directory: $PWD" - ls - python ".github/workflows/interpret_load_test.py" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: ${{ github.workspace }} - - name: Upload CSV as Asset to Latest Release - uses: xresloader/upload-to-github-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - file: "load_test_stats.csv;load_test.html" - update_latest_release: true - tag_name: "load-test" - overwrite: true \ No newline at end of file diff --git a/.github/workflows/locustfile.py b/.github/workflows/locustfile.py deleted file mode 100644 index 36dbeee9c48..00000000000 --- a/.github/workflows/locustfile.py +++ /dev/null @@ -1,28 +0,0 @@ -from locust import HttpUser, task, between - - -class MyUser(HttpUser): - wait_time = between(1, 5) - - @task - def chat_completion(self): - headers = { - "Content-Type": "application/json", - "Authorization": "Bearer sk-8N1tLOOyH8TIxwOLahhIVg", - # Include any additional headers you may need for authentication, etc. - } - - # Customize the payload with "model" and "messages" keys - payload = { - "model": "fake-openai-endpoint", - "messages": [ - {"role": "system", "content": "You are a chat bot."}, - {"role": "user", "content": "Hello, how are you?"}, - ], - # Add more data as necessary - } - - # Make a POST request to the "chat/completions" endpoint - response = self.client.post("chat/completions", json=payload, headers=headers) - - # Print or log the response if needed diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 23e4a06da9e..00000000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Publish Dev Release to PyPI - -on: - workflow_dispatch: - -jobs: - publish-dev-release: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 # Adjust the Python version as needed - - - name: Install dependencies - run: pip install toml twine - - - name: Read version from pyproject.toml - id: read-version - run: | - version=$(python -c 'import toml; print(toml.load("pyproject.toml")["tool"]["commitizen"]["version"])') - printf "LITELLM_VERSION=%s" "$version" >> $GITHUB_ENV - - - name: Check if version exists on PyPI - id: check-version - run: | - set -e - if twine check --repository-url https://pypi.org/simple/ "litellm==$LITELLM_VERSION" >/dev/null 2>&1; then - echo "Version $LITELLM_VERSION already exists on PyPI. Skipping publish." - diff --git a/.github/workflows/publish-migrations.yml b/.github/workflows/publish-migrations.yml deleted file mode 100644 index a5187cb2f55..00000000000 --- a/.github/workflows/publish-migrations.yml +++ /dev/null @@ -1,207 +0,0 @@ -name: Publish Prisma Migrations - -permissions: - contents: write - pull-requests: write - -on: - push: - paths: - - 'schema.prisma' # Check root schema.prisma - branches: - - main - -jobs: - publish-migrations: - if: github.repository == 'BerriAI/litellm' - runs-on: ubuntu-latest - services: - postgres: - image: postgres:14 - env: - POSTGRES_DB: temp_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - # Add shadow database service - postgres_shadow: - image: postgres:14 - env: - POSTGRES_DB: shadow_db - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - ports: - - 5433:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - - name: Install Dependencies - run: | - pip install prisma - pip install python-dotenv - - - name: Generate Initial Migration if None Exists - env: - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - DIRECT_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - SHADOW_DATABASE_URL: "postgresql://postgres:postgres@localhost:5433/shadow_db" - run: | - mkdir -p deploy/migrations - echo 'provider = "postgresql"' > deploy/migrations/migration_lock.toml - - if [ -z "$(ls -A deploy/migrations/2* 2>/dev/null)" ]; then - echo "No existing migrations found, creating baseline..." - VERSION=$(date +%Y%m%d%H%M%S) - mkdir -p deploy/migrations/${VERSION}_initial - - echo "Generating initial migration..." - # Save raw output for debugging - prisma migrate diff \ - --from-empty \ - --to-schema-datamodel schema.prisma \ - --shadow-database-url "${SHADOW_DATABASE_URL}" \ - --script > deploy/migrations/${VERSION}_initial/raw_migration.sql - - echo "Raw migration file content:" - cat deploy/migrations/${VERSION}_initial/raw_migration.sql - - echo "Cleaning migration file..." - # Clean the file - sed '/^Installing/d' deploy/migrations/${VERSION}_initial/raw_migration.sql > deploy/migrations/${VERSION}_initial/migration.sql - - # Verify the migration file - if [ ! -s deploy/migrations/${VERSION}_initial/migration.sql ]; then - echo "ERROR: Migration file is empty after cleaning" - echo "Original content was:" - cat deploy/migrations/${VERSION}_initial/raw_migration.sql - exit 1 - fi - - echo "Final migration file content:" - cat deploy/migrations/${VERSION}_initial/migration.sql - - # Verify it starts with SQL - if ! head -n 1 deploy/migrations/${VERSION}_initial/migration.sql | grep -q "^--\|^CREATE\|^ALTER"; then - echo "ERROR: Migration file does not start with SQL command or comment" - echo "First line is:" - head -n 1 deploy/migrations/${VERSION}_initial/migration.sql - echo "Full content is:" - cat deploy/migrations/${VERSION}_initial/migration.sql - exit 1 - fi - - echo "Initial migration generated at $(date -u)" > deploy/migrations/${VERSION}_initial/README.md - fi - - - name: Compare and Generate Migration - if: success() - env: - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - DIRECT_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - SHADOW_DATABASE_URL: "postgresql://postgres:postgres@localhost:5433/shadow_db" - run: | - # Create temporary migration workspace - mkdir -p temp_migrations - - # Copy existing migrations (will not fail if directory is empty) - cp -r deploy/migrations/* temp_migrations/ 2>/dev/null || true - - VERSION=$(date +%Y%m%d%H%M%S) - - # Generate diff against existing migrations or empty state - prisma migrate diff \ - --from-migrations temp_migrations \ - --to-schema-datamodel schema.prisma \ - --shadow-database-url "${SHADOW_DATABASE_URL}" \ - --script > temp_migrations/migration_${VERSION}.sql - - # Check if there are actual changes - if [ -s temp_migrations/migration_${VERSION}.sql ]; then - echo "Changes detected, creating new migration" - mkdir -p deploy/migrations/${VERSION}_schema_update - mv temp_migrations/migration_${VERSION}.sql deploy/migrations/${VERSION}_schema_update/migration.sql - echo "Migration generated at $(date -u)" > deploy/migrations/${VERSION}_schema_update/README.md - else - echo "No schema changes detected" - exit 0 - fi - - - name: Verify Migration - if: success() - env: - DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - DIRECT_URL: "postgresql://postgres:postgres@localhost:5432/temp_db" - SHADOW_DATABASE_URL: "postgresql://postgres:postgres@localhost:5433/shadow_db" - run: | - # Create test database - psql "${SHADOW_DATABASE_URL}" -c 'CREATE DATABASE migration_test;' - - # Apply all migrations in order to verify - for migration in deploy/migrations/*/migration.sql; do - echo "Applying migration: $migration" - psql "${SHADOW_DATABASE_URL}" -f $migration - done - - # Add this step before create-pull-request to debug permissions - - name: Check Token Permissions - run: | - echo "Checking token permissions..." - curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/BerriAI/litellm/collaborators - - echo "\nChecking if token can create PRs..." - curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/BerriAI/litellm - - # Add this debug step before git push - - name: Debug Changed Files - run: | - echo "Files staged for commit:" - git diff --name-status --staged - - echo "\nAll changed files:" - git status - - - name: Create Pull Request - if: success() - uses: peter-evans/create-pull-request@v5 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "chore: update prisma migrations" - title: "Update Prisma Migrations" - body: | - Auto-generated migration based on schema.prisma changes. - - Generated files: - - deploy/migrations/${VERSION}_schema_update/migration.sql - - deploy/migrations/${VERSION}_schema_update/README.md - branch: feat/prisma-migration-${{ env.VERSION }} - base: main - delete-branch: true - - - name: Generate and Save Migrations - run: | - # Only add migration files - git add deploy/migrations/ - git status # Debug what's being committed - git commit -m "chore: update prisma migrations" diff --git a/.github/workflows/publish_enterprise.yml b/.github/workflows/publish_enterprise.yml deleted file mode 100644 index 459a233cb71..00000000000 --- a/.github/workflows/publish_enterprise.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: Publish litellm-enterprise to PyPI - -on: - workflow_dispatch: - inputs: - bump: - description: "Version bump type" - required: true - default: "patch" - type: choice - options: - - patch - - minor - - major - -jobs: - publish: - runs-on: ubuntu-latest - if: github.repository == 'BerriAI/litellm' - permissions: - contents: write - pull-requests: write - defaults: - run: - working-directory: enterprise - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install Poetry - run: pip install poetry - - - name: Bump version - id: bump - run: | - OLD=$(poetry version -s) - poetry version ${{ github.event.inputs.bump }} - NEW=$(poetry version -s) - echo "old=$OLD" >> $GITHUB_OUTPUT - echo "new=$NEW" >> $GITHUB_OUTPUT - - - name: Update version refs in root pyproject.toml and requirements.txt - run: | - OLD=${{ steps.bump.outputs.old }} - NEW=${{ steps.bump.outputs.new }} - sed -i "s/litellm-enterprise = {version = \"${OLD}\"/litellm-enterprise = {version = \"${NEW}\"/" ../pyproject.toml - sed -i "s/litellm-enterprise==${OLD}/litellm-enterprise==${NEW}/" ../requirements.txt - - - name: Update poetry.lock - working-directory: . - run: poetry lock - - - name: Build - run: poetry build - - - name: Commit version bump and create PR - id: create-pr - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - cd .. - BRANCH="bump/enterprise-${{ steps.bump.outputs.new }}" - git checkout -b "$BRANCH" - git add enterprise/pyproject.toml pyproject.toml requirements.txt poetry.lock - git commit -m "bump: litellm-enterprise ${{ steps.bump.outputs.old }} → ${{ steps.bump.outputs.new }}" - git push origin "$BRANCH" --force - gh pr create \ - --title "bump: litellm-enterprise ${{ steps.bump.outputs.old }} → ${{ steps.bump.outputs.new }}" \ - --body "Version bump for litellm-enterprise. Merge to update main." \ - --head "$BRANCH" \ - --base main \ - || true - PR_URL=$(gh pr list --head "$BRANCH" --json url -q '.[0].url') - echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT - env: - GH_TOKEN: ${{ github.token }} - - - name: Enable auto-merge - run: | - gh pr merge "${{ steps.create-pr.outputs.pr_url }}" --auto --squash - env: - GH_TOKEN: ${{ github.token }} - - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_ENTERPRISE }} - run: | - pip install twine - twine upload dist/litellm_enterprise-${{ steps.bump.outputs.new }}* diff --git a/.github/workflows/publish_proxy_extras.yml b/.github/workflows/publish_proxy_extras.yml deleted file mode 100644 index fa30b153163..00000000000 --- a/.github/workflows/publish_proxy_extras.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Publish litellm-proxy-extras to PyPI - -on: - workflow_dispatch: - inputs: - bump: - description: "Version bump type" - required: true - default: "patch" - type: choice - options: - - patch - - minor - - major - -jobs: - publish: - runs-on: ubuntu-latest - if: github.repository == 'BerriAI/litellm' - permissions: - contents: write - defaults: - run: - working-directory: litellm-proxy-extras - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install Poetry - run: pip install poetry - - - name: Bump version - id: bump - run: | - OLD=$(poetry version -s) - poetry version ${{ github.event.inputs.bump }} - NEW=$(poetry version -s) - echo "old=$OLD" >> $GITHUB_OUTPUT - echo "new=$NEW" >> $GITHUB_OUTPUT - - - name: Update version refs in root pyproject.toml and requirements.txt - run: | - OLD=${{ steps.bump.outputs.old }} - NEW=${{ steps.bump.outputs.new }} - sed -i "s/litellm-proxy-extras = {version = \"${OLD}\"/litellm-proxy-extras = {version = \"${NEW}\"/" ../pyproject.toml - sed -i "s/litellm-proxy-extras==${OLD}/litellm-proxy-extras==${NEW}/" ../requirements.txt - - - name: Update poetry.lock - working-directory: . - run: poetry lock - - - name: Build - run: poetry build - - - name: Commit version bump - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - cd .. - git add litellm-proxy-extras/pyproject.toml pyproject.toml requirements.txt poetry.lock - git commit -m "bump: litellm-proxy-extras ${{ steps.bump.outputs.old }} → ${{ steps.bump.outputs.new }}" - git push - - - name: Publish to PyPI - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_PASSWORD }} - run: | - pip install twine - twine upload dist/litellm_proxy_extras-${{ steps.bump.outputs.new }}* diff --git a/.github/workflows/publish_to_pypi.yml b/.github/workflows/publish_to_pypi.yml new file mode 100644 index 00000000000..8f675bb3075 --- /dev/null +++ b/.github/workflows/publish_to_pypi.yml @@ -0,0 +1,136 @@ +name: Publish to PyPI + +on: + workflow_dispatch: + +jobs: + preflight-checks: + name: Preflight Checks + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + # No environment — read-only checks, no approval needed + outputs: + needs_publish: ${{ steps.check-litellm.outputs.needs_publish }} + version: ${{ steps.check-litellm.outputs.version }} + + steps: + - name: Checkout repo + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Check litellm version on PyPI + id: check-litellm + run: | + VERSION=$(grep -m1 '^version' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Checking if litellm $VERSION exists on PyPI..." + + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/litellm/$VERSION/json") + if [ "$HTTP_STATUS" = "200" ]; then + echo "litellm $VERSION already exists on PyPI. Skipping publish." + echo "needs_publish=false" >> "$GITHUB_OUTPUT" + else + echo "litellm $VERSION not found on PyPI. Publish needed." + echo "needs_publish=true" >> "$GITHUB_OUTPUT" + fi + + - name: Sanity check proxy-extras version + run: | + # Read pinned version from requirements.txt + REQ_VERSION=$(grep -oP 'litellm-proxy-extras==\K[0-9.]+' requirements.txt) + if [ -z "$REQ_VERSION" ]; then + echo "::error::Could not find litellm-proxy-extras version in requirements.txt" + exit 1 + fi + echo "requirements.txt pins litellm-proxy-extras==$REQ_VERSION" + + # Read pinned version from pyproject.toml dependency + PYPROJECT_VERSION=$(python3 -c " + import re + with open('pyproject.toml') as f: + content = f.read() + match = re.search(r'litellm-proxy-extras\s*=\s*\{version\s*=\s*\"([^\"]+)\"', content) + if match: + print(match.group(1).lstrip('^~>=')) + else: + import sys + print('::error::Could not find litellm-proxy-extras dependency in pyproject.toml', file=sys.stderr) + sys.exit(1) + ") + echo "pyproject.toml pins litellm-proxy-extras version: $PYPROJECT_VERSION" + + # Check that both pinned versions match + if [ "$REQ_VERSION" != "$PYPROJECT_VERSION" ]; then + echo "::error::Version mismatch: requirements.txt has $REQ_VERSION but pyproject.toml has $PYPROJECT_VERSION" + exit 1 + fi + + # Check that the pinned version exists on PyPI + echo "Checking if litellm-proxy-extras $REQ_VERSION exists on PyPI..." + HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/litellm-proxy-extras/$REQ_VERSION/json") + if [ "$HTTP_STATUS" != "200" ]; then + echo "::error::litellm-proxy-extras $REQ_VERSION is not published on PyPI yet. Publish it before releasing litellm." + exit 1 + fi + echo "litellm-proxy-extras $REQ_VERSION exists on PyPI. Sanity check passed." + + publish-litellm: + name: Publish litellm to PyPI + needs: preflight-checks + if: needs.preflight-checks.outputs.needs_publish == 'true' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + id-token: write + contents: read + environment: pypi-publish + + steps: + - name: Checkout repo + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Copy model prices backup + run: cp model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json + + - name: Install build tools + run: python -m pip install --upgrade pip build==1.4.2 + + - name: Build package + run: | + rm -rf build dist + python -m build + + - name: Verify build artifacts + env: + EXPECTED_VERSION: ${{ needs.preflight-checks.outputs.version }} + run: | + echo "Contents of dist/:" + ls -la dist/ + # Ensure we have both sdist and wheel + ls dist/*.tar.gz + ls dist/*.whl + # Verify built version matches expected + ls dist/ | grep -q "litellm-${EXPECTED_VERSION}" || { + echo "::error::Built artifacts do not match expected version $EXPECTED_VERSION" + ls dist/ + exit 1 + } + + - name: Validate package metadata + run: | + pip install twine==6.2.0 + twine check dist/* + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/read_pyproject_version.yml b/.github/workflows/read_pyproject_version.yml index 8f6310f935b..04b4a38ce19 100644 --- a/.github/workflows/read_pyproject_version.yml +++ b/.github/workflows/read_pyproject_version.yml @@ -3,7 +3,10 @@ name: Read Version from pyproject.toml on: push: branches: - - main # Change this to the default branch of your repository + - main # Change this to the default branch of your repository + +permissions: + contents: read jobs: read-version: @@ -11,20 +14,14 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: - python-version: 3.8 # Adjust the Python version as needed - - - name: Install dependencies - run: pip install toml + persist-credentials: false - name: Read version from pyproject.toml id: read-version run: | - version=$(python -c 'import toml; print(toml.load("pyproject.toml")["tool"]["commitizen"]["version"])') + version=$(grep -m1 '^version' pyproject.toml | sed 's/version = "\(.*\)"/\1/') printf "LITELLM_VERSION=%s" "$version" >> $GITHUB_ENV - name: Display version diff --git a/.github/workflows/redeploy_proxy.py b/.github/workflows/redeploy_proxy.py deleted file mode 100644 index ed46bef73a2..00000000000 --- a/.github/workflows/redeploy_proxy.py +++ /dev/null @@ -1,20 +0,0 @@ -""" - -redeploy_proxy.py -""" - -import os -import requests -import time - -# send a get request to this endpoint -deploy_hook1 = os.getenv("LOAD_TEST_REDEPLOY_URL1") -response = requests.get(deploy_hook1, timeout=20) - - -deploy_hook2 = os.getenv("LOAD_TEST_REDEPLOY_URL2") -response = requests.get(deploy_hook2, timeout=20) - -print("SENT GET REQUESTS to re-deploy proxy") -print("sleeeping.... for 60s") -time.sleep(60) diff --git a/.github/workflows/regenerate-poetry-lock.yml b/.github/workflows/regenerate-poetry-lock.yml deleted file mode 100644 index c0844f1c705..00000000000 --- a/.github/workflows/regenerate-poetry-lock.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Regenerate poetry.lock - -# Runs whenever pyproject.toml is merged into main (the most common cause of -# the "pyproject.toml changed significantly since poetry.lock was last generated" -# CI failure). Can also be triggered manually. -on: - push: - branches: - - main - paths: - - pyproject.toml - workflow_dispatch: - -permissions: - contents: write # needed to push the auto/regenerate-poetry-lock-* branch - pull-requests: write # needed to open the PR and enable auto-merge - -jobs: - regenerate-lock: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.11" - - - name: Install Poetry - run: pip install poetry - - - name: Regenerate poetry.lock - run: poetry lock - - - name: Check whether poetry.lock actually changed - id: diff - run: | - if git diff --quiet poetry.lock; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Open PR with the refreshed lock file - if: steps.diff.outputs.changed == 'true' - id: open-pr - run: | - BRANCH="auto/regenerate-poetry-lock-$(date +'%Y%m%d%H%M%S')" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git checkout -b "$BRANCH" - git add poetry.lock - git commit -m "chore: regenerate poetry.lock to match pyproject.toml" - git push -f origin "$BRANCH" - - cat > /tmp/pr-body.md << 'BODY' - Automated regeneration of `poetry.lock` after `pyproject.toml` was updated on `main`. - - Fixes the recurring CI failure: - ``` - pyproject.toml changed significantly since poetry.lock was last generated. - Run `poetry lock` to fix the lock file. - ``` - BODY - - PR_URL=$(gh pr create \ - --title "chore: regenerate poetry.lock to match pyproject.toml" \ - --body-file /tmp/pr-body.md \ - --head "$BRANCH" \ - --base main) - echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - env: - GH_TOKEN: ${{ github.token }} - - - name: Enable auto-merge - if: steps.diff.outputs.changed == 'true' - run: | - gh pr merge "${{ steps.open-pr.outputs.pr_url }}" --auto --squash - env: - GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/reset_stable.yml b/.github/workflows/reset_stable.yml deleted file mode 100644 index f6fed672d47..00000000000 --- a/.github/workflows/reset_stable.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Reset litellm_stable branch - -on: - release: - types: [published, created] -jobs: - update-stable-branch: - if: ${{ startsWith(github.event.release.tag_name, 'v') && !endsWith(github.event.release.tag_name, '-stable') }} - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Reset litellm_stable_release_branch branch to the release commit - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Configure Git user - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - # Fetch all branches and tags - git fetch --all - - # Check if the litellm_stable_release_branch branch exists - if git show-ref --verify --quiet refs/remotes/origin/litellm_stable_release_branch; then - echo "litellm_stable_release_branch branch exists." - git checkout litellm_stable_release_branch - else - echo "litellm_stable_release_branch branch does not exist. Creating it." - git checkout -b litellm_stable_release_branch - fi - - # Reset litellm_stable_release_branch branch to the release commit - git reset --hard $GITHUB_SHA - - # Push the updated litellm_stable_release_branch branch - git push origin litellm_stable_release_branch --force diff --git a/.github/workflows/run_observatory_tests.yml b/.github/workflows/run_observatory_tests.yml index d343098ed32..a25b96766d7 100644 --- a/.github/workflows/run_observatory_tests.yml +++ b/.github/workflows/run_observatory_tests.yml @@ -33,7 +33,9 @@ jobs: timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Validate tag input env: @@ -49,11 +51,12 @@ jobs: TAG: ${{ inputs.tag }} AZURE_API_KEY: ${{ secrets.AZURE_API_KEY }} AZURE_API_BASE: ${{ secrets.AZURE_API_BASE }} + WORKSPACE: ${{ github.workspace }} run: | docker run -d \ --name litellm-rc \ -p 4000:4000 \ - -v "${{ github.workspace }}/.github/observatory/litellm_config.yaml:/app/config.yaml" \ + -v "${WORKSPACE}/.github/observatory/litellm_config.yaml:/app/config.yaml" \ -e LITELLM_MASTER_KEY="${LITELLM_MASTER_KEY}" \ -e AZURE_API_KEY="${AZURE_API_KEY}" \ -e AZURE_API_BASE="${AZURE_API_BASE}" \ @@ -77,8 +80,9 @@ jobs: - name: Start cloudflared tunnel run: | - # Install cloudflared + # Install cloudflared (pinned version + checksum) curl -sL https://github.com/cloudflare/cloudflared/releases/download/2025.2.1/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared + echo "afdfadd1ef552e66bffc35246fe30a9bd578356d2d386de95585ccfc432472b8 /usr/local/bin/cloudflared" | sha256sum -c - chmod +x /usr/local/bin/cloudflared # Start a quick tunnel (no account needed) and capture the URL @@ -103,11 +107,11 @@ jobs: - name: Verify tunnel connectivity run: | - echo "Testing tunnel at ${{ env.TUNNEL_URL }}..." + echo "Testing tunnel at ${TUNNEL_URL}..." # Quick tunnels need time for DNS propagation; retry to avoid # transient NXDOMAIN (curl exit code 6) on first attempt. for i in $(seq 1 10); do - if curl -sf "${{ env.TUNNEL_URL }}/health/liveliness" > /dev/null 2>&1; then + if curl -sf "${TUNNEL_URL}/health/liveliness" > /dev/null 2>&1; then echo "Tunnel is working (attempt $i)" exit 0 fi @@ -221,5 +225,5 @@ jobs: - name: Cleanup if: always() run: | - kill "${{ env.CLOUDFLARED_PID }}" 2>/dev/null || true + kill "$CLOUDFLARED_PID" 2>/dev/null || true docker rm -f litellm-rc 2>/dev/null || true diff --git a/.github/workflows/scan_duplicate_issues.yml b/.github/workflows/scan_duplicate_issues.yml index 06e8f453a8c..222ff11f304 100644 --- a/.github/workflows/scan_duplicate_issues.yml +++ b/.github/workflows/scan_duplicate_issues.yml @@ -21,14 +21,15 @@ jobs: contents: read steps: - name: Checkout scripts - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: sparse-checkout: .github/scripts + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: - python-version: "3.11" + python-version: "3.13" - name: Scan for duplicate issues env: diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000000..7cd12bb219c --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,47 @@ +name: Scorecard supply-chain security + +on: + branch_protection_rule: + schedule: + - cron: '27 12 * * 4' + push: + branches: ["main"] + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + if: github.event.repository.default_branch == github.ref_name + permissions: + security-events: write + id-token: write + # Uncomment for private repos if needed: + # contents: read + # actions: read + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Run analysis + uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload artifact + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + - name: Upload to code scanning + uses: github/codeql-action/upload-sarif@c10b806170c8ee63ea24152429041b5624f0baf5 # v4.35.1 + with: + sarif_file: results.sarif diff --git a/.github/workflows/simple_pypi_publish.yml b/.github/workflows/simple_pypi_publish.yml deleted file mode 100644 index e1830556819..00000000000 --- a/.github/workflows/simple_pypi_publish.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Simple PyPI Publish - -on: - workflow_dispatch: - inputs: - version: - description: 'Version to publish (e.g., 1.74.10)' - required: true - type: string - -env: - TWINE_USERNAME: __token__ - -jobs: - publish: - runs-on: ubuntu-latest - if: github.repository == 'BerriAI/litellm' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.8' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install toml build wheel twine - - - name: Update version in pyproject.toml - run: | - python -c " - import toml - - with open('pyproject.toml', 'r') as f: - data = toml.load(f) - - data['tool']['poetry']['version'] = '${{ github.event.inputs.version }}' - - with open('pyproject.toml', 'w') as f: - toml.dump(data, f) - - print(f'Updated version to ${{ github.event.inputs.version }}') - " - - - name: Copy model prices file - run: | - cp model_prices_and_context_window.json litellm/model_prices_and_context_window_backup.json - - - name: Build package - run: | - rm -rf build dist - python -m build - - - name: Publish to PyPI - env: - TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_PASSWORD }} - run: | - twine upload dist/* - - - name: Output success - run: | - echo "✅ Successfully published litellm v${{ github.event.inputs.version }} to PyPI" - echo "📦 Package: https://pypi.org/project/litellm/${{ github.event.inputs.version }}/" \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 5a9b19fc9ca..c905bb12312 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,19 +2,24 @@ name: "Stale Issue Management" on: schedule: - - cron: '0 0 * * *' # Runs daily at midnight UTC + - cron: "0 0 * * *" # Runs daily at midnight UTC workflow_dispatch: +permissions: + issues: write + pull-requests: write + jobs: stale: + if: github.repository == 'BerriAI/litellm' runs-on: ubuntu-latest steps: - - uses: actions/stale@v8 + - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" stale-issue-message: "This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs." stale-pr-message: "This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs." - days-before-stale: 90 # Revert to 60 days - days-before-close: 7 # Revert to 7 days + days-before-stale: 90 # Revert to 60 days + days-before-close: 7 # Revert to 7 days stale-issue-label: "stale" - operations-per-run: 1000 \ No newline at end of file + operations-per-run: 1000 diff --git a/.github/workflows/sync-schema.yml b/.github/workflows/sync-schema.yml new file mode 100644 index 00000000000..72a5c56293e --- /dev/null +++ b/.github/workflows/sync-schema.yml @@ -0,0 +1,73 @@ +name: Sync schema.prisma copies + +on: + pull_request: + paths: + - 'schema.prisma' + +# Scoped to ONLY the permissions needed: +# - contents:write to push the sync commit to the PR branch +# - pull-requests:read is implicit (needed to check out the PR) +permissions: + contents: write + +jobs: + sync: + name: Copy root schema to proxy and proxy-extras + runs-on: ubuntu-latest + timeout-minutes: 5 + # Only run on PRs from branches in THIS repo (not forks). + # Fork PRs cannot push back to the head branch with GITHUB_TOKEN, + # and pull_request events from forks have read-only tokens anyway. + # Also reject PRs from branches named after protected branches to + # prevent pushing directly to main/master. + if: >- + github.event.pull_request.head.repo.full_name == github.repository + && github.head_ref != 'main' + && github.head_ref != 'master' + steps: + - name: Checkout PR branch by SHA + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + # Use the merge commit SHA for safety — github.head_ref is an + # attacker-controlled string (the branch name) and could contain + # unusual characters that cause unexpected git behavior. + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: true # needed for git push + + - name: Reject symlinked schema files + run: | + for f in schema.prisma litellm/proxy/schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma; do + if [ -L "$f" ]; then + echo "::error file=$f::$f is a symlink, which is not allowed" + exit 1 + fi + done + + - name: Copy root schema to other locations + run: | + cp schema.prisma litellm/proxy/schema.prisma + cp schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma + + - name: Check for changes + id: diff + run: | + if git diff --quiet -- litellm/proxy/schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "Schemas already in sync. Nothing to do." + else + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Schema copies need updating." + fi + + - name: Commit synced schemas + if: steps.diff.outputs.changed == 'true' + run: | + # Push to the PR's head branch (need the branch name for git push). + # We checked out by SHA above for safety, so configure the push target explicitly. + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -B "$GITHUB_HEAD_REF" + git add -- litellm/proxy/schema.prisma litellm-proxy-extras/litellm_proxy_extras/schema.prisma + git commit -m "chore: sync schema.prisma copies from root" + git push origin "HEAD:$GITHUB_HEAD_REF" diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index fc0f84a20d4..5bb85716a17 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -2,7 +2,10 @@ name: LiteLLM Linting on: pull_request: - branches: [ main ] + branches: [main] + +permissions: + contents: read jobs: lint: @@ -10,69 +13,73 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - clean: true + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 + clean: true + persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Install Poetry + run: pip install 'poetry==2.3.2' - - name: Clean Python cache - run: | - find . -type d -name "__pycache__" -exec rm -rf {} + || true - find . -name "*.pyc" -delete || true + - name: Clean Python cache + run: | + find . -type d -name "__pycache__" -exec rm -rf {} + || true + find . -name "*.pyc" -delete || true - - name: Install dependencies - run: | - poetry lock - poetry install --with dev + - name: Check poetry.lock is up to date + run: | + poetry check --lock || (echo "❌ poetry.lock is out of sync with pyproject.toml. Run 'poetry lock' locally and commit the result." && exit 1) - - name: Check Black formatting - run: | - cd litellm - poetry run black --check --exclude '/enterprise/' . - cd .. + - name: Install dependencies + run: | + poetry install --with dev - - name: Debug - Check file state - run: | - echo "Current branch:" - git branch --show-current - echo "Last 3 commits:" - git log --oneline -3 - echo "File content around line 43:" - head -50 litellm/litellm_core_utils/custom_logger_registry.py | tail -10 - - - name: Run Ruff linting - run: | - cd litellm - poetry run ruff check . - cd .. + - name: Check Black formatting + run: | + cd litellm + poetry run black --check --exclude '/enterprise/' . + cd .. - - name: Print OpenAI version - run: | - poetry run python -c "import openai; print(f'OpenAI version: {openai.__version__}')" + - name: Debug - Check file state + run: | + echo "Current branch:" + git branch --show-current + echo "Last 3 commits:" + git log --oneline -3 + echo "File content around line 43:" + head -50 litellm/litellm_core_utils/custom_logger_registry.py | tail -10 - - name: Run MyPy type checking - run: | - cd litellm - poetry run mypy . - cd .. + - name: Run Ruff linting + run: | + cd litellm + poetry run ruff check . + cd .. - - name: Check for circular imports - run: | - cd litellm - poetry run python ../tests/documentation_tests/test_circular_imports.py - cd .. + - name: Print OpenAI version + run: | + poetry run python -c "import openai; print(f'OpenAI version: {openai.__version__}')" - - name: Check import safety - run: | - poetry run python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1) + - name: Run MyPy type checking + run: | + cd litellm + poetry run mypy . + cd .. + + - name: Check for circular imports + run: | + cd litellm + poetry run python ../tests/documentation_tests/test_circular_imports.py + cd .. + + - name: Check import safety + run: | + poetry run python -c "from litellm import *" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1) secret-scan: runs-on: ubuntu-latest @@ -81,27 +88,28 @@ jobs: contents: read steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + fetch-depth: 0 + persist-credentials: false - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" - - name: Run secret scan test - run: | - pip install pytest - pytest tests/litellm/test_no_hardcoded_secrets.py -v + - name: Run secret scan test + run: | + pip install 'pytest==9.0.2' + pytest tests/litellm/test_no_hardcoded_secrets.py -v - - name: Run ggshield secret scan - env: - GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} - run: | - if [ -n "$GITGUARDIAN_API_KEY" ]; then - pip install ggshield - ggshield secret scan repo . - else - echo "GITGUARDIAN_API_KEY not set, skipping ggshield scan" - fi + - name: Run ggshield secret scan + env: + GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} + run: | + if [ -n "$GITGUARDIAN_API_KEY" ]; then + pip install 'ggshield==1.48.0' + ggshield secret scan repo . + else + echo "GITGUARDIAN_API_KEY not set, skipping ggshield scan" + fi diff --git a/.github/workflows/test-litellm-matrix.yml b/.github/workflows/test-litellm-matrix.yml index d0ac28ab41a..860d25636c5 100644 --- a/.github/workflows/test-litellm-matrix.yml +++ b/.github/workflows/test-litellm-matrix.yml @@ -4,6 +4,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + # Cancel in-progress runs for the same PR concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -12,7 +15,7 @@ concurrency: jobs: test: runs-on: ubuntu-latest - timeout-minutes: 20 # Increased from 15 to 20 + timeout-minutes: 20 # Increased from 15 to 20 strategy: fail-fast: false matrix: @@ -43,7 +46,7 @@ jobs: - name: "integrations" path: "tests/test_litellm/integrations" workers: 2 - reruns: 3 # Integration tests tend to be flakier + reruns: 3 # Integration tests tend to be flakier - name: "core-utils" path: "tests/test_litellm/litellm_core_utils" workers: 2 @@ -117,18 +120,20 @@ jobs: name: test (${{ matrix.test-group.name }}) steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install Poetry - uses: snok/install-poetry@v1 + run: pip install 'poetry==2.3.2' - name: Cache Poetry dependencies - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.0 with: path: | ~/.cache/pypoetry @@ -144,14 +149,17 @@ jobs: poetry install --with dev,proxy-dev --extras "proxy semantic-router" # pytest-rerunfailures and pytest-xdist are in pyproject.toml dev dependencies poetry run pip install google-genai==1.22.0 \ - google-cloud-aiplatform>=1.38 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core + google-cloud-aiplatform==1.115.0 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core==0.23.0 - name: Setup litellm-enterprise run: | poetry run pip install --force-reinstall --no-deps -e enterprise/ - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 poetry run prisma generate --schema litellm/proxy/schema.prisma - name: Run tests - ${{ matrix.test-group.name }} diff --git a/.github/workflows/test-litellm-ui-build.yml b/.github/workflows/test-litellm-ui-build.yml index b0a8b648a44..6b0b3a413a6 100644 --- a/.github/workflows/test-litellm-ui-build.yml +++ b/.github/workflows/test-litellm-ui-build.yml @@ -16,10 +16,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0 with: node-version: "20" cache: "npm" diff --git a/.github/workflows/test-litellm.yml b/.github/workflows/test-litellm.yml index 3f8369df926..0c040b3ebe7 100644 --- a/.github/workflows/test-litellm.yml +++ b/.github/workflows/test-litellm.yml @@ -4,45 +4,50 @@ name: LiteLLM Mock Tests (folder - tests/test_litellm) # the same tests in parallel across 10 jobs for faster CI times. # Kept for manual debugging only. on: - workflow_dispatch: # Manual trigger only + workflow_dispatch: # Manual trigger only # pull_request: # branches: [ main ] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest timeout-minutes: 25 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - - name: Thank You Message - run: | - echo "### 🙏 Thank you for contributing to LiteLLM!" >> $GITHUB_STEP_SUMMARY - echo "Your PR is being tested now. We appreciate your help in making LiteLLM better!" >> $GITHUB_STEP_SUMMARY + - name: Thank You Message + run: | + echo "### 🙏 Thank you for contributing to LiteLLM!" >> $GITHUB_STEP_SUMMARY + echo "Your PR is being tested now. We appreciate your help in making LiteLLM better!" >> $GITHUB_STEP_SUMMARY - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Install Poetry + run: pip install 'poetry==2.3.2' - - name: Install dependencies - run: | - poetry lock - poetry install --with dev,proxy-dev --extras "proxy semantic-router" - poetry run pip install "pytest-retry==1.6.3" - poetry run pip install pytest-xdist - poetry run pip install "google-genai==1.22.0" - poetry run pip install "google-cloud-aiplatform>=1.38" - poetry run pip install "fastapi-offline==1.7.3" - poetry run pip install "python-multipart>=0.0.20" - poetry run pip install "openapi-core" - - name: Setup litellm-enterprise as local package - run: | - poetry run pip install --force-reinstall --no-deps -e enterprise/ - - name: Run tests - run: | - poetry run pytest tests/test_litellm --tb=short -vv --maxfail=10 -n 4 --durations=50 + - name: Install dependencies + run: | + poetry lock + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install "pytest-retry==1.6.3" + poetry run pip install 'pytest-xdist==3.8.0' + poetry run pip install "google-genai==1.22.0" + poetry run pip install "google-cloud-aiplatform==1.115.0" + poetry run pip install "fastapi-offline==1.7.3" + poetry run pip install "python-multipart==0.0.22" + poetry run pip install "openapi-core==0.23.0" + - name: Setup litellm-enterprise as local package + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ + - name: Run tests + run: | + poetry run pytest tests/test_litellm --tb=short -vv --maxfail=10 -n 4 --durations=50 diff --git a/.github/workflows/test-mcp.yml b/.github/workflows/test-mcp.yml index 2e32aae7680..1b228ab76bb 100644 --- a/.github/workflows/test-mcp.yml +++ b/.github/workflows/test-mcp.yml @@ -2,7 +2,10 @@ name: LiteLLM MCP Tests (folder - tests/mcp_tests) on: pull_request: - branches: [ main ] + branches: [main] + +permissions: + contents: read jobs: test: @@ -10,38 +13,40 @@ jobs: timeout-minutes: 25 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - - name: Thank You Message - run: | - echo "### 🙏 Thank you for contributing to LiteLLM!" >> $GITHUB_STEP_SUMMARY - echo "Your PR is being tested now. We appreciate your help in making LiteLLM better!" >> $GITHUB_STEP_SUMMARY + - name: Thank You Message + run: | + echo "### 🙏 Thank you for contributing to LiteLLM!" >> $GITHUB_STEP_SUMMARY + echo "Your PR is being tested now. We appreciate your help in making LiteLLM better!" >> $GITHUB_STEP_SUMMARY - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" - - name: Install Poetry - uses: snok/install-poetry@v1 + - name: Install Poetry + run: pip install 'poetry==2.3.2' - - name: Install dependencies - run: | - poetry lock - poetry install --with dev,proxy-dev --extras "proxy semantic-router" - poetry run pip install "pytest==7.3.1" - poetry run pip install "pytest-retry==1.6.3" - poetry run pip install "pytest-cov==5.0.0" - poetry run pip install "pytest-asyncio==0.21.1" - poetry run pip install "respx==0.22.0" - poetry run pip install "pydantic==2.11.0" - poetry run pip install "mcp==1.25.0" - poetry run pip install pytest-xdist + - name: Install dependencies + run: | + poetry lock + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install "pytest==7.3.1" + poetry run pip install "pytest-retry==1.6.3" + poetry run pip install "pytest-cov==5.0.0" + poetry run pip install "pytest-asyncio==0.21.1" + poetry run pip install "respx==0.22.0" + poetry run pip install "pydantic==2.11.0" + poetry run pip install "mcp==1.25.0" + poetry run pip install 'pytest-xdist==3.8.0' - - name: Setup litellm-enterprise as local package - run: | - poetry run pip install --force-reinstall --no-deps -e enterprise/ + - name: Setup litellm-enterprise as local package + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ - - name: Run MCP tests - run: | - poetry run pytest tests/mcp_tests -x -vv -n 4 --cov=litellm --cov-report=xml --durations=5 + - name: Run MCP tests + run: | + poetry run pytest tests/mcp_tests -x -vv -n 4 --cov=litellm --cov-report=xml --durations=5 diff --git a/.github/workflows/test-model-map.yaml b/.github/workflows/test-model-map.yaml index ae5ac402e23..429f9e1ce0a 100644 --- a/.github/workflows/test-model-map.yaml +++ b/.github/workflows/test-model-map.yaml @@ -2,13 +2,18 @@ name: Validate model_prices_and_context_window.json on: pull_request: - branches: [ main ] + branches: [main] + +permissions: + contents: read jobs: validate-model-prices-json: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Validate model_prices_and_context_window.json run: | diff --git a/.github/workflows/test-proxy-e2e-azure-batches.yml b/.github/workflows/test-proxy-e2e-azure-batches.yml index 4d74f3db0ac..7cbbe0b338f 100644 --- a/.github/workflows/test-proxy-e2e-azure-batches.yml +++ b/.github/workflows/test-proxy-e2e-azure-batches.yml @@ -9,6 +9,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +permissions: + contents: read + jobs: proxy_e2e_azure_batches_tests: runs-on: ubuntu-latest @@ -30,18 +33,20 @@ jobs: --health-retries 5 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Install Poetry - uses: snok/install-poetry@v1 + run: pip install 'poetry==2.3.2' - name: Cache Poetry dependencies - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.0.0 with: path: | ~/.cache/pypoetry @@ -56,14 +61,17 @@ jobs: run: | poetry config virtualenvs.in-project true poetry install --with dev,proxy-dev --extras "proxy" - poetry run pip install psycopg2-binary uvicorn fastapi httpx tenacity + poetry run pip install psycopg2-binary==2.9.11 uvicorn==0.42.0 fastapi==0.135.2 httpx==0.28.1 tenacity==9.1.4 - name: Setup litellm-enterprise run: | poetry run pip install --force-reinstall --no-deps -e enterprise/ - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 poetry run prisma generate --schema litellm/proxy/schema.prisma - name: Run Prisma migrations @@ -87,4 +95,3 @@ jobs: --tb=short \ --maxfail=3 \ --durations=10 - diff --git a/.github/workflows/test-unit-caching-redis.yml b/.github/workflows/test-unit-caching-redis.yml new file mode 100644 index 00000000000..ca274324f2f --- /dev/null +++ b/.github/workflows/test-unit-caching-redis.yml @@ -0,0 +1,38 @@ +name: "Unit Tests: Caching (Redis)" + +# Uses cloud Redis credentials — only runs on trusted branches, not PRs. +# This prevents external PRs from accessing Redis credentials. +on: + push: + branches: [main, "litellm_*"] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + caching-redis: + uses: ./.github/workflows/_test-unit-services-base.yml + with: + # Redis-only tests that do NOT require provider API keys. + # Tests needing API keys (test_caching.py, test_caching_ssl.py, test_prometheus_service.py, + # test_router_caching.py) are in Phase 3 integration workflows. + test-path: >- + tests/local_testing/test_dual_cache.py + tests/local_testing/test_redis_batch_optimizations.py + tests/local_testing/test_router_utils.py + workers: 2 + reruns: 2 + timeout-minutes: 20 + enable-redis: true + enable-postgres: false + secrets: + REDIS_HOST: ${{ secrets.REDIS_HOST }} + REDIS_PORT: ${{ secrets.REDIS_PORT }} + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} diff --git a/.github/workflows/test-unit-core-utils.yml b/.github/workflows/test-unit-core-utils.yml new file mode 100644 index 00000000000..2f3698fdf60 --- /dev/null +++ b/.github/workflows/test-unit-core-utils.yml @@ -0,0 +1,20 @@ +name: "Unit Tests: Core Utilities" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + core-utils: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/litellm_core_utils" + workers: 2 + reruns: 1 diff --git a/.github/workflows/test-unit-documentation.yml b/.github/workflows/test-unit-documentation.yml new file mode 100644 index 00000000000..d8b30de6844 --- /dev/null +++ b/.github/workflows/test-unit-documentation.yml @@ -0,0 +1,67 @@ +name: "Unit Tests: Documentation Validation" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + documentation: + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install Poetry + run: pip install 'poetry==2.3.2' + + - name: Cache Poetry dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pypoetry + ~/.cache/pip + .venv + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install google-genai==1.22.0 \ + google-cloud-aiplatform==1.115.0 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core==0.23.0 + + - name: Setup litellm-enterprise + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ + + - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache + run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 + poetry run prisma generate --schema litellm/proxy/schema.prisma + + # Run the same documentation tests that CircleCI ran (as direct Python scripts) + - name: Run documentation validation tests + run: | + poetry run python ./tests/documentation_tests/test_env_keys.py + poetry run python ./tests/documentation_tests/test_router_settings.py + poetry run python ./tests/documentation_tests/test_api_docs.py + poetry run python ./tests/documentation_tests/test_circular_imports.py diff --git a/.github/workflows/test-unit-enterprise-routing.yml b/.github/workflows/test-unit-enterprise-routing.yml new file mode 100644 index 00000000000..13ae3efedba --- /dev/null +++ b/.github/workflows/test-unit-enterprise-routing.yml @@ -0,0 +1,24 @@ +name: "Unit Tests: Enterprise, Google GenAI & Routing" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + enterprise-routing: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: >- + tests/test_litellm/enterprise + tests/test_litellm/google_genai + tests/test_litellm/router_utils + tests/test_litellm/router_strategy + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-integrations.yml b/.github/workflows/test-unit-integrations.yml new file mode 100644 index 00000000000..2789f99d81c --- /dev/null +++ b/.github/workflows/test-unit-integrations.yml @@ -0,0 +1,20 @@ +name: "Unit Tests: Integrations (Callbacks & Logging)" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + integrations: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/integrations" + workers: 2 + reruns: 3 diff --git a/.github/workflows/test-unit-llm-providers.yml b/.github/workflows/test-unit-llm-providers.yml new file mode 100644 index 00000000000..6c00272b0c8 --- /dev/null +++ b/.github/workflows/test-unit-llm-providers.yml @@ -0,0 +1,29 @@ +name: "Unit Tests: LLM Provider Transformations" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + vertex-ai: + name: Vertex AI + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/llms/vertex_ai" + workers: 1 + reruns: 2 + + other-providers: + name: All Other Providers + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/llms --ignore=tests/test_litellm/llms/vertex_ai" + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-misc.yml b/.github/workflows/test-unit-misc.yml new file mode 100644 index 00000000000..9228decd7cc --- /dev/null +++ b/.github/workflows/test-unit-misc.yml @@ -0,0 +1,31 @@ +name: "Unit Tests: MCP, Secrets, Containers & Misc" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + misc: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: >- + tests/test_litellm/secret_managers + tests/test_litellm/a2a_protocol + tests/test_litellm/anthropic_interface + tests/test_litellm/completion_extras + tests/test_litellm/containers + tests/test_litellm/experimental_mcp_client + tests/test_litellm/images + tests/test_litellm/interactions + tests/test_litellm/passthrough + tests/test_litellm/vector_stores + tests/test_litellm/test_*.py + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-proxy-auth.yml b/.github/workflows/test-unit-proxy-auth.yml new file mode 100644 index 00000000000..e71821db701 --- /dev/null +++ b/.github/workflows/test-unit-proxy-auth.yml @@ -0,0 +1,20 @@ +name: "Unit Tests: Proxy Auth & Key Management" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + proxy-auth: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/proxy/auth tests/test_litellm/proxy/hooks tests/test_litellm/proxy/policy_engine tests/test_litellm/proxy/client" + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-proxy-db.yml b/.github/workflows/test-unit-proxy-db.yml new file mode 100644 index 00000000000..bdfb6efeef1 --- /dev/null +++ b/.github/workflows/test-unit-proxy-db.yml @@ -0,0 +1,45 @@ +name: "Unit Tests: Proxy DB Operations" + +# Uses DATABASE_URL secret — only runs on trusted branches, not PRs. +on: + push: + branches: [main, "litellm_*"] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + proxy-db: + strategy: + fail-fast: false + matrix: + include: + # Key generation tests must NOT run in parallel (event loop conflicts with logging worker) + - test-group: key-generation + test-path: "tests/proxy_unit_tests/test_key_generate_prisma.py" + workers: 0 + timeout: 30 + - test-group: auth-checks + test-path: "tests/proxy_unit_tests/test_auth_checks.py tests/proxy_unit_tests/test_user_api_key_auth.py" + workers: 8 + timeout: 20 + - test-group: remaining + test-path: "tests/proxy_unit_tests --ignore=tests/proxy_unit_tests/test_key_generate_prisma.py --ignore=tests/proxy_unit_tests/test_auth_checks.py --ignore=tests/proxy_unit_tests/test_user_api_key_auth.py" + workers: 8 + timeout: 20 + uses: ./.github/workflows/_test-unit-services-base.yml + with: + test-path: ${{ matrix.test-path }} + workers: ${{ matrix.workers }} + reruns: 2 + timeout-minutes: ${{ matrix.timeout }} + enable-redis: false + enable-postgres: true + secrets: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} diff --git a/.github/workflows/test-unit-proxy-endpoints.yml b/.github/workflows/test-unit-proxy-endpoints.yml new file mode 100644 index 00000000000..caff3b3ae06 --- /dev/null +++ b/.github/workflows/test-unit-proxy-endpoints.yml @@ -0,0 +1,35 @@ +name: "Unit Tests: Proxy API Endpoints" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + proxy-endpoints: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: >- + tests/test_litellm/proxy/management_endpoints + tests/test_litellm/proxy/guardrails + tests/test_litellm/proxy/management_helpers + tests/test_litellm/proxy/anthropic_endpoints + tests/test_litellm/proxy/google_endpoints + tests/test_litellm/proxy/openai_files_endpoint + tests/test_litellm/proxy/response_api_endpoints + tests/test_litellm/proxy/image_endpoints + tests/test_litellm/proxy/vector_store_endpoints + tests/test_litellm/proxy/agent_endpoints + tests/test_litellm/proxy/discovery_endpoints + tests/test_litellm/proxy/health_endpoints + tests/test_litellm/proxy/public_endpoints + tests/test_litellm/proxy/prompts + tests/test_litellm/proxy/ui_crud_endpoints + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-proxy-infra.yml b/.github/workflows/test-unit-proxy-infra.yml new file mode 100644 index 00000000000..4dfbbe317ed --- /dev/null +++ b/.github/workflows/test-unit-proxy-infra.yml @@ -0,0 +1,28 @@ +name: "Unit Tests: Proxy Infrastructure" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + proxy-infra: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: >- + tests/test_litellm/proxy/db + tests/test_litellm/proxy/middleware + tests/test_litellm/proxy/spend_tracking + tests/test_litellm/proxy/pass_through_endpoints + tests/test_litellm/proxy/_experimental + tests/test_litellm/proxy/experimental + tests/test_litellm/proxy/common_utils + tests/test_litellm/proxy/test_*.py + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-proxy-legacy.yml b/.github/workflows/test-unit-proxy-legacy.yml new file mode 100644 index 00000000000..a9391137263 --- /dev/null +++ b/.github/workflows/test-unit-proxy-legacy.yml @@ -0,0 +1,96 @@ +name: "Unit Tests: Proxy Legacy Tests" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + test-group: + - name: "auth-and-jwt" + path: "tests/proxy_unit_tests/test_[a-j]*.py" + - name: "key-generation" + path: "tests/proxy_unit_tests/test_[k-o]*.py" + - name: "proxy-config" + path: "tests/proxy_unit_tests/test_prisma*.py tests/proxy_unit_tests/test_project*.py tests/proxy_unit_tests/test_prompt*.py tests/proxy_unit_tests/test_proxy_[c-r]*.py" + - name: "proxy-server" + path: "tests/proxy_unit_tests/test_proxy_server.py" + - name: "proxy-server-extras" + path: "tests/proxy_unit_tests/test_proxy_server_*.py tests/proxy_unit_tests/test_proxy_setting_guardrails.py" + - name: "proxy-utils" + path: "tests/proxy_unit_tests/test_proxy_utils.py" + - name: "proxy-token-counter" + path: "tests/proxy_unit_tests/test_proxy_token_counter.py" + - name: "proxy-response-and-misc" + path: "tests/proxy_unit_tests/test_[r-t]*.py" + - name: "proxy-user-auth-and-spend" + path: "tests/proxy_unit_tests/test_[u-z]*.py" + + name: ${{ matrix.test-group.name }} + + steps: + - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + + - name: Install Poetry + run: pip install 'poetry==2.3.2' + + - name: Cache Poetry dependencies + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/pypoetry + ~/.cache/pip + .venv + key: ${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }} + restore-keys: | + ${{ runner.os }}-poetry- + + - name: Install dependencies + run: | + poetry config virtualenvs.in-project true + poetry install --with dev,proxy-dev --extras "proxy semantic-router" + poetry run pip install google-genai==1.22.0 \ + google-cloud-aiplatform==1.115.0 fastapi-offline==1.7.3 python-multipart==0.0.22 openapi-core==0.23.0 + + - name: Setup litellm-enterprise + run: | + poetry run pip install --force-reinstall --no-deps -e enterprise/ + + - name: Generate Prisma client + env: + PRISMA_BINARY_CACHE_DIR: ${{ runner.temp }}/prisma-cache + run: | + poetry run pip install nodejs-wheel-binaries==24.13.1 + poetry run prisma generate --schema litellm/proxy/schema.prisma + + - name: Run tests - ${{ matrix.test-group.name }} + env: + TEST_PATH: ${{ matrix.test-group.path }} + run: | + poetry run pytest ${TEST_PATH} \ + --tb=short -vv \ + --maxfail=10 \ + -n 2 \ + --reruns 1 \ + --reruns-delay 1 \ + --dist=loadscope \ + --durations=20 diff --git a/.github/workflows/test-unit-responses-caching-types.yml b/.github/workflows/test-unit-responses-caching-types.yml new file mode 100644 index 00000000000..7f3acac2803 --- /dev/null +++ b/.github/workflows/test-unit-responses-caching-types.yml @@ -0,0 +1,20 @@ +name: "Unit Tests: Responses, Caching & Types" + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + responses-caching-types: + uses: ./.github/workflows/_test-unit-base.yml + with: + test-path: "tests/test_litellm/responses tests/test_litellm/caching tests/test_litellm/types" + workers: 2 + reruns: 2 diff --git a/.github/workflows/test-unit-security.yml b/.github/workflows/test-unit-security.yml new file mode 100644 index 00000000000..b38c82b1c24 --- /dev/null +++ b/.github/workflows/test-unit-security.yml @@ -0,0 +1,28 @@ +name: "Unit Tests: Security" + +# Uses DATABASE_URL secret — only runs on trusted branches, not PRs. +on: + push: + branches: [main, "litellm_*"] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + security: + uses: ./.github/workflows/_test-unit-services-base.yml + with: + test-path: "tests/proxy_security_tests/" + workers: 1 + reruns: 2 + timeout-minutes: 20 + enable-redis: false + enable-postgres: true + secrets: + DATABASE_URL: ${{ secrets.DATABASE_URL }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} diff --git a/.github/workflows/test_server_root_path.yml b/.github/workflows/test_server_root_path.yml index c359e38bff9..47636ce8e92 100644 --- a/.github/workflows/test_server_root_path.yml +++ b/.github/workflows/test_server_root_path.yml @@ -17,13 +17,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12 - name: Build Docker image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 #v6.14 with: context: . file: ./docker/Dockerfile.non_root diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 00000000000..9a1e899fed5 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,31 @@ +name: GitHub Actions Security Analysis + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + security-events: write + contents: read + actions: read + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9396f323e45..2bc361bc48f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,12 +14,12 @@ repos: types: [python] files: (litellm/|litellm_proxy_extras/|enterprise/).*\.py exclude: ^litellm/__init__.py$ - # - id: black - # name: black - # entry: poetry run black - # language: system - # types: [python] - # files: (litellm/|litellm_proxy_extras/|enterprise/).*\.py + - id: black + name: black + entry: poetry run black + language: system + types: [python] + files: (litellm/|litellm_proxy_extras/).*\.py - repo: https://github.com/pycqa/flake8 rev: 7.0.0 # The version of flake8 to use hooks: diff --git a/.semgrep/rules/security/no-claude-directory.yml b/.semgrep/rules/security/no-claude-directory.yml new file mode 100644 index 00000000000..7d120a7c23c --- /dev/null +++ b/.semgrep/rules/security/no-claude-directory.yml @@ -0,0 +1,18 @@ +rules: + - id: no-claude-directory-committed + message: > + .claude/ directory must not be committed to the repository. + It contains local Claude Code settings (permissions, worktree paths) that are + developer-machine-specific and may expose internal paths or credentials. + Add .claude/ to .gitignore instead. + severity: ERROR + languages: [generic] + paths: + include: + - "/.claude/**" + - "/.claude/*" + pattern-regex: '[\s\S]+' + metadata: + category: security + tags: [supply-chain, secrets] + confidence: HIGH diff --git a/CLAUDE.md b/CLAUDE.md index d9061b5e2be..f0478120181 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -140,6 +140,11 @@ LiteLLM is a unified interface for 100+ LLM providers with two main components: - **Check index coverage.** For new or modified queries, check `schema.prisma` for a supporting index. Prefer extending an existing index (e.g. `@@index([a])` → `@@index([a, b])`) over adding a new one, unless it's a `@@unique`. Only add indexes for large/frequent queries. - **Keep schema files in sync.** Apply schema changes to all `schema.prisma` copies (`schema.prisma`, `litellm/proxy/`, `litellm-proxy-extras/`, `litellm-js/spend-logs/` for SpendLogs) with a migration under `litellm-proxy-extras/litellm_proxy_extras/migrations/`. +### Setup Wizard (`litellm/setup_wizard.py`) +- The wizard is implemented as a single `SetupWizard` class with `@staticmethod` methods — keep it that way. No module-level functions except `run_setup_wizard()` (the public entrypoint) and pure helpers (color, ANSI). +- Use `litellm.utils.check_valid_key(model, api_key)` for credential validation — never roll a custom completion call. +- Do not hardcode provider env-key names or model lists that already exist in the codebase. Add a `test_model` field to each provider entry to drive `check_valid_key`; set it to `None` for providers that can't be validated with a single API key (Azure, Bedrock, Ollama). + ### Enterprise Features - Enterprise-specific code in `enterprise/` directory - Optional features enabled via environment variables diff --git a/README.md b/README.md index 67f2f3a2048..a6dc597574e 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ Support for more providers. Missing a provider or LLM Platform, raise a [feature + @@ -402,7 +403,7 @@ Support for more providers. Missing a provider or LLM Platform, raise a [feature # Enterprise For companies that need better security, user management and professional support -[Talk to founders](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Talk to founders](https://enterprise.litellm.ai/demo) This covers: - ✅ **Features under the [LiteLLM Commercial License](https://docs.litellm.ai/docs/proxy/enterprise):** @@ -452,7 +453,6 @@ All these checks must pass before your PR can be merged. - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) - [Community Slack 💭](https://www.litellm.ai/support) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai # Why did we build this diff --git a/ci_cd/security_scans.sh b/ci_cd/security_scans.sh index e0f370e0035..061e454465a 100755 --- a/ci_cd/security_scans.sh +++ b/ci_cd/security_scans.sh @@ -10,13 +10,13 @@ echo "Starting security scans for LiteLLM..." # Function to install Trivy and required tools install_trivy() { echo "Installing Trivy and required tools..." + TRIVY_VERSION="0.35.0" sudo apt-get update - sudo apt-get install -y wget apt-transport-https gnupg lsb-release jq curl bsdmainutils - wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add - - echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee -a /etc/apt/sources.list.d/trivy.list - sudo apt-get update - sudo apt-get install trivy - echo "Trivy and required tools installed successfully" + sudo apt-get install -y wget jq curl bsdmainutils + wget -qO trivy.deb "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.deb" + sudo dpkg -i trivy.deb + rm trivy.deb + echo "Trivy ${TRIVY_VERSION} installed successfully" } # Function to install Grype @@ -163,6 +163,9 @@ run_grype_scans() { "CVE-2026-25639" # axios - full fix requires 1.x major version bump; pinned to >=0.30.2 to clear other axios CVEs, upgrade to 1.x in follow-up "CVE-2026-2297" # Python 3.13 SourcelessFileLoader audit hook bypass - no fix available in base image "GHSA-qffp-2rhf-9h96" # tar hardlink path traversal - from nodejs_wheel bundled npm, not used in application runtime code + "CVE-2026-2673" # OpenSSL 3.6.1 TLS 1.3 key exchange group negotiation issue - no fix available yet + "CVE-2026-3644" # Python 3.13 vulnerability - no fix available in base image + "CVE-2026-4224" # Python 3.13 Expat parser stack overflow in ElementDeclHandler - no fix available in base image ) # Build JSON array of allowlisted CVE IDs for jq diff --git a/cookbook/benchmark/readme.md b/cookbook/benchmark/readme.md index 57115eb96a9..afa59aa91ee 100644 --- a/cookbook/benchmark/readme.md +++ b/cookbook/benchmark/readme.md @@ -178,4 +178,4 @@ Benchmark Results for 'When will BerriAI IPO?': ``` ## Support -**🤝 Schedule a 1-on-1 Session:** Book a [1-on-1 session](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) with Krrish and Ishaan, the founders, to discuss any issues, provide feedback, or explore how we can improve LiteLLM for you. +**🤝 Schedule a 1-on-1 Session:** Book a [1-on-1 session](https://enterprise.litellm.ai/demo) with Krrish and Ishaan, the founders, to discuss any issues, provide feedback, or explore how we can improve LiteLLM for you. diff --git a/cookbook/codellama-server/README.MD b/cookbook/codellama-server/README.MD index b158bb083f2..82a7e62f40a 100644 --- a/cookbook/codellama-server/README.MD +++ b/cookbook/codellama-server/README.MD @@ -143,7 +143,6 @@ All responses from the server are returned in the following format (for all LLM - [Our calendar 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / +1 (412) 618-6238 - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai ## Roadmap diff --git a/cookbook/litellm_proxy_server/readme.md b/cookbook/litellm_proxy_server/readme.md index d0b0592c433..2c1eab72c24 100644 --- a/cookbook/litellm_proxy_server/readme.md +++ b/cookbook/litellm_proxy_server/readme.md @@ -164,7 +164,6 @@ All responses from the server are returned in the following format (for all LLM - [Our calendar 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / +1 (412) 618-6238 - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai ## Roadmap diff --git a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md index 8a54426dfb0..21ba3d60790 100644 --- a/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md +++ b/docs/my-website/blog/anthropic_opus_4_5_and_advanced_features/index.md @@ -3,18 +3,9 @@ slug: anthropic_advanced_features title: "Day 0 Support: Claude 4.5 Opus (+Advanced Features)" date: 2025-11-25T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Guide to Claude Opus 4.5 and advanced features in LiteLLM: Tool Search, Programmatic Tool Calling, and Effort Parameter." tags: [anthropic, claude, tool search, programmatic tool calling, effort, advanced features] hide_table_of_contents: false @@ -25,6 +16,8 @@ import TabItem from '@theme/TabItem'; This guide covers Anthropic's latest model (Claude Opus 4.5) and its advanced features now available in LiteLLM: Tool Search, Programmatic Tool Calling, Tool Input Examples, and the Effort Parameter. +{/* truncate */} + --- | Feature | Supported Models | diff --git a/docs/my-website/blog/anthropic_wildcard_model_access_incident/index.md b/docs/my-website/blog/anthropic_wildcard_model_access_incident/index.md index f6172cd6744..8d58e18e580 100644 --- a/docs/my-website/blog/anthropic_wildcard_model_access_incident/index.md +++ b/docs/my-website/blog/anthropic_wildcard_model_access_incident/index.md @@ -3,18 +3,9 @@ slug: anthropic-wildcard-model-access-incident title: "Incident Report: Wildcard Blocking New Models After Cost Map Reload" date: 2026-02-23T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt tags: [incident-report, proxy, auth, model-access] hide_table_of_contents: false --- diff --git a/docs/my-website/blog/authors.yml b/docs/my-website/blog/authors.yml index 2a49a736333..1b1ef4d34c4 100644 --- a/docs/my-website/blog/authors.yml +++ b/docs/my-website/blog/authors.yml @@ -4,6 +4,12 @@ litellm: url: https://github.com/BerriAI/litellm image_url: https://github.com/BerriAI.png +sameer: + name: Sameer Kankute + title: SWE @ LiteLLM (LLM Translation) + url: https://www.linkedin.com/in/sameer-kankute/ + image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg + krrish: name: Krrish Dholakia title: CEO, LiteLLM @@ -22,3 +28,21 @@ ishaan-alt: title: CTO, LiteLLM url: https://www.linkedin.com/in/reffajnaahsi/ image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + +ryan: + name: Ryan Crabbe + title: Performance Engineer, LiteLLM + url: https://www.linkedin.com/in/ryan-crabbe-0b9687214 + image_url: https://media.licdn.com/dms/image/v2/D5603AQHt1t9Z4BJ6Gw/profile-displayphoto-shrink_400_400/profile-displayphoto-shrink_400_400/0/1724453682340?e=1772064000&v=beta&t=VXdmr13rsNB05wyA2F1TENOB5UuDHUZ0FCHTolNyR5M + +alexsander: + name: Alexsander Hamir + title: Performance Engineer, LiteLLM + url: https://www.linkedin.com/in/alexsander-baptista/ + image_url: https://github.com/AlexsanderHamir.png + +yuneng: + name: Yuneng Jiang + title: SWE @ LiteLLM (Full Stack) + url: https://www.linkedin.com/in/yuneng-david-jiang-455676139/ + image_url: https://avatars.githubusercontent.com/u/171294688?v=4 diff --git a/docs/my-website/blog/ci_cd_v2_improvements/index.md b/docs/my-website/blog/ci_cd_v2_improvements/index.md new file mode 100644 index 00000000000..84f8f7bda6b --- /dev/null +++ b/docs/my-website/blog/ci_cd_v2_improvements/index.md @@ -0,0 +1,55 @@ +--- +slug: ci-cd-v2-improvements +title: "Announcing CI/CD v2 for LiteLLM" +date: 2026-03-30T21:30:00 +authors: + - krrish +description: "CI/CD v2 introduces isolated environments, stronger security gates, and safer release separation for LiteLLM." +tags: [engineering, ci-cd, security] +hide_table_of_contents: false +--- + +import Image from '@theme/IdealImage'; + +The CI/CD v2 is now live for LiteLLM. + + + +
+Building on the roadmap from our [security incident](https://docs.litellm.ai/blog/security-townhall-updates#roadmap), CI/CD v2 introduces isolated environments, stronger security gates, and safer release separation for LiteLLM. + +## What changed + +- Security scans and unit tests run in isolated environments. +- Validation and release are separated into different repositories, making it harder for an attacker to reach release credentials. +- Trusted Publishing for PyPI releases - this means no long-lived credentials are used to publish releases. +- Immutable Docker release tags - this means no tampering of Docker release tags after they are published [Learn more](https://docs.docker.com/docker-hub/repos/manage/hub-images/immutable-tags/). Note: work for GHCR docker releases is planned as well. + +## What's next + +Moving forward, we plan on: +- Adopting OpenSSF (this is a set of security criteria that projects should meet to demonstrate a strong security posture - [Learn more](https://baseline.openssf.org/versions/2026-02-19.html)) + - We've added Scorecard and Allstar to our Github + +- Adding SLSA Build Provenance to our CI/CD pipeline - this means we allow users to independently verify that a release came from us and prevent silent modifications of releases after they are published. + + +We hope that this will mean you can be confident that the releases you are using are safe and from us. + + +## The principle + +The new CI/CD pipeline reflects the principles, outlined below, and is designed to be more secure and reliable: + +- **Limit** what each package can access +- **Reduce** the number of sensitive environment variables +- **Avoid** compromised packages +- **Prevent** release tampering + + +## How to help: + +Help us plan April's stability sprint - https://github.com/BerriAI/litellm/issues/24825 \ No newline at end of file diff --git a/docs/my-website/blog/claude_code_beta_headers/index.md b/docs/my-website/blog/claude_code_beta_headers/index.md index 44567f616aa..ee07da79397 100644 --- a/docs/my-website/blog/claude_code_beta_headers/index.md +++ b/docs/my-website/blog/claude_code_beta_headers/index.md @@ -3,18 +3,9 @@ slug: claude-code-beta-headers-incident title: "Incident Report: Invalid beta headers with Claude Code" date: 2026-02-16T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - sameer + - ishaan-alt + - krrish tags: [incident-report, anthropic, stability] hide_table_of_contents: false --- @@ -173,5 +164,5 @@ curl -X POST "https://your-proxy-url/reload/anthropic_beta_headers" \ ## Related documentation -- [Managing Anthropic Beta Headers](../proxy/sync_anthropic_beta_headers.md) - Complete configuration guide +- [Managing Anthropic Beta Headers](../../docs/proxy/sync_anthropic_beta_headers) - Complete configuration guide - [`anthropic_beta_headers_config.json`](https://github.com/BerriAI/litellm/blob/main/litellm/anthropic_beta_headers_config.json) - Current configuration file diff --git a/docs/my-website/blog/claude_opus_4_6/index.md b/docs/my-website/blog/claude_opus_4_6/index.md index e44420bd570..eeb5d5ff2f8 100644 --- a/docs/my-website/blog/claude_opus_4_6/index.md +++ b/docs/my-website/blog/claude_opus_4_6/index.md @@ -3,18 +3,9 @@ slug: claude_opus_4_6 title: "Day 0 Support: Claude Opus 4.6" date: 2026-02-05T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - sameer + - ishaan-alt + - krrish description: "Day 0 support for Claude Opus 4.6 on LiteLLM AI Gateway - use across Anthropic, Azure, Vertex AI, and Bedrock." tags: [anthropic, claude, opus 4.6] hide_table_of_contents: false @@ -25,6 +16,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports Claude Opus 4.6 on Day 0. Use it across Anthropic, Azure, Vertex AI, and Bedrock through the LiteLLM AI Gateway. +{/* truncate */} + ## Docker Image ```bash diff --git a/docs/my-website/blog/claude_sonnet_4_6/index.md b/docs/my-website/blog/claude_sonnet_4_6/index.md index df54fa09792..12446c82c60 100644 --- a/docs/my-website/blog/claude_sonnet_4_6/index.md +++ b/docs/my-website/blog/claude_sonnet_4_6/index.md @@ -3,14 +3,8 @@ slug: claude_sonnet_4_6 title: "Day 0 Support: Claude Sonnet 4.6" date: 2026-02-17T10:00:00 authors: - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - ishaan-alt + - krrish description: "Day 0 support for Claude Sonnet 4.6 on LiteLLM AI Gateway - use across Anthropic, Azure, Vertex AI, and Bedrock." tags: [anthropic, claude, sonnet 4.6] hide_table_of_contents: false @@ -21,6 +15,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports Claude Sonnet 4.6 on Day 0. Use it across Anthropic, Azure, Vertex AI, and Bedrock through the LiteLLM AI Gateway. +{/* truncate */} + ## Docker Image ```bash diff --git a/docs/my-website/blog/fastapi_middleware_performance/index.mdx b/docs/my-website/blog/fastapi_middleware_performance/index.mdx index b0c5ba13634..e373326c071 100644 --- a/docs/my-website/blog/fastapi_middleware_performance/index.mdx +++ b/docs/my-website/blog/fastapi_middleware_performance/index.mdx @@ -3,18 +3,9 @@ slug: fastapi-middleware-performance title: "Your Middleware Could Be a Bottleneck" date: 2026-02-07T10:00:00 authors: - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Ryan Crabbe - title: "Performance Engineer, LiteLLM" - url: https://www.linkedin.com/in/ryan-crabbe-0b9687214 - image_url: https://media.licdn.com/dms/image/v2/D5603AQHt1t9Z4BJ6Gw/profile-displayphoto-shrink_400_400/profile-displayphoto-shrink_400_400/0/1724453682340?e=1772064000&v=beta&t=VXdmr13rsNB05wyA2F1TENOB5UuDHUZ0FCHTolNyR5M + - krrish + - ishaan-alt + - ryan description: "How we improved LiteLLM proxy latency and throughput by replacing a single middleware base class" tags: [performance, fastapi, middleware] hide_table_of_contents: false diff --git a/docs/my-website/blog/gemin_3.1/index.md b/docs/my-website/blog/gemin_3.1/index.md index b81595e4bd5..0afccb49d98 100644 --- a/docs/my-website/blog/gemin_3.1/index.md +++ b/docs/my-website/blog/gemin_3.1/index.md @@ -3,18 +3,9 @@ slug: gemini_3_1_pro title: "DAY 0 Support: Gemini 3.1 Pro on LiteLLM" date: 2026-02-19T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Guide to using Gemini 3.1 Pro on LiteLLM Proxy and SDK with day 0 support." tags: [gemini, day 0 support, llms] hide_table_of_contents: false @@ -28,6 +19,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports `gemini-3.1-pro-preview` and all the new API changes along with it. +{/* truncate */} + ## Deploy this version @@ -67,7 +60,7 @@ LiteLLM provides **full end-to-end support** for Gemini 3.1 Pro on: - ✅ `/v1/chat/completions` - OpenAI-compatible chat completions endpoint - ✅ `/v1/responses` - OpenAI Responses API endpoint (streaming and non-streaming) - ✅ [`/v1/messages`](../../docs/anthropic_unified) - Anthropic-compatible messages endpoint -- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent.md) compatible endpoint +- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent) compatible endpoint All endpoints support: - Streaming and non-streaming responses @@ -147,4 +140,3 @@ curl -X POST http://localhost:4000/v1/chat/completions \ | `high` | `high` | | `disable` | `minimal` | | `none` | `minimal` | - diff --git a/docs/my-website/blog/gemini_3/index.md b/docs/my-website/blog/gemini_3/index.md index 7263acc12c9..a5b94382b6f 100644 --- a/docs/my-website/blog/gemini_3/index.md +++ b/docs/my-website/blog/gemini_3/index.md @@ -3,18 +3,9 @@ slug: gemini_3 title: "DAY 0 Support: Gemini 3 on LiteLLM" date: 2025-11-19T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Common questions and best practices for using gemini-3-pro-preview with LiteLLM Proxy and SDK." tags: [gemini, day 0 support, llms] hide_table_of_contents: false @@ -29,6 +20,8 @@ This guide covers common questions and best practices for using `gemini-3-pro-pr ::: +{/* truncate */} + ## Quick Start @@ -976,8 +969,7 @@ messages.append(response.choices[0].message) # ✅ Includes thought signatures ## Additional Resources -- [Gemini Provider Documentation](../gemini.md) -- [Thought Signatures Guide](../gemini.md#thought-signatures) -- [Reasoning Content Documentation](../../reasoning_content.md) -- [Function Calling Guide](../../function_calling.md) - +- [Gemini Provider Documentation](../../docs/providers/gemini) +- [Thought Signatures Guide](../../docs/providers/gemini#thought-signatures) +- [Reasoning Content Documentation](../../docs/reasoning_content) +- [Function Calling Guide](../../docs/completion/function_call) diff --git a/docs/my-website/blog/gemini_3_1_flash_lite/index.md b/docs/my-website/blog/gemini_3_1_flash_lite/index.md index 9ef4bacb2ad..0ae79e5fa67 100644 --- a/docs/my-website/blog/gemini_3_1_flash_lite/index.md +++ b/docs/my-website/blog/gemini_3_1_flash_lite/index.md @@ -3,18 +3,9 @@ slug: gemini_3_1_flash_lite_preview title: "DAY 0 Support: Gemini 3.1 Flash Lite Preview on LiteLLM" date: 2026-03-03T08:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Guide to using Gemini 3.1 Flash Lite Preview on LiteLLM Proxy and SDK with day 0 support." tags: [gemini, day 0 support, llms, supernova] hide_table_of_contents: false @@ -32,6 +23,8 @@ LiteLLM now supports `gemini-3.1-flash-lite-preview` with full day 0 support! If you only want cost tracking, you need no change in your current Litellm version. But if you want the support for new features introduced along with it like thinking levels, you will need to use v1.80.8-stable.1 or above. ::: +{/* truncate */} + ## Deploy this version @@ -150,7 +143,7 @@ LiteLLM provides **full end-to-end support** for Gemini 3.1 Flash Lite Preview o - ✅ `/v1/chat/completions` - OpenAI-compatible chat completions endpoint - ✅ `/v1/responses` - OpenAI Responses API endpoint (streaming and non-streaming) - ✅ [`/v1/messages`](../../docs/anthropic_unified) - Anthropic-compatible messages endpoint -- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent.md) compatible endpoint +- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent) compatible endpoint All endpoints support: - Streaming and non-streaming responses @@ -172,4 +165,4 @@ LiteLLM automatically maps OpenAI's `reasoning_effort` parameter to Gemini's `th | `medium` | `medium` | Balanced reasoning for moderate complexity | | `high` | `high` | Maximum reasoning depth, complex problems | | `disable` | `minimal` | Disable extended reasoning | -| `none` | `minimal` | No extended reasoning | \ No newline at end of file +| `none` | `minimal` | No extended reasoning | diff --git a/docs/my-website/blog/gemini_3_flash/index.md b/docs/my-website/blog/gemini_3_flash/index.md index 830c21e5f66..5e98d2136b4 100644 --- a/docs/my-website/blog/gemini_3_flash/index.md +++ b/docs/my-website/blog/gemini_3_flash/index.md @@ -3,18 +3,9 @@ slug: gemini_3_flash title: "DAY 0 Support: Gemini 3 Flash on LiteLLM" date: 2025-12-17T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Guide to using Gemini 3 Flash on LiteLLM Proxy and SDK with day 0 support." tags: [gemini, day 0 support, llms] hide_table_of_contents: false @@ -32,6 +23,8 @@ LiteLLM now supports `gemini-3-flash-preview` and all the new API changes along If you only want cost tracking, you need no change in your current Litellm version. But if you want the support for new features introduced along with it like thinking levels, you will need to use v1.80.8-stable.1 or above. ::: +{/* truncate */} + ## Deploy this version @@ -80,7 +73,7 @@ LiteLLM provides **full end-to-end support** for Gemini 3 Flash on: - ✅ `/v1/chat/completions` - OpenAI-compatible chat completions endpoint - ✅ `/v1/responses` - OpenAI Responses API endpoint (streaming and non-streaming) - ✅ [`/v1/messages`](../../docs/anthropic_unified) - Anthropic-compatible messages endpoint -- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent.md) compatible endpoint +- ✅ `/v1/generateContent` – [Google Gemini API](../../docs/generateContent) compatible endpoint All endpoints support: - Streaming and non-streaming responses - Function calling with thought signatures @@ -252,4 +245,3 @@ If using this model via vertex_ai, keep the location as global as this is the on | `high` | `high` | | `disable` | `minimal` | | `none` | `minimal` | - diff --git a/docs/my-website/blog/gemini_embedding_2_multimodal/index.md b/docs/my-website/blog/gemini_embedding_2_multimodal/index.md index 8c09432e3b6..d66de1b6c79 100644 --- a/docs/my-website/blog/gemini_embedding_2_multimodal/index.md +++ b/docs/my-website/blog/gemini_embedding_2_multimodal/index.md @@ -3,10 +3,7 @@ slug: gemini_embedding_2_multimodal title: "Gemini Embedding 2 Preview: Multimodal Embeddings on LiteLLM" date: 2025-03-11T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg + - sameer description: "Generate embeddings from text, images, audio, video, and PDFs with gemini-embedding-2-preview on LiteLLM via Gemini API and Vertex AI." tags: [gemini, embeddings, multimodal, vertex ai] hide_table_of_contents: false @@ -19,6 +16,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports **multimodal embeddings** with `gemini-embedding-2-preview`—generating a single embedding from a mix of text, images, audio, video, and PDF content. Available via both the **Gemini API** (API key) and **Vertex AI** (GCP credentials). +{/* truncate */} + ## Supported Input Types | Modality | Supported Formats | diff --git a/docs/my-website/blog/gpt_5_3_codex/index.md b/docs/my-website/blog/gpt_5_3_codex/index.md index 850586538f6..1dfab1f7ac7 100644 --- a/docs/my-website/blog/gpt_5_3_codex/index.md +++ b/docs/my-website/blog/gpt_5_3_codex/index.md @@ -3,18 +3,9 @@ slug: gpt_5_3_codex title: "Day 0 Support: GPT-5.3-Codex" date: 2026-02-24T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Day 0 support for GPT-5.3-Codex on LiteLLM, including phase parameter handling for Responses API." tags: [openai, gpt-5.3-codex, codex, day 0 support] hide_table_of_contents: false @@ -25,6 +16,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports GPT-5.3-Codex on Day 0, including support for the new assistant `phase` metadata on Responses API output items. +{/* truncate */} + ## Why `phase` matters for GPT-5.3-Codex `phase` appears on assistant output items and helps distinguish preamble/commentary turns from final closeout responses. diff --git a/docs/my-website/blog/gpt_5_4/index.md b/docs/my-website/blog/gpt_5_4/index.md index de099736f00..4f7e4344157 100644 --- a/docs/my-website/blog/gpt_5_4/index.md +++ b/docs/my-website/blog/gpt_5_4/index.md @@ -3,18 +3,9 @@ slug: gpt_5_4 title: "Day 0 Support: GPT-5.4" date: 2026-03-05T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "GPT-5.4 model support in LiteLLM" tags: [openai, gpt-5.4, completion] hide_table_of_contents: false @@ -25,6 +16,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports fully GPT-5.4! +{/* truncate */} + ## Docker Image ```bash diff --git a/docs/my-website/blog/gpt_5_4_mini_nano/index.md b/docs/my-website/blog/gpt_5_4_mini_nano/index.md new file mode 100644 index 00000000000..6d7c2b33f72 --- /dev/null +++ b/docs/my-website/blog/gpt_5_4_mini_nano/index.md @@ -0,0 +1,106 @@ +--- +slug: gpt_5_4_mini_nano +title: "Day 0 Support: GPT-5.4-mini and GPT-5.4-nano" +date: 2026-03-17T10:00:00 +authors: + - name: Sameer Kankute + title: SWE @ LiteLLM (LLM Translation) + url: https://www.linkedin.com/in/sameer-kankute/ + image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg + - name: Krrish Dholakia + title: "CEO, LiteLLM" + url: https://www.linkedin.com/in/krish-d/ + image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - name: Ishaan Jaff + title: "CTO, LiteLLM" + url: https://www.linkedin.com/in/reffajnaahsi/ + image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg +description: "GPT-5.4-mini and GPT-5.4-nano model support in LiteLLM" +tags: [openai, gpt-5.4-mini, gpt-5.4-nano, completion] +hide_table_of_contents: false +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +LiteLLM now supports GPT-5.4-mini and GPT-5.4-nano — cost-effective models for simple completions and high-throughput workloads. + +:::note +If you're on **v1.82.3-stable** or above, you don't need any update to use these models. +::: + +## Usage + + + + +**1. Setup config.yaml** + +```yaml +model_list: + - model_name: gpt-5.4-mini + litellm_params: + model: openai/gpt-5.4-mini + api_key: os.environ/OPENAI_API_KEY + - model_name: gpt-5.4-nano + litellm_params: + model: openai/gpt-5.4-nano + api_key: os.environ/OPENAI_API_KEY +``` + +**2. Start the proxy** + +```bash +litellm --config /path/to/config.yaml +``` + +**3. Test it** + +```bash +# GPT-5.4-mini +curl -X POST "http://localhost:4000/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_KEY" \ + -d '{ + "model": "gpt-5.4-mini", + "messages": [{"role": "user", "content": "What is the capital of France?"}] + }' + +# GPT-5.4-nano +curl -X POST "http://localhost:4000/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $LITELLM_KEY" \ + -d '{ + "model": "gpt-5.4-nano", + "messages": [{"role": "user", "content": "What is 2 + 2?"}] + }' +``` + + + + +```python +from litellm import completion + +# GPT-5.4-mini +response = completion( + model="openai/gpt-5.4-mini", + messages=[{"role": "user", "content": "What is the capital of France?"}], +) +print(response.choices[0].message.content) + +# GPT-5.4-nano +response = completion( + model="openai/gpt-5.4-nano", + messages=[{"role": "user", "content": "What is 2 + 2?"}], +) +print(response.choices[0].message.content) +``` + + + + +## Notes + +- Both models support function calling, vision, and tool-use — see the [OpenAI provider docs](../../docs/providers/openai) for advanced usage. +- GPT-5.4-nano is the most cost-effective option for simple tasks; GPT-5.4-mini offers a balance of speed and capability. diff --git a/docs/my-website/blog/guardrail_logging_secret_exposure_incident/index.md b/docs/my-website/blog/guardrail_logging_secret_exposure_incident/index.md new file mode 100644 index 00000000000..71f9e3da011 --- /dev/null +++ b/docs/my-website/blog/guardrail_logging_secret_exposure_incident/index.md @@ -0,0 +1,78 @@ +--- +slug: guardrail-logging-secret-exposure-incident +title: "Incident Report: Guardrail logging exposed secret headers in spend logs and traces" +date: 2026-03-18T10:00:00 +authors: + - litellm +tags: [incident-report, security, guardrails] +hide_table_of_contents: false +--- + +**Date:** March 18, 2026 +**Duration:** Unknown +**Severity:** High +**Status:** Resolved + +## Summary + +When a custom guardrail returned the full LiteLLM request/data dictionary, the guardrail response logged by LiteLLM could include `secret_fields.raw_headers`, including plaintext `Authorization` headers containing API keys or other credentials. + +This information could then propagate to logging and observability surfaces that consume guardrail metadata, including: + +- **Spend logs in the LiteLLM UI:** visible to admins with access to spend-log data +- **OpenTelemetry traces:** visible to anyone with access to the relevant telemetry backend + +LLM calls, proxy routing, and provider execution were not blocked by this bug. The impact was exposure of sensitive request headers in observability and logging paths. + +{/* truncate */} + +--- + +## Background + +LiteLLM keeps internal request data (including request headers) for use during the call. That data is not meant to be written to logs or telemetry. + +When custom guardrails run, their outcomes are logged so they can appear in spend logs, OpenTelemetry traces, and other observability backends. If a guardrail returned the full request payload instead of a minimal result, that internal request data could be included in what was logged. Before the fix, the guardrail logging path did not strip that data before sending it to those systems. + +```mermaid +flowchart TD + inboundRequest["1. Incoming proxy request"] --> storeSecrets["2. Store internal request data"] + storeSecrets --> guardrailRuns["3. Custom guardrail runs"] + guardrailRuns --> fullDataReturn["4. Guardrail returns full request payload"] + fullDataReturn --> loggingBuild["5. Build guardrail log payload"] + loggingBuild --> spendLogs["6a. Persist to spend logs / UI"] + loggingBuild --> otelTraces["6b. Attach to OTEL guardrail spans"] +``` + +--- + +## Root Cause + +The root cause was incomplete sanitization in the guardrail logging path. When building the payload that gets sent to spend logs and traces, LiteLLM prepared guardrail responses for logging but did not strip internal request data (such as headers) from them. If a guardrail returned a response that included that data, it was passed through to the logging and observability systems unchanged. + +--- + +## Impact + +This issue required all of the following: + +1. A custom guardrail returned the full LiteLLM request/data dictionary, or another response object containing `secret_fields`. +2. LiteLLM logged that guardrail response through the standard guardrail logging path. +3. An operator, admin, or telemetry consumer had access to the resulting logs or traces. + +When those conditions were met, sensitive values could become visible through: + +- **Spend logs / UI responses:** guardrail metadata could be included in spend-log payloads rendered in the admin UI. +- **OpenTelemetry traces:** `guardrail_response` could be written as a span attribute on guardrail spans. +- **Other downstream observability backends:** any integration consuming the same guardrail metadata could receive the leaked values. + +This was a logging and telemetry exposure bug. It did not let callers bypass auth, access other tenants directly, or change model behavior, but it could expose plaintext credentials to people with access to those observability systems. + +--- + +## Guidance For Users + +- Upgrade to LiteLLM 1.82.3+. +- If you operated custom guardrails that return the full request/data dict, review whether spend logs or telemetry traces were retained during the affected period. +- Rotate any credentials that may have appeared in `Authorization` or other forwarded request headers in those systems. +- Apply least-privilege access controls to spend-log views and telemetry backends that may contain request-derived metadata. diff --git a/docs/my-website/blog/httpx_cache_eviction_incident/index.md b/docs/my-website/blog/httpx_cache_eviction_incident/index.md index 9e6152d0e63..7fc3789a91d 100644 --- a/docs/my-website/blog/httpx_cache_eviction_incident/index.md +++ b/docs/my-website/blog/httpx_cache_eviction_incident/index.md @@ -3,17 +3,9 @@ slug: httpx-cache-eviction-incident title: "Incident Report: Cache Eviction Closes In-Use httpx Clients" date: 2026-02-27T10:00:00 authors: - - name: Ryan Crabbe - title: Performance Engineer, LiteLLM - url: https://www.linkedin.com/in/ryan-crabbe-0b9687214 - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - ryan + - ishaan-alt + - krrish tags: [incident-report, caching, stability] hide_table_of_contents: false --- @@ -31,6 +23,8 @@ A change to improve Redis connection pool cleanup introduced a regression that c **Impact:** Any proxy instance that hit the cache TTL (default 10 minutes) or capacity limit (200 entries) would have its httpx clients closed out from under it, causing requests to LLM providers to fail with connection errors. +{/* truncate */} + --- ## Background diff --git a/docs/my-website/blog/litellm_observatory/index.md b/docs/my-website/blog/litellm_observatory/index.md index 4554f77fb85..36366e5de22 100644 --- a/docs/my-website/blog/litellm_observatory/index.md +++ b/docs/my-website/blog/litellm_observatory/index.md @@ -3,18 +3,9 @@ slug: litellm-observatory title: "Improve release stability with 24 hour load tests" date: 2026-02-06T10:00:00 authors: - - name: Alexsander Hamir - title: "Performance Engineer, LiteLLM" - url: https://www.linkedin.com/in/alexsander-baptista/ - image_url: https://github.com/AlexsanderHamir.png - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - alexsander + - krrish + - ishaan-alt description: "How we built a long-running, release-validation system to catch regressions before they reach users." tags: [testing, observability, reliability, releases] hide_table_of_contents: false @@ -28,6 +19,8 @@ As LiteLLM adoption has grown, so have expectations around reliability, performa This post introduces **LiteLLM Observatory**, a long-running release-validation system we built to catch regressions before they reach users. +{/* truncate */} + --- ## Why We Built the Observatory @@ -133,4 +126,3 @@ Reliability is an ongoing investment. LiteLLM Observatory is one of several systems we’re building to continuously raise the bar on release quality and operational safety. As LiteLLM evolves, so will our validation tooling, informed by real-world usage and lessons learned. We’ll continue to share those improvements openly as we go. - diff --git a/docs/my-website/blog/minimax_m2_5/index.md b/docs/my-website/blog/minimax_m2_5/index.md index 50084fcc1e5..9bccbdf7979 100644 --- a/docs/my-website/blog/minimax_m2_5/index.md +++ b/docs/my-website/blog/minimax_m2_5/index.md @@ -3,18 +3,9 @@ slug: minimax_m2_5 title: "Day 0 Support: MiniMax-M2.5" date: 2026-02-12T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Day 0 support for MiniMax-M2.5 on LiteLLM" tags: [minimax, M2.5, llm] hide_table_of_contents: false @@ -25,6 +16,8 @@ import TabItem from '@theme/TabItem'; LiteLLM now supports MiniMax-M2.5 on Day 0. Use it across OpenAI-compatible and Anthropic-compatible APIs through the LiteLLM AI Gateway. +{/* truncate */} + ## Supported Models LiteLLM supports the following MiniMax models: diff --git a/docs/my-website/blog/model_cost_map_incident/index.md b/docs/my-website/blog/model_cost_map_incident/index.md index b9ff20e4128..5b4499cc31c 100644 --- a/docs/my-website/blog/model_cost_map_incident/index.md +++ b/docs/my-website/blog/model_cost_map_incident/index.md @@ -3,10 +3,7 @@ slug: model-cost-map-incident title: "Incident Report: Invalid model cost map on main" date: 2026-02-10T10:00:00 authors: - - name: Ishaan Jaffer - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/ishaanjaffer/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - ishaan tags: [incident-report, stability] hide_table_of_contents: false --- diff --git a/docs/my-website/blog/realtime_webrtc_http_endpoints/index.md b/docs/my-website/blog/realtime_webrtc_http_endpoints/index.md index 04c3d3c9097..70fc5b2c48e 100644 --- a/docs/my-website/blog/realtime_webrtc_http_endpoints/index.md +++ b/docs/my-website/blog/realtime_webrtc_http_endpoints/index.md @@ -3,18 +3,9 @@ slug: realtime_webrtc_http_endpoints title: "Realtime WebRTC HTTP Endpoints" date: 2026-03-12T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "Use the LiteLLM proxy to route OpenAI-style WebRTC realtime via HTTP: client_secrets and SDP exchange." tags: [realtime, webrtc, proxy, openai] hide_table_of_contents: false @@ -24,6 +15,8 @@ import WebRTCTester from '@site/src/components/WebRTCTester'; Connect to the Realtime API via WebRTC from browser/mobile clients. LiteLLM handles auth and key management. +{/* truncate */} + ## How it works ![WebRTC flow: Browser, LiteLLM Proxy, and OpenAI/Azure](../../img/webrtc_flow.png) @@ -116,4 +109,3 @@ A: Set the correct `api_version` in `litellm_params` (or via the `AZURE_API_VERS **Q: What if I get no audio?** A: Make sure you grant microphone permission, ensure `pc.ontrack` assigns the audio element with `autoplay` enabled, check your network/firewall for WebRTC traffic, and inspect the browser console for ICE or SDP errors. - diff --git a/docs/my-website/blog/responses_api_encrypted_content_incident/index.md b/docs/my-website/blog/responses_api_encrypted_content_incident/index.md index 19b55898caa..fd5a9e76c42 100644 --- a/docs/my-website/blog/responses_api_encrypted_content_incident/index.md +++ b/docs/my-website/blog/responses_api_encrypted_content_incident/index.md @@ -3,18 +3,9 @@ slug: responses-api-encrypted-content-incident title: "Incident Report: Encrypted Content Failures in Multi-Region Responses API Load Balancing" date: 2026-02-24T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt tags: [incident-report, proxy, responses-api, load-balancing] hide_table_of_contents: false --- diff --git a/docs/my-website/blog/security_townhall_updates/index.md b/docs/my-website/blog/security_townhall_updates/index.md new file mode 100644 index 00000000000..b997de9c185 --- /dev/null +++ b/docs/my-website/blog/security_townhall_updates/index.md @@ -0,0 +1,190 @@ +--- +slug: security-townhall-updates +title: "Security Townhall Updates" +date: 2026-03-27T12:00:00 +authors: + - krrish + - ishaan-alt +description: "What happened, what we've done, and what comes next for LiteLLM's release and security processes." +tags: [security, incident-report] +hide_table_of_contents: false +--- + +import Image from '@theme/IdealImage'; + +Thank you to everyone who joined our town hall. + +We wanted to use that time to walk through what we know, what we've done so far, and how we're improving LiteLLM's release and security processes going forward. This post is a written version of that update. [Slides available here](https://drive.google.com/file/d/17hsSG7nk-OYL7VRCTbTa7McrWREtS9OO/view?usp=sharing) + +{/* truncate */} + +## What happened + +On March 24, 2026 at 10:39 UTC, LiteLLM v1.82.7 was pushed to PyPI. Version v1.82.8 was published soon after. Those packages were live for about 40 minutes before being quarantined by PyPI. By 16:00 UTC, the LiteLLM team had worked with PyPI to delete the affected packages. + +At this point, our understanding is that this was a supply-chain incident affecting those two published versions. + +## How did this happen? + +Our understanding is that the issue came from the [compromised Trivy security scanner](https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/) dependency in our CI/CD pipeline. + + + +There were three major contributing factors: + +### 1. Shared CI/CD environment + +At the time, everything was running on CircleCI, and all steps shared a common environment. That increased blast radius: if one component was compromised, it could potentially access credentials or context intended for other parts of the pipeline. + +### 2. Static credentials in environment variables + +Release credentials, including credentials for PyPI, GHCR, and Docker publishing, were available as static secrets in the environment. That meant a compromised step could access long-lived release credentials. + +### 3. Unpinned Trivy dependency + +In our security scanning component, we had an unpinned Trivy dependency. Our present understanding is that a compromised Trivy package ran during the scan, had access to environment variables, and enabled attackers to obtain those credentials. + +**In summary:** a compromised package in CI had access to secrets it should not have had, and those secrets were then used in the release path. + +## What we've already done + + +In the last 3 days, we've taken the following steps: + +### 1. Minimize Scope of Impact + +#### Prevented further key abuse + +We deleted or rotated all impacted or adjacent secret keys, including PyPI, GitHub, Docker, and related credentials. Out of an abundance of caution, we've also rotated LiteLLM maintainer accounts. + +#### Prevent branch attacks + +We removed roughly 6,000 open branches and added an auto-deletion policy for branches merged into `main`. This reduces the surface area for branch-based abuse. + +#### Pinned CI/CD dependencies + +We've pinned all Github Actions, and are working on pinning all CircleCI dependencies as well. + +#### Paused releases + +We've paused new releases until we've confirmed codebase security and put stronger release controls in place. + +### 2. Secured LiteLLM + +#### Forensic analysis + +We are working with Google's Mandiant cybersecurity team to confirm the source of the attack and verify the security of the codebase. We also confirmed that no malicious code was pushed to `main`. + +#### Confirm Application Security + +In parallel, we are working with whitehat hackers at [Veria Labs](https://verialabs.com/) to verify application security and review improvements to our CI/CD process. + +We have also confirmed that the last 20 LiteLLM releases contain no indicators of compromise, and that no unauthenticated attacks can be made against LiteLLM Proxy based on our current investigation. [Check Security Blog for release verification.](https://docs.litellm.ai/blog/security-update-march-2026#verified-safe-versions) + +#### Created a security working group + +We created a new security working group inside LiteLLM focused on: + +- Building threat models +- Auditing the build process and dependencies + +If you're interested in joining the security working group, please file an issue [here](https://github.com/BerriAI/litellm-security-wg). + +### 3. Improved CI/CD + +We've already begun making structural changes to how releases are built and published. These align with our goals (covered in the next section) around isolated environments, ephemeral credentials, and release auditing. + +## Roadmap + +We plan on following 4 guiding principles for our new CI/CD pipeline: + +1. **Limit** what each package can access +2. **Reduce** the number of sensitive environment variables +3. **Avoid** compromised packages +4. **Prevent** release tampering + + +### Isolated environments + + + +We are breaking our CI/CD into 4 semantic concepts: + +1. Unit tests +2. Integration tests +3. Security scans +4. Release publishing + +And will be running each of these in isolated environments. + +This will limit the damage that any single compromised component can cause. + +### Ephemeral credentials + +We plan to move to ephemeral credentials for PyPI (Trusted Publisher) and GHCR (Token-based authentication) releases. This will reduce the risk of credentials being leaked or compromised. + +We have already begun doing this: + +- PyPI Trusted Publisher on GitHub Actions [PR](https://github.com/BerriAI/litellm/pull/24654) +- GHCR Token-based authentication on GitHub Actions [PR](https://github.com/BerriAI/litellm/pull/24683) + +### Release auditing + +Our goal is to allow users to independently verify that a release came from us and prevent silent modifications of releases after they are published. + +This will ensure, your releases are safe, even when: +- Stolen PyPI/GHCR credentials are used to publish malicious releases +- Tampered registry artifacts are published +- Tag mutations are made after the release is published + +We believe that [Cosign](https://github.com/sigstore/cosign) is a good fit for this, and have already begun working on it [PR](https://github.com/BerriAI/litellm/pull/24683). + + +### Avoid Compromised Packages + +- Move to pinned, verified SHAs for packages and actions used in CI/CD, avoiding `latest` wherever possible. +- Add a cooldown period before upgrading to a new version of a package - allows more time to investigate and verify the new version. + +We've added zizmor to help us catch issues such as unpinned dependencies and credential leakage. [commit](https://github.com/BerriAI/litellm/commit/a671275f5c5b0e1fb1adacdf3b6ef779aaa5d56c). + + +## Frequently Asked Questions + +**Q: Did you observe any lateral movement into your corporate environment during this incident?** + +A: No. Our investigation to date, conducted in coordination with external security experts, has found no evidence of lateral movement into our internal corporate systems. The incident was isolated to the CI/CD pipeline and the release path for specific versions (v1.82.7 and v1.82.8). As a proactive measure, we have rotated all potentially impacted or adjacent secrets—including PyPI, GitHub, and Docker credentials—and updated maintainer account security to ensure continued isolation. + +**Q: Do you expect delays in future product releases due to these new security measures?** + +A: We are committed to balancing security with speed. While we have temporarily paused releases to implement stronger controls, we are moving quickly to automate our new security protocols. We are currently implementing isolated CI/CD environments, ephemeral credentials (via Trusted Publishers), and release auditing with Cosign. These improvements are designed to be integrated into our automated pipeline, allowing us to maintain a fast release cadence while ensuring every package is verified and secure. + +**Q: Were older packages impacted?** + +Our current findings show no indicators of compromise in the last 20 versions of LiteLLM. This was manually verified by our team and independently reviewed by Veria Labs. + +We have also published the verified versions for users to use. [Check Security Blog for release verification.](https://docs.litellm.ai/blog/security-update-march-2026#verified-safe-versions) + + + +## Questions & Support + +If you believe your systems may be affected, contact us immediately: + +- **Security:** security@berri.ai +- **Support:** support@berri.ai +- **Slack:** Reach out to the LiteLLM team directly [here](https://join.slack.com/t/litellmossslack/shared_invite/zt-3o7nkuyfr-p_kbNJj8taRfXGgQI1~YyA) + +## Hiring + +We are currently hiring for: + +- DevOps Engineer - to keep ci/cd secure and running smoothly +- Security Engineer - to keep the application secure + +If you're interest in joining, please apply [here](https://jobs.ashbyhq.com/litellm) \ No newline at end of file diff --git a/docs/my-website/blog/security_townhall_updates/shared_ci_cd_environment.png b/docs/my-website/blog/security_townhall_updates/shared_ci_cd_environment.png new file mode 100644 index 0000000000000000000000000000000000000000..29ec195b7fbaca2fffd448faed069d5024003015 GIT binary patch literal 45892 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 z eAC#IiE1ykwvkiue0S2jW|77Cbh3ciN5WrqbWQ==5$9W}(Fm?hxJA{CpmvOD(I zE6ur*1yZlI1=q?>1_BhS8OAykjbdB%U{?r0_wo`}&9;rVspDfvwU~a;QZLg287{Y- zMAX3=^ke-}>pvK{W)tnG8cnDBET#Jrd4~=QuwC~g{+jrn(2RJuf}IF$5~X$zS}K<( zH*2v*%p2v_Z+xTj(dN}d>BrN!&dI3v2EqfZ2~;df!`U67Qly}5^h-vS3bP|@lqrxH zB!@F6Hmnys$Z;pElgQ)_qhPBYz#vJ|TB2cd=&SMm9I>1Me=l25^p6alI{>;iHzMO! zWAOwi&c^l`R4EEF!cu@afPDyu>b=MhNt;#`>hbVzKBEFrt>toY`$U+qlF_NOX zcU?7~MNB*1j?2rvgQWlr)7519>znR)gg0h8X~OTOUJ9yt__{ryBw3SY)t=!{T+7ZS z1jv>bcZW6Le@pXF^PfwL(-TEIswpTmcWE?d-_R-uwtbc-@ROR3JF#nXU{2A;RMUsk^(Y|f9-Gym)c%Syce$itqdns57(E^qcq=e)3P zsljFZkA4?cLYbv1Z{rjMD+npXSJjeKKUj0^i0g2dFPkK40Bd=y(fY%jUOw1}20w9pEJK?A z66f>B?_Kuq;o$k0)wf^P_I&zr=sJ`0XzJXjySMZ>nZmmL@fQn+5noQ@sS#?P2q&Lc z@w{(d1$_SgTQhFl%uk^vk+D~hLl`y4-f@*#R18weIeU8M=*ecL+ZK8lhTHkqpru8W zDd3=^j18+sR9nVxwf(yo*3+k_`q&$;kfbA<_IKpR{{A|4QuBGQlb=xB{$1uX1G{Fw z*8m>2vWXI}LN!mt33-I>34M(1ecShA&B1SoJI-)^i2da`fLF`iD_IixXKSS;F8?sA z|Gm<`aA*@+Ef~?;b|lqHotJibFR%bHL-n_uBFLoA*U5of4l-kYtB*#Atgd$VXA3u4VhdhnUMk^@q zl%#yQt6R23jZpxY07H*;8bJ`InByJ_e$ZaJ_I8w+Gp%1%jpq<}Nv zLjY9mO*y`KnHbMXzQCBX<$NyXNl}XS+scFPo(}7XiJp=|B}0}u)oPC|3w}F|E`lQ2 zF3R|?aY)f9|Mhlrq@?%0WJ$Jq))prcN^OVc7M+gbQ`}A+sR=Ie4oX@{I<<7BT7&>$ zS@f=jr-6VdEkTJ=^;2D5+@@_`$EFX}58IXu;azfYrJi0vrI6#C)(iQk5EP1sPa8dz zXD9z1# z0*;&ucx+!vMqrIu3ZPM)G7lNPpIMfzCx)>}TmY9Azh&{Q_87rbDH$7hP^%O_=~S7}oCpqC-Kj;%=JuK7>0i ze@e6ZQ1piI0qf|)A_bRB4p}*gpjGU#60D%G<3dfPI)b+;z-CmrMD#1Te9b8%SVMmW zPPJr)(klXW=@ph8AJ-Z4DD4L)t*jlIQ{R$z^4}V1WD_d33t8dgAzjG`B;p}ic3Tiu z9V7Qwn|B0!b2JEJeZgX#L}bQ$9moSHH%2ENCkNKKCAY&+8U?8^)Ptj-wD6;dRh8Fy zY~OeHHj+vgIJr90-Ww-Yzf?H%o64=b&ZwA_P|}{Y?$O!0#0Tw|+gdAhv@(;@*Tt;y z)NN3vjIEU;IgR+&b;F5`p-rtlP_j@Eg!pk>mM^Zi?v<&Hzem=eganH3c*XJgRx@U-j&gCp1srDrl$h~P*t#lpJut@1q#_Q{U~v}RYlcW z{A7jQuM|QLL=lmN3M@#Kxbgsu3W8E$TD4SS;d=w=a8lz!AsVE`sJREDM^NqlgYWEM z*x`aPVx!}<;MJ5;Yhq0^#B>Siq|$hAdYYK3b>XuZ%a??}YA#5sp@!C?2IW}t3UZKH z(r1CT&}qCuX+tg*BGYO-T(z**W?on8uVvKx#`eQ7+lxArp#A8mMMgs#K~QOc;25e` zR0zoKYxL&Jp&bPUJ+Kvm?VsgT3tZWhq=do2#r$drZ&!2m=j_L|^l&XsY!uCo4rHTu zG3AdELi!FKzzUUYpw5(~Z3H?Zw*(N9W0Nqtk-Pt|88^>w_FUs&I+p{PGp-&IO!Iol&ngR&#Jsn+gAU;=2hQ_2aA)c{ko5^#y^4}5T9nr*asWh?+|!Xv)4smzIH~n0F?eORBOgs&OFA>b1JAD6PSG4K zQgunp^R4Wi_b2a2T^O*K1w>Yi`s!zpRxG{&ZP|Imbwambn`~*KNcK>q$EaRCn@}3$ zMw%R_Xd0W7Qioqrm9SkrWYt%wtzV!&)Y^V|c->3pQ`kuWljcWoRD6CPFEuly5|6PS zqi8MTs8Bl&Txv`D;FM+N09eRCHdX65oO44al68jLmNCI$E?5WFAASB@Q~RxcHHKSf zG_`!-+MbKlqx9zwc%G#Mx{YiW9Xu+}(pec-e}@HjHcU+)AKwE^K5%vf94X-AXt%{b+nKZnfHbHaEeMD^ONMcRQ<-1n*7D@Y7n#pWx=Wf)`xh{ zyl$;(U)KW;NYy22Jv~V3s8CIKHV&~{Lm%c%xCs@_*QBrO)?FEk^s1-x@Q@p}Hw>4Mg`!iXFxgk(FS75aoh2LF1!*v}6eca0-w5_Sogd z7@puuKH*F0YtfJYvaJ87WPjbpZc5Qnh){`+rhak!w8?=@z5hf02mRf%OV0UzD*Y9# z6nMYaAY%ZQ_UjAtPVBTT+Ig`IPVk{j{$o!GW}s&I-e@haqV%B^y@OoQAo+>2=1fGm ztZoLrbu`{o#A&J_#0mShE(+|DcI@aHX8w$l)IpJ*$=3ZYW60=Sru?aToQRMPb!X#d@qk4UiIFT>(+{gq!dRq0P%#)2XtpvyX~Uhi7Vz z`V2k2Ko>ENeY?6<73n_DGdI0Fao2YZhF)MhJ9L)S03fwhMMVZ1NJPcVPd-1fKh|3Q z>)D~fY>n;l^tX*DL?YX@!5>U1*KbsMZhgW`rb9xLuOKv}co7ME20Zo!H9@lmVw0cC z%N8}5N5~ijf-?|{#z$XUa48+Cw!&5>doPDH*W0%Dw?$gWxVMlOMCi)69WmRr6(@AM>t(!;@v)sDmg1`BBM(6D_1WBiksZ(z{DSGQG^q6CB(hW=f?` z`ud#Bxw_EPIC9;Y?yEbq=GvZWz%|gD_@xP0VSLNRyAwkad^1APTe z7QKXN}4xR*J^Z$;&^>S4M@T$=~T4 zadQKK>r;4{znD|LX5CuYJ5iP}_mzsE4Zf-tI|+BWl1xu!NHSsx=7xgo)*uA#mFoVN!v7+AKMNUQmS50LrRf;lsOo@NA{Y zT84E0s~77YoFmn-tH!hU+RR(M^URv^tk2Irw|z+|>8*jsB>H$i{XAr+5pQLcA;B2K z@sWJW=@YC@c+|Ri;&}{1b{No+*yyN8v}GACL|COiQQyO-7nP6zA`2GNk9olWEs3>` zc%#1{>9SepSl3jgD-_l`;x=j2JGytQ#%J%h_d}qFFI2nPyPD5BxwAjFIz0x`JDDs% zVyG+0nW~@en*VlpWzE}?HO|J~@EJdAlu?=PCDS9cQH+yea=u^U)3hr7tz`*%DK$`4 zZOM*&h~eds6VdX(q?E1v=r);beK}q`id?eG#&gkT*W}u_!+2g9GXWwpi6kKHWZ>O- zU7mMi4Vpe>nHozBJ)#<>%jKl>K>F;;@%YzS(zhzJ6_b)8_(%>Tt{Cz)rUntk4sCW2 zd7@K?)Q-_wpS_RF&PuCQ+Y@N(`;IjaMGSf!3sZCZyFFwGPG48aIjDlbu%@3lEeR&g z@aXNY$%mE3^`ylhSVK9nCnJpaA%QOadIhEHhNoxO+eiLe`Mqwz{p6``dj?km^+~zQ zcGLPrZFP3jKTgo)N$m{1`|Cf*w#jAdpS*(fd=&5U91bZkD<_ zrGw}+kDg_X=TS#sT+0M_Tq(CB&}(FgJ*F=>m31&Wm}`WOQ6-K9>V|gPNiVo=Fj~!n zPcczhTqqhtL?SsrM5_?~7>AthpJwWRnY?#Ve5H%;)!KVbpNwanbFpA`Y-AaQ@@I}8 zQ;uFiFplovKKt@p>H9*#I;HPv z0(?IZ;duXRYU`@}t zl>Hrp6%0SLX!Xf}6yTcfmC+Oy!-8+L>SpHOJmR&LogTvX!ST7zCQ-I!kSd>5-zi(0 zEi_qG;e;y}MtdUUlk+^@N;UgSf(@FTFNWcc;#Gq1B#szGe4pY4cNS)a&R(=2{qAB? zK3o2mdxJ4^uwgT1&-3*cjiqV(DhgO=AQA|4WzWp$Bi)cn*+^J< z&NZ~zOZM9%aCi^7%kIFMQg&MF*H&%$@`g0hEba?|FGTeE!i^A04!!L37Pw2|ULXdvLxSwx|&%J6`urra%*mCQO3`XV#YU$w;$8VR=H>cp!_Am{1;_ed2wwH=^THY zV(Kpc{XRk62W0)^-K?7`ziR%A40|(dTfnDJm$`61M=cyhBLtHS1bO~p7g{4r`yYO9KtnTPN+~M zR~F>;Es`kT``c8<9O9Y*w)urGzMtPIV^6`q6=YJ6fByCGQ(D(l<85l5 zUleb%@2?DhQvYSv3%PHB*R!9wk=-k>?>|-fu&90Nb{*&Mx<=IZ#uJ7Vn90G}^D(*r zs%cGZWR1b=cmLPVslm;QjIXrId5@ zj3|qO^DpYQ-QT=Ri=O7gx0hd)y;sxdeo>FN;atda z5Bx>F{ZwOIHiQyOXvWPG{za+O|7sxBFY51`%F{Y^{KY2B?X|hIqiwuH1Q9?g$N5C$ z2}#Y0kG*3gk^J2e3NIGdlgI8Izo;&g_c7PR!oA|#*9Zw!ZfuLp7*gik(D*eX{xUXn zzb}4KTS!|fuYLZj%(aJq4!1w4xIcydDkClX=Lr6jV)FkbRQTV}Ps|YO_w1Wf`ifxrBLE)0B^^l#$6ezADs zAShwI`=-Ws^t*=5_pSUx2uVBcf}d7+tXnkaxxS+l=YJ6Vf&NC-`|jMOXBWmxNiW*n zN)a+6MM(Q#-={D9zptJj>j?g);b-=lh1W-_FSpt6=qQ=h&u>0`zWd*9Nvzrl$z?ET zg<21-0j4LN>u*i`@IP;9x_|xKp zup<9mjgXU#E1=rRaXytn!)G>w#<$duCcmT4C}9Ck?uzi8m>}glGO82OqpASM+Mq#R zdhn^HYom{yHm|;$x5(q+yJ#%9IboNB+&AMGYCLgw2Vl3p7W(s<&=DJi+1^3QJ#e$WoR_RQZ`nir%_^_Zzue%;q9 zy-AyS!tM^Apo}T}K6k?4yL3mG#iOukO?}}Sat&S(;9dXSU~(_rlAw;mggdT}ImV)) zv5rAW4tkZUWw%nT4WJI1$e#bOQ`^uH)Sz5rI@^d-@^ zIyTZs*%%trV4J^z z=7csCD7M;kMx)!)d-PKSHUg9X!S!|N7b=g6l%zo63otOoGHe>@y>V<^woTyl|2ID7 z|IbgSa*Ax@sxiW}&EC6EgkmGSa%C656$&VtVfM^}!Q}l&=cTT2U?3EClzu)XHAd~X z-3@@f;Ee$#*kW>C#n9&7K=i?1(#JMqCJS54;Wb)@;(@v)vE`yU%o=PM!COz_o$EZ- z?A&XgjFZ5M)4|Iw(rH>+W_!HAdC>2e`jBO6^jJ+LX;NimwtksY$aBmLM=*I+a%FJw zcmjg1Z|L|U{FMBU(K!>rZK%a|;r*FHy>J5o*RUdToe(4k2+cHVYO&uTPOlzGX885y zXxHPjprKm5`<)nBL96u$Bm8DuBAa~lpZ_=0rwS}_C&b3c0lY%bh3^EgCy~7A6Ec4K zL%yi;sJrN0B=De3esK&tFlxMLzec0vEd#G`m6}J`kUP7yzp`0gTv)1CfC8s9o^ypi z3oy;MNK6P)F3?^3Q&qmR<5Qfn-bhtvecW*Dao zGc~nWN`c{FtQy3U0rrwQFI$L`t`#QERT9#9mn@PWZb&wxRZ!R5x8P=)$B@T?@f^3qP6lK)WYv<%`PF!ZF|BHluZK2KCjbtFN|X-6q^B>E7*><5Vyh2bfV-I}_>>esaBQtMnSR70;kd}4j3Jmk*RsxOG?6%0ZsgRD!+Y(N%&ANkd z%ATZWNZL^^H@SOwc--^KCwM;`3(8fY#N+p$s9CQ~yEscE(ge_y7rdQXw)>JJ1o#+y zM1W#Wd)#PXtT%i~H^q>OjxR4nw zGecwtTGINqh*7Pqr^=+jW5pVIR|#qKe$5q)}658d7rC zyg#fqj6jL?V2d^9a}E9}QykMG%d%X6F68ZXr4xS<}8 z?voI(dv>yZ{PkPcl}Jns2le(~U+w&Fz3s3}(-lL390T{3p2 zy>B(UZ=Y-K7~-6C(Y#k*E2CRDOUyL(_Esc>@N>Vi3b$KN2aA}w!`Ukqv zhJo}a06x9l(JTkcWvI&N*~IA<*}A}bmQavwh8Oekug3E|R8w)mfC7Q2i2!9;bWVF1 zS}bj8UZe+Ka4`KbP!8Rx^Vl0AW|h{QZvVxc6-j&Z0!74gT$V@Y59S^cgi;(J5J{ZZ zL>o?PJ+oa_^|chF(&Z$lXrYa@ApWRjsOO{oMFR0$ba+!6l{dE#?H9M|p@M<+TE37v zM}E1{9|Zt9UTn{*E$c~PubtVDQ$3EdTgBxuWOH(UX=*7iXm@yzw$CdI;ID=~%8icB ztx4L-I~0ugLUbn7FL44?MTz^bc}`~*K*9G-*Q_Fg&*gq7g#v)$H>-OX@pT@8^y*u? zFQ>?F3HK&dInrSTI3$^ZoNj|7%)hcltt%mrFfT!$k0XP;PqCu zWI$fuUX^Kk1OBY#I>W)U;Gv})FT&w!uevhaCvYVlN53I-s%cGooWCBW7O%eP_*{v- zcE9qF%%1^*lP~4&*eV`0+qaIl+Iy*!c1X@1^3&-VGSSIkMgbYJ&)sqI-lEctM-vKqrMz>_E5E!wR# ze7H+QTS=%7d#Lil0ITf~MWEH7r;Ze`7s)R_{rim+Z%@4%rFW6A$~nJyw5(#Dnt8CG z=_v_N^H}nX!kZzAdAWpPReK6Mr-452w&Lp@O_ev7mrA*EP#(7>r(>_MvFY8Ym!G7NdMz3d$F@#SCjrMC&n$iVdHR#7)Oa12zI;L84R}xGMbguWr~-ZsLNG0 z5Q)0I`>(2-ipttpHNzLC={8$h%vtCB_Oo*L0g>BUZKq0(5>uO)Q^oF-DIaMoI_N;{BEyc=K#=`+< zE}qIC*=fLam}V9@29BYG-H+zMc#IdGpOEGGl7VzvU+_uH7d#EE48sIXo9APvv_b38 zM{wKx%HdG+XgPfcO(MK%e}p4tAu5>fmcnzY%`DUefq>bJ)clEfeA5S17OZYg6@XZ~rV$A#0q+-i+$_M$2~j!% zqu;Iz@qvzG<58d6py!oAhGLtpWj@5x`8erxbQ%~es>FCnGz#m7+2_vOBhRq<`ctDi zK?onW8kG}B3$7JQJ^ikoFGc`&wyWG(O47AB8`N1QzQNp0?lMC%!|(}zb&LIHT>ldd z1>ITFJpc(QQE>|+xSe8=2R}jAYVJKbi%&-Pm4UCip<;TY!Gwa!@&SH?|KUNcA?;8V zmQ0^D>K<)r5lGyMf#mR98i!eISH{2OBSsCzfY0(B2TW|3R!S zcH?BM7^Jcx_Srd{dq`OQALYl_1P)WbvIXvYkFk9IE&t=O?Pruur2-p(Xyu)odL|uZ zw;qz+moxnPl9wZ_^cXL1yJ}y3yNu@@WrDT`R#buvV&=&ss|rfDti!fF%WRc2oYqk( UxK_3+itqk2^8JrS;r=@GU+A2;EdT%j literal 0 HcmV?d00001 diff --git a/docs/my-website/blog/security_update_march_2026/index.md b/docs/my-website/blog/security_update_march_2026/index.md new file mode 100644 index 00000000000..1c298fe372f --- /dev/null +++ b/docs/my-website/blog/security_update_march_2026/index.md @@ -0,0 +1,786 @@ +--- +slug: security-update-march-2026 +title: "Security Update: Suspected Supply Chain Incident" +date: 2026-03-24T14:00:00 +authors: + - krrish + - ishaan-alt +description: "As of 2:00 PM ET on March 24, 2026" +tags: [security, incident-report] +hide_table_of_contents: false +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import VersionVerificationTable from '@site/src/components/VersionVerificationTable'; + +> **Status:** Active investigation +> **Last updated:** March 27, 2026 + +> **Update (March 30):** A new **clean** version of LiteLLM is now available (v1.83.0). This was released by our new [CI/CD v2](https://docs.litellm.ai/blog/ci-cd-v2-improvements) pipeline which added isolated environments, stronger security gates, and safer release separation for LiteLLM. + +> **Update (March 27):** Review Townhall updates, including explanation of the incident, what we've done, and what comes next. [Learn more](https://docs.litellm.ai/blog/security-townhall-updates) + +> **Update (March 27):** Added [Verified safe versions](#verified-safe-versions) section with SHA-256 checksums for all audited PyPI and Docker releases. + +> **Update (March 26):** Added `checkmarx[.]zone` to [Indicators of compromise](#indicators-of-compromise-iocs) + +> **Update (March 25):** Added community-contributed scripts for scanning GitHub Actions and GitLab CI pipelines for the compromised versions. See [How to check if you are affected](#how-to-check-if-you-are-affected). s/o [@Zach Fury](https://www.linkedin.com/in/fryware/) for these scripts. + + +## TLDR; +- The compromised PyPI packages were **litellm==1.82.7** and **litellm==1.82.8**. Those packages were live on March 24, 2026 from 10:39 UTC for about 40 minutes before being quarantined by PyPI. +- We believe that the compromise originated from the [Trivy dependency](https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/) used in our CI/CD security scanning workflow. +- Customers running the official LiteLLM Proxy Docker image were not impacted. That deployment path pins dependencies in requirements.txt and does not rely on the compromised PyPI packages. +- ~~We have paused all new LiteLLM releases until we complete a broader supply-chain review and confirm the release path is safe.~~ **Updated:** We have now released a new **safe** version of LiteLLM (v1.83.0) by our new [CI/CD v2](https://docs.litellm.ai/blog/ci-cd-v2-improvements) pipeline which added isolated environments, stronger security gates, and safer release separation for LiteLLM. We have also verified the codebase is safe and no malicious code was pushed to `main`. + + +## Overview + +LiteLLM AI Gateway is investigating a suspected supply chain attack involving unauthorized PyPI package publishes. Current evidence suggests a maintainer's PyPI account may have been compromised and used to distribute malicious code. + +At this time, we believe this incident may be linked to the broader [Trivy security compromise](https://www.aquasec.com/blog/trivy-supply-chain-attack-what-you-need-to-know/), in which stolen credentials were reportedly used to gain unauthorized access to the LiteLLM publishing pipeline. + +This investigation is ongoing. Details below may change as we confirm additional findings. + +## Confirmed affected versions + +The following LiteLLM versions published to PyPI were impacted: + +- **v1.82.7**: contained a malicious payload in the LiteLLM AI Gateway `proxy_server.py` +- **v1.82.8**: contained `litellm_init.pth` and a malicious payload in the LiteLLM AI Gateway `proxy_server.py` + +If you installed or ran either of these versions, review the recommendations below immediately. + +Note: These versions have already been removed from PyPI. + +## What happened + +Initial evidence suggests the attacker bypassed official CI/CD workflows and uploaded malicious packages directly to PyPI. + +These compromised versions appear to have included a credential stealer designed to: + +- Harvest secrets by scanning for: + - environment variables + - SSH keys + - cloud provider credentials (AWS, GCP, Azure) + - Kubernetes tokens + - database passwords +- Encrypt and exfiltrate data via a `POST` request to `models.litellm.cloud`, which is **not** an official BerriAI / LiteLLM domain + +## Who is affected + +You may be affected if **any** of the following are true: + +- You installed or upgraded LiteLLM via `pip` on **March 24, 2026**, between **10:39 UTC and 16:00 UTC** +- You ran `pip install litellm` without pinning a version and received **v1.82.7** or **v1.82.8** +- You built a Docker image during this window that included `pip install litellm` without a pinned version +- A dependency in your project pulled in LiteLLM as a transitive, unpinned dependency + (for example through AI agent frameworks, MCP servers, or LLM orchestration tools) + +You are **not** affected if any of the following are true: + +**LiteLLM AI Gateway/Proxy users:** Customers running the official LiteLLM Proxy Docker image were not impacted. That deployment path pins dependencies in requirements.txt and does not rely on the compromised PyPI packages. + +- You are using **LiteLLM Cloud** +- You are using the official LiteLLM AI Gateway Docker image: `ghcr.io/berriai/litellm` +- You are on **v1.82.6 or earlier** and did not upgrade during the affected window +- You installed LiteLLM from source via the GitHub repository, which was **not** compromised + + +### How to check if you are affected + + + + +```bash +pip show litellm +``` + + + +Go to the proxy base url, and check the version of the installed LiteLLM. + +![Proxy version check](../../img/security_update_march_2026/proxy_version.png) + + + +Scans all repositories in a GitHub organization for workflow jobs that installed the compromised versions. + +**Requirements:** Python 3 and `requests` (`pip install requests`). + +**Setup:** + +```bash +export GITHUB_TOKEN="your-github-pat" +``` + +**Run:** + +```bash +python find_litellm_github.py +``` + +Set the `ORG` variable in the script to your GitHub organization name. + +Both scripts default to scanning jobs from **today**. Adjust the `WINDOW_START` and `WINDOW_END` constants to cover **March 24, 2026** (the incident date) if running on a different day. + +
+View full script (find_litellm_github.py) + +```python +#!/usr/bin/env python3 +""" +Scan all GitHub Actions jobs in a GitHub org that ran between +0800-1244 UTC today and identify any that installed litellm 1.82.7 or 1.82.8. + +Adjust WINDOW_START / WINDOW_END to cover March 24, 2026 if running later. +""" + +import io +import os +import re +import sys +import zipfile +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone + +import requests + +GITHUB_URL = "https://api.github.com" +ORG = "your-org" # <-- set to your GitHub organization +TOKEN = os.environ.get("GITHUB_TOKEN", "") + +TODAY = datetime.now(timezone.utc).date() +WINDOW_START = datetime(TODAY.year, TODAY.month, TODAY.day, 8, 0, 0, tzinfo=timezone.utc) +WINDOW_END = datetime(TODAY.year, TODAY.month, TODAY.day, 12, 44, 0, tzinfo=timezone.utc) + +TARGET_VERSIONS = {"1.82.7", "1.82.8"} +VERSION_PATTERN = re.compile(r"litellm[=\-](\d+\.\d+\.\d+)", re.IGNORECASE) + +SESSION = requests.Session() +SESSION.headers.update({ + "Authorization": f"Bearer {TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", +}) + + +def get_paginated(url, params=None): + params = dict(params or {}) + params.setdefault("per_page", 100) + page = 1 + while True: + params["page"] = page + resp = SESSION.get(url, params=params, timeout=30) + if resp.status_code == 404: + return + resp.raise_for_status() + data = resp.json() + if isinstance(data, dict): + items = next((v for v in data.values() if isinstance(v, list)), []) + else: + items = data + if not items: + break + yield from items + if len(items) < params["per_page"]: + break + page += 1 + + +def parse_ts(ts_str): + if not ts_str: + return None + return datetime.fromisoformat(ts_str.replace("Z", "+00:00")) + + +def get_repos(): + repos = [] + for r in get_paginated(f"{GITHUB_URL}/orgs/{ORG}/repos", {"type": "all"}): + repos.append({"id": r["id"], "name": r["name"], "full_name": r["full_name"]}) + return repos + + +def get_runs_in_window(repo_full_name): + created_filter = ( + f"{WINDOW_START.strftime('%Y-%m-%dT%H:%M:%SZ')}" + f"..{WINDOW_END.strftime('%Y-%m-%dT%H:%M:%SZ')}" + ) + url = f"{GITHUB_URL}/repos/{repo_full_name}/actions/runs" + runs = [] + for run in get_paginated(url, {"created": created_filter, "per_page": 100}): + ts = parse_ts(run.get("run_started_at") or run.get("created_at")) + if ts and WINDOW_START <= ts <= WINDOW_END: + runs.append(run) + return runs + + +def get_jobs_for_run(repo_full_name, run_id): + url = f"{GITHUB_URL}/repos/{repo_full_name}/actions/runs/{run_id}/jobs" + jobs = [] + for job in get_paginated(url, {"filter": "all"}): + ts = parse_ts(job.get("started_at")) + if ts and WINDOW_START <= ts <= WINDOW_END: + jobs.append(job) + return jobs + + +def fetch_job_log(repo_full_name, job_id): + url = f"{GITHUB_URL}/repos/{repo_full_name}/actions/jobs/{job_id}/logs" + resp = SESSION.get(url, timeout=60, allow_redirects=True) + if resp.status_code in (403, 404, 410): + return "" + resp.raise_for_status() + + content_type = resp.headers.get("Content-Type", "") + if "zip" in content_type or resp.content[:2] == b"PK": + try: + with zipfile.ZipFile(io.BytesIO(resp.content)) as zf: + parts = [] + for name in sorted(zf.namelist()): + with zf.open(name) as f: + parts.append(f.read().decode("utf-8", errors="replace")) + return "\n".join(parts) + except zipfile.BadZipFile: + pass + return resp.text + + +def check_job(repo_full_name, job): + job_id = job["id"] + job_name = job["name"] + run_id = job["run_id"] + started = job.get("started_at", "") + + log_text = fetch_job_log(repo_full_name, job_id) + if not log_text: + return None + + found_versions = set() + context_lines = [] + for line in log_text.splitlines(): + m = VERSION_PATTERN.search(line) + if m: + ver = m.group(1) + if ver in TARGET_VERSIONS: + found_versions.add(ver) + context_lines.append(line.strip()) + + if not found_versions: + return None + + return { + "repo": repo_full_name, + "run_id": run_id, + "job_id": job_id, + "job_name": job_name, + "started_at": started, + "versions": sorted(found_versions), + "context": context_lines[:10], + "job_url": job.get("html_url", f"https://github.com/{repo_full_name}/actions/runs/{run_id}"), + } + + +def main(): + if not TOKEN: + print("ERROR: Set GITHUB_TOKEN environment variable.", file=sys.stderr) + sys.exit(1) + + print(f"Time window : {WINDOW_START.isoformat()} -> {WINDOW_END.isoformat()}") + print(f"Hunting for : litellm {', '.join(sorted(TARGET_VERSIONS))}") + print() + + print(f"Fetching repositories for org '{ORG}'...") + repos = get_repos() + print(f" Found {len(repos)} repositories") + print() + + jobs_to_check = [] + + print("Scanning workflow runs for time window...") + for repo in repos: + full_name = repo["full_name"] + try: + runs = get_runs_in_window(full_name) + except requests.HTTPError as e: + print(f" WARN: {full_name} - {e}", file=sys.stderr) + continue + if not runs: + continue + print(f" {full_name}: {len(runs)} run(s) in window") + for run in runs: + try: + jobs = get_jobs_for_run(full_name, run["id"]) + except requests.HTTPError as e: + print(f" WARN: run {run['id']} - {e}", file=sys.stderr) + continue + for job in jobs: + jobs_to_check.append((full_name, job)) + + total = len(jobs_to_check) + print(f"\nFetching logs for {total} job(s)...") + print() + + hits = [] + with ThreadPoolExecutor(max_workers=8) as pool: + futures = { + pool.submit(check_job, full_name, job): (full_name, job["id"]) + for full_name, job in jobs_to_check + } + done = 0 + for future in as_completed(futures): + done += 1 + full_name, jid = futures[future] + try: + result = future.result() + except Exception as e: + print(f" ERROR {full_name} job {jid}: {e}", file=sys.stderr) + continue + if result: + hits.append(result) + print( + f" [{done}/{total}] {full_name} job {jid}" + + (f" *** HIT: litellm {result['versions']} ***" if result else ""), + flush=True, + ) + + print() + print("=" * 72) + print(f"RESULTS: {len(hits)} job(s) installed litellm {' or '.join(sorted(TARGET_VERSIONS))}") + print("=" * 72) + + if not hits: + print("No matches found.") + return + + for h in sorted(hits, key=lambda x: x["started_at"]): + print() + print(f" Repo : {h['repo']}") + print(f" Job : {h['job_name']} (#{h['job_id']})") + print(f" Run ID : {h['run_id']}") + print(f" Started : {h['started_at']}") + print(f" Versions : litellm {', '.join(h['versions'])}") + print(f" URL : {h['job_url']}") + print(f" Log lines :") + for line in h["context"]: + print(f" {line}") + + +if __name__ == "__main__": + main() +``` + +
+ +
+ + +Scans all projects in a GitLab group (including subgroups) for CI/CD jobs that installed the compromised versions. + +**Requirements:** Python 3 and `requests` (`pip install requests`). + +**Setup:** + +```bash +export GITLAB_TOKEN="your-gitlab-pat" +``` + +**Run:** + +```bash +python find_litellm_jobs.py +``` + +Set the `GROUP_NAME` variable in the script to your GitLab group name. + +Both scripts default to scanning jobs from **today**. Adjust the `WINDOW_START` and `WINDOW_END` constants to cover **March 24, 2026** (the incident date) if running on a different day. + +
+View full script (find_litellm_jobs.py) + +```python +#!/usr/bin/env python3 +""" +Scan all GitLab CI/CD jobs in a GitLab group that ran between +0800-1244 UTC today and identify any that installed litellm 1.82.7 or 1.82.8. + +Adjust WINDOW_START / WINDOW_END to cover March 24, 2026 if running later. +""" + +import os +import re +import sys +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone + +import requests + +GITLAB_URL = "https://gitlab.com" +GROUP_NAME = "YourGroup" # <-- set to your GitLab group name +TOKEN = os.environ.get("GITLAB_TOKEN", "") + +TODAY = datetime.now(timezone.utc).date() +WINDOW_START = datetime(TODAY.year, TODAY.month, TODAY.day, 8, 0, 0, tzinfo=timezone.utc) +WINDOW_END = datetime(TODAY.year, TODAY.month, TODAY.day, 12, 44, 0, tzinfo=timezone.utc) + +TARGET_VERSIONS = {"1.82.7", "1.82.8"} +VERSION_PATTERN = re.compile(r"litellm[=\-](\d+\.\d+\.\d+)", re.IGNORECASE) + +HEADERS = {"PRIVATE-TOKEN": TOKEN} +SESSION = requests.Session() +SESSION.headers.update(HEADERS) + + +def get_paginated(url, params=None): + params = dict(params or {}) + params.setdefault("per_page", 100) + page = 1 + while True: + params["page"] = page + resp = SESSION.get(url, params=params, timeout=30) + resp.raise_for_status() + data = resp.json() + if not data: + break + yield from data + if len(data) < params["per_page"]: + break + page += 1 + + +def get_group_id(group_name): + resp = SESSION.get(f"{GITLAB_URL}/api/v4/groups/{group_name}", timeout=30) + resp.raise_for_status() + return resp.json()["id"] + + +def get_all_projects(group_id): + projects = [] + for p in get_paginated( + f"{GITLAB_URL}/api/v4/groups/{group_id}/projects", + {"include_subgroups": "true", "archived": "false"}, + ): + projects.append({"id": p["id"], "name": p["path_with_namespace"]}) + return projects + + +def parse_ts(ts_str): + if not ts_str: + return None + ts_str = ts_str.replace("Z", "+00:00") + return datetime.fromisoformat(ts_str) + + +def jobs_in_window(project_id): + matching = [] + url = f"{GITLAB_URL}/api/v4/projects/{project_id}/jobs" + params = {"per_page": 100, "scope[]": ["success", "failed", "canceled", "running"]} + + page = 1 + while True: + params["page"] = page + resp = SESSION.get(url, params=params, timeout=30) + if resp.status_code == 403: + return matching + resp.raise_for_status() + jobs = resp.json() + if not jobs: + break + + stop_early = False + for job in jobs: + ts = parse_ts(job.get("started_at") or job.get("created_at")) + if ts is None: + continue + if ts > WINDOW_END: + continue + if ts < WINDOW_START: + stop_early = True + continue + matching.append(job) + + if stop_early or len(jobs) < 100: + break + page += 1 + + return matching + + +def fetch_trace(project_id, job_id): + url = f"{GITLAB_URL}/api/v4/projects/{project_id}/jobs/{job_id}/trace" + resp = SESSION.get(url, timeout=60) + if resp.status_code in (403, 404): + return "" + resp.raise_for_status() + return resp.text + + +def check_job(project_name, project_id, job): + job_id = job["id"] + job_name = job["name"] + ref = job.get("ref", "") + started = job.get("started_at", job.get("created_at", "")) + + trace = fetch_trace(project_id, job_id) + if not trace: + return None + + found_versions = set() + for match in VERSION_PATTERN.finditer(trace): + ver = match.group(1) + if ver in TARGET_VERSIONS: + found_versions.add(ver) + + if not found_versions: + return None + + context_lines = [] + for line in trace.splitlines(): + if VERSION_PATTERN.search(line): + ver_match = VERSION_PATTERN.search(line) + if ver_match and ver_match.group(1) in TARGET_VERSIONS: + context_lines.append(line.strip()) + + return { + "project": project_name, + "project_id": project_id, + "job_id": job_id, + "job_name": job_name, + "ref": ref, + "started_at": started, + "versions": sorted(found_versions), + "context": context_lines[:10], + "job_url": f"{GITLAB_URL}/{project_name}/-/jobs/{job_id}", + } + + +def main(): + if not TOKEN: + print("ERROR: Set GITLAB_TOKEN environment variable.", file=sys.stderr) + sys.exit(1) + + print(f"Time window : {WINDOW_START.isoformat()} -> {WINDOW_END.isoformat()}") + print(f"Hunting for : litellm {', '.join(sorted(TARGET_VERSIONS))}") + print() + + print(f"Resolving group '{GROUP_NAME}'...") + group_id = get_group_id(GROUP_NAME) + + print("Fetching projects...") + projects = get_all_projects(group_id) + print(f" Found {len(projects)} projects") + print() + + all_jobs_to_check = [] + + print("Scanning job listings for time window...") + for proj in projects: + try: + jobs = jobs_in_window(proj["id"]) + except requests.HTTPError as e: + print(f" WARN: {proj['name']} - {e}", file=sys.stderr) + continue + if jobs: + print(f" {proj['name']}: {len(jobs)} job(s) in window") + for j in jobs: + all_jobs_to_check.append((proj["name"], proj["id"], j)) + + total = len(all_jobs_to_check) + print(f"\nFetching traces for {total} job(s)...") + print() + + hits = [] + with ThreadPoolExecutor(max_workers=10) as pool: + futures = { + pool.submit(check_job, pname, pid, job): (pname, job["id"]) + for pname, pid, job in all_jobs_to_check + } + done = 0 + for future in as_completed(futures): + done += 1 + pname, jid = futures[future] + try: + result = future.result() + except Exception as e: + print(f" ERROR checking {pname} job {jid}: {e}", file=sys.stderr) + continue + if result: + hits.append(result) + print(f" [{done}/{total}] checked {pname} job {jid}" + + (f" *** HIT: litellm {result['versions']} ***" if result else ""), + flush=True) + + print() + print("=" * 72) + print(f"RESULTS: {len(hits)} job(s) installed litellm {' or '.join(sorted(TARGET_VERSIONS))}") + print("=" * 72) + + if not hits: + print("No matches found.") + return + + for h in sorted(hits, key=lambda x: x["started_at"]): + print() + print(f" Project : {h['project']}") + print(f" Job : {h['job_name']} (#{h['job_id']})") + print(f" Branch/tag: {h['ref']}") + print(f" Started : {h['started_at']}") + print(f" Versions : litellm {', '.join(h['versions'])}") + print(f" URL : {h['job_url']}") + print(f" Log lines :") + for line in h["context"]: + print(f" {line}") + + +if __name__ == "__main__": + main() +``` + +
+ +
+
+ +*CI/CD scripts contributed by the community ([original gist](https://gist.github.com/fryz/93ec8d4898ffe5b5ac5706a208823ef3)). Review before running.* + + +## Indicators of compromise (IoCs) + +Review affected systems for the following indicators: + +- `litellm_init.pth` present in your `site-packages` +- Outbound traffic or requests to `models.litellm[.]cloud` + This domain is **not** affiliated with LiteLLM +- Outbound traffic or requests to `checkmarx[.]zone` + This domain is **not** affiliated with LiteLLM + + +## Immediate actions for affected users + +If you installed or ran **v1.82.7** or **v1.82.8**, take the following actions immediately. + +### 1. Rotate all secrets + +Treat any credentials present on the affected systems as compromised, including: + +- API keys +- Cloud access keys +- Database passwords +- SSH keys +- Kubernetes tokens +- Any secrets stored in environment variables or configuration files + +### 2. Inspect your filesystem + +Check your `site-packages` directory for a file named `litellm_init.pth`: + +```bash +find /usr/lib/python3.13/site-packages/ -name "litellm_init.pth" +``` + +If present: + +- remove it immediately +- investigate the host for further compromise +- preserve relevant artifacts if your security team is performing forensics + +### 3. Audit version history + +Review your: + +- Local environments +- CI/CD pipelines +- Docker builds +- Deployment logs + +Confirm whether **v1.82.7** or **v1.82.8** was installed anywhere. + +Pin LiteLLM to a known safe version such as **v1.82.6 or earlier**, or to a later verified release once announced. + + +## Response and remediation + +The LiteLLM AI Gateway team has already taken the following steps: + +- Removed compromised packages from PyPI +- Rotated maintainer credentials and established new authorized maintainers +- Engaged Google's Mandiant security team to assist with forensic analysis of the build and publishing chain + + +## Verified safe versions + +We have audited every LiteLLM release published between v1.78.0 and v1.82.6 across both PyPI and Docker. Each artifact was verified by: + +1. Downloading the published artifact and computing its SHA-256 digest +2. Scanning for the known [indicators of compromise](#indicators-of-compromise-iocs) (IOCs) +3. Comparing the artifact contents against the corresponding Git commit in the BerriAI/litellm repository + +**All versions listed below are confirmed clean.** + + + + + + + + + + + + + + + +## Questions and support + +If you believe your systems may be affected, contact us immediately: + +- **Security:** `security@berri.ai` +- **Support:** `support@berri.ai` +- **Slack:** Reach out to the LiteLLM team directly + +For real-time updates, follow [LiteLLM (YC W23) on X](https://x.com/LiteLLM). + diff --git a/docs/my-website/blog/server_root_path/index.md b/docs/my-website/blog/server_root_path/index.md index d7925baf6b4..13b7365cc9e 100644 --- a/docs/my-website/blog/server_root_path/index.md +++ b/docs/my-website/blog/server_root_path/index.md @@ -3,17 +3,9 @@ slug: server-root-path-incident title: "Incident Report: SERVER_ROOT_PATH regression broke UI routing" date: 2026-02-21T10:00:00 authors: - - name: Yuneng Jiang - title: SWE @ LiteLLM (Full Stack) - url: https://www.linkedin.com/in/yunengjiang/ - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg + - yuneng + - ishaan-alt + - krrish tags: [incident-report, ui, stability] hide_table_of_contents: false --- diff --git a/docs/my-website/blog/sub_millisecond_proxy_overhead/index.md b/docs/my-website/blog/sub_millisecond_proxy_overhead/index.md index 1857383363c..7f8ac086a21 100644 --- a/docs/my-website/blog/sub_millisecond_proxy_overhead/index.md +++ b/docs/my-website/blog/sub_millisecond_proxy_overhead/index.md @@ -3,18 +3,9 @@ slug: sub-millisecond-proxy-overhead title: "Achieving Sub-Millisecond Proxy Overhead" date: 2026-02-02T10:00:00 authors: - - name: Alexsander Hamir - title: "Performance Engineer, LiteLLM" - url: https://www.linkedin.com/in/alexsander-baptista/ - image_url: https://github.com/AlexsanderHamir.png - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - alexsander + - krrish + - ishaan-alt description: "Our Q1 performance target and architectural direction for achieving sub-millisecond proxy overhead on modest hardware." tags: [performance, architecture] hide_table_of_contents: false @@ -32,6 +23,8 @@ Proxy overhead refers to the latency introduced by LiteLLM itself, independent o To measure it, we run the same workload directly against the provider and through LiteLLM at identical QPS (for example, 1,000 QPS) and compare the latency delta. To reduce noise, the load generator, LiteLLM, and a mock LLM endpoint all run on the same machine, ensuring the difference reflects proxy overhead rather than network latency. +{/* truncate */} + --- ## Where We're Coming From diff --git a/docs/my-website/blog/vanta_compliance_recertification/index.md b/docs/my-website/blog/vanta_compliance_recertification/index.md new file mode 100644 index 00000000000..d05c113967f --- /dev/null +++ b/docs/my-website/blog/vanta_compliance_recertification/index.md @@ -0,0 +1,18 @@ +--- +slug: vanta-compliance-recertification +title: "LiteLLM + Vanta: SOC 2 Type 2 and ISO 27001 Recertification" +date: 2026-03-30T10:00:00 +authors: + - krrish +description: "LiteLLM is partnering with Vanta on SOC 2 Type 2 and ISO 27001 recertification and engaging independent auditors for verification." +tags: [security, compliance] +hide_table_of_contents: true +--- + +![LiteLLM x Vanta SOC-2 Recertification](/img/blog/vanta_soc2_recertification.png) + +We are partnering with [Vanta](https://www.vanta.com/) to recertify LiteLLM's compliance for SOC 2 Type 2 and ISO 27001. + +As part of this process, we are also identifying independent auditors to validate and verify our compliance posture. + +This is part of our commitment to being the most secure and transparent AI Gateway possible. diff --git a/docs/my-website/blog/video_characters_litellm/index.md b/docs/my-website/blog/video_characters_litellm/index.md index 263a17d7191..a0f87385d81 100644 --- a/docs/my-website/blog/video_characters_litellm/index.md +++ b/docs/my-website/blog/video_characters_litellm/index.md @@ -3,18 +3,9 @@ slug: video_characters_api title: "New Video Characters, Edit and Extension API support" date: 2026-03-16T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt description: "LiteLLM now supports creating, retrieving, and managing reusable video characters across multiple video generations." tags: [videos, characters, proxy, routing] hide_table_of_contents: false @@ -22,6 +13,8 @@ hide_table_of_contents: false LiteLLM now supoports videos character, edit and extension apis. +{/* truncate */} + ## What's New Four new endpoints for video character operations: @@ -125,4 +118,4 @@ Router knows exactly which deployment to use **Behind the scenes:** - Character ID format: `character_` - Metadata includes: provider, model_id, original_character_id -- Transparent to you - just use the ID, LiteLLM handles routing \ No newline at end of file +- Transparent to you - just use the ID, LiteLLM handles routing diff --git a/docs/my-website/blog/vllm_embeddings_incident/index.md b/docs/my-website/blog/vllm_embeddings_incident/index.md index a1ce8152857..26387e66dd0 100644 --- a/docs/my-website/blog/vllm_embeddings_incident/index.md +++ b/docs/my-website/blog/vllm_embeddings_incident/index.md @@ -3,18 +3,9 @@ slug: vllm-embeddings-incident title: "Incident Report: vLLM Embeddings Broken by encoding_format Parameter" date: 2026-02-18T10:00:00 authors: - - name: Sameer Kankute - title: SWE @ LiteLLM (LLM Translation) - url: https://www.linkedin.com/in/sameer-kankute/ - image_url: https://pbs.twimg.com/profile_images/2001352686994907136/ONgNuSk5_400x400.jpg - - name: Krrish Dholakia - title: "CEO, LiteLLM" - url: https://www.linkedin.com/in/krish-d/ - image_url: https://pbs.twimg.com/profile_images/1298587542745358340/DZv3Oj-h_400x400.jpg - - name: Ishaan Jaff - title: "CTO, LiteLLM" - url: https://www.linkedin.com/in/reffajnaahsi/ - image_url: https://pbs.twimg.com/profile_images/1613813310264340481/lz54oEiB_400x400.jpg + - sameer + - krrish + - ishaan-alt tags: [incident-report, embeddings, vllm] hide_table_of_contents: false --- diff --git a/docs/my-website/docs/anthropic_unified/index.md b/docs/my-website/docs/anthropic_unified/index.md index 9981547ce1f..f8a50e14da5 100644 --- a/docs/my-website/docs/anthropic_unified/index.md +++ b/docs/my-website/docs/anthropic_unified/index.md @@ -506,12 +506,15 @@ Request body will be in the Anthropic messages API format. **litellm follows the A system prompt providing context or specific instructions to the model. - **temperature** (number): Controls randomness in the model's responses. Valid range: `0 < temperature < 1`. -- **thinking** (object): +- **thinking** (object): Configuration for enabling extended thinking. If enabled, it includes: - - **budget_tokens** (integer): + - **budget_tokens** (integer): Minimum of 1024 tokens (and less than `max_tokens`). - - **type** (enum): + - **type** (enum): E.g., `"enabled"`. + - **summary** (string, optional): + Enables the summary style for thinking blocks. Possible values: `"auto"`, `"concise"`, `"detailed"`, `"disabled"`. + When routing to non-Anthropic providers (e.g., `openai/gpt-5.1`), the `summary` value is preserved and forwarded to the downstream API. - **tool_choice** (object): Instructs how the model should utilize any provided tools. - **tools** (array of objects): diff --git a/docs/my-website/docs/completion/prompt_caching.md b/docs/my-website/docs/completion/prompt_caching.md index dca5f5c0cff..402c7b9f4c7 100644 --- a/docs/my-website/docs/completion/prompt_caching.md +++ b/docs/my-website/docs/completion/prompt_caching.md @@ -6,6 +6,8 @@ import TabItem from '@theme/TabItem'; Supported Providers: - OpenAI (`openai/`) - Anthropic API (`anthropic/`) +- Google AI Studio (`gemini/`) +- Vertex AI (`vertex_ai/`, `vertex_ai_beta/`) - Bedrock (`bedrock/`, `bedrock/invoke/`, `bedrock/converse`) ([All models bedrock supports prompt caching on](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)) - Deepseek API (`deepseek/`) @@ -257,7 +259,7 @@ Anthropic charges for cache writes. Specify the content to cache with `"cache_control": {"type": "ephemeral"}`. -If you pass that in for any other llm provider, it will be ignored. +This same format also works for [Gemini / Vertex AI](#google-ai-studio--vertex-ai-gemini-example). For other providers, it will be ignored. @@ -356,6 +358,208 @@ print(response.usage) +### Google AI Studio / Vertex AI (Gemini) Example + +Use the same Anthropic-style `cache_control` format — LiteLLM automatically translates it to Google's [context caching API](https://ai.google.dev/api/caching). + +**How it works under the hood:** +1. Messages with `cache_control` are separated and sent to Google's `cachedContents` API +2. The cached content ID is then passed as `cachedContent` in the Gemini request body +3. Works across all three providers: `gemini/` (Google AI Studio), `vertex_ai/`, and `vertex_ai_beta/` +4. Requires a minimum of **1024 tokens** in the cached content — below that, caching is silently skipped + + + + +```python +from litellm import completion +import os + +os.environ["GEMINI_API_KEY"] = "" + +response = completion( + model="gemini/gemini-2.5-flash", + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "You are an AI assistant tasked with analyzing legal documents.", + }, + { + "type": "text", + "text": "Here is the full text of a complex legal agreement" * 400, + "cache_control": {"type": "ephemeral"}, + }, + ], + }, + { + "role": "user", + "content": "what are the key terms and conditions in this agreement?", + }, + ], +) + +print(response.usage) +``` + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: gemini-2.5-flash + litellm_params: + model: gemini/gemini-2.5-flash + api_key: os.environ/GEMINI_API_KEY +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```python +from openai import OpenAI + +client = OpenAI( + api_key="LITELLM_PROXY_KEY", # sk-1234 + base_url="LITELLM_PROXY_BASE", # http://0.0.0.0:4000 +) + +response = client.chat.completions.create( + model="gemini-2.5-flash", + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "You are an AI assistant tasked with analyzing legal documents.", + }, + { + "type": "text", + "text": "Here is the full text of a complex legal agreement" * 400, + "cache_control": {"type": "ephemeral"}, + }, + ], + }, + { + "role": "user", + "content": "what are the key terms and conditions in this agreement?", + }, + ], +) + +print(response.usage) +``` + + + + +#### Vertex AI + +For Vertex AI, use `vertex_ai/` prefix: + + + + +```python +from litellm import completion + +response = completion( + model="vertex_ai/gemini-2.5-flash", + vertex_project="my-gcp-project", + vertex_location="us-central1", + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "You are an AI assistant tasked with analyzing legal documents.", + }, + { + "type": "text", + "text": "Here is the full text of a complex legal agreement" * 400, + "cache_control": {"type": "ephemeral"}, + }, + ], + }, + { + "role": "user", + "content": "what are the key terms and conditions in this agreement?", + }, + ], +) + +print(response.usage) +``` + + + +1. Setup config.yaml + +```yaml +model_list: + - model_name: gemini-2.5-flash + litellm_params: + model: vertex_ai/gemini-2.5-flash + vertex_project: my-gcp-project + vertex_location: us-central1 +``` + +2. Start proxy + +```bash +litellm --config /path/to/config.yaml +``` + +3. Test it! + +```python +from openai import OpenAI + +client = OpenAI( + api_key="LITELLM_PROXY_KEY", # sk-1234 + base_url="LITELLM_PROXY_BASE", # http://0.0.0.0:4000 +) + +response = client.chat.completions.create( + model="gemini-2.5-flash", + messages=[ + { + "role": "system", + "content": [ + { + "type": "text", + "text": "You are an AI assistant tasked with analyzing legal documents.", + }, + { + "type": "text", + "text": "Here is the full text of a complex legal agreement" * 400, + "cache_control": {"type": "ephemeral"}, + }, + ], + }, + { + "role": "user", + "content": "what are the key terms and conditions in this agreement?", + }, + ], +) + +print(response.usage) +``` + + + + ### Deepeek Example Works the same as OpenAI. diff --git a/docs/my-website/docs/data_security.md b/docs/my-website/docs/data_security.md index 2c4b1247e2b..d93d17aa0de 100644 --- a/docs/my-website/docs/data_security.md +++ b/docs/my-website/docs/data_security.md @@ -128,8 +128,6 @@ We'll review all reports promptly. Note that we don't currently offer a bug boun Legal Entity Name: Berrie AI Incorporated -Company Phone Number: 7708783106 - Point of contact email address for security incidents: krrish@berri.ai Point of contact email address for general security-related questions: krrish@berri.ai diff --git a/docs/my-website/docs/debugging/local_debugging.md b/docs/my-website/docs/debugging/local_debugging.md index 8a56d6c34a0..53daa4e366b 100644 --- a/docs/my-website/docs/debugging/local_debugging.md +++ b/docs/my-website/docs/debugging/local_debugging.md @@ -67,6 +67,6 @@ response = completion("command-nightly", messages, logger_fn=my_custom_logging_f ## Still Seeing Issues? -Text us @ +17708783106 or Join the [Discord](https://discord.com/invite/wuPM9dRgDw). +Join the [Discord](https://discord.com/invite/wuPM9dRgDw). We promise to help you in `lite`ning speed ❤️ diff --git a/docs/my-website/docs/enterprise.md b/docs/my-website/docs/enterprise.md index 6dccf7ff4e7..a3fc9e38b6e 100644 --- a/docs/my-website/docs/enterprise.md +++ b/docs/my-website/docs/enterprise.md @@ -4,7 +4,7 @@ import Image from '@theme/IdealImage'; :::info - ✨ SSO is free for up to 5 users. After that, an enterprise license is required. [Get Started with Enterprise here](https://www.litellm.ai/enterprise) -- Who is Enterprise for? Companies giving access to 100+ users **OR** 10+ AI use-cases. If you're not sure, [get in touch with us](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) to discuss your needs. +- Who is Enterprise for? Companies giving access to 100+ users **OR** 10+ AI use-cases. If you're not sure, [get in touch with us](https://enterprise.litellm.ai/demo) to discuss your needs. ::: For companies that need SSO, user management and professional support for LiteLLM Proxy @@ -36,7 +36,7 @@ Manage Yourself - you can deploy our Docker Image or build a custom image from o ### What’s the cost of the Self-Managed Enterprise edition? -Self-Managed Enterprise deployments require our team to understand your exact needs. [Get in touch with us to learn more](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +Self-Managed Enterprise deployments require our team to understand your exact needs. [Get in touch with us to learn more](https://enterprise.litellm.ai/demo) ### How does deployment with Enterprise License work? @@ -106,7 +106,7 @@ Professional Support can assist with LLM/Provider integrations, deployment, upgr Pricing is based on usage. We can figure out a price that works for your team, on the call. -[**Contact Us to learn more**](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[**Contact Us to learn more**](https://enterprise.litellm.ai/demo) diff --git a/docs/my-website/docs/fine_tuning.md b/docs/my-website/docs/fine_tuning.md index d0bd98a76f9..52e96f28688 100644 --- a/docs/my-website/docs/fine_tuning.md +++ b/docs/my-website/docs/fine_tuning.md @@ -6,7 +6,7 @@ import TabItem from '@theme/TabItem'; :::info -This is an Enterprise only endpoint [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +This is an Enterprise only endpoint [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/guides/index.md b/docs/my-website/docs/guides/index.md new file mode 100644 index 00000000000..1641600dab2 --- /dev/null +++ b/docs/my-website/docs/guides/index.md @@ -0,0 +1,78 @@ +--- +title: Guides +sidebar_label: Overview +--- + +import NavigationCards from '@site/src/components/NavigationCards'; + +**Guides** are focused references organized by the job you are trying to do with LiteLLM: make requests, use tools, handle media, manage context, or operate the gateway safely. + +> New to LiteLLM or not sure whether you need the SDK or Gateway path first? Start at [Learn →](/docs/learn) + +--- + +## Build With LiteLLM + + + +--- + +## Operate & Extend + + diff --git a/docs/my-website/docs/index.md b/docs/my-website/docs/index.md index ba605e316d3..ca63c9e39ff 100644 --- a/docs/my-website/docs/index.md +++ b/docs/my-website/docs/index.md @@ -1,689 +1,462 @@ +--- +id: index +title: Getting Started +sidebar_label: Quickstart +--- + import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import NavigationCards from '@site/src/components/NavigationCards'; +import Image from '@theme/IdealImage'; -# LiteLLM - Getting Started + -https://github.com/BerriAI/litellm +**LiteLLM** is an open-source library that gives you a single, unified interface to call 100+ LLMs — OpenAI, Anthropic, Vertex AI, Bedrock, and more — using the OpenAI format. -## **Call 100+ LLMs using the OpenAI Input/Output Format** +- Call any provider using the same `completion()` interface — no re-learning the API for each one +- Consistent output format regardless of which provider or model you use +- Built-in retry / fallback logic across multiple deployments via the [Router](./routing.md) +- Self-hosted [LLM Gateway (Proxy)](./simple_proxy) with virtual keys, cost tracking, and an admin UI -- Translate inputs to provider's endpoints (`/chat/completions`, `/responses`, `/embeddings`, `/images`, `/audio`, `/batches`, and more) -- [Consistent output](https://docs.litellm.ai/docs/supported_endpoints) - same response format regardless of which provider you use -- Retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - [Router](https://docs.litellm.ai/docs/routing) -- Track spend & set budgets per project [LiteLLM Proxy Server](https://docs.litellm.ai/docs/simple_proxy) +[![PyPI](https://img.shields.io/pypi/v/litellm.svg)](https://pypi.org/project/litellm/) +[![GitHub Stars](https://img.shields.io/github/stars/BerriAI/litellm?style=social)](https://github.com/BerriAI/litellm) -## How to use LiteLLM +--- -You can use LiteLLM through either the Proxy Server or Python SDK. Both gives you a unified interface to access multiple LLMs (100+ LLMs). Choose the option that best fits your needs: - -
Stripeimage Google ADK Greptile OpenHands
- - - - - - - - - - - - - - - - - - - - - - - - -
LiteLLM Proxy ServerLiteLLM Python SDK
Use CaseCentral service (LLM Gateway) to access multiple LLMsUse LiteLLM directly in your Python code
Who Uses It?Gen AI Enablement / ML Platform TeamsDevelopers building LLM projects
Key Features• Centralized API gateway with authentication & authorization
• Multi-tenant cost tracking and spend management per project/user
• Per-project customization (logging, guardrails, caching)
• Virtual keys for secure access control
• Admin dashboard UI for monitoring and management
• Direct Python library integration in your codebase
• Router with retry/fallback logic across multiple deployments (e.g. Azure/OpenAI) - Router
• Application-level load balancing and cost tracking
• Exception handling with OpenAI-compatible errors
• Observability callbacks (Lunary, MLflow, Langfuse, etc.)
- - -## **LiteLLM Python SDK** - -### Basic usage - - - Open In Colab - +## Installation ```shell pip install litellm ``` - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["OPENAI_API_KEY"] = "your-api-key" - -response = completion( - model="openai/gpt-4o", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["ANTHROPIC_API_KEY"] = "your-api-key" - -response = completion( - model="anthropic/claude-3-sonnet-20240229", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["XAI_API_KEY"] = "your-api-key" - -response = completion( - model="xai/grok-2-latest", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - -```python -from litellm import completion -import os - -# auth: run 'gcloud auth application-default' -os.environ["VERTEXAI_PROJECT"] = "hardy-device-386718" -os.environ["VERTEXAI_LOCATION"] = "us-central1" - -response = completion( - model="vertex_ai/gemini-1.5-pro", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["NVIDIA_NIM_API_KEY"] = "nvidia_api_key" -os.environ["NVIDIA_NIM_API_BASE"] = "nvidia_nim_endpoint_url" - -response = completion( - model="nvidia_nim/", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - - -```python -from litellm import completion -import os - -os.environ["HUGGINGFACE_API_KEY"] = "huggingface_api_key" - -# e.g. Call 'WizardLM/WizardCoder-Python-34B-V1.0' hosted on HF Inference endpoints -response = completion( - model="huggingface/WizardLM/WizardCoder-Python-34B-V1.0", - messages=[{ "content": "Hello, how are you?","role": "user"}], - api_base="https://my-endpoint.huggingface.cloud" -) - -print(response) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["AZURE_API_KEY"] = "" -os.environ["AZURE_API_BASE"] = "" -os.environ["AZURE_API_VERSION"] = "" - -# azure call -response = completion( - "azure/", - messages = [{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - - -```python -from litellm import completion - -response = completion( - model="ollama/llama2", - messages = [{ "content": "Hello, how are you?","role": "user"}], - api_base="http://localhost:11434" -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["OPENROUTER_API_KEY"] = "openrouter_api_key" - -response = completion( - model="openrouter/google/palm-2-chat-bison", - messages = [{ "content": "Hello, how are you?","role": "user"}], -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key -os.environ["NOVITA_API_KEY"] = "novita-api-key" - -response = completion( - model="novita/deepseek/deepseek-r1", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables. Visit https://vercel.com/docs/ai-gateway#using-the-ai-gateway-with-an-api-key for insturctions on obtaining a key -os.environ["VERCEL_AI_GATEWAY_API_KEY"] = "your-vercel-api-key" - -response = completion( - model="vercel_ai_gateway/openai/gpt-4o", - messages=[{ "content": "Hello, how are you?","role": "user"}] -) -``` - - - - - -### Response Format (OpenAI Chat Completions Format) - -```json -{ - "id": "chatcmpl-565d891b-a42e-4c39-8d14-82a1f5208885", - "created": 1734366691, - "model": "gpt-4o-2024-08-06", - "object": "chat.completion", - "system_fingerprint": null, - "choices": [ - { - "finish_reason": "stop", - "index": 0, - "message": { - "content": "Hello! As an AI language model, I don't have feelings, but I'm operating properly and ready to assist you with any questions or tasks you may have. How can I help you today?", - "role": "assistant", - "tool_calls": null, - "function_call": null - } - } - ], - "usage": { - "completion_tokens": 43, - "prompt_tokens": 13, - "total_tokens": 56, - "completion_tokens_details": null, - "prompt_tokens_details": { - "audio_tokens": null, - "cached_tokens": 0 - }, - "cache_creation_input_tokens": 0, - "cache_read_input_tokens": 0 - } -} -``` - -### Streaming -Set `stream=True` in the `completion` args. - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["OPENAI_API_KEY"] = "your-api-key" - -response = completion( - model="openai/gpt-4o", - messages=[{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["ANTHROPIC_API_KEY"] = "your-api-key" - -response = completion( - model="anthropic/claude-3-sonnet-20240229", - messages=[{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["XAI_API_KEY"] = "your-api-key" - -response = completion( - model="xai/grok-2-latest", - messages=[{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - -```python -from litellm import completion -import os - -# auth: run 'gcloud auth application-default' -os.environ["VERTEX_PROJECT"] = "hardy-device-386718" -os.environ["VERTEX_LOCATION"] = "us-central1" - -response = completion( - model="vertex_ai/gemini-1.5-pro", - messages=[{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["NVIDIA_NIM_API_KEY"] = "nvidia_api_key" -os.environ["NVIDIA_NIM_API_BASE"] = "nvidia_nim_endpoint_url" - -response = completion( - model="nvidia_nim/", - messages=[{ "content": "Hello, how are you?","role": "user"}] - stream=True, -) -``` - - - - -```python -from litellm import completion -import os - -os.environ["HUGGINGFACE_API_KEY"] = "huggingface_api_key" - -# e.g. Call 'WizardLM/WizardCoder-Python-34B-V1.0' hosted on HF Inference endpoints -response = completion( - model="huggingface/WizardLM/WizardCoder-Python-34B-V1.0", - messages=[{ "content": "Hello, how are you?","role": "user"}], - api_base="https://my-endpoint.huggingface.cloud", - stream=True, -) - -print(response) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["AZURE_API_KEY"] = "" -os.environ["AZURE_API_BASE"] = "" -os.environ["AZURE_API_VERSION"] = "" - -# azure call -response = completion( - "azure/", - messages = [{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - - -```python -from litellm import completion - -response = completion( - model="ollama/llama2", - messages = [{ "content": "Hello, how are you?","role": "user"}], - api_base="http://localhost:11434", - stream=True, -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables -os.environ["OPENROUTER_API_KEY"] = "openrouter_api_key" - -response = completion( - model="openrouter/google/palm-2-chat-bison", - messages = [{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - -```python -from litellm import completion -import os - -## set ENV variables. Visit https://novita.ai/settings/key-management to get your API key -os.environ["NOVITA_API_KEY"] = "novita_api_key" - -response = completion( - model="novita/deepseek/deepseek-r1", - messages = [{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - - -```python -from litellm import completion -import os - -## set ENV variables. Visit https://vercel.com/docs/ai-gateway#using-the-ai-gateway-with-an-api-key for insturctions on obtaining a key -os.environ["VERCEL_AI_GATEWAY_API_KEY"] = "your-vercel-api-key" - -response = completion( - model="vercel_ai_gateway/openai/gpt-4o", - messages = [{ "content": "Hello, how are you?","role": "user"}], - stream=True, -) -``` - - - - - -### Streaming Response Format (OpenAI Format) - -```json -{ - "id": "chatcmpl-2be06597-eb60-4c70-9ec5-8cd2ab1b4697", - "created": 1734366925, - "model": "claude-3-sonnet-20240229", - "object": "chat.completion.chunk", - "system_fingerprint": null, - "choices": [ - { - "finish_reason": null, - "index": 0, - "delta": { - "content": "Hello", - "role": "assistant", - "function_call": null, - "tool_calls": null, - "audio": null - }, - "logprobs": null - } - ] -} -``` - -### Exception handling - -LiteLLM maps exceptions across all supported providers to the OpenAI exceptions. All our exceptions inherit from OpenAI's exception types, so any error-handling you have for that, should work out of the box with LiteLLM. - -```python -import litellm -from litellm import completion -import os - -os.environ["ANTHROPIC_API_KEY"] = "bad-key" -try: - completion(model="anthropic/claude-instant-1", messages=[{"role": "user", "content": "Hey, how's it going?"}]) -except litellm.AuthenticationError as e: - # Thrown when the API key is invalid - print(f"Authentication failed: {e}") -except litellm.RateLimitError as e: - # Thrown when you've exceeded your rate limit - print(f"Rate limited: {e}") -except litellm.APIError as e: - # Thrown for general API errors - print(f"API error: {e}") -``` -### See How LiteLLM Transforms Your Requests - -Want to understand how LiteLLM parses and normalizes your LLM API requests? Use the `/utils/transform_request` endpoint to see exactly how your request is transformed internally. - -You can try it out now directly on our Demo App! -Go to the [LiteLLM API docs for transform_request](https://litellm-api.up.railway.app/#/llm%20utils/transform_request_utils_transform_request_post) - -LiteLLM will show you the normalized, provider-agnostic version of your request. This is useful for debugging, learning, and understanding how LiteLLM handles different providers and options. - - -### Logging Observability - Log LLM Input/Output ([Docs](https://docs.litellm.ai/docs/observability/callbacks)) -LiteLLM exposes pre defined callbacks to send data to Lunary, MLflow, Langfuse, Helicone, Promptlayer, Traceloop, Slack - -```python -from litellm import completion - -## set env variables for logging tools (API key set up is not required when using MLflow) -os.environ["LUNARY_PUBLIC_KEY"] = "your-lunary-public-key" # get your public key at https://app.lunary.ai/settings -os.environ["HELICONE_API_KEY"] = "your-helicone-key" -os.environ["LANGFUSE_PUBLIC_KEY"] = "" -os.environ["LANGFUSE_SECRET_KEY"] = "" - -os.environ["OPENAI_API_KEY"] - -# set callbacks -litellm.success_callback = ["lunary", "mlflow", "langfuse", "helicone"] # log input/output to lunary, mlflow, langfuse, helicone - -#openai call -response = completion(model="gpt-3.5-turbo", messages=[{"role": "user", "content": "Hi 👋 - i'm openai"}]) -``` - -### Track Costs, Usage, Latency for streaming -Use a callback function for this - more info on custom callbacks: https://docs.litellm.ai/docs/observability/custom_callback - -```python -import litellm - -# track_cost_callback -def track_cost_callback( - kwargs, # kwargs to completion - completion_response, # response from completion - start_time, end_time # start/end time -): - try: - response_cost = kwargs.get("response_cost", 0) - print("streaming response_cost", response_cost) - except: - pass -# set callback -litellm.success_callback = [track_cost_callback] # set custom callback function - -# litellm.completion() call -response = completion( - model="gpt-3.5-turbo", - messages=[ - { - "role": "user", - "content": "Hi 👋 - i'm openai" - } - ], - stream=True -) -``` - -## **LiteLLM Proxy Server (LLM Gateway)** - -Track spend across multiple projects/people - -![ui_3](https://github.com/BerriAI/litellm/assets/29436595/47c97d5e-b9be-4839-b28c-43d7f4f10033) - -The proxy provides: - -1. [Hooks for auth](https://docs.litellm.ai/docs/proxy/virtual_keys#custom-auth) -2. [Hooks for logging](https://docs.litellm.ai/docs/proxy/logging#step-1---create-your-custom-litellm-callback-class) -3. [Cost tracking](https://docs.litellm.ai/docs/proxy/virtual_keys#tracking-spend) -4. [Rate Limiting](https://docs.litellm.ai/docs/proxy/users#set-rate-limits) - -### 📖 Proxy Endpoints - [Swagger Docs](https://litellm-api.up.railway.app/) - -Go here for a complete tutorial with keys + rate limits - [**here**](./proxy/docker_quick_start.md) - -### Quick Start Proxy - CLI +To run the full Proxy Server (LLM Gateway): ```shell pip install 'litellm[proxy]' ``` -#### Step 1: Start litellm proxy +--- + +## Quick Start + +Make your first LLM call using the provider of your choice: + - +```python +from litellm import completion +import os -```shell -$ litellm --model huggingface/bigcode/starcoder +os.environ["OPENAI_API_KEY"] = "your-api-key" -#INFO: Proxy running on http://0.0.0.0:4000 +response = completion( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "Hello, how are you?"}] +) +print(response.choices[0].message.content) ``` + - +```python +from litellm import completion +import os +os.environ["ANTHROPIC_API_KEY"] = "your-api-key" -Step 1. CREATE config.yaml +response = completion( + model="anthropic/claude-3-5-sonnet-20241022", + messages=[{"role": "user", "content": "Hello, how are you?"}] +) +print(response.choices[0].message.content) +``` -Example `litellm_config.yaml` + + -```yaml +```python +from litellm import completion +import os + +# auth: run 'gcloud auth application-default login' +os.environ["VERTEXAI_PROJECT"] = "your-project-id" +os.environ["VERTEXAI_LOCATION"] = "us-central1" + +response = completion( + model="vertex_ai/gemini-1.5-pro", + messages=[{"role": "user", "content": "Hello, how are you?"}] +) +print(response.choices[0].message.content) +``` + + + + +```python +from litellm import completion +import os + +os.environ["AWS_ACCESS_KEY_ID"] = "your-key" +os.environ["AWS_SECRET_ACCESS_KEY"] = "your-secret" +os.environ["AWS_REGION_NAME"] = "us-east-1" + +response = completion( + model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", + messages=[{"role": "user", "content": "Hello, how are you?"}] +) +print(response.choices[0].message.content) +``` + + + + +```python +from litellm import completion + +response = completion( + model="ollama/llama3", + messages=[{"role": "user", "content": "Hello, how are you?"}], + api_base="http://localhost:11434" +) +print(response.choices[0].message.content) +``` + + + + +```python +from litellm import completion +import os + +os.environ["AZURE_API_KEY"] = "your-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" +os.environ["AZURE_API_VERSION"] = "2024-02-01" + +response = completion( + model="azure/your-deployment-name", + messages=[{"role": "user", "content": "Hello, how are you?"}] +) +print(response.choices[0].message.content) +``` + + + + +Every response follows the OpenAI Chat Completions format, regardless of provider. ✅ + +### Response Format + +Non-streaming responses return a `ModelResponse` object: + +```json +{ + "id": "chatcmpl-abc123", + "object": "chat.completion", + "created": 1677858242, + "model": "gpt-4o", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! I'm doing well, thanks for asking." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 13, + "completion_tokens": 12, + "total_tokens": 25 + } +} +``` + +Streaming responses (`stream=True`) yield `ModelResponseStream` chunks: + +```json +{ + "id": "chatcmpl-abc123", + "object": "chat.completion.chunk", + "created": 1677858242, + "model": "gpt-4o", + "choices": [ + { + "index": 0, + "delta": { + "role": "assistant", + "content": "Hello" + }, + "finish_reason": null + } + ] +} +``` + +📖 [Full output format reference →](./completion/output) + +:::tip Open in Colab + +Open In Colab + +::: + +--- + +## New to LiteLLM? + +**Want to get started fast?** Head to [Tutorials](/docs/tutorials) for step-by-step walkthroughs — AI coding tools, agent SDKs, proxy setup, and more. + +**Need to understand a specific feature?** Check [Guides](/docs/guides) for streaming, function calling, prompt caching, and other how-tos. + +--- + +## Choose Your Path + + + +--- + +## LiteLLM Python SDK + +### Streaming + +Add `stream=True` to receive chunks as they are generated: + +```python +from litellm import completion +import os + +os.environ["OPENAI_API_KEY"] = "your-api-key" + +for chunk in completion( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "Write a short poem"}], + stream=True, +): + print(chunk.choices[0].delta.content or "", end="") +``` + +### Exception Handling + +LiteLLM maps every provider's errors to the OpenAI exception types — your existing error handling works out of the box: + +```python +import litellm + +try: + litellm.completion( + model="anthropic/claude-instant-1", + messages=[{"role": "user", "content": "Hey!"}] + ) +except litellm.AuthenticationError as e: + print(f"Bad API key: {e}") +except litellm.RateLimitError as e: + print(f"Rate limited: {e}") +except litellm.APIError as e: + print(f"API error: {e}") +``` + +### Logging & Observability + +Send input/output to Langfuse, MLflow, Helicone, Lunary, and more with a single line: + +```python +import litellm + +litellm.success_callback = ["langfuse", "mlflow", "helicone"] + +response = litellm.completion( + model="gpt-4o", + messages=[{"role": "user", "content": "Hi!"}] +) +``` + +📖 [See all observability integrations →](/docs/observability/agentops_integration) + +### Track Costs & Usage + +Use a callback to capture cost per response: + +```python +import litellm + +def track_cost(kwargs, completion_response, start_time, end_time): + print("Cost:", kwargs.get("response_cost", 0)) + +litellm.success_callback = [track_cost] + +litellm.completion( + model="gpt-4o", + messages=[{"role": "user", "content": "Hello!"}], + stream=True +) +``` + +📖 [Custom callback docs →](./observability/custom_callback) + +--- + +## LiteLLM Proxy Server (LLM Gateway) + +The proxy is a self-hosted OpenAI-compatible gateway. Any client that works with OpenAI works with the proxy — no code changes needed. + +![LiteLLM Proxy Dashboard](https://github.com/BerriAI/litellm/assets/29436595/47c97d5e-b9be-4839-b28c-43d7f4f10033) + +#### Step 1 — Start the proxy + + + + +```shell +litellm --model huggingface/bigcode/starcoder +# Proxy running on http://0.0.0.0:4000 +``` + + + + +```yaml title="litellm_config.yaml" model_list: - model_name: gpt-3.5-turbo litellm_params: - model: azure/ - api_base: os.environ/AZURE_API_BASE # runs os.getenv("AZURE_API_BASE") - api_key: os.environ/AZURE_API_KEY # runs os.getenv("AZURE_API_KEY") + model: azure/your-deployment + api_base: os.environ/AZURE_API_BASE + api_key: os.environ/AZURE_API_KEY api_version: "2023-07-01-preview" ``` -Step 2. RUN Docker Image - ```shell docker run \ - -v $(pwd)/litellm_config.yaml:/app/config.yaml \ - -e AZURE_API_KEY=d6*********** \ - -e AZURE_API_BASE=https://openai-***********/ \ - -p 4000:4000 \ - docker.litellm.ai/berriai/litellm:main-latest \ - --config /app/config.yaml --detailed_debug + -v $(pwd)/litellm_config.yaml:/app/config.yaml \ + -e AZURE_API_KEY=your-key \ + -e AZURE_API_BASE=https://your-resource.openai.azure.com/ \ + -p 4000:4000 \ + docker.litellm.ai/berriai/litellm:main-latest \ + --config /app/config.yaml --detailed_debug ``` - -#### Step 2: Make ChatCompletions Request to Proxy +#### Step 2 — Call it with the OpenAI client ```python -import openai # openai v1.0.0+ -client = openai.OpenAI(api_key="anything",base_url="http://0.0.0.0:4000") # set proxy to base_url -# request sent to model set on litellm proxy, `litellm --model` -response = client.chat.completions.create(model="gpt-3.5-turbo", messages = [ - { - "role": "user", - "content": "this is a test request, write a short poem" - } -]) +import openai -print(response) +client = openai.OpenAI(api_key="anything", base_url="http://0.0.0.0:4000") + +response = client.chat.completions.create( + model="gpt-3.5-turbo", + messages=[{"role": "user", "content": "Write a short poem"}] +) +print(response.choices[0].message.content) ``` -## More details +👉 [Full proxy quickstart with Docker →](./proxy/docker_quick_start) -- [exception mapping](./exception_mapping.md) -- [retries + model fallbacks for completion()](./completion/reliable_completions.md) -- [proxy virtual keys & spend management](./proxy/virtual_keys.md) -- [E2E Tutorial for LiteLLM Proxy Server](./proxy/docker_quick_start.md) +:::tip Debugging tool +Use [**`/utils/transform_request`**](./utils/transform_request) to inspect exactly what LiteLLM sends to any provider — useful for debugging prompt formatting, header issues, and provider-specific parameters. +::: + +🔗 [Interactive API explorer (Swagger) →](https://litellm-api.up.railway.app/) + +--- + +## Agent & MCP Gateway + +LiteLLM is a unified gateway for **LLMs, agents, and MCP** — you don't need a separate agent or MCP gateway. One endpoint for 100+ models, A2A agents, and MCP tools. + + + +--- + +## What to Explore Next + + diff --git a/docs/my-website/docs/integrations/index.md b/docs/my-website/docs/integrations/index.md index 95c922cce89..0ad934d5b41 100644 --- a/docs/my-website/docs/integrations/index.md +++ b/docs/my-website/docs/integrations/index.md @@ -1,18 +1,336 @@ -# Integrations +--- +title: Integrations +sidebar_label: Overview +--- + +import NavigationCards from '@site/src/components/NavigationCards'; This section covers integrations with various tools and services that can be used with LiteLLM (either Proxy or SDK). -## AI Agent Frameworks -- **[Letta](./letta.md)** - Build stateful LLM agents with persistent memory using LiteLLM Proxy +--- -## Development Tools -- **[OpenWebUI](../tutorials/openweb_ui.md)** - Self-hosted ChatGPT-style interface +## Observability -## Observability & Monitoring -- **[Langfuse](../observability/langfuse_integration.md)** - LLM observability and analytics -- **[Prometheus](../proxy/prometheus.md)** - Metrics collection and monitoring -- **[PagerDuty](../proxy/pagerduty.md)** - Incident response and alerting -- **[Datadog](../observability/datadog.md)** +Track, debug, and analyze LLM calls with observability platforms. + -Click into each section to learn more about the integrations. \ No newline at end of file +[View all observability integrations →](/docs/integrations/observability_integrations) + +--- + +## Alerting & Monitoring + +Set up alerts, metrics collection, and infrastructure monitoring. + + + +--- + +## Guardrail Providers + +Add safety and content filtering to LLM calls. + + + +[View all guardrail providers →](/docs/guardrail_providers) + +--- + +## Policies + +Define and enforce usage policies across your LLM deployment. + + + +--- + +## AI Tools + +Connect LiteLLM to AI-powered coding and productivity tools. + + + +--- + +## Agent SDKs + +Use LiteLLM with agent frameworks and SDKs. + + + +--- + +## Prompt Management + +Manage, version, and deploy prompts. + + + +--- + +## Manage with AI Agents + +Use AI agents to manage your LiteLLM deployment — create users, teams, keys, models, and more via natural language. + + diff --git a/docs/my-website/docs/integrations/letta.md b/docs/my-website/docs/integrations/letta.md index 2afb82542f2..9711999df5e 100644 --- a/docs/my-website/docs/integrations/letta.md +++ b/docs/my-website/docs/integrations/letta.md @@ -920,9 +920,9 @@ for model in models: ## Resources -- [Letta Documentation](https://docs.letta.ai/) -- [LiteLLM Proxy Documentation](../proxy/quick_start.md) -- [LiteLLM SDK Documentation](../completion/input.md) -- [Function Calling Guide](../completion/function_call.md) -- [Observability Setup](../observability/langfuse_integration.md) -- [Router Configuration](../routing.md) \ No newline at end of file +- [Letta Documentation](https://docs.letta.com/) +- [LiteLLM Proxy Documentation](/docs/simple_proxy) +- [LiteLLM SDK Documentation](/docs/#litellm-python-sdk) +- [Function Calling Guide](/docs/completion/function_call) +- [Observability Setup](/docs/integrations/observability_integrations) +- [Router Configuration](/docs/routing) \ No newline at end of file diff --git a/docs/my-website/docs/integrations/observability_index.md b/docs/my-website/docs/integrations/observability_index.md new file mode 100644 index 00000000000..8ab83950cdc --- /dev/null +++ b/docs/my-website/docs/integrations/observability_index.md @@ -0,0 +1,28 @@ +--- +title: Observability +sidebar_label: Overview +slug: observability_integrations +--- + +Track, debug, and analyze LLM calls with observability platforms. + +import NavigationCards from '@site/src/components/NavigationCards'; + +## Observability Integrations + + + +[View all observability integrations →](/docs/observability/callbacks) diff --git a/docs/my-website/docs/integrations/websearch_interception.md b/docs/my-website/docs/integrations/websearch_interception.md index 0c5d8927013..bc5e8ec0b39 100644 --- a/docs/my-website/docs/integrations/websearch_interception.md +++ b/docs/my-website/docs/integrations/websearch_interception.md @@ -375,7 +375,7 @@ search_tools: - [Search Providers](../search/index.md) - Detailed search provider setup - [Claude Code WebSearch](../tutorials/claude_code_websearch.md) - Using with Claude Code - [Tool Calling](../completion/function_call.md) - General tool calling documentation -- [Callbacks](./custom_callback.md) - Custom callback documentation +- [Callbacks](../observability/custom_callback.md) - Custom callback documentation ## Technical Details diff --git a/docs/my-website/docs/learn/gateway_quickstart.md b/docs/my-website/docs/learn/gateway_quickstart.md new file mode 100644 index 00000000000..acec259758c --- /dev/null +++ b/docs/my-website/docs/learn/gateway_quickstart.md @@ -0,0 +1,174 @@ +--- +title: Gateway Quickstart +sidebar_label: Gateway Quickstart +description: Start LiteLLM Gateway, add models and keys, then connect applications and SDKs to one shared endpoint. +--- + +import NavigationCards from '@site/src/components/NavigationCards'; + +Use this path if you need one shared OpenAI-compatible endpoint for a team or platform. + +If you need a Docker or database-first setup, use the [Docker + Database tutorial](/docs/proxy/docker_quick_start). Otherwise, use the steps below to get to a working request fast. + +## 1. Install The Gateway + +```bash +pip install 'litellm[proxy]' +``` + +## 2. Set One Provider Key + +```bash +export OPENAI_API_KEY="your-api-key" +``` + +## 3. Create `config.yaml` + +```yaml +model_list: + - model_name: gpt-4o-mini + litellm_params: + model: openai/gpt-4o-mini + api_key: os.environ/OPENAI_API_KEY + +general_settings: + master_key: sk-1234 +``` + +## 4. Start The Gateway + +```bash +litellm --config config.yaml +``` + +You should see the proxy start on `http://0.0.0.0:4000`. + +## 5. Send Your First Request + +```bash +curl -X POST 'http://0.0.0.0:4000/chat/completions' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer sk-1234' \ + -d '{ + "model": "gpt-4o-mini", + "messages": [ + {"role": "user", "content": "Hello from LiteLLM Gateway"} + ] + }' +``` + +## 6. Check The Response + +If the request succeeds, the proxy returns `200 OK` with an OpenAI-style response. + +The assistant text will be in: + +```json +choices[0].message.content +``` + +If your gateway is routing to OpenAI, a real response can look like this: + +```json +{ + "id": "chatcmpl-abc123", + "created": 1677858242, + "model": "gpt-4o-mini-2024-07-18", + "object": "chat.completion", + "system_fingerprint": "fp_406d6473f8", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! How can I assist you today?", + "tool_calls": null, + "function_call": null, + "annotations": [] + } + } + ], + "usage": { + "completion_tokens": 9, + "prompt_tokens": 13, + "total_tokens": 22, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 0 + } + }, + "service_tier": "default" +} +``` + +`id`, `created`, the resolved model version, token counts, and message text will vary by request. Other providers may return a smaller or slightly different set of fields, but `choices[0].message.content` is the main field to read. + +## 7. Add Keys And The UI + +If you need virtual keys, spend tracking, or the admin UI, add a database next. + +- Add `database_url` under `general_settings` +- Use [Virtual keys](/docs/proxy/virtual_keys) for key creation and budgets +- Use [Admin UI](/docs/proxy/ui) to manage models and keys +- Use the [Docker + Database tutorial](/docs/proxy/docker_quick_start) if you want a fuller setup + +## 8. Pick Your Next Step + + + +## When To Use The SDK Path Instead + +If you only need to call models from one application and do not need centralized auth or shared infrastructure, start with the [SDK Quickstart](/docs/learn/sdk_quickstart) instead. diff --git a/docs/my-website/docs/learn/index.md b/docs/my-website/docs/learn/index.md new file mode 100644 index 00000000000..018aec5af00 --- /dev/null +++ b/docs/my-website/docs/learn/index.md @@ -0,0 +1,117 @@ +--- +title: Learn LiteLLM +sidebar_label: Learn +slug: /learn +--- + +import NavigationCards from '@site/src/components/NavigationCards'; + +LiteLLM gives you one OpenAI-compatible interface for 100+ LLM providers. Start with the path that matches your setup. + +--- + +## Start Here + +Pick one path first. + + + +--- + +## Common Tasks + +Jump to a specific task. + + + +--- + +## Docs Map + +Use these when you already know the type of doc you want. + + + +Not sure where to start? Use [SDK Quickstart](/docs/learn/sdk_quickstart) for app code or [Gateway Quickstart](/docs/learn/gateway_quickstart) for shared infrastructure. diff --git a/docs/my-website/docs/learn/sdk_quickstart.md b/docs/my-website/docs/learn/sdk_quickstart.md new file mode 100644 index 00000000000..0fb8c3f02a5 --- /dev/null +++ b/docs/my-website/docs/learn/sdk_quickstart.md @@ -0,0 +1,174 @@ +--- +title: SDK Quickstart +sidebar_label: SDK Quickstart +description: Make your first LiteLLM SDK call, then jump to the right docs for the next feature you need. +--- + +import NavigationCards from '@site/src/components/NavigationCards'; + +Use this path if you are integrating LiteLLM directly into application code. + +## 1. Install LiteLLM + +```bash +pip install litellm==1.82.6 +``` + +## 2. Set Provider Credentials + +Start with one provider and set its environment variables. + +- OpenAI: `OPENAI_API_KEY` +- Anthropic: `ANTHROPIC_API_KEY` +- Azure OpenAI: `AZURE_API_KEY`, `AZURE_API_BASE`, `AZURE_API_VERSION` +- Bedrock: standard AWS credentials +- Vertex AI: `VERTEXAI_PROJECT`, `VERTEXAI_LOCATION` + +If you have not picked a provider yet, browse [all supported providers](/docs/providers). + +## 3. Make Your First Call + +```python +from litellm import completion +import os + +os.environ["OPENAI_API_KEY"] = "your-api-key" + +response = completion( + model="openai/gpt-4o", + messages=[{"role": "user", "content": "Hello, how are you?"}], +) + +print(response.choices[0].message.content) +``` + +## 4. Check The Response + +The line below: + +```python +print(response.choices[0].message.content) +``` + +prints the assistant text, for example: + +```text +Hello! I'm doing well, thanks for asking. +``` + +If you print the full object with: + +```python +print(response) +``` + +you will see a Python `ModelResponse(...)` object. For an OpenAI-backed model, it can look like this: + +```python +ModelResponse( + id='chatcmpl-abc123', + created=1773782130, + model='gpt-4o-2024-08-06', + object='chat.completion', + system_fingerprint='fp_4ff89bf575', + choices=[ + Choices( + finish_reason='stop', + index=0, + message=Message( + content="Hello! I'm just a program, but I'm here to help you. How can I assist you today?", + role='assistant', + tool_calls=None, + function_call=None, + provider_specific_fields={'refusal': None}, + annotations=[] + ), + provider_specific_fields={} + ) + ], + usage=Usage( + completion_tokens=21, + prompt_tokens=13, + total_tokens=34, + completion_tokens_details=CompletionTokensDetailsWrapper(...), + prompt_tokens_details=PromptTokensDetailsWrapper(...) + ), + service_tier='default' +) +``` + +The same response follows an OpenAI-style shape. Conceptually, it looks like this: + +```json +{ + "id": "chatcmpl-abc123", + "object": "chat.completion", + "created": 1677858242, + "model": "gpt-4o", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hello! I'm doing well, thanks for asking." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 13, + "completion_tokens": 12, + "total_tokens": 25 + } +} +``` + +`id`, `created`, token counts, and message text will vary by request. + +If you call an OpenAI-backed model, you may also see extra fields such as `system_fingerprint`, `service_tier`, `tool_calls`, `function_call`, `annotations`, `provider_specific_fields`, and detailed token usage. For the full output reference, see [completion output](/docs/completion/output). + +Need more provider examples? See the main [Getting Started](/docs/#quick-start) page. + +## 5. Pick Your Next Step + + + +## When To Use Gateway Instead + +Use LiteLLM Gateway if you need centralized auth, virtual keys, spend tracking, shared logging, or one OpenAI-compatible endpoint for multiple apps. + +[Go to Gateway Quickstart →](/docs/learn/gateway_quickstart) diff --git a/docs/my-website/docs/load_test_advanced.md b/docs/my-website/docs/load_test_advanced.md index d35b5f74784..d7bc35e74e0 100644 --- a/docs/my-website/docs/load_test_advanced.md +++ b/docs/my-website/docs/load_test_advanced.md @@ -11,7 +11,7 @@ Tutorial on how to get to 1K+ RPS with LiteLLM Proxy on locust - [Github releases](https://github.com/BerriAI/litellm/releases) - [litellm docker containers](https://github.com/BerriAI/litellm/pkgs/container/litellm) - [litellm database docker container](https://github.com/BerriAI/litellm/pkgs/container/litellm-database) -- [ ] Ensure you're following **ALL** [best practices for production](./proxy/production_setup.md) +- [ ] Ensure you're following **ALL** [best practices for production](./proxy/prod.md) - [ ] Locust - Ensure you're Locust instance can create 1K+ requests per second - 👉 You can use our **[maintained locust instance here](https://locust-load-tester-production.up.railway.app/)** - If you're self hosting locust @@ -222,4 +222,4 @@ class MyUser(HttpUser): def on_start(self): self.api_key = os.getenv('API_KEY', 'sk-1234') self.client.headers.update({'Authorization': f'Bearer {self.api_key}'}) -``` \ No newline at end of file +``` diff --git a/docs/my-website/docs/migration.md b/docs/my-website/docs/migration.md index e1af07d4684..fda1155905d 100644 --- a/docs/my-website/docs/migration.md +++ b/docs/my-website/docs/migration.md @@ -31,5 +31,4 @@ When we have breaking changes (i.e. going from 1.x.x to 2.x.x), we will document **How can we communicate changes better?** Tell us - [Discord](https://discord.com/invite/wuPM9dRgDw) -- Email (krrish@berri.ai/ishaan@berri.ai) -- Text us (+17708783106) +- Email (support@berri.ai) diff --git a/docs/my-website/docs/observability/arize_integration.md b/docs/my-website/docs/observability/arize_integration.md index b3ccf98ea3b..4486fb2b718 100644 --- a/docs/my-website/docs/observability/arize_integration.md +++ b/docs/my-website/docs/observability/arize_integration.md @@ -194,5 +194,4 @@ print(response) - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / +1 (412) 618-6238 - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/gcs_bucket_integration.md b/docs/my-website/docs/observability/gcs_bucket_integration.md index 69b956950e5..5f8d42508ae 100644 --- a/docs/my-website/docs/observability/gcs_bucket_integration.md +++ b/docs/my-website/docs/observability/gcs_bucket_integration.md @@ -6,7 +6,7 @@ Log LLM Logs to [Google Cloud Storage Buckets](https://cloud.google.com/storage? :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: @@ -79,5 +79,4 @@ curl --location 'http://0.0.0.0:4000/chat/completions' \ - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/langfuse_integration.md b/docs/my-website/docs/observability/langfuse_integration.md index d3c5a44d481..32849ebdb92 100644 --- a/docs/my-website/docs/observability/langfuse_integration.md +++ b/docs/my-website/docs/observability/langfuse_integration.md @@ -342,5 +342,4 @@ Be aware that if you are continuing an existing trace, and you set `update_trace - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/langfuse_otel_integration.md b/docs/my-website/docs/observability/langfuse_otel_integration.md index b4c9a2bd1ad..79ad2f6f75d 100644 --- a/docs/my-website/docs/observability/langfuse_otel_integration.md +++ b/docs/my-website/docs/observability/langfuse_otel_integration.md @@ -83,6 +83,9 @@ os.environ["LANGFUSE_OTEL_HOST"] = "https://cloud.langfuse.com" # EU region # Or use self-hosted instance # os.environ["LANGFUSE_OTEL_HOST"] = "https://my-langfuse.company.com" +# Optional: Ignore otel context propagation to prevent parent-child relationships with spans from other providers +# os.environ["OTEL_IGNORE_CONTEXT_PROPAGATION"] = "true" + litellm.callbacks = ["langfuse_otel"] ``` @@ -124,6 +127,9 @@ export LANGFUSE_PUBLIC_KEY="pk-lf-..." export LANGFUSE_SECRET_KEY="sk-lf-..." export LANGFUSE_OTEL_HOST="https://us.cloud.langfuse.com" # Default US region # export LANGFUSE_OTEL_HOST="https://otel.my-langfuse.company.com" # custom OTEL endpoint + +# Optional: Ignore otel context propagation to prevent parent-child relationships with spans from other providers +# export OTEL_IGNORE_CONTEXT_PROPAGATION="true" ``` 2. Setup config.yaml diff --git a/docs/my-website/docs/observability/langsmith_integration.md b/docs/my-website/docs/observability/langsmith_integration.md index cada4122b20..bf867319de8 100644 --- a/docs/my-website/docs/observability/langsmith_integration.md +++ b/docs/my-website/docs/observability/langsmith_integration.md @@ -225,5 +225,4 @@ environment_variables: - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/logfire_integration.md b/docs/my-website/docs/observability/logfire_integration.md index a1bd43a4bc4..00652c0f1a1 100644 --- a/docs/my-website/docs/observability/logfire_integration.md +++ b/docs/my-website/docs/observability/logfire_integration.md @@ -63,5 +63,4 @@ response = litellm.completion( - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/lunary_integration.md b/docs/my-website/docs/observability/lunary_integration.md index 8d28321c807..4f1dca9d4af 100644 --- a/docs/my-website/docs/observability/lunary_integration.md +++ b/docs/my-website/docs/observability/lunary_integration.md @@ -176,5 +176,4 @@ You can find more details about the different ways of making requests to the Lit - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/opik_integration.md b/docs/my-website/docs/observability/opik_integration.md index d28c46f0b4b..5b5cbe0f185 100644 --- a/docs/my-website/docs/observability/opik_integration.md +++ b/docs/my-website/docs/observability/opik_integration.md @@ -261,5 +261,4 @@ All requests made with this key will automatically be tracked in the "TestProjec - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/phoenix_integration.md b/docs/my-website/docs/observability/phoenix_integration.md index 191f1f8044a..67ba815557e 100644 --- a/docs/my-website/docs/observability/phoenix_integration.md +++ b/docs/my-website/docs/observability/phoenix_integration.md @@ -127,5 +127,4 @@ Depending on which Phoenix Cloud version or deployment you are using, you should - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/promptlayer_integration.md b/docs/my-website/docs/observability/promptlayer_integration.md index 7f62a316972..9462e755f74 100644 --- a/docs/my-website/docs/observability/promptlayer_integration.md +++ b/docs/my-website/docs/observability/promptlayer_integration.md @@ -84,5 +84,4 @@ Credits to [Nick Bradford](https://github.com/nsbradford), from [Vim-GPT](https: - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai \ No newline at end of file diff --git a/docs/my-website/docs/observability/slack_integration.md b/docs/my-website/docs/observability/slack_integration.md index 0ca7f616683..468d8b5945c 100644 --- a/docs/my-website/docs/observability/slack_integration.md +++ b/docs/my-website/docs/observability/slack_integration.md @@ -101,5 +101,4 @@ response = litellm.completion( - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/sumologic_integration.md b/docs/my-website/docs/observability/sumologic_integration.md index c30ee94dad4..87e20ca57ed 100644 --- a/docs/my-website/docs/observability/sumologic_integration.md +++ b/docs/my-website/docs/observability/sumologic_integration.md @@ -328,5 +328,4 @@ If you get authentication errors, regenerate the HTTP Source URL in Sumo Logic: - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/supabase_integration.md b/docs/my-website/docs/observability/supabase_integration.md index fd3f1c3d5a0..c29871d752f 100644 --- a/docs/my-website/docs/observability/supabase_integration.md +++ b/docs/my-website/docs/observability/supabase_integration.md @@ -105,5 +105,4 @@ litellm.modify_integration("supabase",{"table_name": "litellm_logs"}) - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/observability/wandb_integration.md b/docs/my-website/docs/observability/wandb_integration.md index 37057f43db5..3c1a3363957 100644 --- a/docs/my-website/docs/observability/wandb_integration.md +++ b/docs/my-website/docs/observability/wandb_integration.md @@ -57,5 +57,4 @@ response = litellm.completion( - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai \ No newline at end of file diff --git a/docs/my-website/docs/prompt_management.md b/docs/my-website/docs/prompt_management.md new file mode 100644 index 00000000000..c4e606674b1 --- /dev/null +++ b/docs/my-website/docs/prompt_management.md @@ -0,0 +1,48 @@ +--- +title: Prompt Management with Responses API +--- + +# Prompt Management with Responses API + +Use LiteLLM Prompt Management with `/v1/responses` by passing `prompt_id` and optional `prompt_variables`. + +## Basic Usage + +```bash +curl -X POST "http://localhost:4000/v1/responses" \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o", + "prompt_id": "my-responses-prompt", + "prompt_variables": {"topic": "large language models"}, + "input": [] + }' +``` + +## Multi-turn Follow-up in `input` + +To send follow-up turns in one request, pass message history in `input`. + +```bash +curl -X POST "http://localhost:4000/v1/responses" \ + -H "Authorization: Bearer sk-1234" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "gpt-4o", + "prompt_id": "my-responses-prompt", + "prompt_variables": {"topic": "large language models"}, + "input": [ + {"role": "user", "content": "Topic is LLMs. Start short."}, + {"role": "assistant", "content": "Sure, go ahead."}, + {"role": "user", "content": "Now give me 3 bullets and include pricing caveat."} + ] + }' +``` + +## Notes + +- Prompt template messages are merged with your `input` messages. +- Prompt variable substitution applies to prompt message content. +- Tool call payload fields are not substituted by prompt variables. +- For follow-ups with `previous_response_id`, include `prompt_id` again if you want prompt management applied on that turn. diff --git a/docs/my-website/docs/providers/azure/azure.md b/docs/my-website/docs/providers/azure/azure.md index 12ddc1bd98e..682f263c108 100644 --- a/docs/my-website/docs/providers/azure/azure.md +++ b/docs/my-website/docs/providers/azure/azure.md @@ -1032,7 +1032,7 @@ print("list_batches_response=", list_batches_response) -### [Health Check Azure Batch models](./proxy/health.md#batch-models-azure-only) +### [Health Check Azure Batch models](../../proxy/health.md#batch-models-azure-only) ### [BETA] Loadbalance Multiple Azure Deployments diff --git a/docs/my-website/docs/providers/azure/azure_anthropic.md b/docs/my-website/docs/providers/azure/azure_anthropic.md index 4c722b30397..e7cd8fffbf0 100644 --- a/docs/my-website/docs/providers/azure/azure_anthropic.md +++ b/docs/my-website/docs/providers/azure/azure_anthropic.md @@ -372,7 +372,6 @@ response = completion( ## Related Documentation -- [Anthropic Provider Documentation](./anthropic.md) - For standard Anthropic API usage +- [Anthropic Provider Documentation](../anthropic.md) - For standard Anthropic API usage - [Azure OpenAI Documentation](./azure.md) - For Azure OpenAI models -- [Azure Authentication Guide](../secret_managers/azure_key_vault.md) - For Azure AD token setup - +- [Azure Authentication Guide](../../secret_managers/azure_key_vault.md) - For Azure AD token setup diff --git a/docs/my-website/docs/providers/gemini.md b/docs/my-website/docs/providers/gemini.md index 0aaf3d5ae81..87ab5ad40f4 100644 --- a/docs/my-website/docs/providers/gemini.md +++ b/docs/my-website/docs/providers/gemini.md @@ -11,6 +11,7 @@ import TabItem from '@theme/TabItem'; | Provider Doc | [Google AI Studio ↗](https://aistudio.google.com/) | | API Endpoint for Provider | https://generativelanguage.googleapis.com | | Supported OpenAI Endpoints | `/chat/completions`, [`/embeddings`](../embedding/supported_embedding#gemini-ai-embedding-models), `/completions`, [`/videos`](./gemini/videos.md), [`/images/edits`](../image_edits.md) | +| Lyria (music) | [Cost map & notes](./gemini/music.md) | | Pass-through Endpoint | [Supported](../pass_through/google_ai_studio.md) |
@@ -54,6 +55,7 @@ response = completion( - stream - tools - tool_choice +- include_server_side_tool_invocations - functions - response_format - n @@ -856,7 +858,112 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \ -### URL Context +### Context Circulation (Server-Side Tool Combination) + +Context circulation allows Gemini 3+ models to combine **built-in tools** (like Google Search) with **your custom functions** in the same request. Without it, Gemini returns an error if you try to use both. + +When enabled, Gemini can execute Google Search server-side, use those results to decide whether to call your custom functions, and return the full chain of reasoning. + +**How it works:** +1. You pass `include_server_side_tool_invocations=True` along with both Google Search and your function tools +2. Gemini executes server-side tools internally and returns `toolCall`/`toolResponse` parts alongside any `functionCall` parts +3. LiteLLM extracts the server-side invocations into `provider_specific_fields["server_side_tool_invocations"]` +4. On subsequent turns, include the full assistant message in your conversation history — LiteLLM re-injects the server-side parts automatically + + + + +```python +from litellm import completion + +response = completion( + model="gemini/gemini-3-flash-preview", + messages=[{"role": "user", "content": "What's the weather in Buenos Aires? If it's raining, schedule a meeting."}], + tools=[ + {"type": "web_search_preview"}, # Google Search (server-side) + { + "type": "function", + "function": { + "name": "schedule_meeting", + "description": "Schedule a meeting", + "parameters": { + "type": "object", + "properties": {"reason": {"type": "string"}}, + "required": ["reason"], + }, + }, + }, + ], + include_server_side_tool_invocations=True, +) + +msg = response.choices[0].message + +# Server-side tool results are in provider_specific_fields +psf = msg.provider_specific_fields or {} +for invocation in psf.get("server_side_tool_invocations", []): + print(invocation["tool_type"]) # e.g. "GOOGLE_SEARCH_WEB" + print(invocation["id"]) + print(invocation["args"]) # e.g. {"queries": ["weather Buenos Aires"]} + print(invocation["response"]) # Search results from Google + +# For multi-turn: just append the full message to history +messages.append(msg) +messages.append({"role": "user", "content": "Thanks!"}) +# LiteLLM automatically re-injects the server-side parts + thought signatures +response2 = completion( + model="gemini/gemini-3-flash-preview", + messages=messages, + tools=tools, + include_server_side_tool_invocations=True, +) +``` + + + + +1. Setup config.yaml +```yaml +model_list: + - model_name: gemini-3-flash + litellm_params: + model: gemini/gemini-3-flash-preview + api_key: os.environ/GEMINI_API_KEY +``` + +2. Start Proxy +```bash +$ litellm --config /path/to/config.yaml +``` + +3. Make Request +```bash +curl -X POST 'http://0.0.0.0:4000/chat/completions' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-1234' \ +-d '{ + "model": "gemini-3-flash", + "messages": [{"role": "user", "content": "What is the weather in Buenos Aires?"}], + "tools": [ + {"type": "web_search_preview"}, + {"type": "function", "function": {"name": "schedule_meeting", "description": "Schedule a meeting", "parameters": {"type": "object", "properties": {"reason": {"type": "string"}}}}} + ], + "include_server_side_tool_invocations": true +}' +``` + + + + +:::info + +- Context circulation requires **Gemini 3+** models +- Server-side tool invocations (`toolCall`/`toolResponse`) are **not** included in `tool_calls` — they are in `provider_specific_fields["server_side_tool_invocations"]` because they were already executed by Google, not by your code +- `thought_signatures` are automatically preserved alongside server-side invocations for multi-turn coherence + +::: + +### URL Context diff --git a/docs/my-website/docs/providers/gemini/music.md b/docs/my-website/docs/providers/gemini/music.md new file mode 100644 index 00000000000..f3968f2db39 --- /dev/null +++ b/docs/my-website/docs/providers/gemini/music.md @@ -0,0 +1,28 @@ +# Gemini — Lyria (music generation) + +Google Lyria 3 preview models are listed in LiteLLM’s [model cost map](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) under the `gemini/` provider for metadata and spend tracking. + +| Property | Details | +|----------|---------| +| Provider route | `gemini/` | +| Models | `gemini/lyria-3-clip-preview`, `gemini/lyria-3-pro-preview` | +| Provider docs | [Gemini API pricing / models ↗](https://ai.google.dev/gemini-api/docs/pricing) | + +## Models + +| Model | Notes | +|-------|--------| +| `gemini/lyria-3-clip-preview` | ~30s clip; paid tier listed as per generated song in Google’s pricing | +| `gemini/lyria-3-pro-preview` | Full song; paid tier listed as per generated song in Google’s pricing | + +Input context limit in the cost map: **131,072** tokens. For modalities, limits, and features, see [Google’s Gemini API docs ↗](https://ai.google.dev/gemini-api/docs/models). + +## LiteLLM behavior + +- **Cost map**: Per-song paid pricing is stored as `output_cost_per_image` on those entries (flat per generation unit). Token-based completion cost may not reflect music billing until a dedicated path exists. +- **API calls**: Use the Gemini API as documented by Google. LiteLLM does not ship a separate `music_generation` helper like Veo’s `video_generation`. + +## Auth + +Same as other Gemini API models: `GEMINI_API_KEY` or `GOOGLE_API_KEY`. + diff --git a/docs/my-website/docs/providers/openai.md b/docs/my-website/docs/providers/openai.md index 80931ad8217..1f4a1687e8b 100644 --- a/docs/my-website/docs/providers/openai.md +++ b/docs/my-website/docs/providers/openai.md @@ -581,6 +581,90 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \ See [OpenAI Reasoning documentation](https://platform.openai.com/docs/guides/reasoning) for more details on organization verification requirements. +### Multi-turn Conversations with `reasoning_items` + +For multi-turn conversations you need `reasoning_items`: structured blocks that include the `encrypted_content` token OpenAI uses to restore reasoning state on the next request. Pass `include=["reasoning.encrypted_content"]` on every call where you want that token returned. + + + + +```python showLineNumbers title="Non-streaming: round-trip reasoning_items" +import litellm + +messages = [{"role": "user", "content": "Solve this step by step: 2 + 2"}] + +# Turn 1 — get reasoning_items (encrypted_content); +response = litellm.completion( + model="openai/responses/gpt-5-mini", + messages=messages, + reasoning_effort="low", + include=["reasoning.encrypted_content"], +) + +assistant_msg = response.choices[0].message + +# Turn 2 — pass reasoning_items back; LiteLLM converts to the correct Responses API format +messages.append({ + "role": "assistant", + "content": assistant_msg.content, + "reasoning_items": assistant_msg.reasoning_items, +}) +messages.append({"role": "user", "content": "Now summarize your reasoning."}) + +response2 = litellm.completion( + model="openai/responses/gpt-5-mini", + messages=messages, + reasoning_effort="low", + include=["reasoning.encrypted_content"], +) +``` + + + + +`reasoning_items` (with `encrypted_content`) arrive on the final chunk when the full response completes: + +```python showLineNumbers title="Streaming: collect and round-trip reasoning_items" +import litellm + +messages = [{"role": "user", "content": "Solve this step by step: 2 + 2"}] + +collected_content = [] +collected_reasoning_items = [] + +stream = litellm.completion( + model="openai/responses/gpt-5-mini", + messages=messages, + stream=True, + reasoning_effort="low", + include=["reasoning.encrypted_content"], +) + +for chunk in stream: + delta = chunk.choices[0].delta + if delta.content: + collected_content.append(delta.content) + if getattr(delta, "reasoning_items", None): + collected_reasoning_items.extend(delta.reasoning_items) + +messages.append({ + "role": "assistant", + "content": "".join(collected_content), + "reasoning_items": collected_reasoning_items or None, +}) +messages.append({"role": "user", "content": "Continue the conversation."}) + +response2 = litellm.completion( + model="openai/responses/gpt-5-mini", + messages=messages, + reasoning_effort="low", + include=["reasoning.encrypted_content"], +) +``` + + + + ### Verbosity Control for GPT-5 Models The `verbosity` parameter controls the length and detail of responses from GPT-5 family models. It accepts three values: `"low"`, `"medium"`, or `"high"`. @@ -1153,4 +1237,4 @@ response = completion( LiteLLM supports OpenAI's video generation models including Sora. -For detailed documentation on video generation, see [OpenAI Video Generation →](./openai/video_generation.md) +For detailed documentation on video generation, see [OpenAI Video Generation →](./openai/videos.md) diff --git a/docs/my-website/docs/proxy/call_hooks.md b/docs/my-website/docs/proxy/call_hooks.md index 17354725fd5..5935a29c50b 100644 --- a/docs/my-website/docs/proxy/call_hooks.md +++ b/docs/my-website/docs/proxy/call_hooks.md @@ -7,7 +7,7 @@ import Image from '@theme/IdealImage'; - Enforce 'user' param for all openai endpoint calls :::tip -**Understanding Callback Hooks?** Check out our [Callback Management Guide](../observability/callback_management.md) to understand the differences between proxy-specific hooks like `async_pre_call_hook` and general logging hooks like `async_log_success_event`. +**Understanding Callback Hooks?** Check out our [Callback Guide](../observability/callbacks.md) to understand the differences between proxy-specific hooks like `async_pre_call_hook` and general logging hooks like `async_log_success_event`. ::: ## Which Hook Should I Use? diff --git a/docs/my-website/docs/proxy/config_settings.md b/docs/my-website/docs/proxy/config_settings.md index a0e404e3a18..cc9090c2de6 100644 --- a/docs/my-website/docs/proxy/config_settings.md +++ b/docs/my-website/docs/proxy/config_settings.md @@ -206,7 +206,7 @@ router_settings: | Name | Type | Description | |------|------|-------------| -| completion_model | string | The default model to use for completions when `model` is not specified in the request | +| completion_model | string | The model to use for all completions, overriding any `model` specified in the request | | disable_spend_logs | boolean | If true, turns off writing each transaction to the database | | disable_spend_updates | boolean | If true, turns off all spend updates to the DB. Including key/user/team spend updates. | | disable_master_key_return | boolean | If true, turns off returning master key on UI. (checked on '/user/info' endpoint) | @@ -279,6 +279,33 @@ router_settings: | forward_client_headers_to_llm_api | boolean | If true, forwards the client headers (any `x-` headers and `anthropic-beta` headers) to the backend LLM call | | maximum_spend_logs_retention_period | str | Used to set the max retention time for spend logs in the db, after which they will be auto-purged | | maximum_spend_logs_retention_interval | str | Used to set the interval in which the spend log cleanup task should run in. | +| alert_type_config | dict | Configuration mapping alert types to their handler settings | +| always_include_stream_usage | boolean | If true, includes usage metrics in every streaming response chunk | +| auto_redirect_ui_login_to_sso | boolean | If true, automatically redirects UI login page to SSO provider | +| control_plane_url | string | URL of the control plane for cross-instance state sharing | +| custom_auth_run_common_checks | boolean | If true, runs standard auth validation checks alongside custom auth handlers | +| custom_ui_sso_sign_in_handler | string | Custom handler for SSO sign-in logic in the UI | +| database_connection_pool_timeout | integer | Database connection pool timeout in seconds | +| disable_error_logs | boolean | If true, suppresses error tracking and storage in the database | +| enable_health_check_routing | boolean | If true, enables health check-driven request routing to avoid unhealthy deployments | +| enable_mcp_registry | boolean | If true, enables access to the centralized MCP server registry | +| enforce_rbac | boolean | If true, enables role-based access control (RBAC) for all proxy operations | +| forward_llm_provider_auth_headers | boolean | If true, forwards provider-specific auth headers to LLM API calls | +| health_check_concurrency | integer | Maximum number of concurrent health check operations | +| health_check_staleness_threshold | integer | Maximum age in seconds for health check results before marking deployments as stale | +| maximum_spend_logs_cleanup_cron | string | Cron expression for scheduling automatic spend log cleanup tasks | +| mcp_client_side_auth_header_name | string | HTTP header name for client-side MCP server credentials | +| mcp_internal_ip_ranges | list | CIDR ranges considered internal for non-public MCP server access control | +| mcp_required_fields | list | List of required field names for MCP server submissions | +| mcp_trusted_proxy_ranges | list | CIDR ranges of proxies trusted to forward X-Forwarded-For headers for MCP | +| require_end_user_mcp_access_defined | boolean | If true, requires end users to have explicit MCP access permissions defined | +| role_permissions | list | List of role-based permission configurations | +| search_tools | list | List of search tool configurations for enabling web search capabilities | +| token_rate_limit_type | string | Rate limit counting method: "total", "output", or "input" tokens | +| use_redis_transaction_buffer | boolean | If true, buffers database transactions in Redis before writing | +| use_shared_health_check | boolean | If true, uses Redis-backed shared health check state across multiple proxy instances | +| user_header_mappings | dict | Map custom request headers to user IDs using lookup rules | +| user_header_name | string | HTTP header name to extract user identity from requests | ### router_settings - Reference @@ -361,11 +388,14 @@ router_settings: | redis_url | str | URL for Redis server. **Known performance issue with Redis URL.** | | cache_responses | boolean | Flag to enable caching LLM Responses, if cache set under `router_settings`. If true, caches responses. Defaults to False. | | router_general_settings | RouterGeneralSettings | [SDK-Only] Router general settings - contains optimizations like 'async_only_mode'. [Docs](../routing.md#router-general-settings) | -| optional_pre_call_checks | List[str] | List of pre-call checks to add to the router. Supported: `router_budget_limiting`, `prompt_caching`, `responses_api_deployment_check`, `encrypted_content_affinity`, `deployment_affinity`, `session_affinity`, `forward_client_headers_by_model_group` | +| optional_pre_call_checks | List[str] | List of pre-call checks to add to the router. Supported: `router_budget_limiting`, `prompt_caching`, `responses_api_deployment_check`, `encrypted_content_affinity` (requires LiteLLM >= 1.82.3), `deployment_affinity`, `session_affinity`, `forward_client_headers_by_model_group` | | deployment_affinity_ttl_seconds | int | TTL (seconds) for user-key → deployment affinity mapping when `deployment_affinity` is enabled (configured at Router init / proxy startup). Defaults to `3600` (1 hour). | +| model_group_affinity_config | Dict[str, List[str]] | Per-model-group affinity flags. Keys are model group names; values are lists of checks to enable (`deployment_affinity`, `responses_api_deployment_check`, `session_affinity`). Groups not listed fall back to the global `optional_pre_call_checks`. [Docs](../response_api.md#per-model-group-affinity-configuration) | | ignore_invalid_deployments | boolean | If true, ignores invalid deployments. Default for proxy is True - to prevent invalid models from blocking other models from being loaded. | -| search_tools | List[SearchToolTypedDict] | List of search tool configurations for Search API integration. Each tool specifies a search_tool_name and litellm_params with search_provider, api_key, api_base, etc. [Further Docs](../search.md) | +| search_tools | List[SearchToolTypedDict] | List of search tool configurations for Search API integration. Each tool specifies a search_tool_name and litellm_params with search_provider, api_key, api_base, etc. [Further Docs](../search/index.md) | | guardrail_list | List[GuardrailTypedDict] | List of guardrail configurations for guardrail load balancing. Enables load balancing across multiple guardrail deployments with the same guardrail_name. [Further Docs](./guardrails/guardrail_load_balancing.md) | +| enable_health_check_routing | boolean | If true, enables health check-driven deployment filtering to avoid routing requests to unhealthy deployments | +| health_check_staleness_threshold | integer | Maximum age in seconds for cached health check results before marking deployments as stale | ### environment variables - Reference @@ -401,8 +431,10 @@ router_settings: | AUTH_STRATEGY | Strategy used for authentication (e.g., OAuth, API key) | AUTO_REDIRECT_UI_LOGIN_TO_SSO | Flag to enable automatic redirect of UI login page to SSO when SSO is configured. Default is **false** | AUDIO_SPEECH_CHUNK_SIZE | Chunk size for audio speech processing. Default is 1024 -| ANTHROPIC_API_KEY | API key for Anthropic service +| ANTHROPIC_API_KEY | API key for Anthropic service. Uses `x-api-key` header for authentication. +| ANTHROPIC_AUTH_TOKEN | Alternative auth token for Anthropic service. Uses `Authorization: Bearer` header instead of `x-api-key`. Used as fallback when `ANTHROPIC_API_KEY` is not set. | ANTHROPIC_API_BASE | Base URL for Anthropic API. Default is https://api.anthropic.com +| ANTHROPIC_BASE_URL | Alternative to `ANTHROPIC_API_BASE` for setting the Anthropic API base URL. Used as fallback when `ANTHROPIC_API_BASE` is not set. | ANTHROPIC_TOKEN_COUNTING_BETA_VERSION | Beta version header for Anthropic token counting API. Default is `token-counting-2024-11-01` | AWS_ACCESS_KEY_ID | Access Key ID for AWS services | AWS_BATCH_ROLE_ARN | ARN of the AWS IAM role for batch operations @@ -801,6 +833,7 @@ router_settings: | LITELLM_OTEL_INTEGRATION_ENABLE_EVENTS | Optionally enable semantic logs for OTEL | LITELLM_OTEL_INTEGRATION_ENABLE_METRICS | Optionally enable emantic metrics for OTEL | LITELLM_ENABLE_PYROSCOPE | If true, enables Pyroscope CPU profiling. Profiles are sent to PYROSCOPE_SERVER_ADDRESS. Off by default. See [Pyroscope profiling](/proxy/pyroscope_profiling). +| LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS | When `true`, if a team's legacy `model_aliases` entry maps a public model name to an internal `model_name__` deployment, pre-call handling can skip that rewrite when team-scoped sibling deployments exist for the public name—so load balancing / `order` apply across siblings. Default is `false` for backwards compatibility. See [Team-scoped models and legacy aliases](./load_balancing#team-scoped-models-and-legacy-model_aliases). When stale aliases are detected and this flag is off, the proxy may log a one-time warning. | PYROSCOPE_APP_NAME | Application name reported to Pyroscope. Required when LITELLM_ENABLE_PYROSCOPE is true. No default. | PYROSCOPE_SERVER_ADDRESS | Pyroscope server URL to send profiles to. Required when LITELLM_ENABLE_PYROSCOPE is true. No default. | PYROSCOPE_SAMPLE_RATE | Optional. Sample rate for Pyroscope profiling (integer). No default; when unset, the pyroscope-io library default is used. @@ -811,7 +844,7 @@ router_settings: | LITELLM_MODE | Operating mode for LiteLLM (e.g., production, development) | LITELLM_NON_ROOT | Flag to run LiteLLM in non-root mode for enhanced security in Docker containers | LITELLM_RATE_LIMIT_WINDOW_SIZE | Rate limit window size for LiteLLM. Default is 60 -| LITELLM_REASONING_AUTO_SUMMARY | If set to "true", automatically enables detailed reasoning summaries for reasoning models (e.g., o1, o3-mini, deepseek-reasoner). When enabled, adds `summary: "detailed"` to reasoning effort configurations. Default is "false" +| LITELLM_REASONING_AUTO_SUMMARY | If set to "true", automatically enables detailed reasoning summaries (`summary: "detailed"`) for reasoning models across all translation paths (Anthropic adapter, Responses API, etc.). Default is "false" | LITELLM_SALT_KEY | Salt key for encryption in LiteLLM | LITELLM_SSL_CIPHERS | SSL/TLS cipher configuration for faster handshakes. Controls cipher suite preferences for OpenSSL connections. | LITELLM_SECRET_AWS_KMS_LITELLM_LICENSE | AWS KMS encrypted license for LiteLLM @@ -902,6 +935,7 @@ router_settings: | OTEL_SERVICE_NAME | Service name identifier for OpenTelemetry | OTEL_TRACER_NAME | Tracer name for OpenTelemetry tracing | OTEL_LOGS_EXPORTER | Exporter type for OpenTelemetry logs (e.g., console) +| OTEL_IGNORE_CONTEXT_PROPAGATION | When true, ignore parent span context propagation in OpenTelemetry callbacks | PAGERDUTY_API_KEY | API key for PagerDuty Alerting | PANW_PRISMA_AIRS_API_KEY | API key for PANW Prisma AIRS service | PANW_PRISMA_AIRS_API_BASE | Base URL for PANW Prisma AIRS service @@ -949,6 +983,8 @@ router_settings: | QDRANT_URL | Connection URL for Qdrant database | QDRANT_VECTOR_SIZE | Vector size for Qdrant operations. Default is 1536 | REDIS_CONNECTION_POOL_TIMEOUT | Timeout in seconds for Redis connection pool. Default is 5 +| REDIS_CIRCUIT_BREAKER_FAILURE_THRESHOLD | Number of consecutive failures before the Redis circuit breaker opens. Default is 5 +| REDIS_CIRCUIT_BREAKER_RECOVERY_TIMEOUT | Time in seconds before the Redis circuit breaker attempts recovery after opening. Default is 60 | REDIS_CLUSTER_NODES | JSON-formatted list of Redis cluster startup nodes for Redis Cluster mode. Example: `[{"host": "node1", "port": 6379}]` | REDIS_HOST | Hostname for Redis server | REDIS_PASSWORD | Password for Redis service diff --git a/docs/my-website/docs/proxy/configs.md b/docs/my-website/docs/proxy/configs.md index 56a8b9566db..84a6fac1210 100644 --- a/docs/my-website/docs/proxy/configs.md +++ b/docs/my-website/docs/proxy/configs.md @@ -602,6 +602,22 @@ Since you shouldn't use 12.5, round down to **10** to leave a safety buffer. Thi - Total maximum connections: 8 workers × 10 connections = 80 connections - This stays safely under your database's 100 connection limit +## LiteLLM License Key (Enterprise) + +To enable [LiteLLM Enterprise features](https://docs.litellm.ai/docs/proxy/enterprise), set your license key as an environment variable: + +```bash +export LITELLM_LICENSE="eyJ..." +``` + +The license key is a JWT token provided when you purchase a LiteLLM Enterprise license. Once set, LiteLLM will automatically detect and activate enterprise features. + +You can also add it to your `.env` file: + +```env +LITELLM_LICENSE="eyJ..." +``` + ## Extras diff --git a/docs/my-website/docs/proxy/cost_tracking.md b/docs/my-website/docs/proxy/cost_tracking.md index f28eec287d4..f9e22cfecd3 100644 --- a/docs/my-website/docs/proxy/cost_tracking.md +++ b/docs/my-website/docs/proxy/cost_tracking.md @@ -163,7 +163,7 @@ Use this when you want non-proxy admins to access `/spend` endpoints :::info -Schedule a [meeting with us to get your Enterprise License](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +Schedule a [meeting with us to get your Enterprise License](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/docker_quick_start.md b/docs/my-website/docs/proxy/docker_quick_start.md index efdc73de43e..58a56604751 100644 --- a/docs/my-website/docs/proxy/docker_quick_start.md +++ b/docs/my-website/docs/proxy/docker_quick_start.md @@ -1,25 +1,90 @@ - import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; +import Image from '@theme/IdealImage'; # Getting Started Tutorial End-to-End tutorial for LiteLLM Proxy to: -- Add an Azure OpenAI model -- Make a successful /chat/completion call -- Generate a virtual key -- Set RPM limit on virtual key +- Add an Azure OpenAI model +- Make a successful /chat/completion call +- Generate a virtual key +- Set RPM limit on virtual key +## Quick Install (Recommended for local / beginners) + +New to LiteLLM? This is the easiest way to get started locally. One command installs LiteLLM and walks you through setup interactively — no config files to write by hand. + +### 1. Install + +```bash +curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/install.sh | sh +``` + +This detects your OS, installs `litellm[proxy]`, and drops you straight into the setup wizard. + +### 2. Follow the wizard + +``` +$ litellm --setup + + Welcome to LiteLLM + + Choose your LLM providers + ○ 1. OpenAI GPT-4o, GPT-4o-mini, o1 + ○ 2. Anthropic Claude Opus, Sonnet, Haiku + ○ 3. Azure OpenAI GPT-4o via Azure + ○ 4. Google Gemini Gemini 2.0 Flash, 1.5 Pro + ○ 5. AWS Bedrock Claude, Llama via AWS + ○ 6. Ollama Local models + + ❯ Provider(s): 1,2 + + ❯ OpenAI API key: sk-... + ❯ Anthropic API key: sk-ant-... + + ❯ Port [4000]: + ❯ Master key [auto-generate]: + + ✔ Config saved → ./litellm_config.yaml + + ❯ Start the proxy now? (Y/n): +``` + +The wizard walks you through: +1. Pick your LLM providers (OpenAI, Anthropic, Azure, Bedrock, Gemini, Ollama) +2. Enter API keys for each provider +3. Set a port and master key (or accept the defaults) +4. Config is saved to `./litellm_config.yaml` and the proxy starts immediately + +### 3. Make a call + +Your proxy is running on `http://0.0.0.0:4000`. Test it: + +```bash +curl -X POST 'http://0.0.0.0:4000/chat/completions' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer ' \ +-d '{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello!"}] +}' +``` + +:::tip Already have pip installed? +You can skip the curl install and run `litellm --setup` directly after `pip install 'litellm[proxy]'`. +::: + +--- ## Pre-Requisites -- Install LiteLLM Docker Image **OR** LiteLLM CLI (pip package) +Choose your install method. **Docker Compose** users complete their full setup inside the tab and are done. **Docker** and **pip** users continue with the steps below the tabs. -``` +```bash docker pull docker.litellm.ai/berriai/litellm:main-latest ``` @@ -37,7 +102,25 @@ $ pip install 'litellm[proxy]' -Use this docker compose to spin up the proxy with a postgres database running locally. +Docker Compose bundles LiteLLM with a Postgres database. Follow the steps below — the proxy will be fully running by the end. + +### Step 1 — Pull the LiteLLM database image + +LiteLLM provides a dedicated `litellm-database` image for proxy deployments that connect to Postgres. + +```bash +docker pull ghcr.io/berriai/litellm-database:main-latest +``` + +See all available tags on the [GitHub Container Registry](https://github.com/BerriAI/litellm/pkgs/container/litellm-database). + +--- + +### Step 2 — Set up a database + +Complete all three config files **before** running `docker compose up`. The proxy server will not start correctly if any of these are missing. + +#### 2.1 — Get `docker-compose.yml` and create `.env` ```bash # Get the docker compose file @@ -46,26 +129,154 @@ curl -O https://raw.githubusercontent.com/BerriAI/litellm/main/docker-compose.ym # Add the master key - you can change this after setup echo 'LITELLM_MASTER_KEY="sk-1234"' > .env -# Add the litellm salt key - you cannot change this after adding a model -# It is used to encrypt / decrypt your LLM API Key credentials -# We recommend - https://1password.com/password-generator/ -# password generator to get a random hash for litellm salt key +# Add the litellm salt key — cannot be changed after adding a model +# Used to encrypt/decrypt your LLM API key credentials +# Generate a strong random value: https://1password.com/password-generator/ echo 'LITELLM_SALT_KEY="sk-1234"' >> .env -# Start +# Add your model credentials +echo 'AZURE_API_BASE="https://openai-***********/"' >> .env +echo 'AZURE_API_KEY="your-azure-api-key"' >> .env +``` + +#### 2.2 — Create `config.yaml` + +The default `docker-compose.yml` starts a Postgres container at `db:5432`. Your `config.yaml` must include `database_url` pointing to it: + +```yaml +model_list: + - model_name: gpt-4o + litellm_params: + model: azure/my_azure_deployment + api_base: os.environ/AZURE_API_BASE + api_key: os.environ/AZURE_API_KEY + api_version: "2025-01-01-preview" + +general_settings: + master_key: sk-1234 # 🔑 your proxy admin key (must start with sk-) + database_url: "postgresql://llmproxy:dbpassword9090@db:5432/litellm" +``` + +:::tip +`database_url` enables virtual keys, spend tracking, and the UI. Replace it with your [Supabase](https://supabase.com/) or [Neon](https://neon.tech/) connection string if you prefer a managed database. +::: + +#### 2.3 — Create `prometheus.yml` + +This file **must exist as a file** before `docker compose up`. If it is missing, Docker auto-creates it as an empty directory and the Prometheus container fails to start. + +```yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "litellm" + static_configs: + - targets: ["litellm:4000"] +``` + +Also verify that the `config.yaml` volume mount and `--config` flag are **not commented out** in `docker-compose.yml`: + +```yaml +services: + litellm: + volumes: + - ./config.yaml:/app/config.yaml # ✅ must be uncommented + command: + - "--config=/app/config.yaml" # ✅ must be uncommented +``` + +:::warning +All three files (`.env`, `config.yaml`, `prometheus.yml`) must be present before running `docker compose up`. See [Troubleshooting](#troubleshooting) if you run into issues. +::: + +--- + +### Step 3 — Start the proxy server and test it + +After `config.yaml`, `prometheus.yml`, and `.env` are complete, start the proxy: + +```bash docker compose up ``` +Once running, test it with a curl request: + +```bash +curl -X POST 'http://0.0.0.0:4000/chat/completions' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer sk-1234' \ + -d '{ + "model": "gpt-4o", + "messages": [{"role": "user", "content": "Hello!"}] + }' +``` + +**Expected response:** + +```json +{ + "id": "chatcmpl-abcd", + "created": 1773817678, + "model": "gpt-4o", + "object": "chat.completion", + "system_fingerprint": "fp_6b1ef07cda", + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "Hello! How can I assist you today?", + "role": "assistant", + "annotations": [] + } + } + ], + "usage": { + "completion_tokens": 9, + "prompt_tokens": 9, + "total_tokens": 18, + "completion_tokens_details": { + "accepted_prediction_tokens": 0, + "audio_tokens": 0, + "reasoning_tokens": 0, + "rejected_prediction_tokens": 0 + }, + "prompt_tokens_details": { + "audio_tokens": 0, + "cached_tokens": 0 + } + }, + "service_tier": "default" +} +``` + +--- + +### Optional — Navigate to the LiteLLM UI and generate a virtual key + +Open [http://localhost:4000/ui](http://localhost:4000/ui) in your browser and log in with your master key (`sk-1234`). + +Navigate to **Virtual Keys** and click **+ Create New Key**: + +LiteLLM UI — Create Virtual Key + +Virtual keys let you track spend, set rate limits, and control model access per user or team. + + -## 1. Add a model +:::note Docker Compose users +Your setup is complete — the steps below are for **Docker** and **pip** users only. +::: -Control LiteLLM Proxy with a config.yaml file. +--- -Setup your config.yaml with your azure model. +## Step 1 — Add a model -Note: When using the proxy with a database, you can also **just add models via UI** (UI is available on `/ui` route). +Control LiteLLM Proxy with a `config.yaml` file. Create one with your Azure model: ```yaml model_list: @@ -89,8 +300,6 @@ You can read more about how model resolution works in the [Model Configuration]( - **`api_base`** (`str`) - The API base for your azure deployment. - **`api_version`** (`str`) - The API Version to use when calling Azure's OpenAI API. Get the latest Inference API version [here](https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation?source=recommendations#latest-preview-api-releases). ---- - --- @@ -138,19 +347,19 @@ $ litellm --config /app/config.yaml --detailed_debug +Confirm your config was loaded correctly — you should see this in the logs: -Confirm your config.yaml got mounted correctly - -```bash +``` Loaded config YAML (api_key and environment_variables are not shown): { -"model_list": [ -{ -"model_name ... + "model_list": [ + { + "model_name": ... ``` ### 2.2 Make Call +LiteLLM Proxy is 100% OpenAI-compatible. Test your model via `/chat/completions`: ```bash curl -X POST 'http://0.0.0.0:4000/chat/completions' \ @@ -244,15 +453,17 @@ curl -X POST 'http://0.0.0.0:4000/chat/completions' \ - [Other/Non-Chat Completion Endpoints](../embedding/supported_embedding.md) - [Pass-through for VertexAI, Bedrock, etc.](../pass_through/vertex_ai.md) -## 3. Generate a virtual key +## Optional: Generate a virtual key -Track Spend, and control model access via virtual keys for the proxy +Track spend and control model access via virtual keys for the proxy. -### 3.1 Set up a Database +### Prerequisite — Set up a database -**Requirements** -- Need a postgres database (e.g. [Supabase](https://supabase.com/), [Neon](https://neon.tech/), etc) +:::note Docker Compose users +Your Postgres container is already running — skip ahead to [Create Key w/ RPM Limit](#create-key-w-rpm-limit) below. +::: +**Docker / pip users** — you need a Postgres database (e.g. [Supabase](https://supabase.com/), [Neon](https://neon.tech/), or self-hosted). Add `general_settings` to your `config.yaml`: ```yaml model_list: @@ -268,7 +479,9 @@ general_settings: database_url: "postgresql://:@:/" # 👈 KEY CHANGE ``` -Save config.yaml as `litellm_config.yaml` (used in 3.2). +Save config.yaml as `litellm_config.yaml` before continuing. + +You must finish this setup before starting the proxy server. --- @@ -294,7 +507,7 @@ See All General Settings [here](http://localhost:3000/docs/proxy/configs#all-set `database_url: "postgresql://..."` - Set `DATABASE_URL=postgresql://:@:/` in your env -### 3.2 Start Proxy +### Start Proxy ```bash docker run \ @@ -302,12 +515,11 @@ docker run \ -e AZURE_API_KEY=d6*********** \ -e AZURE_API_BASE=https://openai-***********/ \ -p 4000:4000 \ - docker.litellm.ai/berriai/litellm:main-latest \ + ghcr.io/berriai/litellm-database:main-latest \ --config /app/config.yaml --detailed_debug ``` - -### 3.3 Create Key w/ RPM Limit +### Create Key w/ RPM Limit Create a key with `rpm_limit: 1`. This will only allow 1 request per minute for calls to proxy with this key. @@ -330,9 +542,9 @@ curl -L -X POST 'http://0.0.0.0:4000/key/generate' \ } ``` -### 3.4 Test it! +### Test it! -**Use your virtual key from step 3.3** +**Use the virtual key you just created.** 1st call - Expect to work! @@ -546,6 +758,24 @@ model_list: ## Troubleshooting +### `prometheus.yml` mount error — "not a directory" + +If you see: + +```bash +Error: cannot create subdirectories in ".../prometheus.yml": not a directory +``` + +Docker created `prometheus.yml` as an **empty directory** instead of a file. This happens when the file is missing at `docker compose up` time. + +Fix it: +Then create the file (see [Step 2.3 — Create `prometheus.yml`](#23--create-prometheusyml)) and run `docker compose up` again. +```bash +rm -rf prometheus.yml +``` + +Then create the file (see [Step 2.4](#step-24--create-prometheusyml)) and run `docker compose up` again. + ### Non-root docker image? If you need to run the docker image as a non-root user, use [this](https://github.com/BerriAI/litellm/pkgs/container/litellm-non_root). @@ -645,6 +875,3 @@ LiteLLM Proxy uses the [LiteLLM Python SDK](https://docs.litellm.ai/docs/routing - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai [![Chat on WhatsApp](https://img.shields.io/static/v1?label=Chat%20on&message=WhatsApp&color=success&logo=WhatsApp&style=flat-square)](https://wa.link/huol9n) [![Chat on Discord](https://img.shields.io/static/v1?label=Chat%20on&message=Discord&color=blue&logo=Discord&style=flat-square)](https://discord.gg/wuPM9dRgDw) - - - diff --git a/docs/my-website/docs/proxy/email.md b/docs/my-website/docs/proxy/email.md index 86a79cbcfc8..ba737c6782c 100644 --- a/docs/my-website/docs/proxy/email.md +++ b/docs/my-website/docs/proxy/email.md @@ -203,7 +203,7 @@ After regenerating the key, the user will receive an email notification with: :::info -Customizing Email Branding is an Enterprise Feature [Get in touch with us for a Free Trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +Customizing Email Branding is an Enterprise Feature [Get in touch with us for a Free Trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/endpoint_activity.md b/docs/my-website/docs/proxy/endpoint_activity.md index a66c0f7a5e5..d06727ce4b4 100644 --- a/docs/my-website/docs/proxy/endpoint_activity.md +++ b/docs/my-website/docs/proxy/endpoint_activity.md @@ -114,4 +114,4 @@ Understand spend distribution across endpoints: - [Customer Usage](./customer_usage.md) - Track spend and usage for individual customers - [Cost Tracking](./cost_tracking.md) - Comprehensive cost tracking and analytics -- [Spend Logs](./spend_logs.md) - Detailed request-level spend logs +- [Spend Logs](./cost_tracking.md#-spend-logs-api---individual-transaction-logs) - Detailed request-level spend logs diff --git a/docs/my-website/docs/proxy/enterprise.md b/docs/my-website/docs/proxy/enterprise.md index 4b525837a20..09b103ca4a0 100644 --- a/docs/my-website/docs/proxy/enterprise.md +++ b/docs/my-website/docs/proxy/enterprise.md @@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem'; # ✨ Enterprise Features :::tip -To get a license, get in touch with us [here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +To get a license, get in touch with us [here](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/guardrails/akto.md b/docs/my-website/docs/proxy/guardrails/akto.md new file mode 100644 index 00000000000..67ae741d11e --- /dev/null +++ b/docs/my-website/docs/proxy/guardrails/akto.md @@ -0,0 +1,139 @@ +# Akto + +## Overview +[Akto](https://www.akto.io/) provides API security guardrails and data ingestion for LLM traffic. + +Akto now uses a **two-entry guardrail pattern** in LiteLLM: +- `akto-validate` (`pre_call`) for request validation +- `akto-ingest` (`post_call`) for request/response ingestion + +There is no `on_flagged` setting anymore. + +Use these as two separate guardrails in `config.yaml`: +- `guardrail_name: "akto-validate"` +- `guardrail_name: "akto-ingest"` + +## 1. Get Your Akto Credentials + +Set up the Akto Guardrail API Service and grab: +- `AKTO_GUARDRAIL_API_BASE` — your Guardrail API Base URL +- `AKTO_API_KEY` — your API key + +## 2. Configure in `config.yaml` + +### Block + Ingest (recommended) + +Use both entries below. This gives you: +- pre-call block decision +- post-call ingestion for allowed traffic + +Keep these as two separate entries (`akto-validate` and `akto-ingest`). + +```yaml +guardrails: + - guardrail_name: "akto-validate" + litellm_params: + guardrail: akto + mode: pre_call + akto_base_url: os.environ/AKTO_GUARDRAIL_API_BASE + akto_api_key: os.environ/AKTO_API_KEY + default_on: true + unreachable_fallback: fail_closed # optional: fail_open | fail_closed (default: fail_closed) + guardrail_timeout: 5 # optional, default: 5 + akto_account_id: "1000000" # optional, env fallback: AKTO_ACCOUNT_ID + akto_vxlan_id: "0" # optional, env fallback: AKTO_VXLAN_ID + + - guardrail_name: "akto-ingest" + litellm_params: + guardrail: akto + mode: post_call + akto_base_url: os.environ/AKTO_GUARDRAIL_API_BASE + akto_api_key: os.environ/AKTO_API_KEY + default_on: true +``` + +### Monitor-only mode + +If you only want logging/ingestion and no blocking, keep only `akto-ingest`. + +```yaml +guardrails: + - guardrail_name: "akto-ingest" + litellm_params: + guardrail: akto + mode: post_call + akto_base_url: os.environ/AKTO_GUARDRAIL_API_BASE + akto_api_key: os.environ/AKTO_API_KEY + default_on: true +``` + +## 3. Test It + +```shell +curl -i http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer " \ + -d '{ + "model": "gpt-3.5-turbo", + "messages": [ + {"role": "user", "content": "Hello, how are you?"} + ] + }' +``` + +If a request gets blocked: + +```json +{ + "error": { + "message": "Prompt injection detected", + "type": "None", + "param": "None", + "code": "403" + } +} +``` + +## 4. How It Works + +**Block + Ingest mode:** +``` +Request → LiteLLM → Akto guardrail check + → Allowed → forward to LLM → ingest response + → Blocked → ingest blocked marker → 403 error +``` + +**Monitor-only mode:** +``` +Request → LiteLLM → forward to LLM → get response + → Send to Akto (guardrails + ingest) → log only +``` + +## 5. Event behavior + +| Entry | LiteLLM hook | Akto call behavior | +|------|---|---| +| `akto-validate` | `pre_call` | Awaited call with `guardrails=true`, `ingest_data=false` | +| `akto-ingest` | `post_call` | Fire-and-forget call with `guardrails=true`, `ingest_data=true` | + +When blocked in `pre_call`, LiteLLM sends one fire-and-forget ingest payload with blocked metadata and returns `403`. + +## 6. Parameters + +| Parameter | Env Variable | Default | Description | +|-----------|-------------|---------|-------------| +| `akto_base_url` | `AKTO_GUARDRAIL_API_BASE` | *required* | Akto Guardrail API Base URL | +| `akto_api_key` | `AKTO_API_KEY` | *required* | API key (sent as `Authorization` header) | +| `akto_account_id` | `AKTO_ACCOUNT_ID` | `1000000` | Akto account id included in payload | +| `akto_vxlan_id` | `AKTO_VXLAN_ID` | `0` | Akto vxlan id included in payload | +| `unreachable_fallback` | — | `fail_closed` | `fail_open` or `fail_closed` | +| `guardrail_timeout` | — | `5` | Timeout in seconds | +| `default_on` | — | `true` (recommended) | Enables the guardrail entry by default | + +## 7. Error Handling + +| Scenario | `fail_closed` (default) | `fail_open` | +|----------|------------------------|-------------| +| Akto unreachable | ❌ Blocked (503) | ✅ Passes through | +| Akto returns error | ❌ Blocked (503) | ✅ Passes through | +| Guardrail says no | ❌ Blocked (403) | ❌ Blocked (403) | diff --git a/docs/my-website/docs/proxy/guardrails/aporia_api.md b/docs/my-website/docs/proxy/guardrails/aporia_api.md index ceafc19a1cc..e6ff0d5fed3 100644 --- a/docs/my-website/docs/proxy/guardrails/aporia_api.md +++ b/docs/my-website/docs/proxy/guardrails/aporia_api.md @@ -139,7 +139,7 @@ curl -i http://localhost:4000/v1/chat/completions \ :::info -✨ This is an Enterprise only feature [Contact us to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Contact us to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/guardrails/custom_guardrail.md b/docs/my-website/docs/proxy/guardrails/custom_guardrail.md index c9115cf8265..37579ad870d 100644 --- a/docs/my-website/docs/proxy/guardrails/custom_guardrail.md +++ b/docs/my-website/docs/proxy/guardrails/custom_guardrail.md @@ -117,6 +117,14 @@ guardrails: ::: +:::note Streaming and post_call guardrails + +For **streaming responses**, `post_call` guardrails run on the fully assembled response **after** all chunks have been delivered to the client. This means `post_call` guardrails on streaming are **audit-only** — they can inspect and log the complete response, but cannot block content delivery. Guardrail results are recorded in `guardrail_information` within the logging payload for compliance and auditing. + +To filter or block streaming content in real-time, use `async_post_call_streaming_iterator_hook` instead, which processes chunks as they arrive. + +::: +
Advanced: Multiple modes with individual event hooks @@ -409,7 +417,7 @@ curl -i -X POST http://localhost:4000/v1/chat/completions \ :::info -✨ This is an Enterprise only feature [Contact us to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Contact us to get a free trial](https://enterprise.litellm.ai/demo) ::: @@ -655,8 +663,8 @@ class myCustomGuardrail(CustomGuardrail): | `apply_guardrail` | Simple method to check and optionally modify text | ✅ | INPUT or OUTPUT | ✅ | ✅ | ✅ | | `async_pre_call_hook` | A hook that runs before the LLM API call | ✅ | INPUT | ✅ | ❌ | ✅ | | `async_moderation_hook` | A hook that runs during the LLM API call| ✅ | INPUT | ❌ | ❌ | ✅ | -| `async_post_call_success_hook` | A hook that runs after a successful LLM API call| ✅ | INPUT, OUTPUT | ❌ | ✅ | ✅ | -| `async_post_call_streaming_iterator_hook` | A hook that processes streaming responses | ✅ | OUTPUT | ❌ | ✅ | ✅ | +| `async_post_call_success_hook` | A hook that runs after a successful LLM API call. For streaming, runs on the assembled response after delivery (audit-only, cannot block). | ✅ | INPUT, OUTPUT | ❌ | ✅ | ✅ (non-streaming only) | +| `async_post_call_streaming_iterator_hook` | A hook that processes streaming responses in real-time (can filter/block chunks) | ✅ | OUTPUT | ❌ | ✅ | ✅ | ## Frequently Asked Questions diff --git a/docs/my-website/docs/proxy/guardrails/guardrails_ai.md b/docs/my-website/docs/proxy/guardrails/guardrails_ai.md index 55d586aee7b..19ae34014a4 100644 --- a/docs/my-website/docs/proxy/guardrails/guardrails_ai.md +++ b/docs/my-website/docs/proxy/guardrails/guardrails_ai.md @@ -59,7 +59,7 @@ curl -i http://localhost:4000/v1/chat/completions \ :::info -✨ This is an Enterprise only feature [Contact us to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Contact us to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/health.md b/docs/my-website/docs/proxy/health.md index 2764a6f0d4f..530bea3d06b 100644 --- a/docs/my-website/docs/proxy/health.md +++ b/docs/my-website/docs/proxy/health.md @@ -314,6 +314,89 @@ general_settings: health_check_details: False ``` +## Health Check Driven Routing + +By default, background health checks are observability-only — they populate the `/health` endpoint but don't affect routing. Unhealthy deployments still receive traffic until request failures trigger cooldown. + +With `enable_health_check_routing: true`, the router **excludes deployments that failed their last background health check** before selecting a candidate. This gives you proactive failover instead of reactive cooldown. + +### How it works + +1. Background health checks run on their configured interval +2. After each cycle, every deployment is marked healthy or unhealthy +3. On each incoming request, the router filters out unhealthy deployments **before** cooldown filtering and load balancing +4. If all deployments are unhealthy, the filter is bypassed (safety net — never causes a total outage) +5. If health state is stale (older than `health_check_staleness_threshold`), it is ignored + +### Quick start + +```yaml +model_list: + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: os.environ/OPENAI_API_KEY + - model_name: gpt-4 + litellm_params: + model: openai/gpt-4 + api_key: os.environ/OPENAI_API_KEY_SECONDARY + +general_settings: + background_health_checks: true + health_check_interval: 60 + enable_health_check_routing: true +``` + +### Configuration + +| Setting | Where | Default | Description | +|---------|-------|---------|-------------| +| `enable_health_check_routing` | `general_settings` | `false` | Enable/disable health-check-driven routing | +| `health_check_staleness_threshold` | `general_settings` | `health_check_interval * 2` | Seconds before health state is considered stale and ignored | +| `background_health_checks` | `general_settings` | `false` | Must be `true` for health check routing to work | +| `health_check_interval` | `general_settings` | `300` | Seconds between health check cycles | + +### Interaction with cooldown + +Health check filtering and cooldown are **additive**. A deployment can be excluded by either mechanism: + +- **Health check filter** — proactive, runs on the configured interval, excludes deployments that failed the last check +- **Cooldown** — reactive, triggered by request failures, excludes deployments for a short TTL + +This means request failures still provide fast detection between health check intervals. + +### Staleness + +If a health check result is older than `health_check_staleness_threshold`, it is ignored and the deployment is treated as eligible. This prevents stale data from permanently excluding a deployment if the health check loop stops or slows down. + +The default staleness threshold is `health_check_interval * 2`. For a 60s interval, health state expires after 120s. + +### Example: custom staleness + +```yaml +general_settings: + background_health_checks: true + health_check_interval: 30 + enable_health_check_routing: true + health_check_staleness_threshold: 90 # ignore health state older than 90s +``` + +### Debugging + +Run the proxy with `--detailed_debug` and look for: + +``` +health_check_routing_state_updated healthy=3 unhealthy=1 +``` + +This is logged after each health check cycle when routing state is written. + +If the safety net triggers (all deployments unhealthy), you'll see: + +``` +All deployments marked unhealthy by health checks, bypassing health filter +``` + ## Health Check Timeout The health check timeout is set in `litellm/constants.py` and defaults to 60 seconds. diff --git a/docs/my-website/docs/proxy/high_availability_control_plane.md b/docs/my-website/docs/proxy/high_availability_control_plane.md new file mode 100644 index 00000000000..324fba3a180 --- /dev/null +++ b/docs/my-website/docs/proxy/high_availability_control_plane.md @@ -0,0 +1,190 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; +import { ControlPlaneArchitecture } from '@site/src/components/ControlPlaneArchitecture'; + +# [BETA] High Availability Control Plane + +Deploy a single LiteLLM UI that manages multiple independent LiteLLM proxy instances, each with its own database, Redis, and master key. + +:::info + +This is an Enterprise feature. + +[Enterprise Pricing](https://www.litellm.ai/#pricing) + +[Get free 7-day trial key](https://www.litellm.ai/enterprise#trial) + +::: + +## Why This Architecture? + +In the [standard multi-region setup](./control_plane_and_data_plane.md), all instances share a single database and master key. This works, but introduces a shared dependency. If the database goes down, every instance is affected. + +The **High Availability Control Plane** takes a different approach: + +| | Shared Database (Standard) | High Availability Control Plane | +|---|---|---| +| **Database** | Single shared DB for all instances | Each instance has its own DB | +| **Redis** | Shared Redis | Each instance has its own Redis | +| **Master Key** | Same key across all instances | Each instance has its own key | +| **Failure isolation** | DB outage affects all instances | Failure is isolated to one instance | +| **User management** | Centralized, one user table | Independent, each worker manages its own users | +| **UI** | One UI per admin instance | Single control plane UI manages all workers | + +### Benefits + +- **True high availability**: no shared infrastructure means no single point of failure +- **Blast radius containment**: a misconfiguration or outage on one worker doesn't affect others +- **Regional isolation**: workers can run in different regions with data residency requirements +- **Simpler operations**: each worker is a self-contained LiteLLM deployment + +## Architecture + + + +The **control plane** is a LiteLLM instance that serves the admin UI and knows about all the workers. It does not proxy LLM requests, it is purely for administration. + +Each **worker** is a fully independent LiteLLM proxy that handles LLM requests for its region or team. Workers have their own users, keys, teams, and budgets. + +## Setup + +### 1. Control Plane Configuration + +The control plane needs a `worker_registry` that lists all worker instances. + +```yaml title="cp_config.yaml" +model_list: [] + +general_settings: + master_key: sk-1234 + database_url: os.environ/DATABASE_URL + +worker_registry: + - worker_id: "worker-a" + name: "Worker A" + url: "http://localhost:4001" + - worker_id: "worker-b" + name: "Worker B" + url: "http://localhost:4002" +``` + +Start the control plane: + +```bash +litellm --config cp_config.yaml --port 4000 +``` + +### 2. Worker Configuration + +Each worker needs `control_plane_url` in its `general_settings` to enable cross-origin authentication from the control plane UI. + +`PROXY_BASE_URL` must also be set for each worker so that SSO callback redirects resolve correctly. + + + + +```yaml title="worker_a_config.yaml" +model_list: [] + +general_settings: + master_key: sk-worker-a-1234 + database_url: os.environ/WORKER_A_DATABASE_URL + control_plane_url: "http://localhost:4000" +``` + +```bash +PROXY_BASE_URL=http://localhost:4001 litellm --config worker_a_config.yaml --port 4001 +``` + + + + +```yaml title="worker_b_config.yaml" +model_list: [] + +general_settings: + master_key: sk-worker-b-1234 + database_url: os.environ/WORKER_B_DATABASE_URL + control_plane_url: "http://localhost:4000" +``` + +```bash +PROXY_BASE_URL=http://localhost:4002 litellm --config worker_b_config.yaml --port 4002 +``` + + + + +:::important +Each worker must have its own `master_key` and `database_url`. The whole point of this architecture is that workers are independent. +::: + +### 3. SSO Configuration (Optional) + +SSO is configured on the **control plane** instance the same way as a standard LiteLLM proxy. See the [SSO setup guide](./admin_ui_sso.md) for full instructions. + +If using SSO, make sure to register each worker URL and the control plane URL as allowed callback URLs in your SSO provider's dashboard. + +## How It Works + +### Login Flow + +1. User visits the control plane UI (`http://localhost:4000/ui`) +2. The login page shows a **worker selector** dropdown listing all registered workers +3. User selects a worker (e.g. "Worker A") and logs in with username/password or SSO +4. The UI authenticates against the **selected worker** using the `/v3/login` endpoint +5. On success, the UI stores the worker's JWT and points all subsequent API calls at the worker +6. The user can now manage keys, teams, models, and budgets on that worker, all from the control plane UI + +### Switching Workers + +Once logged in, users can switch workers from the **navbar dropdown** without leaving the UI. Switching redirects back to the login page to authenticate against the new worker. + +### Discovery + +The control plane exposes a `/.well-known/litellm-ui-config` endpoint that the UI reads on load. This endpoint returns: +- `is_control_plane: true` +- The list of workers with their IDs, names, and URLs + +This is how the login page knows to show the worker selector. + +## Local Testing + +To try this out locally, start each instance in a separate terminal: + +```bash +# Terminal 1: Control Plane +litellm --config cp_config.yaml --port 4000 + +# Terminal 2: Worker A +PROXY_BASE_URL=http://localhost:4001 litellm --config worker_a_config.yaml --port 4001 + +# Terminal 3: Worker B +PROXY_BASE_URL=http://localhost:4002 litellm --config worker_b_config.yaml --port 4002 +``` + +Then open `http://localhost:4000/ui`. You should see the worker selector on the login page. + +## Configuration Reference + +### Control Plane Settings + +| Field | Location | Description | +|---|---|---| +| `worker_registry` | Top-level config | List of worker instances | +| `worker_registry[].worker_id` | Required | Unique identifier for the worker | +| `worker_registry[].name` | Required | Display name shown in the UI | +| `worker_registry[].url` | Required | Full URL of the worker instance | + +### Worker Settings + +| Field | Location | Description | +|---|---|---| +| `general_settings.control_plane_url` | Required | URL of the control plane instance. Enables `/v3/login` and `/v3/login/exchange` endpoints on this worker. | +| `PROXY_BASE_URL` | Environment variable | The worker's own external URL. Required for SSO callback redirects. | + +## Related Documentation + +- [Standard Multi-Region Setup](./control_plane_and_data_plane.md) - shared-database architecture for admin/worker split +- [SSO Setup](./admin_ui_sso.md) - configuring SSO for the admin UI +- [Production Deployment](./prod.md) - production best practices diff --git a/docs/my-website/docs/proxy/ip_address.md b/docs/my-website/docs/proxy/ip_address.md index 8f042d9f183..4c469b81e0b 100644 --- a/docs/my-website/docs/proxy/ip_address.md +++ b/docs/my-website/docs/proxy/ip_address.md @@ -3,7 +3,7 @@ :::info -You need a LiteLLM License to unlock this feature. [Grab time](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions), to get one today! +You need a LiteLLM License to unlock this feature. [Grab time](https://enterprise.litellm.ai/demo), to get one today! ::: diff --git a/docs/my-website/docs/proxy/keys_teams_router_settings.md b/docs/my-website/docs/proxy/keys_teams_router_settings.md index ec59e8f271b..6a1744ca951 100644 --- a/docs/my-website/docs/proxy/keys_teams_router_settings.md +++ b/docs/my-website/docs/proxy/keys_teams_router_settings.md @@ -146,5 +146,5 @@ Test new router settings on specific keys or teams before applying globally: - [Router Settings Reference](./config_settings.md#router_settings---reference) - Complete reference of all router settings - [Load Balancing](./load_balancing.md) - Learn about routing strategies and load balancing - [Reliability](./reliability.md) - Configure fallbacks, retries, and error handling -- [Keys](./keys.md) - Manage API keys and their settings -- [Teams](./teams.md) - Organize keys into teams +- [Keys](./virtual_keys.md) - Manage API keys and their settings +- [Teams](./multi_tenant_architecture.md) - Organize keys into teams diff --git a/docs/my-website/docs/proxy/load_balancing.md b/docs/my-website/docs/proxy/load_balancing.md index 5bf39d179f6..93f3d944340 100644 --- a/docs/my-website/docs/proxy/load_balancing.md +++ b/docs/my-website/docs/proxy/load_balancing.md @@ -324,17 +324,58 @@ model_list: litellm_params: model: azure/gpt-4-fallback api_key: os.environ/AZURE_API_KEY_2 - order: 2 # 👈 Used when order=1 is unavailable - -router_settings: - enable_pre_call_checks: true # 👈 Required for 'order' to work + order: 2 # 👈 Used when order=1 fails ``` -:::important -The `order` parameter requires `enable_pre_call_checks: true` in `router_settings`. -::: +### How order-based fallback works -If `order=1` deployment is unavailable (e.g., rate-limited), the router falls back to `order=2` deployments. +When a request to an `order=1` deployment fails (connection error, 404, 429, etc.), the router automatically tries `order=2` deployments, then `order=3`, and so on. Each order level gets its own set of retries before escalating to the next. + +If all order levels are exhausted, the router falls through to any configured [model-level fallbacks](#fallbacks). + +```yaml +model_list: + - model_name: gpt-4 + litellm_params: + model: azure/gpt-4-primary + api_key: os.environ/AZURE_API_KEY + order: 1 + + - model_name: gpt-4 + litellm_params: + model: azure/gpt-4-secondary + api_key: os.environ/AZURE_API_KEY_2 + order: 2 + + - model_name: gpt-4-fallback + litellm_params: + model: openai/gpt-4 + api_key: os.environ/OPENAI_API_KEY + +router_settings: + fallbacks: + - gpt-4: + - gpt-4-fallback # tried after all order levels fail +``` + +The fallback chain for the above config: `order=1` → `order=2` → `gpt-4-fallback`. + +For 429 (rate limit) errors specifically, the failed deployment is immediately placed on cooldown. If all `order=1` deployments are on cooldown, the router picks `order=2` deployments directly during retries without waiting for the fallback path. + +### Team-scoped models and legacy `model_aliases` {#team-scoped-models-and-legacy-model_aliases} + +Team-scoped deployments are identified by `model_info.team_id` and `model_info.team_public_model_name`. Requests should use the **public** model name; the router resolves all sibling deployments (same public name, different `api_base` / `order`, etc.) for routing, failover, and deployment `order`. + +For router internals: when a `team_id` is in scope, optimized lookups key off `(team_id, team_public_model_name)`. If code passes an internal deployment id (e.g. `model_name__`) instead of the public name, routing still works via the usual deployment-name paths, but the team-specific fast path applies only to the public name. + +**Legacy teams:** Older proxy versions could persist `model_aliases` on the team row mapping a public name to a single internal deployment id (`model_name__`). On each request, pre-call logic may still rewrite `model` to that internal name **before** routing, which collapses to one deployment and can make newer sibling deployments unreachable. + +**Migration options:** + +1. **Recommended for upgrades:** Set environment variable `LITELLM_ENABLE_TEAM_STALE_ALIAS_BYPASS=true` so that when sibling team deployments exist for the public name, the stale alias rewrite is skipped and team-scoped routing (including `order` and failover) applies. See the [Environment variables](./config_settings) table in the proxy settings doc. +2. **Data cleanup:** Remove obsolete `model_aliases` entries for team public names from the team record in the database so only `team_public_model_name` + team model list drive access. + +If a stale alias is detected and the bypass is **not** enabled, the proxy may emit a **one-time** warning in logs explaining that sibling deployments may be unreachable until the flag is set or aliases are cleaned up. ### When You'll See Load Balancing in Action @@ -352,7 +393,7 @@ If `order=1` deployment is unavailable (e.g., rate-limited), the router falls ba When load balancing OpenAI's Responses API across deployments with **different API keys** (e.g., different Azure regions or organizations), encrypted content items (like `rs_...` reasoning items) can only be decrypted by the originating API key. -**Solution:** Use the `encrypted_content_affinity` pre-call check to automatically route follow-up requests containing encrypted items to the correct deployment: +**Solution:** Use the `encrypted_content_affinity` pre-call check (requires LiteLLM >= 1.82.3) to automatically route follow-up requests containing encrypted items to the correct deployment: ```yaml model_list: diff --git a/docs/my-website/docs/proxy/logging.md b/docs/my-website/docs/proxy/logging.md index 74a79776fbd..2f81498799a 100644 --- a/docs/my-website/docs/proxy/logging.md +++ b/docs/my-website/docs/proxy/logging.md @@ -1109,7 +1109,7 @@ Log LLM Logs to [Google Cloud Storage Buckets](https://cloud.google.com/storage? :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: @@ -1194,7 +1194,7 @@ Log LLM Logs/SpendLogs to [Google Cloud Storage PubSub Topic](https://cloud.goog :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: @@ -1497,7 +1497,7 @@ Log LLM Logs to [Azure Data Lake Storage](https://learn.microsoft.com/en-us/azur :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/model_compare_ui.md b/docs/my-website/docs/proxy/model_compare_ui.md index bd6f5414224..ee0376ed2fa 100644 --- a/docs/my-website/docs/proxy/model_compare_ui.md +++ b/docs/my-website/docs/proxy/model_compare_ui.md @@ -187,7 +187,7 @@ Use tags and multiple comparisons to run structured A/B tests: ## Related Features -- [Playground Chat UI](./playground.md) - Single model testing interface +- [Playground Chat UI](./ui.md) - Single model testing interface - [Model Management](./model_management.md) - Configure and manage models -- [Guardrails](./guardrails.md) - Set up safety filters +- [Guardrails](./guardrails/quick_start.md) - Set up safety filters - [AI Hub](./ai_hub.md) - Share models and agents with your organization diff --git a/docs/my-website/docs/proxy/multiple_admins.md b/docs/my-website/docs/proxy/multiple_admins.md index 8d39674df19..83d0c5863df 100644 --- a/docs/my-website/docs/proxy/multiple_admins.md +++ b/docs/my-website/docs/proxy/multiple_admins.md @@ -20,7 +20,7 @@ LiteLLM tracks changes to the following entities and actions: :::tip -Requires Enterprise License, Get in touch with us [here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +Requires Enterprise License, Get in touch with us [here](https://enterprise.litellm.ai/demo) ::: @@ -56,6 +56,40 @@ On the LiteLLM UI, navigate to Logs -> Audit Logs. You should see the audit log /> +## Export Audit Logs to External Storage + +You can export audit logs to an external storage backend (e.g. S3) in addition to storing them in the database. Logs are batched and uploaded asynchronously, so they do not block your proxy requests. + +### S3 Example + +Add `audit_log_callbacks` and `s3_callback_params` to your `litellm_settings`: + +```yaml +litellm_settings: + store_audit_logs: true + audit_log_callbacks: ["s3_v2"] + s3_callback_params: + s3_bucket_name: my-audit-logs-bucket # AWS Bucket Name + s3_region_name: us-west-2 # AWS Region + s3_aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID + s3_aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY + s3_path: litellm-audit # [OPTIONAL] prefix path in the bucket +``` + +Audit logs are written as JSON files to: + +``` +s3:///audit_logs//_.json +# or, when s3_path is set: +s3:////audit_logs//_.json +``` + +:::info + +Both `store_audit_logs: true` and `audit_log_callbacks` must be set. If `store_audit_logs` is not enabled, the callbacks will not fire. + +::: + ## Advanced ### Attribute Management changes to Users diff --git a/docs/my-website/docs/proxy/oauth2.md b/docs/my-website/docs/proxy/oauth2.md index 41c4110e447..204a01538cc 100644 --- a/docs/my-website/docs/proxy/oauth2.md +++ b/docs/my-website/docs/proxy/oauth2.md @@ -4,7 +4,7 @@ Use this if you want to use an Oauth2.0 token to make `/chat`, `/embeddings` req :::info -This is an Enterprise Feature - [get in touch with us if you want a free trial to test if this feature meets your needs]((https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions)) +This is an Enterprise Feature - [get in touch with us if you want a free trial to test if this feature meets your needs]((https://enterprise.litellm.ai/demo)) ::: diff --git a/docs/my-website/docs/proxy/pass_through.md b/docs/my-website/docs/proxy/pass_through.md index f47d7064140..700bfb0831d 100644 --- a/docs/my-website/docs/proxy/pass_through.md +++ b/docs/my-website/docs/proxy/pass_through.md @@ -422,6 +422,5 @@ general_settings: [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/proxy/prod.md b/docs/my-website/docs/proxy/prod.md index 26cb484cbe9..d40a0343106 100644 --- a/docs/my-website/docs/proxy/prod.md +++ b/docs/my-website/docs/proxy/prod.md @@ -47,7 +47,7 @@ export LITELLM_LOG="ERROR" :::info -Need Help or want dedicated support ? Talk to a founder [here]: (https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +Need Help or want dedicated support ? Talk to a founder [here]: (https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/prompt_management.md b/docs/my-website/docs/proxy/prompt_management.md index 08307ba99ec..5a3e411e984 100644 --- a/docs/my-website/docs/proxy/prompt_management.md +++ b/docs/my-website/docs/proxy/prompt_management.md @@ -311,7 +311,7 @@ litellm_settings: 1. **At Startup**: When the proxy starts, it reads the `prompts` field from `config.yaml` 2. **Initialization**: Each prompt is initialized based on its `prompt_integration` type 3. **In-Memory Storage**: Prompts are stored in the `IN_MEMORY_PROMPT_REGISTRY` -4. **Access**: Use these prompts via the `/v1/chat/completions` endpoint with `prompt_id` in the request +4. **Access**: Use these prompts via `/v1/chat/completions` or `/v1/responses` with `prompt_id` in the request ### Using Config-Loaded Prompts @@ -331,6 +331,23 @@ curl -L -X POST 'http://0.0.0.0:4000/v1/chat/completions' \ }' ``` +You can also use the same `prompt_id` with the Responses API: + +```bash +curl -L -X POST 'http://0.0.0.0:4000/v1/responses' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer sk-1234' \ +-d '{ + "model": "gpt-4o", + "prompt_id": "coding_assistant", + "prompt_variables": { + "language": "python", + "task": "create a web scraper" + }, + "input": [] +}' +``` + ### Prompt Schema Reference Each prompt in the `prompts` list requires: diff --git a/docs/my-website/docs/proxy/public_routes.md b/docs/my-website/docs/proxy/public_routes.md index d5f3941751f..e53548349dc 100644 --- a/docs/my-website/docs/proxy/public_routes.md +++ b/docs/my-website/docs/proxy/public_routes.md @@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem'; :::info -Requires a LiteLLM Enterprise License. [Get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions). +Requires a LiteLLM Enterprise License. [Get a free trial](https://enterprise.litellm.ai/demo). ::: diff --git a/docs/my-website/docs/proxy/sync_anthropic_beta_headers.md b/docs/my-website/docs/proxy/sync_anthropic_beta_headers.md index e1645082d97..0373d20c879 100644 --- a/docs/my-website/docs/proxy/sync_anthropic_beta_headers.md +++ b/docs/my-website/docs/proxy/sync_anthropic_beta_headers.md @@ -125,4 +125,4 @@ curl -X DELETE "https://your-proxy-url/schedule/anthropic_beta_headers_reload" \ ## Related - [Model Cost Map Sync](./sync_models_github.md) - Auto-sync model pricing data -- [Anthropic Beta Headers](../completion/anthropic.md#beta-features) - Using Anthropic beta features +- [Anthropic Beta Headers](../providers/anthropic.md) - Using Anthropic beta features diff --git a/docs/my-website/docs/proxy/tag_routing.md b/docs/my-website/docs/proxy/tag_routing.md index a1ae52e5e45..57d16a59b54 100644 --- a/docs/my-website/docs/proxy/tag_routing.md +++ b/docs/my-website/docs/proxy/tag_routing.md @@ -315,7 +315,7 @@ LiteLLM Proxy supports team-based tag routing, allowing you to associate specifi :::info -This is an enterprise feature, [Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +This is an enterprise feature, [Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/team_logging.md b/docs/my-website/docs/proxy/team_logging.md index 2ad7e2a4a8e..3f57d0d6d8b 100644 --- a/docs/my-website/docs/proxy/team_logging.md +++ b/docs/my-website/docs/proxy/team_logging.md @@ -26,7 +26,7 @@ Team 3 -> Disabled Logging (for GDPR compliance) :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: @@ -248,7 +248,7 @@ Use the `/key/generate` or `/key/update` endpoints to add logging callbacks to a :::info -✨ This is an Enterprise only feature [Get Started with Enterprise here](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +✨ This is an Enterprise only feature [Get Started with Enterprise here](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/team_model_add.md b/docs/my-website/docs/proxy/team_model_add.md index 7db59a3300e..bb4238055b5 100644 --- a/docs/my-website/docs/proxy/team_model_add.md +++ b/docs/my-website/docs/proxy/team_model_add.md @@ -5,7 +5,7 @@ This is an Enterprise feature. [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/token_auth.md b/docs/my-website/docs/proxy/token_auth.md index 7364ae0fb56..bc6fde7c840 100644 --- a/docs/my-website/docs/proxy/token_auth.md +++ b/docs/my-website/docs/proxy/token_auth.md @@ -11,7 +11,7 @@ Use JWT's to auth admins / users / projects into the proxy. [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/proxy/ui_store_model_db_setting.md b/docs/my-website/docs/proxy/ui_store_model_db_setting.md index 5f860137d0f..4b0bc690f9a 100644 --- a/docs/my-website/docs/proxy/ui_store_model_db_setting.md +++ b/docs/my-website/docs/proxy/ui_store_model_db_setting.md @@ -87,6 +87,6 @@ Change the setting from the UI and have it take effect immediately—perfect for ## Related Documentation -- [Admin UI Overview](./ui_overview.md) – General guide to the LiteLLM Admin UI -- [Models and Endpoints](./models_and_endpoints.md) – Managing models and API endpoints +- [Admin UI Overview](./ui.md) – General guide to the LiteLLM Admin UI +- [Models and Endpoints](./model_management.md) – Managing models and API endpoints - [Config Settings](./config_settings.md) – `store_model_in_db` in `general_settings` diff --git a/docs/my-website/docs/proxy/user_onboarding.md b/docs/my-website/docs/proxy/user_onboarding.md index baa241d6cdf..ecbdc11db43 100644 --- a/docs/my-website/docs/proxy/user_onboarding.md +++ b/docs/my-website/docs/proxy/user_onboarding.md @@ -79,4 +79,4 @@ curl -X POST http://localhost:4000/v1/chat/completions \ ## See Also - [Proxy Quick Start](./quick_start.md) - [User Management](./users.md) -- [Key Management](./key_management.md) +- [Key Management](./virtual_keys.md) diff --git a/docs/my-website/docs/proxy_server.md b/docs/my-website/docs/proxy_server.md index e23d64e443b..7b6f15a604b 100644 --- a/docs/my-website/docs/proxy_server.md +++ b/docs/my-website/docs/proxy_server.md @@ -813,5 +813,4 @@ Thread Stats Avg Stdev Max +/- Stdev - [Schedule Demo 👋](https://calendly.com/d/4mp-gd3-k5k/berriai-1-1-onboarding-litellm-hosted-version) - [Community Discord 💭](https://discord.gg/wuPM9dRgDw) -- Our numbers 📞 +1 (770) 8783-106 / ‭+1 (412) 618-6238‬ - Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/reasoning_content.md b/docs/my-website/docs/reasoning_content.md index 8bf59f66a33..6e6a30cdb49 100644 --- a/docs/my-website/docs/reasoning_content.md +++ b/docs/my-website/docs/reasoning_content.md @@ -13,6 +13,7 @@ Supported Providers: - Deepseek (`deepseek/`) - Anthropic API (`anthropic/`) - Bedrock (Anthropic + Deepseek + GPT-OSS) (`bedrock/`) +- OpenAI Responses API (`openai/responses/`) - Vertex AI (Anthropic) (`vertexai/`) - OpenRouter (`openrouter/`) - XAI (`xai/`) @@ -594,9 +595,26 @@ Expected Response :::tip gpt-5.4: reasoning_effort + function tools -LiteLLM drops `reasoning_effort` from `gpt-5.4` requests to `litellm.completion()` that include tools, since that combination is supported in the Responses API. +When `gpt-5.4+` requests to `litellm.completion()` include both `reasoning_effort` and `tools`, LiteLLM **automatically routes** the request through the Responses API bridge. This works for both **OpenAI** (`openai/gpt-5.4`) and **Azure** (`azure/gpt-5.4`) providers — no extra configuration needed. -If you need reasoning **and** tools together, use `openai/responses/gpt-5.4` to route through the Responses API instead. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details. +You can also route explicitly via `openai/responses/gpt-5.4` or `azure/responses/gpt-5.4`. See [Responses API Bridge](/docs/providers/openai#openai-chat-completion-to-responses-api-bridge) for details. + +**Azure custom deployment names:** Auto-routing relies on the deployment name matching the `gpt-5.4*` pattern. If you use a custom deployment name (e.g. `"my-reasoning-model"`), enable routing via: + +**SDK:** +```python +litellm.completion(model="azure/responses/my-reasoning-model", ...) +``` + +**Proxy config:** +```yaml +model_list: + - model_name: my-reasoning-model + litellm_params: + model: azure/my-reasoning-model + model_info: + mode: responses +``` ::: @@ -683,3 +701,69 @@ response = litellm.completion( reasoning_effort={"effort": "low", "summary": "detailed"}, # Explicit control ) ``` + +### Summary Preservation via `/v1/messages` Adapter + +When using the Anthropic `/v1/messages` adapter to route non-Claude models (e.g., `openai/gpt-5.1`), the `thinking.summary` value is preserved and forwarded to the downstream provider. For example: + +```python +import litellm + +response = await litellm.anthropic.messages.acreate( + model="openai/gpt-5.1", + messages=[{"role": "user", "content": "Hello"}], + max_tokens=8096, + thinking={"type": "enabled", "budget_tokens": 5000, "summary": "concise"}, +) +# The summary="concise" is preserved when routing to OpenAI's Responses API +``` + +### Enabling Default Summary Injection for `/v1/messages` Adapter + +When the Anthropic `/v1/messages` adapter translates `thinking` parameters to OpenAI `reasoning_effort` for non-Claude models, you can opt-in to automatic `summary="detailed"` injection using the `reasoning_auto_summary` flag. This ensures that reasoning text is returned in the response (matching the Anthropic thinking behavior). + +To **enable** this default injection, use the `reasoning_auto_summary` flag: + + + + +```python +import litellm + +# Enable default summary="detailed" injection +litellm.reasoning_auto_summary = True + +response = await litellm.anthropic.messages.acreate( + model="openai/gpt-5.1", + messages=[{"role": "user", "content": "Hello"}], + max_tokens=8096, + thinking={"type": "enabled", "budget_tokens": 5000}, +) +# summary="detailed" will be automatically added to reasoning_effort +``` + + + + + +```bash +export LITELLM_REASONING_AUTO_SUMMARY=true +``` + + + + + +```yaml +litellm_settings: + reasoning_auto_summary: true +``` + + + + +:::info + +This flag only affects the automatic injection of `summary="detailed"` when no user-provided summary is present. If you explicitly pass `thinking.summary` (e.g., `"concise"` or `"auto"`), your value is always preserved regardless of this flag. + +::: diff --git a/docs/my-website/docs/response_api.md b/docs/my-website/docs/response_api.md index fb55ae9f9d0..0c428000c72 100644 --- a/docs/my-website/docs/response_api.md +++ b/docs/my-website/docs/response_api.md @@ -1160,12 +1160,12 @@ follow_up = await router.aresponses( To enable session continuity for Responses API in your LiteLLM proxy, set `optional_pre_call_checks` in your proxy config.yaml. - `responses_api_deployment_check`: high priority routing when `previous_response_id` is provided -- `encrypted_content_affinity`: **[Recommended]** content-aware routing for encrypted items (e.g., `rs_...` reasoning items) +- `encrypted_content_affinity`: **[Recommended]** content-aware routing for encrypted items (e.g., `rs_...` reasoning items) (**requires LiteLLM >= 1.82.3**) - `session_affinity`: sticky sessions based on session id (takes priority over `deployment_affinity`) - `deployment_affinity`: sticky sessions based on user key (applies even without `previous_response_id`) :::tip Recommended: Use `encrypted_content_affinity` -For Responses API with load balancing across deployments with **different API keys**, use `encrypted_content_affinity` instead of `deployment_affinity`. It only pins requests that contain encrypted content, avoiding quota reduction while preventing `invalid_encrypted_content` errors. +For Responses API with load balancing across deployments with **different API keys**, use `encrypted_content_affinity` instead of `deployment_affinity`. It only pins requests that contain encrypted content, avoiding quota reduction while preventing `invalid_encrypted_content` errors. (Requires LiteLLM >= 1.82.3.) ::: Notes: @@ -1364,6 +1364,85 @@ litellm --config config.yaml | `deployment_affinity` | Simple sticky sessions | All requests from same API key | ❌ Reduces quota by # of users | +## Per-Model-Group Affinity Configuration + +By default, `optional_pre_call_checks` applies globally to all model groups. Use `model_group_affinity_config` when you want different affinity behavior per model group — for example, enabling stickiness only for models spread across providers (Azure + Bedrock) while leaving single-provider groups free to load-balance. + +Groups not listed fall back to the global `optional_pre_call_checks` settings. + + + + +```python +router = litellm.Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": {"model": "azure/gpt-4", "api_key": "...", "api_base": "https://endpoint1.openai.azure.com"}, + }, + { + "model_name": "gpt-4", + "litellm_params": {"model": "bedrock/anthropic.claude-v2", "aws_region_name": "us-east-1"}, + }, + { + "model_name": "text-embedding-ada-002", + "litellm_params": {"model": "azure/text-embedding-ada-002", "api_key": "...", "api_base": "https://endpoint1.openai.azure.com"}, + }, + { + "model_name": "text-embedding-ada-002", + "litellm_params": {"model": "azure/text-embedding-ada-002", "api_key": "...", "api_base": "https://endpoint2.openai.azure.com"}, + }, + ], + # gpt-4: cross-provider (Azure + Bedrock) — enable deployment affinity + # text-embedding-ada-002: same provider — no affinity, let it load balance freely + model_group_affinity_config={ + "gpt-4": ["deployment_affinity", "responses_api_deployment_check"], + }, +) +``` + + + + +```yaml title="config.yaml" +model_list: + - model_name: gpt-4 + litellm_params: + model: azure/gpt-4 + api_key: os.environ/AZURE_API_KEY_1 + api_base: https://endpoint1.openai.azure.com + + - model_name: gpt-4 + litellm_params: + model: bedrock/anthropic.claude-v2 + aws_region_name: us-east-1 + + - model_name: text-embedding-ada-002 + litellm_params: + model: azure/text-embedding-ada-002 + api_key: os.environ/AZURE_API_KEY_1 + api_base: https://endpoint1.openai.azure.com + + - model_name: text-embedding-ada-002 + litellm_params: + model: azure/text-embedding-ada-002 + api_key: os.environ/AZURE_API_KEY_2 + api_base: https://endpoint2.openai.azure.com + +router_settings: + # gpt-4: cross-provider — enable stickiness + # text-embedding-ada-002: not listed — load balances freely + model_group_affinity_config: + "gpt-4": + - deployment_affinity + - responses_api_deployment_check +``` + + + + +**Supported values:** `deployment_affinity`, `responses_api_deployment_check`, `session_affinity` + ## Calling non-Responses API endpoints (`/responses` to `/chat/completions` Bridge) LiteLLM allows you to call non-Responses API models via a bridge to LiteLLM's `/chat/completions` endpoint. This is useful for calling Anthropic, Gemini and even non-Responses API OpenAI models. @@ -1556,6 +1635,12 @@ curl -X POST "http://localhost:4000/v1/responses" \ }' ``` +## File Search (Vector Stores) + +For full `file_search` usage (native + emulated fallback), SDK/Proxy examples, architecture diagram, and Q&A, see: + +- [`File Search in the Responses API — E2E Testing Guide`](/docs/tutorials/file_search_responses_api) + ## Session Management LiteLLM Proxy supports session management for all supported models. This allows you to store and fetch conversation history (state) in LiteLLM Proxy. diff --git a/docs/my-website/docs/routing.md b/docs/my-website/docs/routing.md index 67e7f681147..5aa655ae212 100644 --- a/docs/my-website/docs/routing.md +++ b/docs/my-website/docs/routing.md @@ -842,6 +842,8 @@ Traffic mirroring allows you to "mimic" production traffic to a secondary (silen Set `order` in `litellm_params` to prioritize deployments. Lower values = higher priority. When multiple deployments share the same `order`, the routing strategy picks among them. +When a request to an `order=1` deployment fails (connection error, 404, 429, etc.), the router automatically tries `order=2` deployments, then `order=3`, and so on. Each order level gets its own set of retries before escalating to the next. If all order levels are exhausted, the router falls through to any configured [fallbacks](#fallbacks). + @@ -862,18 +864,14 @@ model_list = [ "litellm_params": { "model": "azure/gpt-4-fallback", "api_key": os.getenv("AZURE_API_KEY_2"), - "order": 2, # 👈 Used when order=1 is unavailable + "order": 2, # 👈 Tried when order=1 fails }, }, ] -router = Router(model_list=model_list, enable_pre_call_checks=True) # 👈 Required for 'order' to work +router = Router(model_list=model_list) ``` -:::important -The `order` parameter requires `enable_pre_call_checks=True` to be set on the Router. -::: - @@ -889,10 +887,7 @@ model_list: litellm_params: model: azure/gpt-4-fallback api_key: os.environ/AZURE_API_KEY_2 - order: 2 # 👈 Used when order=1 is unavailable - -router_settings: - enable_pre_call_checks: true # 👈 Required for 'order' to work + order: 2 # 👈 Tried when order=1 fails ``` diff --git a/docs/my-website/docs/secret.md b/docs/my-website/docs/secret.md index c5c80311475..57f576fd56a 100644 --- a/docs/my-website/docs/secret.md +++ b/docs/my-website/docs/secret.md @@ -6,7 +6,7 @@ [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/aws_kms.md b/docs/my-website/docs/secret_managers/aws_kms.md index 7f69d91fe87..806223a2539 100644 --- a/docs/my-website/docs/secret_managers/aws_kms.md +++ b/docs/my-website/docs/secret_managers/aws_kms.md @@ -6,7 +6,7 @@ [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/aws_secret_manager.md b/docs/my-website/docs/secret_managers/aws_secret_manager.md index c49797a15dd..a7e24ea69ae 100644 --- a/docs/my-website/docs/secret_managers/aws_secret_manager.md +++ b/docs/my-website/docs/secret_managers/aws_secret_manager.md @@ -9,7 +9,7 @@ import TabItem from '@theme/TabItem'; [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/azure_key_vault.md b/docs/my-website/docs/secret_managers/azure_key_vault.md index 81aeaa32159..4ea53d2ea9e 100644 --- a/docs/my-website/docs/secret_managers/azure_key_vault.md +++ b/docs/my-website/docs/secret_managers/azure_key_vault.md @@ -6,7 +6,7 @@ [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/cyberark.md b/docs/my-website/docs/secret_managers/cyberark.md index 0a17c0afc30..cd7c0ea5d25 100644 --- a/docs/my-website/docs/secret_managers/cyberark.md +++ b/docs/my-website/docs/secret_managers/cyberark.md @@ -8,7 +8,7 @@ import Image from '@theme/IdealImage'; [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/google_kms.md b/docs/my-website/docs/secret_managers/google_kms.md index 31fd6195bdb..152ecbaae80 100644 --- a/docs/my-website/docs/secret_managers/google_kms.md +++ b/docs/my-website/docs/secret_managers/google_kms.md @@ -6,7 +6,7 @@ [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/google_secret_manager.md b/docs/my-website/docs/secret_managers/google_secret_manager.md index 81878b7e398..f3e7367e8a4 100644 --- a/docs/my-website/docs/secret_managers/google_secret_manager.md +++ b/docs/my-website/docs/secret_managers/google_secret_manager.md @@ -6,7 +6,7 @@ [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/hashicorp_vault.md b/docs/my-website/docs/secret_managers/hashicorp_vault.md index 52d9b556200..11e25e88a7d 100644 --- a/docs/my-website/docs/secret_managers/hashicorp_vault.md +++ b/docs/my-website/docs/secret_managers/hashicorp_vault.md @@ -8,7 +8,7 @@ import Image from '@theme/IdealImage'; [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/secret_managers/overview.md b/docs/my-website/docs/secret_managers/overview.md index bf7386ab89c..f02362f4932 100644 --- a/docs/my-website/docs/secret_managers/overview.md +++ b/docs/my-website/docs/secret_managers/overview.md @@ -8,7 +8,7 @@ import Image from '@theme/IdealImage'; [Enterprise Pricing](https://www.litellm.ai/#pricing) -[Contact us here to get a free trial](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) +[Contact us here to get a free trial](https://enterprise.litellm.ai/demo) ::: diff --git a/docs/my-website/docs/troubleshoot.md b/docs/my-website/docs/troubleshoot.md index 1539e1959f7..1afa35df8e4 100644 --- a/docs/my-website/docs/troubleshoot.md +++ b/docs/my-website/docs/troubleshoot.md @@ -50,7 +50,6 @@ Full error logs, stack traces, and any images from service metrics (CPU, memory, [Community Discord 💭](https://discord.gg/wuPM9dRgDw) [Community Slack 💭](https://www.litellm.ai/support) -Our numbers 📞 +1 (770) 8783-106 / +1 (412) 618-6238 Our emails ✉️ ishaan@berri.ai / krrish@berri.ai diff --git a/docs/my-website/docs/tutorials/claude_code_plugin_marketplace.md b/docs/my-website/docs/tutorials/claude_code_plugin_marketplace.md index 9d93c717c4f..d8175f51aca 100644 --- a/docs/my-website/docs/tutorials/claude_code_plugin_marketplace.md +++ b/docs/my-website/docs/tutorials/claude_code_plugin_marketplace.md @@ -37,7 +37,7 @@ Click **+ Add New Plugin** to register a plugin in your marketplace. Enter the plugin information: - **Name**: Plugin identifier (kebab-case, e.g., `my-plugin`) -- **Source Type**: Choose GitHub or URL +- **Source Type**: Choose GitHub, Git URL, or Git Subdir - **Repository/URL**: The git source (e.g., `org/repo` for GitHub) - **Version**: Semantic version (optional) - **Description**: What the plugin does @@ -216,6 +216,22 @@ curl -X DELETE http://localhost:4000/claude-code/plugins/my-plugin \ Use this format for GitLab, Bitbucket, or self-hosted git repositories. + + + +```json +{ + "name": "my-plugin", + "source": { + "source": "git-subdir", + "url": "https://github.com/org/repo.git", + "path": "plugins/my-plugin" + } +} +``` + +Use this format when your plugin lives in a subdirectory of a git repository. The `path` field must be a relative path of slash-separated segments (alphanumeric, dots, hyphens, underscores only). + diff --git a/docs/my-website/docs/tutorials/compare_llms.md b/docs/my-website/docs/tutorials/compare_llms.md index 02877b46607..0252263a16e 100644 --- a/docs/my-website/docs/tutorials/compare_llms.md +++ b/docs/my-website/docs/tutorials/compare_llms.md @@ -82,7 +82,7 @@ Benchmark Results for 'When will BerriAI IPO?': +-----------------+----------------------------------------------------------------------------------+---------------------------+------------+ ``` ## Support -**🤝 Schedule a 1-on-1 Session:** Book a [1-on-1 session](https://calendly.com/d/cx9p-5yf-2nm/litellm-introductions) with Krrish and Ishaan, the founders, to discuss any issues, provide feedback, or explore how we can improve LiteLLM for you. +**🤝 Schedule a 1-on-1 Session:** Book a [1-on-1 session](https://enterprise.litellm.ai/demo) with Krrish and Ishaan, the founders, to discuss any issues, provide feedback, or explore how we can improve LiteLLM for you. B[LiteLLM Responses API] + B --> C{Provider supports native file_search?} + + C -->|Yes| D[Native passthrough path] + D --> D1[Decode unified vector_store_id if needed] + D1 --> D2[Forward request to provider unchanged] + D2 --> D3[Provider performs file_search] + D3 --> Z[OpenAI-compatible output] + + C -->|No| E[Emulated fallback path] + E --> E1[Convert file_search to litellm_file_search function tool] + E1 --> E2[First model call returns tool call with one or more queries] + E2 --> E3[LiteLLM executes vector search for each query] + E3 --> E4[Second model call with tool_result context] + E4 --> E5[Synthesize file_search_call + message + citations] + E5 --> Z[OpenAI-compatible output] +``` + + + +## Prerequisites + +```bash +pip install 'litellm[proxy]' +export OPENAI_API_KEY="sk-..." # for native path +export ANTHROPIC_API_KEY="sk-ant-..." # for emulated path +``` + + + +## Example response shape + +## Validating the Output Format + +Regardless of which path ran, the response always follows the OpenAI Responses API format: + +```json +{ + "output": [ + { + "type": "file_search_call", + "id": "fs_abc123", + "status": "completed", + "queries": ["What does LiteLLM support?"], + "search_results": null + }, + { + "type": "message", + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": "LiteLLM is a unified interface...", + "annotations": [ + { + "type": "file_citation", + "index": 150, + "file_id": "file-xxxx", + "filename": "knowledge.txt" + } + ] + } + ] + } + ] +} +``` + +**Validation script:** + +```python showLineNumbers title="Validate response structure" +def validate_file_search_response(response): + """Assert that response follows OpenAI file_search output format.""" + output = response.output + assert len(output) >= 2, "Expected at least 2 output items" + + # First item: file_search_call + fs_call = output[0] + fs_type = fs_call["type"] if isinstance(fs_call, dict) else fs_call.type + assert fs_type == "file_search_call", f"Expected file_search_call, got {fs_type}" + + fs_status = fs_call["status"] if isinstance(fs_call, dict) else fs_call.status + assert fs_status == "completed" + + # Second item: message + msg = output[1] + msg_type = msg["type"] if isinstance(msg, dict) else msg.type + assert msg_type == "message" + + content = msg["content"] if isinstance(msg, dict) else msg.content + assert len(content) > 0 + text_block = content[0] + text = text_block["text"] if isinstance(text_block, dict) else text_block.text + assert isinstance(text, str) and len(text) > 0 + + print("✅ Response structure valid") + print(f" Queries: {fs_call['queries'] if isinstance(fs_call, dict) else fs_call.queries}") + print(f" Answer length: {len(text)} chars") + annotations = text_block["annotations"] if isinstance(text_block, dict) else text_block.annotations + print(f" Citations: {len(annotations)}") + +validate_file_search_response(response) +``` + + + +## Q&A + +- **Why do I see `UnsupportedParamsError`?** This usually means `file_search` was passed to a provider that does not support it natively and emulation could not route correctly. Check: + - The model string is valid (for example, `anthropic/claude-sonnet-4-5`). + - `custom_llm_provider` resolves correctly so LiteLLM can load the provider config. +- **Why does vector search return no results?** Common causes: + - The vector store ID is wrong or has no files attached. + - In LiteLLM-managed stores, file ingestion is not complete (`status != completed`). + - The query is too narrow; try a broader query. +- **Why am I getting `403 Access denied` on vector store calls?** The caller does not have access to that vector store. + - The store may belong to another team. + - Use an admin/proxy key if your setup requires cross-team access. +- **Why are `annotations` empty in emulated mode?** `file_citation` annotations require `file_id` metadata in search results. If your vector backend does not return file-level metadata, the answer text is still generated but citations can be empty. + + + +## What to check next + +- [File Search reference in Responses API docs](/docs/response_api#file-search-vector-stores) — full API reference +- [Vector Store management](/docs/vector_store_files) — create and manage vector stores +- [Managed vector stores](/docs/providers/bedrock_vector_store) — provider-specific setup diff --git a/docs/my-website/docs/tutorials/index.md b/docs/my-website/docs/tutorials/index.md new file mode 100644 index 00000000000..7f80cc760ee --- /dev/null +++ b/docs/my-website/docs/tutorials/index.md @@ -0,0 +1,98 @@ +--- +title: Tutorials +sidebar_label: Overview +--- + +import NavigationCards from '@site/src/components/NavigationCards'; + +**Tutorials** are step-by-step walkthroughs for integrating LiteLLM with external tools, frameworks, and services — or building complete end-to-end workflows. + +> Need help choosing the right path before you start? See [Learn →](/docs/learn) + +--- + +## Getting Started + + + +--- + +## Integrations + + + +--- + +## Proxy + + + +--- + +## Observability & Evaluation + + diff --git a/docs/my-website/docs/tutorials/installation.md b/docs/my-website/docs/tutorials/installation.md index ecaed0bec9d..cf39c55bee6 100644 --- a/docs/my-website/docs/tutorials/installation.md +++ b/docs/my-website/docs/tutorials/installation.md @@ -1,7 +1,3 @@ ---- -displayed_sidebar: tutorialSidebar ---- - # Set up environment Let's get the necessary keys to set up our demo environment. @@ -11,7 +7,5 @@ Every LLM provider needs API keys (e.g. `OPENAI_API_KEY`). You can get API keys Let's get them for our demo! **OpenAI**: https://platform.openai.com/account/api-keys -**Cohere**: https://dashboard.cohere.com/welcome/login?redirect_uri=%2Fapi-keys (no credit card required) +**Cohere**: https://dashboard.cohere.com/welcome/login?redirect_uri=%2Fapi-keys (no credit card required) **AI21**: https://studio.ai21.com/account/api-key (no credit card required) - - diff --git a/docs/my-website/docs/tutorials/opencode_integration.md b/docs/my-website/docs/tutorials/opencode_integration.md index e55367833f2..35e00a1de50 100644 --- a/docs/my-website/docs/tutorials/opencode_integration.md +++ b/docs/my-website/docs/tutorials/opencode_integration.md @@ -253,7 +253,7 @@ model_list: litellm_params: model: openai/gpt-4 api_key: os.environ/OPENAI_API_KEY - + - model_name: gpt-4o litellm_params: model: openai/gpt-4o @@ -264,7 +264,7 @@ model_list: litellm_params: model: anthropic/claude-3-5-sonnet-20241022 api_key: os.environ/ANTHROPIC_API_KEY - + # DeepSeek models - model_name: deepseek-chat litellm_params: @@ -272,6 +272,19 @@ model_list: api_key: os.environ/DEEPSEEK_API_KEY ``` +### Dropping OpenCode-specific parameters + +OpenCode sends a `reasoningSummary` parameter with reasoning-capable models such as `gpt-5`. This parameter is not supported by the Chat Completions API and will cause errors. Add `additional_drop_params` to every model entry in your `model_list` that will receive requests from OpenCode with reasoning enabled: + +```yaml +model_list: + - model_name: gpt-5 + litellm_params: + model: openai/gpt-5 + api_key: os.environ/OPENAI_API_KEY + additional_drop_params: ["reasoningSummary"] +``` + ## Troubleshooting **OpenCode not connecting:** @@ -294,6 +307,16 @@ model_list: - Validate JSON syntax using a JSON validator - Ensure the `$schema` URL is accessible +**`Unknown parameter: 'reasoningSummary'` error:** +- OpenCode sends a `reasoningSummary` parameter that is not supported by the Chat Completions API. Add `additional_drop_params: ["reasoningSummary"]` to each affected model entry in your `litellm_params`: + ```yaml + - model_name: gpt-5 + litellm_params: + model: openai/gpt-5 + api_key: os.environ/OPENAI_API_KEY + additional_drop_params: ["reasoningSummary"] + ``` + ## Tips - Add more models to the config as needed - they'll appear in `/models` diff --git a/docs/my-website/docs/tutorials/vertex_ai_pay_go.md b/docs/my-website/docs/tutorials/vertex_ai_pay_go.md new file mode 100644 index 00000000000..87197e5bad5 --- /dev/null +++ b/docs/my-website/docs/tutorials/vertex_ai_pay_go.md @@ -0,0 +1,151 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Vertex AI PayGo and Priority + +## Priority PayGo + +LiteLLM supports Priority PayGo. +Send a priority header, get priority queueing, and pay priority token rates. + +:::info Which models support Priority PayGo? +As of this writing: `gemini/gemini-2.5-pro`, `vertex_ai/gemini-3-pro-preview`, `vertex_ai/gemini-3.1-pro-preview`, `vertex_ai/gemini-3-flash-preview`, and their variants. +Check `supports_service_tier: true` in LiteLLM's [model pricing JSON](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json). +::: + +### Send a priority request + +Use this header: + +`X-Vertex-AI-LLM-Shared-Request-Type: priority` + + + + +```python +import litellm + +response = litellm.completion( + model="vertex_ai/gemini-3-pro-preview", + messages=[{"role": "user", "content": "Summarize the Gettysburg Address."}], + vertex_project="YOUR_PROJECT_ID", + vertex_location="us-central1", + extra_headers={"X-Vertex-AI-LLM-Shared-Request-Type": "priority"}, +) + +print(response.choices[0].message.content) +``` + + + + +```yaml title="config.yaml" +model_list: + - model_name: gemini-priority + litellm_params: + model: vertex_ai/gemini-3-pro-preview + vertex_project: "YOUR_PROJECT_ID" + vertex_location: "us-central1" + vertex_credentials: os.environ/GOOGLE_APPLICATION_CREDENTIALS + extra_headers: + X-Vertex-AI-LLM-Shared-Request-Type: priority +``` + +```bash +curl http://localhost:4000/v1/chat/completions \ + -H "Authorization: Bearer sk-your-key" \ + -H "Content-Type: application/json" \ + -d '{"model": "gemini-priority", "messages": [{"role": "user", "content": "Hello"}]}' +``` + + + + +Use `x-pass-` so LiteLLM forwards provider-specific headers. + +```bash +MODEL_ID="gemini-3-pro-preview-0325" +PROJECT_ID="YOUR_PROJECT_ID" + +curl -X POST \ + "${LITELLM_PROXY_BASE_URL}/vertex_ai/v1/projects/${PROJECT_ID}/locations/global/publishers/google/models/${MODEL_ID}:generateContent" \ + -H "Authorization: Bearer sk-your-litellm-key" \ + -H "Content-Type: application/json" \ + -H "x-pass-X-Vertex-AI-LLM-Shared-Request-Type: priority" \ + -d '{"contents": [{"role": "user", "parts": [{"text": "Hello!"}]}]}' +``` + + + + +### How cost tracking works + +![Vertex AI Priority PayGo Cost Tracking Flow](/img/vertex_cost_tracking_flow.svg) + +**`trafficType` → `service_tier` mapping** + +| `usageMetadata.trafficType` | `service_tier` | Pricing keys used | +|---|---|---| +| `ON_DEMAND` | `None` | `input_cost_per_token` | +| `ON_DEMAND_PRIORITY` | `"priority"` | `input_cost_per_token_priority` | +| `FLEX` / `BATCH` | `"flex"` | `input_cost_per_token_flex` | + +If a tier-specific key is missing, LiteLLM falls back to standard pricing keys. + +--- + +## Standard PayGo vs Provisioned Throughput + +This is a different header from priority routing: + +| Header value | Behavior | +|---|---| +| `X-Vertex-AI-LLM-Request-Type: shared` | Force standard PayGo (bypass PT) | +| `X-Vertex-AI-LLM-Request-Type: dedicated` | Force Provisioned Throughput only (`429` if exhausted) | + +### Native route example + +```python +import litellm + +response = litellm.completion( + model="vertex_ai/gemini-2.0-flash", + messages=[{"role": "user", "content": "Hello!"}], + vertex_project="YOUR_PROJECT_ID", + vertex_location="us-central1", + extra_headers={"X-Vertex-AI-LLM-Request-Type": "shared"}, +) +``` + +### Pass-through example + +```bash +MODEL_ID="gemini-2.0-flash-001" +PROJECT_ID="YOUR_PROJECT_ID" + +curl -X POST \ + "${LITELLM_PROXY_BASE_URL}/vertex_ai/v1/projects/${PROJECT_ID}/locations/global/publishers/google/models/${MODEL_ID}:generateContent" \ + -H "Authorization: Bearer sk-your-litellm-key" \ + -H "Content-Type: application/json" \ + -H "x-pass-X-Vertex-AI-LLM-Request-Type: shared" \ + -d '{ + "contents": [{"role": "user", "parts": [{"text": "Hello!"}]}] + }' +``` + +--- + +## Troubleshooting + +**Q: What does `403 Permission denied` or `IAM_PERMISSION_DENIED` mean?** +A: The service account or Application Default Credentials (ADC) user does not have the `roles/aiplatform.user` role. To resolve this, re-run the `gcloud projects add-iam-policy-binding`. + +**Q: What should I do if I get a `429 Quota exceeded` error?** +A: This means you've hit the per-region QPM (queries per minute) or TPM (tokens per minute) quota. You can: +- Request a quota increase from the [GCP Quotas console](https://console.cloud.google.com/iam-admin/quotas) +- Add more regions to your LiteLLM configuration for load balancing +- Upgrade to [Provisioned Throughput](https://cloud.google.com/vertex-ai/generative-ai/docs/provisioned-throughput) for guaranteed capacity + +**Q: How do I fix the `VERTEXAI_PROJECT not set` error?** +A: Either pass the `vertex_project` parameter explicitly in your LiteLLM call, or set the `VERTEXAI_PROJECT` environment variable before running your code. + diff --git a/docs/my-website/docs/vertex_batch_passthrough.md b/docs/my-website/docs/vertex_batch_passthrough.md index 3203d7d792a..17ffc1e6dbc 100644 --- a/docs/my-website/docs/vertex_batch_passthrough.md +++ b/docs/my-website/docs/vertex_batch_passthrough.md @@ -155,6 +155,6 @@ Common error scenarios and their solutions: ## Related Documentation -- [Vertex AI Provider Documentation](./vertex.md) -- [General Batches API Documentation](../batches.md) -- [Cost Tracking and Monitoring](../observability/telemetry.md) +- [Vertex AI Provider Documentation](./providers/vertex.md) +- [General Batches API Documentation](./batches.md) +- [Cost Tracking and Monitoring](./observability/telemetry.md) diff --git a/docs/my-website/docusaurus.config.js b/docs/my-website/docusaurus.config.js index 32d5d800b71..9e1fae6a34f 100644 --- a/docs/my-website/docusaurus.config.js +++ b/docs/my-website/docusaurus.config.js @@ -2,9 +2,9 @@ // Note: type annotations allow type checking and IDEs autocompletion // @ts-ignore -const lightCodeTheme = require('prism-react-renderer/themes/github'); +const lightCodeTheme = require('prism-react-renderer/themes/vsLight'); // @ts-ignore -const darkCodeTheme = require('prism-react-renderer/themes/dracula'); +const darkCodeTheme = require('prism-react-renderer/themes/nightOwl'); const inkeepConfig = { baseSettings: { @@ -87,18 +87,88 @@ const config = { }, ], [ - '@docusaurus/plugin-content-blog', + '@docusaurus/plugin-content-docs', { - id: 'release_notes', + id: 'release-notes', path: './release_notes', routeBasePath: 'release_notes', - blogTitle: 'Release Notes', - blogSidebarTitle: 'Releases', - blogSidebarCount: 'ALL', - postsPerPage: 'ALL', - showReadingTime: false, - sortPosts: 'descending', - include: ['**/*.{md,mdx}'], + sidebarPath: require.resolve('./sidebars-release-notes.js'), + async sidebarItemsGenerator({defaultSidebarItemsGenerator, docs, ...args}) { + const items = await defaultSidebarItemsGenerator({docs, ...args}); + + // Build map of doc id -> year from frontmatter date + const docYearMap = {}; + for (const doc of docs) { + const date = doc.frontMatter && doc.frontMatter.date; + if (date) { + const year = new Date(date).getFullYear(); + docYearMap[doc.id] = year; + } + } + + function parseVersion(str) { + const match = (str || '').match(/v?(\d+)\.(\d+)\.(\d+)/); + if (!match) return [0, 0, 0]; + return [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])]; + } + function compareVersionsDesc(a, b) { + const [aMaj, aMin, aPatch] = parseVersion(a.label || a.id || ''); + const [bMaj, bMin, bPatch] = parseVersion(b.label || b.id || ''); + if (bMaj !== aMaj) return bMaj - aMaj; + if (bMin !== aMin) return bMin - aMin; + return bPatch - aPatch; + } + + // Flatten and transform doc items (filter index, shorten labels) + function flattenDocs(list) { + const result = []; + for (const item of list) { + if (item.type === 'doc' && item.id === 'index') continue; + if (item.type === 'doc') { + const label = item.id.replace(/\/index$/, ''); + result.push({...item, label}); + } else if (item.type === 'category') { + if (item.link && item.link.type === 'doc' && item.link.id !== 'index') { + const id = item.link.id; + const label = id.replace(/\/index$/, ''); + result.push({type: 'doc', id, label}); + } else { + result.push(...flattenDocs(item.items)); + } + } + } + return result; + } + + const docItems = flattenDocs(items); + + // Group by year + const byYear = {}; + for (const item of docItems) { + const year = docYearMap[item.id] || 'Other'; + if (!byYear[year]) byYear[year] = []; + byYear[year].push(item); + } + + // Sort each year's items by version descending + for (const year of Object.keys(byYear)) { + byYear[year].sort(compareVersionsDesc); + } + + // Build categories sorted by year descending + const years = Object.keys(byYear).sort((a, b) => { + // Object.keys() returns strings; avoid numeric subtraction type errors. + const na = Number.parseInt(a, 10); + const nb = Number.parseInt(b, 10); + return nb - na; + }); + return years.map(year => ({ + type: 'category', + label: String(year), + collapsed: year !== String(years[0]), + items: byYear[year], + })); + }, }, ], [ @@ -130,6 +200,20 @@ const config = { }; }, }), + // Ensure gtag exists before the GA script loads. + () => ({ + name: 'gtag-shim', + injectHtmlTags() { + return { + headTags: [ + { + tagName: 'script', + innerHTML: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}if(!window.gtag){window.gtag=gtag;}`, + }, + ], + }; + }, + }), ], presets: [ @@ -137,10 +221,13 @@ const config = { 'classic', /** @type {import('@docusaurus/preset-classic').Options} */ ({ - gtag: { - trackingID: 'G-K7K215ZVNC', - anonymizeIP: true, - }, + gtag: + process.env.NODE_ENV === 'production' + ? { + trackingID: 'G-K7K215ZVNC', + anonymizeIP: true, + } + : undefined, docs: { sidebarPath: require.resolve('./sidebars.js'), }, @@ -181,34 +268,39 @@ const config = { label: 'Docs', }, { + type: 'docSidebar', + sidebarId: 'learnSidebar', + position: 'left', + label: 'Learn', + }, + { + type: 'docSidebar', sidebarId: 'integrationsSidebar', position: 'left', label: 'Integrations', - to: "docs/integrations" }, { - sidebarId: 'tutorialSidebar', position: 'left', label: 'Enterprise', to: "docs/enterprise" }, - { to: '/release_notes', label: 'Release Notes', position: 'left' }, { to: '/blog', label: 'Blog', position: 'left' }, - { - href: 'https://models.litellm.ai/', - label: '💸 LLM Model Cost Map', - position: 'right', - }, { href: 'https://github.com/BerriAI/litellm', - label: 'GitHub', position: 'right', + className: 'header-github-link', + 'aria-label': 'GitHub repository', }, { href: 'https://www.litellm.ai/support', - label: 'Slack/Discord', position: 'right', - } + className: 'header-discord-link', + 'aria-label': 'Discord / Slack community', + }, + { + type: 'search', + position: 'right', + }, ], }, footer: { diff --git a/docs/my-website/img/ci_cd_architecture.png b/docs/my-website/img/ci_cd_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..111567c11b04d10d78134d35322f84065a05bd5a GIT binary patch literal 162042 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%0AemFEf36~bS9nZdQpYqt4egQnkdvMy0X`E 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 Lbc94iG_{TS5+VSqM2={^G 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 literal 0 HcmV?d00001 diff --git a/docs/my-website/img/hero.png b/docs/my-website/img/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..9f77a28d718abd64b3de1b793c1ccce3c7ff3fcf GIT binary patch literal 6505890 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

-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