From 932706dba6aae9b721817c18c4949fb46fc0f65d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 19:36:33 +0000 Subject: [PATCH] chore: move scripts and runtime sources under litellm --- .agents/skills/litellm-setup/SKILL.md | 38 +- .github/CODEOWNERS | 2 +- .github/CONTRIBUTING.md | 2 +- .github/scripts/ci/classify_changes.sh | 2 +- .github/scripts/verify_linux_native_wheel.py | 3 + .github/workflows/auto-close-duplicates.yml | 8 +- .github/workflows/check-schema-sync.yml | 20 +- .github/workflows/conventional-commits.yml | 4 +- .github/workflows/cost-map-guard.yml | 5 +- .github/workflows/cost-map-pins-recheck.yml | 6 +- .github/workflows/duplicate_issue_check.yml | 10 +- .github/workflows/image-scan.yml | 6 +- .github/workflows/issue_classifier.yml | 18 +- .github/workflows/issue_fixed_comment.yml | 10 +- .github/workflows/issue_label_sync.yml | 12 +- .github/workflows/mutation-test.yml | 2 +- .../workflows/publish-lint-base-counts.yml | 14 +- .github/workflows/spend-logs-guard.yml | 5 +- .github/workflows/sync-schema.yml | 12 +- .github/workflows/test-linting.yml | 18 +- .../test-mcp-dependency-resolution.yml | 14 +- .github/workflows/test-merge-smoke.yml | 2 +- .github/workflows/test-rust.yml | 14 +- .github/workflows/test-unit.yml | 4 +- .gitignore | 2 +- AGENTS.md | 6 +- Makefile | 32 +- README.md | 2 +- litellm/deploy/docker/Dockerfile | 12 +- litellm/deploy/docker/README.md | 4 +- litellm/deploy/docker/docker-entrypoint.sh | 8 +- .../helm/litellm/templates/_helpers.tpl | 4 +- litellm/packaging/litellm-core/pyproject.toml | 5 + .../request_log_indexes.py | 2 +- .../litellm_proxy_extras/utils.py | 8 +- litellm/proxy-extras/migration_runbook.md | 24 +- litellm/proxy/_types.py | 2 +- litellm/proxy/client/cli/README.md | 6 +- litellm/proxy/db/create_views.py | 2 +- .../spend_logs_partition_manager.py | 2 +- {backend => litellm/runtime/backend}/main.py | 4 +- .../runtime/backend}/routes/__init__.py | 0 .../runtime/backend}/routes/allowlist.py | 0 .../runtime/migrations}/run.py | 0 schema.prisma => litellm/schema.prisma | 0 .../scripts}/adaptive_router_demo/README.md | 4 +- .../scripts}/adaptive_router_demo/chat.html | 0 .../adaptive_router_demo/dashboard.html | 0 .../scripts}/adaptive_router_demo/eval.py | 2 +- .../scripts}/adaptive_router_demo/traffic.py | 2 +- .../scripts}/auto-close-duplicates.test.ts | 0 .../scripts}/auto-close-duplicates.ts | 0 .../benchmark_anthropic_messages_perf.py | 4 +- .../benchmark_chat_completions_perf.py | 4 +- .../scripts}/benchmark_mock.py | 0 .../benchmark_model_response_creator.py | 4 +- .../scripts}/benchmark_ocr_callbacks.py | 2 +- .../scripts}/benchmark_proxy_vs_provider.py | 16 +- .../benchmark_streaming_chunk_overhead.py | 2 +- .../scripts}/build_core_distribution.py | 7 +- .../scripts}/check_mcp_operation_boundary.py | 0 .../scripts}/check_mcp_sdk_install.py | 0 .../scripts}/check_openapi_docs.py | 2 +- .../scripts}/check_schema_compatibility.py | 15 +- .../scripts}/check_test_quality.py | 4 +- .../scripts}/check_type_discipline.py | 4 + .../scripts}/ci_cd/TEST_KEY_PATTERNS.md | 0 .../scripts}/ci_cd/check_file_length.py | 0 .../scripts}/ci_cd/check_files_match.py | 0 .../scripts}/ci_cd/cost_map_guard.py | 4 +- .../scripts}/ci_cd/cost_map_pins.json | 0 .../scripts}/ci_cd/run_migration.py | 4 +- .../scripts}/ci_cd/security_scans_readme.md | 0 .../scripts}/ci_cd/spend_logs_guard.py | 0 .../scripts}/classify-issue.test.ts | 0 .../scripts}/classify-issue.ts | 0 .../scripts}/comment-fixed-issue.test.ts | 0 .../scripts}/comment-fixed-issue.ts | 0 .../scripts}/create_litellm_branch.ps1 | 0 .../scripts}/create_litellm_branch.sh | 0 .../create_team_key_and_submit_guardrail.sh | 0 .../scripts}/db/backfill_daily_tool_spend.sql | 2 +- .../scripts}/db/backfill_key_total_spend.sql | 4 +- ...ckfill_key_total_spend_from_spend_logs.sql | 4 +- .../scripts}/db/create_views.py | 0 .../scripts}/db/migrate_keys.py | 0 .../scripts}/db/partition_spend_logs.sql | 2 +- .../strip_vector_store_internal_metadata.py | 0 .../scripts}/db/unpartition_spend_logs.sql | 2 +- .../scripts}/db/update_unassigned_teams.py | 0 .../scripts}/default_branch.py | 0 .../scripts}/eval_compression.py | 6 +- .../scripts}/flag-duplicate-issue.test.ts | 0 .../scripts}/flag-duplicate-issue.ts | 0 .../scripts}/gate_slot_lock.py | 6 +- .../generate_provider_endpoints_support.py | 4 +- .../scripts}/generate_trace_types.py | 6 +- .../scripts}/githooks/commit-msg | 0 .../benchmark_get_all_latest_health_checks.py | 0 .../health_check/health_check_client.py | 0 .../health_check_client_README.md | 6 +- .../health_check_requirements.txt | 0 .../run_parallel_health_checks.ps1 | 0 .../run_parallel_health_checks.sh | 0 {scripts => litellm/scripts}/install-cli.sh | 4 +- {scripts => litellm/scripts}/install.sh | 2 +- .../scripts}/install_git_hooks.sh | 8 +- {scripts => litellm/scripts}/issue-labels.ts | 0 .../scripts}/label-issue.test.ts | 0 {scripts => litellm/scripts}/label-issue.ts | 0 ...missing_request_id_simple_spend_logs.jsonl | 0 ..._missing_request_id_swarm_spend_logs.jsonl | 0 .../claude_agent_sdk_simple_spend_logs.jsonl | 0 .../claude_agent_sdk_swarm_spend_logs.jsonl | 0 .../spend/crewai_simple_spend_logs.jsonl | 0 .../spend/crewai_swarm_spend_logs.jsonl | 0 .../spend/deepagents_simple_spend_logs.jsonl | 0 .../spend/deepagents_swarm_spend_logs.jsonl | 0 ...google_adk_billed_failure_spend_logs.jsonl | 0 .../spend/google_adk_retry_spend_logs.jsonl | 0 .../spend/google_adk_simple_spend_logs.jsonl | 0 .../spend/google_adk_stream_spend_logs.jsonl | 0 .../spend/google_adk_swarm_spend_logs.jsonl | 0 .../spend/langchain_simple_spend_logs.jsonl | 0 .../spend/langchain_swarm_spend_logs.jsonl | 0 .../spend/langgraph_simple_spend_logs.jsonl | 0 .../spend/langgraph_swarm_spend_logs.jsonl | 0 .../spend/llamaindex_simple_spend_logs.jsonl | 0 .../spend/llamaindex_swarm_spend_logs.jsonl | 0 .../spend/mastra_simple_spend_logs.jsonl | 0 .../spend/mastra_swarm_spend_logs.jsonl | 0 .../openai_agents_simple_spend_logs.jsonl | 0 .../openai_agents_swarm_spend_logs.jsonl | 0 .../opentelemetry_simple_spend_logs.jsonl | 0 .../opentelemetry_swarm_spend_logs.jsonl | 0 ...ydantic_ai_billed_failure_spend_logs.jsonl | 0 .../spend/pydantic_ai_retry_spend_logs.jsonl | 0 .../spend/pydantic_ai_simple_spend_logs.jsonl | 0 .../spend/pydantic_ai_stream_spend_logs.jsonl | 0 .../spend/pydantic_ai_swarm_spend_logs.jsonl | 0 .../pydantic_ai_swarm_stream_spend_logs.jsonl | 0 ...ntic_ai_token_limit_swarm_spend_logs.jsonl | 0 .../fixtures/spend/spend_logs.jsonl | 0 .../strands_billed_failure_spend_logs.jsonl | 0 .../spend/strands_retry_spend_logs.jsonl | 0 .../spend/strands_simple_spend_logs.jsonl | 0 .../spend/strands_swarm_spend_logs.jsonl | 0 ...cel_ai_sdk_billed_failure_spend_logs.jsonl | 0 .../vercel_ai_sdk_py_simple_spend_logs.jsonl | 0 .../vercel_ai_sdk_py_swarm_spend_logs.jsonl | 0 .../vercel_ai_sdk_retry_spend_logs.jsonl | 0 .../vercel_ai_sdk_simple_spend_logs.jsonl | 0 .../vercel_ai_sdk_stream_spend_logs.jsonl | 0 .../vercel_ai_sdk_swarm_spend_logs.jsonl | 0 .../claude_agent_sdk_detailed_export.json | 0 .../traces/claude_agent_sdk_export.json | 0 ...e_agent_sdk_missing_request_id_simple.json | 0 ...de_agent_sdk_missing_request_id_swarm.json | 0 .../traces/claude_agent_sdk_simple.json | 0 .../traces/claude_agent_sdk_swarm.json | 0 .../traces/claude_code_native_logs.json | 0 .../claude_code_native_tool_result.json | 0 .../traces/claude_code_native_traces.json | 0 .../fixtures/traces/crewai_simple.json | 0 .../fixtures/traces/crewai_swarm.json | 0 .../fixtures/traces/deepagents_simple.json | 0 .../fixtures/traces/deepagents_swarm.json | 0 .../traces/google_adk_billed_failure.json | 0 .../fixtures/traces/google_adk_retry.json | 0 .../fixtures/traces/google_adk_simple.json | 0 .../fixtures/traces/google_adk_stream.json | 0 .../fixtures/traces/google_adk_swarm.json | 0 .../fixtures/traces/langchain_simple.json | 0 .../fixtures/traces/langchain_swarm.json | 0 .../fixtures/traces/langgraph_simple.json | 0 .../fixtures/traces/langgraph_swarm.json | 0 .../traces/langsmith_deep_agent_export.json | 0 .../fixtures/traces/llamaindex_simple.json | 0 .../fixtures/traces/llamaindex_swarm.json | 0 .../fixtures/traces/mastra_simple.json | 0 .../fixtures/traces/mastra_swarm.json | 0 .../fixtures/traces/openai_agents_simple.json | 0 .../fixtures/traces/openai_agents_swarm.json | 0 .../fixtures/traces/opentelemetry_simple.json | 0 .../fixtures/traces/opentelemetry_swarm.json | 0 .../traces/pydantic_ai_billed_failure.json | 0 .../fixtures/traces/pydantic_ai_retry.json | 0 .../fixtures/traces/pydantic_ai_simple.json | 0 .../fixtures/traces/pydantic_ai_stream.json | 0 .../fixtures/traces/pydantic_ai_swarm.json | 0 .../traces/pydantic_ai_swarm_stream.json | 0 .../traces/pydantic_ai_token_limit_swarm.json | 0 .../fixtures/traces/query_alternate.json | 0 .../fixtures/traces/query_children.json | 0 .../fixtures/traces/query_other_team.json | 0 .../fixtures/traces/query_root.json | 0 .../traces/strands_billed_failure.json | 0 .../fixtures/traces/strands_retry.json | 0 .../fixtures/traces/strands_simple.json | 0 .../fixtures/traces/strands_swarm.json | 0 .../traces/vercel_ai_sdk_billed_failure.json | 0 .../traces/vercel_ai_sdk_py_simple.json | 0 .../traces/vercel_ai_sdk_py_swarm.json | 0 .../fixtures/traces/vercel_ai_sdk_retry.json | 0 .../fixtures/traces/vercel_ai_sdk_simple.json | 0 .../fixtures/traces/vercel_ai_sdk_stream.json | 0 .../fixtures/traces/vercel_ai_sdk_swarm.json | 0 .../schemas/models/ReadQueryName.json | 0 .../schemas/models/TraceAgentRow.json | 0 .../schemas/models/TraceAgentsParams.json | 0 .../schemas/models/TraceQueryHelp.json | 0 .../schemas/requests/TraceDetailRequest.json | 0 .../requests/TraceErrorPageRequest.json | 0 .../schemas/requests/TraceListRequest.json | 0 .../schemas/requests/TraceQueryRequest.json | 0 .../schemas/requests/TraceSpanRequest.json | 0 .../schemas/responses/TraceSQLResponse.json | 0 .../lens_assets/schemas/types/QueryScope.json | 0 .../lens_assets/schemas/types/SpanDetail.json | 0 .../schemas/types/SpanErrorPage.json | 0 .../lens_assets/schemas/types/Tenant.json | 0 .../lens_assets/schemas/types/Trace.json | 0 .../lens_assets/schemas/types/TracePage.json | 0 .../lens_assets/schemas/types/TraceScope.json | 0 .../scripts}/lens_assets/source.json | 0 .../scripts}/lint_base_counts.py | 6 +- .../mock_bedrock_passthrough_target.py | 4 +- .../scripts}/mock_grayswan_timeout_server.py | 2 +- .../scripts}/mutation_report.py | 2 +- .../scripts}/openapi_docs_gate.py | 16 +- .../scripts}/pre_commit_lint.sh | 22 +- .../scripts}/prisma_generate_if_needed.py | 2 +- .../scripts}/ruff_strict_gate.py | 4 +- .../scripts}/seed_request_logs.py | 2 +- .../scripts}/seed_tracing_fixtures.py | 6 +- .../scripts}/sync-issue-labels.test.ts | 0 .../scripts}/sync-issue-labels.ts | 0 .../scripts}/test_agent_mcp_endpoints.sh | 0 .../test_guardrails_register_endpoints.sh | 0 .../scripts}/test_quality_gate.py | 16 +- .../scripts}/test_tool_allowlist_script.py | 4 +- .../scripts}/tpm_headline_test.sh | 2 +- .../scripts}/trace_codegen/README.md | 6 +- .../scripts}/trace_codegen/config.json | 2 +- .../templates/ScalarTypeAliasType.jinja2 | 0 .../templates/TypeAliasType.jinja2 | 0 .../templates/TypedDictClass.jinja2 | 0 .../templates/pydantic_v2/BaseModel.jinja2 | 0 .../templates/pydantic_v2/types.jinja2 | 0 .../trace_codegen/templates/types.jinja2 | 0 .../scripts}/type_check_gate.py | 8 +- .../scripts}/type_discipline_gate.py | 14 +- .../scripts}/verify_adaptive_router.py | 2 +- .../scripts}/with_dashboard_node.sh | 2 +- litellm/tracing/AGENTS.md | 2 +- litellm/tracing/generated/models.py | 2 +- litellm/tracing/generated/requests.py | 2 +- litellm/tracing/generated/responses.py | 2 +- litellm/tracing/generated/types.py | 2 +- litellm/types/interactions/README.md | 2 +- .../src/lib/http/schema.d.ts | 2 +- pyproject.toml | 15 +- scripts/ci_cd/baseline_db.py | 60 - scripts/quickstart.ps1 | 1322 ----------------- scripts/quickstart.sh | 1184 --------------- .../base_sdk_tests/test_core_distribution.py | 18 +- .../check_migrations_no_data_rewrites.py | 2 +- ...est_generate_provider_endpoints_support.py | 4 +- tests/integration/README.md | 4 +- tests/integration/_support/safeguards_rig.py | 2 +- .../database/image/test_image_admin_mcp.py | 2 +- .../migrations/test_db_schema_migration.py | 6 +- .../migrations/test_request_log_indexes.py | 2 +- .../test_request_log_indexes_at_boot.py | 4 +- .../database/test_spend_lock_order.py | 2 +- .../database/test_spendlogs_ledger_parity.py | 2 +- ...st_strip_vector_store_internal_metadata.py | 2 +- .../test_litellm_proxy_extras_utils.py | 2 +- tests/unit/proxy/db/test_prisma_query_span.py | 2 +- tests/unit/proxy/test_component_allowlists.py | 14 +- tests/unit/proxy/test_db_schema_changes.py | 2 +- .../test_verify_linux_native_wheel.py | 3 + .../unit/test_check_mcp_operation_boundary.py | 4 +- tests/unit/test_check_openapi_docs.py | 2 +- tests/unit/test_check_test_quality.py | 4 +- tests/unit/test_check_type_discipline.py | 4 +- tests/unit/test_ci_path_filter.py | 2 +- tests/unit/test_cost_map_guard.py | 2 +- tests/unit/test_default_branch.py | 14 +- tests/unit/test_docker_entrypoint.py | 12 +- tests/unit/test_gate_slot_lock.py | 2 +- tests/unit/test_git_hooks.py | 4 +- tests/unit/test_lint_base_counts.py | 2 +- tests/unit/test_lint_workflow_diff_gates.py | 2 +- tests/unit/test_mutation_report.py | 4 +- tests/unit/test_openapi_docs_gate.py | 4 +- tests/unit/test_pre_commit_lint.py | 14 +- tests/unit/test_prisma_generate_if_needed.py | 2 +- tests/unit/test_seed_tracing_fixtures.py | 4 +- tests/unit/test_spend_logs_guard.py | 2 +- tests/unit/test_test_quality_gate.py | 6 +- tests/unit/test_type_discipline_gate.py | 2 +- tests/unit/test_with_dashboard_node.py | 2 +- 303 files changed, 414 insertions(+), 2962 deletions(-) rename {backend => litellm/runtime/backend}/main.py (93%) rename {backend => litellm/runtime/backend}/routes/__init__.py (100%) rename {backend => litellm/runtime/backend}/routes/allowlist.py (100%) rename {migrations => litellm/runtime/migrations}/run.py (100%) rename schema.prisma => litellm/schema.prisma (100%) rename {scripts => litellm/scripts}/adaptive_router_demo/README.md (98%) rename {scripts => litellm/scripts}/adaptive_router_demo/chat.html (100%) rename {scripts => litellm/scripts}/adaptive_router_demo/dashboard.html (100%) rename {scripts => litellm/scripts}/adaptive_router_demo/eval.py (99%) rename {scripts => litellm/scripts}/adaptive_router_demo/traffic.py (99%) rename {scripts => litellm/scripts}/auto-close-duplicates.test.ts (100%) rename {scripts => litellm/scripts}/auto-close-duplicates.ts (100%) rename {scripts => litellm/scripts}/benchmark_anthropic_messages_perf.py (99%) rename {scripts => litellm/scripts}/benchmark_chat_completions_perf.py (99%) rename {scripts => litellm/scripts}/benchmark_mock.py (100%) rename {scripts => litellm/scripts}/benchmark_model_response_creator.py (96%) rename {scripts => litellm/scripts}/benchmark_ocr_callbacks.py (99%) rename {scripts => litellm/scripts}/benchmark_proxy_vs_provider.py (98%) rename {scripts => litellm/scripts}/benchmark_streaming_chunk_overhead.py (99%) rename {scripts => litellm/scripts}/build_core_distribution.py (94%) rename {scripts => litellm/scripts}/check_mcp_operation_boundary.py (100%) rename {scripts => litellm/scripts}/check_mcp_sdk_install.py (100%) rename {scripts => litellm/scripts}/check_openapi_docs.py (96%) rename {scripts => litellm/scripts}/check_schema_compatibility.py (86%) rename {scripts => litellm/scripts}/check_test_quality.py (99%) rename {scripts => litellm/scripts}/check_type_discipline.py (99%) rename {scripts => litellm/scripts}/ci_cd/TEST_KEY_PATTERNS.md (100%) rename {scripts => litellm/scripts}/ci_cd/check_file_length.py (100%) rename {scripts => litellm/scripts}/ci_cd/check_files_match.py (100%) rename {scripts => litellm/scripts}/ci_cd/cost_map_guard.py (98%) rename {scripts => litellm/scripts}/ci_cd/cost_map_pins.json (100%) rename {scripts => litellm/scripts}/ci_cd/run_migration.py (99%) rename {scripts => litellm/scripts}/ci_cd/security_scans_readme.md (100%) rename {scripts => litellm/scripts}/ci_cd/spend_logs_guard.py (100%) rename {scripts => litellm/scripts}/classify-issue.test.ts (100%) rename {scripts => litellm/scripts}/classify-issue.ts (100%) rename {scripts => litellm/scripts}/comment-fixed-issue.test.ts (100%) rename {scripts => litellm/scripts}/comment-fixed-issue.ts (100%) rename {scripts => litellm/scripts}/create_litellm_branch.ps1 (100%) rename {scripts => litellm/scripts}/create_litellm_branch.sh (100%) rename {scripts => litellm/scripts}/create_team_key_and_submit_guardrail.sh (100%) rename {scripts => litellm/scripts}/db/backfill_daily_tool_spend.sql (97%) rename {scripts => litellm/scripts}/db/backfill_key_total_spend.sql (92%) rename {scripts => litellm/scripts}/db/backfill_key_total_spend_from_spend_logs.sql (95%) rename {scripts => litellm/scripts}/db/create_views.py (100%) rename {scripts => litellm/scripts}/db/migrate_keys.py (100%) rename {scripts => litellm/scripts}/db/partition_spend_logs.sql (99%) rename {scripts => litellm/scripts}/db/strip_vector_store_internal_metadata.py (100%) rename {scripts => litellm/scripts}/db/unpartition_spend_logs.sql (97%) rename {scripts => litellm/scripts}/db/update_unassigned_teams.py (100%) rename {scripts => litellm/scripts}/default_branch.py (100%) rename {scripts => litellm/scripts}/eval_compression.py (99%) rename {scripts => litellm/scripts}/flag-duplicate-issue.test.ts (100%) rename {scripts => litellm/scripts}/flag-duplicate-issue.ts (100%) rename {scripts => litellm/scripts}/gate_slot_lock.py (96%) rename {scripts => litellm/scripts}/generate_provider_endpoints_support.py (98%) rename {scripts => litellm/scripts}/generate_trace_types.py (97%) rename {scripts => litellm/scripts}/githooks/commit-msg (100%) rename {scripts => litellm/scripts}/health_check/benchmark_get_all_latest_health_checks.py (100%) rename {scripts => litellm/scripts}/health_check/health_check_client.py (100%) rename {scripts => litellm/scripts}/health_check/health_check_client_README.md (98%) rename {scripts => litellm/scripts}/health_check/health_check_requirements.txt (100%) rename {scripts => litellm/scripts}/health_check/run_parallel_health_checks.ps1 (100%) rename {scripts => litellm/scripts}/health_check/run_parallel_health_checks.sh (100%) rename {scripts => litellm/scripts}/install-cli.sh (98%) rename {scripts => litellm/scripts}/install.sh (99%) rename {scripts => litellm/scripts}/install_git_hooks.sh (81%) rename {scripts => litellm/scripts}/issue-labels.ts (100%) rename {scripts => litellm/scripts}/label-issue.test.ts (100%) rename {scripts => litellm/scripts}/label-issue.ts (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/claude_agent_sdk_missing_request_id_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/claude_agent_sdk_missing_request_id_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/claude_agent_sdk_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/claude_agent_sdk_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/crewai_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/crewai_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/deepagents_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/deepagents_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/google_adk_billed_failure_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/google_adk_retry_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/google_adk_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/google_adk_stream_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/google_adk_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/langchain_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/langchain_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/langgraph_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/langgraph_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/llamaindex_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/llamaindex_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/mastra_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/mastra_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/openai_agents_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/openai_agents_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/opentelemetry_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/opentelemetry_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_billed_failure_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_retry_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_stream_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_swarm_stream_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/pydantic_ai_token_limit_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/strands_billed_failure_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/strands_retry_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/strands_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/strands_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_billed_failure_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_py_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_py_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_retry_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_simple_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_stream_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/spend/vercel_ai_sdk_swarm_spend_logs.jsonl (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_detailed_export.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_export.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_missing_request_id_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_missing_request_id_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_agent_sdk_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_code_native_logs.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_code_native_tool_result.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/claude_code_native_traces.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/crewai_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/crewai_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/deepagents_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/deepagents_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/google_adk_billed_failure.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/google_adk_retry.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/google_adk_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/google_adk_stream.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/google_adk_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/langchain_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/langchain_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/langgraph_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/langgraph_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/langsmith_deep_agent_export.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/llamaindex_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/llamaindex_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/mastra_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/mastra_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/openai_agents_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/openai_agents_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/opentelemetry_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/opentelemetry_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_billed_failure.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_retry.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_stream.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_swarm_stream.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/pydantic_ai_token_limit_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/query_alternate.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/query_children.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/query_other_team.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/query_root.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/strands_billed_failure.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/strands_retry.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/strands_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/strands_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_billed_failure.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_py_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_py_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_retry.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_simple.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_stream.json (100%) rename {scripts => litellm/scripts}/lens_assets/fixtures/traces/vercel_ai_sdk_swarm.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/models/ReadQueryName.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/models/TraceAgentRow.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/models/TraceAgentsParams.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/models/TraceQueryHelp.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/requests/TraceDetailRequest.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/requests/TraceErrorPageRequest.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/requests/TraceListRequest.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/requests/TraceQueryRequest.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/requests/TraceSpanRequest.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/responses/TraceSQLResponse.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/QueryScope.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/SpanDetail.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/SpanErrorPage.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/Tenant.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/Trace.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/TracePage.json (100%) rename {scripts => litellm/scripts}/lens_assets/schemas/types/TraceScope.json (100%) rename {scripts => litellm/scripts}/lens_assets/source.json (100%) rename {scripts => litellm/scripts}/lint_base_counts.py (98%) rename {scripts => litellm/scripts}/mock_bedrock_passthrough_target.py (98%) rename {scripts => litellm/scripts}/mock_grayswan_timeout_server.py (96%) rename {scripts => litellm/scripts}/mutation_report.py (99%) rename {scripts => litellm/scripts}/openapi_docs_gate.py (91%) rename {scripts => litellm/scripts}/pre_commit_lint.sh (94%) rename {scripts => litellm/scripts}/prisma_generate_if_needed.py (98%) rename {scripts => litellm/scripts}/ruff_strict_gate.py (97%) rename {scripts => litellm/scripts}/seed_request_logs.py (99%) rename {scripts => litellm/scripts}/seed_tracing_fixtures.py (99%) rename {scripts => litellm/scripts}/sync-issue-labels.test.ts (100%) rename {scripts => litellm/scripts}/sync-issue-labels.ts (100%) rename {scripts => litellm/scripts}/test_agent_mcp_endpoints.sh (100%) rename {scripts => litellm/scripts}/test_guardrails_register_endpoints.sh (100%) rename {scripts => litellm/scripts}/test_quality_gate.py (91%) rename {scripts => litellm/scripts}/test_tool_allowlist_script.py (97%) rename {scripts => litellm/scripts}/tpm_headline_test.sh (98%) rename {scripts => litellm/scripts}/trace_codegen/README.md (55%) rename {scripts => litellm/scripts}/trace_codegen/config.json (89%) rename {scripts => litellm/scripts}/trace_codegen/templates/ScalarTypeAliasType.jinja2 (100%) rename {scripts => litellm/scripts}/trace_codegen/templates/TypeAliasType.jinja2 (100%) rename {scripts => litellm/scripts}/trace_codegen/templates/TypedDictClass.jinja2 (100%) rename {scripts => litellm/scripts}/trace_codegen/templates/pydantic_v2/BaseModel.jinja2 (100%) rename {scripts => litellm/scripts}/trace_codegen/templates/pydantic_v2/types.jinja2 (100%) rename {scripts => litellm/scripts}/trace_codegen/templates/types.jinja2 (100%) rename {scripts => litellm/scripts}/type_check_gate.py (97%) rename {scripts => litellm/scripts}/type_discipline_gate.py (93%) rename {scripts => litellm/scripts}/verify_adaptive_router.py (99%) rename {scripts => litellm/scripts}/with_dashboard_node.sh (97%) delete mode 100644 scripts/ci_cd/baseline_db.py delete mode 100644 scripts/quickstart.ps1 delete mode 100755 scripts/quickstart.sh diff --git a/.agents/skills/litellm-setup/SKILL.md b/.agents/skills/litellm-setup/SKILL.md index 7c824751d6f..fc6937454a4 100644 --- a/.agents/skills/litellm-setup/SKILL.md +++ b/.agents/skills/litellm-setup/SKILL.md @@ -19,10 +19,10 @@ Ask everything in one message, with defaults the user can accept by saying "go": - Providers and models. Default is OpenAI with its newest flagship model, using the `OPENAI_API_KEY` in your environment when `[ -n "$OPENAI_API_KEY" ] && echo set` says it is there. Run that check before asking, and say in the question that the default uses that variable, naming it but never its value, since a key in your environment can belong to another account - Whether they want virtual keys, teams, spend tracking, or the Admin UI. Those need Postgres, plain routing does not. Default is no -- uv (default) or Docker. With Postgres, Docker runs the repo's quickstart, which brings its own database, and uv needs a Postgres server they already have, with a new, empty database for LiteLLM, so someone with their own Postgres server picks uv -- Folder and port. Defaults are `./litellm-proxy` (`./litellm-gateway` for the quickstart) and 4000 +- uv (default) or Docker. For Docker with Postgres, use the deployment guide in step 3. uv needs a Postgres server they already have, with a new, empty database for LiteLLM +- Folder and port. Defaults are `./litellm-proxy` and 4000 -Then follow the steps for their answers. Plain routing skips steps 3 and 6. The quickstart is steps 2, 3, 8, and 9. uv with their own Postgres skips step 3 +Then follow the steps for their answers. Plain routing skips steps 3 and 6. uv with their own Postgres skips step 3 In the same message, say that someone who picks uv and would rather answer prompts can run `litellm --setup` in a separate terminal window once LiteLLM is installed, not through Claude Code. Never run it yourself, since it needs a real terminal and without one prints "Setup cancelled." and writes nothing. Give them the command as `LITELLM_MODE=PRODUCTION /litellm --setup`, with `` replaced by the folder `uv tool dir --bin` prints, to run from the folder they want the config in. It writes `litellm_config.yaml` there with the provider keys and master key in plain text and offers to start the proxy, which then listens on every network interface and stops with Ctrl+C in that window. When the user is done, run this in that folder. It makes the file readable only by them, keeps it out of git (the wizard does neither), and prints only the model names. Never print that file or run `grep`, `sed`, or `cat` on it: @@ -43,29 +43,13 @@ export LITELLM_MASTER_KEY="$("$(uv tool dir)/litellm/bin/python" -I -c 'import y uv --version; docker info --format '{{.ServerVersion}}'; docker compose version; curl --version | head -1; openssl version; command -v python3 ``` -On native Windows, ask the user to run Claude Code inside WSL, since every command here is POSIX shell. Every path needs curl, openssl, and python3, so ask the user to install any that is missing. The uv path needs uv, which downloads a supported Python (LiteLLM supports 3.10 to 3.14) when the `--python` range in step 4 rules out the one on the machine. If uv is missing, offer to install it, and only after the user says yes run `curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 sh`, which leaves their shell profile alone (https://docs.astral.sh/uv/getting-started/installation/). It puts uv in `~/.local/bin`, so start every later command that runs `uv` or `litellm` with `export PATH="$HOME/.local/bin:$PATH";`. The Docker path needs a running Docker daemon, and the quickstart also needs Docker Compose v2. If `docker info` says `permission denied`, the user's account cannot use Docker without sudo. Offer the uv path, or ask them to add themselves to the `docker` group the way https://docs.docker.com/engine/install/linux-postinstall/ shows and start a new login session before you check again +On native Windows, ask the user to run Claude Code inside WSL, since every command here is POSIX shell. Every path needs curl, openssl, and python3, so ask the user to install any that is missing. The uv path needs uv, which downloads a supported Python (LiteLLM supports 3.10 to 3.14) when the `--python` range in step 4 rules out the one on the machine. If uv is missing, offer to install it, and only after the user says yes run `curl -LsSf https://astral.sh/uv/install.sh | env UV_NO_MODIFY_PATH=1 sh`, which leaves their shell profile alone (https://docs.astral.sh/uv/getting-started/installation/). It puts uv in `~/.local/bin`, so start every later command that runs `uv` or `litellm` with `export PATH="$HOME/.local/bin:$PATH";`. The Docker path needs a running Docker daemon, and Docker with Postgres also needs Docker Compose v2. If `docker info` says `permission denied`, the user's account cannot use Docker without sudo. Offer the uv path, or ask them to add themselves to the `docker` group the way https://docs.docker.com/engine/install/linux-postinstall/ shows and start a new login session before you check again Check the port is free with `curl -s -o /dev/null --max-time 2 http://127.0.0.1:4000; echo $?`, which prints `7` (could not connect) only when nothing listens there. Don't use `lsof` for this, since it misses ports that root or Docker hold. If it is taken, pick another port and use it everywhere below, because a proxy asked for a busy 4000 silently moves to a random port -## 3. Postgres with Docker: run the quickstart +## 3. Postgres with Docker -The repo's quickstart starts the gateway, Postgres, and the Admin UI in Docker, listening on this machine only, and puts a `litellm-gateway` command in that folder that stops, starts, updates, or removes them. It asks nothing when it has no terminal. It writes the master key, salt key, and database password to its own `.env` and never prints them. Download it, then run it with the folder and port from step 1. It returns once the gateway is ready: - -```bash -curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh -o quickstart.sh -LITELLM_DIR="$PWD/litellm-gateway" LITELLM_PORT=4000 sh quickstart.sh --yes -cd litellm-gateway -``` - -Models live in the database here, so add each one through the API instead of a `config.yaml`, naming it the way step 5 says. Pipe the request body in, so the provider key never shows up in output or on a command line. Unless the user agreed to the key in your environment, as step 5 says, ask them to add the line `OPENAI_API_KEY=` to this `.env` themselves. Run the loader line first: - -```bash -python3 -c 'import json, os, sys; json.dump({"model_name": "gpt-6.1-sol", "litellm_params": {"model": "openai/gpt-6.1-sol", "api_key": os.environ["OPENAI_API_KEY"]}}, sys.stdout)' \ - | curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4000/model/new \ - -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" -d @- -``` - -Run that in one shell command that starts with the loader line, since the master key is in `.env`. `200` means the model was added. Then go to step 8 +Follow [the container deployment guide](../../../litellm/deploy/docker/README.md) instead of the remaining steps in this skill. Deployment tooling is maintained separately from these local uv and standalone Docker instructions. ## 4. Install @@ -170,7 +154,7 @@ Readiness reports `"db":"connected"` with Postgres and `"db":"Not connected"` wi ## 8. Verify with a chat completion -Use a `model_name` from `config.yaml` (or step 3), in one shell command that starts with the loader line: +Use a `model_name` from `config.yaml`, in one shell command that starts with the loader line: ```bash curl -sS http://127.0.0.1:4000/v1/chat/completions \ @@ -186,7 +170,7 @@ Tell the user, without printing any secret: - The base URL is `http://127.0.0.1:4000`. Any OpenAI-compatible client works with it as the base URL and the master key or a virtual key as the API key - The master key is `LITELLM_MASTER_KEY` in the folder's `.env`, or `general_settings.master_key` in `litellm_config.yaml` when they used `litellm --setup` -- To stop it, `kill "$(cat litellm.pid)"` (uv), `docker stop litellm` (Docker), `/litellm-gateway stop` (quickstart), or Ctrl+C in its window (`litellm --setup`). To start it again, `docker start litellm`, `/litellm-gateway start`, on uv the whole step 7 command as you ran it, loader line and PATH export included, wrapped in `( ... )` so their shell keeps its own settings, shown as a code block and never paraphrased, since a new shell has neither, or for `litellm --setup` `LITELLM_MODE=PRODUCTION /litellm --config litellm_config.yaml --host 127.0.0.1 --port ` in a terminal window in that folder, with `` and `` filled in, since running the wizard again writes a new config with a new master key. `update` and `uninstall` update or remove a quickstart install. Give the quickstart commands with the folder's full path, since a bare `litellm-gateway` is on PATH only when `~/.local/bin` is, and can belong to an earlier install +- To stop it, `kill "$(cat litellm.pid)"` (uv), `docker stop litellm` (Docker), or Ctrl+C in its window (`litellm --setup`). To start it again, `docker start litellm`, on uv the whole step 7 command as you ran it, loader line and PATH export included, wrapped in `( ... )` so their shell keeps its own settings, shown as a code block and never paraphrased, since a new shell has neither, or for `litellm --setup` `LITELLM_MODE=PRODUCTION /litellm --config litellm_config.yaml --host 127.0.0.1 --port ` in a terminal window in that folder, with `` and `` filled in, since running the wizard again writes a new config with a new master key. - With Postgres, the Admin UI is at `http://127.0.0.1:4000/ui` (user `admin`, password the master key), and `POST /key/generate` with the master key creates virtual keys with their own budgets and model lists. Without Postgres, both need step 3 or 6 first - The Admin UI warns that the master-key login is a shared secret, which is fine while only this machine reaches the proxy. Before anyone else can reach it, invite an admin with their own password on the Internal Users page, then set `general_settings.disable_env_credential_login: true` in the proxy config - To manage the running proxy from a coding agent (keys, teams, users, models, MCP servers), install `npx skills add BerriAI/litellm-skills -g` @@ -196,10 +180,10 @@ Tell the user, without printing any secret: - `refused to start: no master key is set` means the loader line did not run in the shell command that started `litellm`, or `--env-file .env` is missing from `docker run` - `a database URL is set but the prisma CLI is neither on PATH nor importable` means the install lacks `extra_proxy`, so rerun step 4 with it, or, on plain routing, that the command that started `litellm` did not start with the loader line - `The Client hasn't been generated yet` or `Unable to find Prisma binaries` means the Prisma generate command in step 6 has not run -- `Incorrect API key provided` or another provider `AuthenticationError` means a provider key is wrong. Fix it in `.env` and restart, since the proxy reads the environment only at startup. On uv, stop it and rerun step 7. On Docker, `docker rm -f litellm` and rerun step 7, since `docker restart` keeps the environment the container was created with. On the quickstart the key is saved with the model, so delete the model and add it again: `GET /model/info` lists each model's `model_info.id`, and `POST /model/delete` with `{"id": ""}` deletes it +- `Incorrect API key provided` or another provider `AuthenticationError` means a provider key is wrong. Fix it in `.env` and restart, since the proxy reads the environment only at startup. On uv, stop it and rerun step 7. On Docker, `docker rm -f litellm` and rerun step 7, since `docker restart` keeps the environment the container was created with. - `Cannot automatically baseline this database` means `DATABASE_URL` points at a database that already holds tables LiteLLM did not create. Point it at a new, empty database -- `LLM Provider NOT provided` means a `model` in `config.yaml` (or step 3) lacks its provider in front. Use the name as the step 5 lookup prints it -- `Invalid model name passed in model=...` means the request's `model` is not a `model_name` in `config.yaml` or step 3. `GET /v1/models` lists the ones the key can use +- `LLM Provider NOT provided` means a `model` in `config.yaml` lacks its provider in front. Use the name as the step 5 lookup prints it +- `Invalid model name passed in model=...` means the request's `model` is not a `model_name` in `config.yaml`. `GET /v1/models` lists the ones the key can use - `No connected db.` on a proxy without Postgres means the bearer token is not the master key - `Conflict. The container name "/litellm" is already in use` means an earlier run left a container. `docker start litellm` reuses it, or with the user's OK `docker rm -f litellm` and rerun step 7 - Inside Docker, `localhost` in `config.yaml` (an Ollama `api_base`, say) is the container itself. Use `host.docker.internal`, and on Linux also pass `--add-host=host.docker.internal:host-gateway` diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 26163f5c711..3631faaece8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,6 @@ /model_prices_and_context_window.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri /litellm/model_prices_and_context_window_backup.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri -/scripts/ci_cd/cost_map_pins.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri +/litellm/scripts/ci_cd/cost_map_pins.json @mateo-berri @ryan-crabbe-berri @kerry-berri @joshua-berri /litellm/deploy/helm/ @yassin-berriai /litellm/deploy/terraform/ @yassin-berriai /.github/workflows/zizmor.yml @yuneng-berri diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5880cf7d8c2..02beeb9fac1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -360,7 +360,7 @@ Local lint and budget checks follow origin's current default branch. They refres ## Submitting Your PR 1. **Push your branch**: `git push origin your-feature-branch` -2. **Create a PR**: Go to GitHub and open a pull request against the repository's current default branch. Run `python3 scripts/default_branch.py --branch` to check its name +2. **Create a PR**: Go to GitHub and open a pull request against the repository's current default branch. Run `python3 litellm/scripts/default_branch.py --branch` to check its name 3. **Fill out the PR template**: Provide clear description of changes 4. **Wait for review**: Maintainers will review and provide feedback 5. **Address feedback**: Make requested changes and push updates diff --git a/.github/scripts/ci/classify_changes.sh b/.github/scripts/ci/classify_changes.sh index 30d76f3d085..4f46be4d952 100755 --- a/.github/scripts/ci/classify_changes.sh +++ b/.github/scripts/ci/classify_changes.sh @@ -19,7 +19,7 @@ while IFS= read -r file || [ -n "$file" ]; do esac case "$file" in *.md | *.mdx) : ;; - pyproject.toml | */pyproject.toml | uv.lock | uv.toml | .python-version | rust-toolchain.toml | litellm/rust/* | litellm/__init__.py | litellm/proxy/proxy_server.py | litellm/*mcp* | tests/*mcp* | litellm/integrations/arize/* | tests/base_sdk_tests/* | scripts/check_mcp_sdk_install.py | .github/workflows/test-mcp-dependency-resolution.yml | .github/actions/detect-changes/* | .github/actions/setup-uv-with-retries/* | .github/actions/cache-cargo-build/* | .github/scripts/detect_changes.sh | .github/scripts/uv_sync_with_retries.sh | .github/scripts/ci/classify_changes.sh | tests/unit/test_ci_path_filter.py | tests/unit/test_detect_changes.py) + pyproject.toml | */pyproject.toml | uv.lock | uv.toml | .python-version | rust-toolchain.toml | litellm/rust/* | litellm/__init__.py | litellm/proxy/proxy_server.py | litellm/*mcp* | tests/*mcp* | litellm/integrations/arize/* | tests/base_sdk_tests/* | litellm/scripts/check_mcp_sdk_install.py | .github/workflows/test-mcp-dependency-resolution.yml | .github/actions/detect-changes/* | .github/actions/setup-uv-with-retries/* | .github/actions/cache-cargo-build/* | .github/scripts/detect_changes.sh | .github/scripts/uv_sync_with_retries.sh | .github/scripts/ci/classify_changes.sh | tests/unit/test_ci_path_filter.py | tests/unit/test_detect_changes.py) has_mcp_dependencies=true ;; esac case "$file" in diff --git a/.github/scripts/verify_linux_native_wheel.py b/.github/scripts/verify_linux_native_wheel.py index 490574083e6..610f55a36b9 100644 --- a/.github/scripts/verify_linux_native_wheel.py +++ b/.github/scripts/verify_linux_native_wheel.py @@ -137,6 +137,9 @@ def main( "litellm/proxy-extras/", "litellm/rust/", "litellm/packaging/", + "litellm/scripts/", + "litellm/runtime/", + "litellm/schema.prisma", ) ) or any(part.endswith(".dSYM") for part in PurePosixPath(member.filename).parts) diff --git a/.github/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml index 9a362b4c85c..a6366ab5269 100644 --- a/.github/workflows/auto-close-duplicates.yml +++ b/.github/workflows/auto-close-duplicates.yml @@ -16,8 +16,8 @@ on: pull_request: paths: - .github/workflows/auto-close-duplicates.yml - - scripts/auto-close-duplicates.ts - - scripts/auto-close-duplicates.test.ts + - litellm/scripts/auto-close-duplicates.ts + - litellm/scripts/auto-close-duplicates.test.ts permissions: {} @@ -40,7 +40,7 @@ jobs: bun-version: "1.4.0" - name: Test the sweep - run: bun test scripts/auto-close-duplicates.test.ts + run: bun test litellm/scripts/auto-close-duplicates.test.ts sweep: if: github.event_name != 'pull_request' && github.repository == 'BerriAI/litellm' @@ -62,7 +62,7 @@ jobs: bun-version: "1.4.0" - name: Close unanswered duplicates, reopen ones the reporter answered - run: bun run scripts/auto-close-duplicates.ts + run: bun run litellm/scripts/auto-close-duplicates.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DRY_RUN: ${{ inputs.dry_run == true }} diff --git a/.github/workflows/check-schema-sync.yml b/.github/workflows/check-schema-sync.yml index 17c69033c25..8b2bd707719 100644 --- a/.github/workflows/check-schema-sync.yml +++ b/.github/workflows/check-schema-sync.yml @@ -3,7 +3,7 @@ name: Check Schema Sync on: pull_request: paths: - - 'schema.prisma' + - 'litellm/schema.prisma' - 'litellm/proxy/schema.prisma' - 'litellm/proxy-extras/litellm_proxy_extras/schema.prisma' @@ -16,7 +16,7 @@ concurrency: jobs: check-sync: - name: Verify schema.prisma copies match root + name: Verify litellm/schema.prisma copies match the canonical schema runs-on: ubuntu-latest timeout-minutes: 5 steps: @@ -27,7 +27,7 @@ jobs: - name: Reject symlinked schema files run: | - for f in schema.prisma litellm/proxy/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma; do + for f in litellm/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 @@ -38,24 +38,24 @@ jobs: 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" + diff litellm/schema.prisma litellm/proxy/schema.prisma || { + echo "::error file=litellm/proxy/schema.prisma::litellm/proxy/schema.prisma differs from root litellm/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" + diff litellm/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 litellm/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 "The root litellm/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" + echo " cp litellm/schema.prisma litellm/proxy/schema.prisma" + echo " cp litellm/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma" exit 1 fi diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index f8d416ca487..bd44c3e3c17 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -3,7 +3,7 @@ name: Conventional PR Title # Squash-merge replaces the merge commit subject with the PR title, so # enforcing Conventional Commits at the PR-title level is what actually gates # the commits that land on the default branch. The local commit-msg hook -# (scripts/githooks/commit-msg) is a best-effort assist; this workflow is the gate. +# (litellm/scripts/githooks/commit-msg) is a best-effort assist; this workflow is the gate. # # See https://www.conventionalcommits.org/en/v1.0.0/ @@ -28,7 +28,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - # Must mirror the type list in scripts/githooks/commit-msg. + # Must mirror the type list in litellm/scripts/githooks/commit-msg. types: | feat fix diff --git a/.github/workflows/cost-map-guard.yml b/.github/workflows/cost-map-guard.yml index 672e91accec..90c8d5a3f9e 100644 --- a/.github/workflows/cost-map-guard.yml +++ b/.github/workflows/cost-map-guard.yml @@ -49,8 +49,11 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | - guard=scripts/ci_cd/cost_map_guard.py + guard=litellm/scripts/ci_cd/cost_map_guard.py # TODO: Drop the legacy path after this lands on main. + if [ ! -f "$guard" ]; then + guard=scripts/ci_cd/cost_map_guard.py + fi if [ ! -f "$guard" ]; then guard=ci_cd/cost_map_guard.py fi diff --git a/.github/workflows/cost-map-pins-recheck.yml b/.github/workflows/cost-map-pins-recheck.yml index 98c3371bcf2..34482a842ee 100644 --- a/.github/workflows/cost-map-pins-recheck.yml +++ b/.github/workflows/cost-map-pins-recheck.yml @@ -8,7 +8,7 @@ on: paths: - model_prices_and_context_window.json - litellm/model_prices_and_context_window_backup.json - - scripts/ci_cd/cost_map_pins.json + - litellm/scripts/ci_cd/cost_map_pins.json permissions: {} @@ -28,8 +28,8 @@ jobs: BEFORE_SHA: ${{ github.event.before }} AFTER_SHA: ${{ github.event.after }} FORCED: ${{ github.event.forced }} - COST_MAP_PATHS: '["model_prices_and_context_window.json", "litellm/model_prices_and_context_window_backup.json", "scripts/ci_cd/cost_map_pins.json"]' - PINS_PATHS: '["scripts/ci_cd/cost_map_pins.json"]' + COST_MAP_PATHS: '["model_prices_and_context_window.json", "litellm/model_prices_and_context_window_backup.json", "litellm/scripts/ci_cd/cost_map_pins.json"]' + PINS_PATHS: '["litellm/scripts/ci_cd/cost_map_pins.json"]' run: | if [[ "$BEFORE_SHA" =~ ^0+$ || "$FORCED" == true ]] \ || ! gh api "repos/${GITHUB_REPOSITORY}/compare/${BEFORE_SHA}...${AFTER_SHA}?per_page=1" \ diff --git a/.github/workflows/duplicate_issue_check.yml b/.github/workflows/duplicate_issue_check.yml index 91eee3ca383..0c4de0c3919 100644 --- a/.github/workflows/duplicate_issue_check.yml +++ b/.github/workflows/duplicate_issue_check.yml @@ -13,9 +13,9 @@ on: - .github/workflows/duplicate_issue_check.yml - .github/prompts/duplicate-issue-check.md - .github/prompts/duplicate-issue-check.schema.json - - scripts/flag-duplicate-issue.ts - - scripts/flag-duplicate-issue.test.ts - - scripts/auto-close-duplicates.ts + - litellm/scripts/flag-duplicate-issue.ts + - litellm/scripts/flag-duplicate-issue.test.ts + - litellm/scripts/auto-close-duplicates.ts permissions: {} @@ -38,7 +38,7 @@ jobs: bun-version: "1.4.0" - name: Test the flag step - run: bun test scripts/flag-duplicate-issue.test.ts + run: bun test litellm/scripts/flag-duplicate-issue.test.ts classify: if: github.event_name != 'pull_request' && github.repository == 'BerriAI/litellm' @@ -134,7 +134,7 @@ jobs: bun-version: "1.4.0" - name: Comment and label - run: bun run scripts/flag-duplicate-issue.ts + run: bun run litellm/scripts/flag-duplicate-issue.ts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERDICT: ${{ needs.classify.outputs.verdict }} diff --git a/.github/workflows/image-scan.yml b/.github/workflows/image-scan.yml index e3b8de27b2a..4af242b7d6b 100644 --- a/.github/workflows/image-scan.yml +++ b/.github/workflows/image-scan.yml @@ -10,13 +10,13 @@ on: - litellm/deploy/docker/Dockerfile - litellm/deploy/docker/Dockerfile.dockerignore - litellm/deploy/docker/docker-entrypoint.sh - - migrations/run.py + - litellm/runtime/migrations/run.py - gateway/main.py - gateway/launch.py - gateway/routes/allowlist.py - litellm/cli/** - - backend/main.py - - backend/routes/allowlist.py + - litellm/runtime/backend/main.py + - litellm/runtime/backend/routes/allowlist.py - litellm/rust/** - litellm/proxy/lens/** - litellm/proxy/prisma_migration.py diff --git a/.github/workflows/issue_classifier.yml b/.github/workflows/issue_classifier.yml index 842e4c40b5e..ccb8256609e 100644 --- a/.github/workflows/issue_classifier.yml +++ b/.github/workflows/issue_classifier.yml @@ -16,12 +16,12 @@ on: - .github/issue-labels.json - .github/ISSUE_TEMPLATE/bug_report.yml - .github/ISSUE_TEMPLATE/feature_request.yml - - scripts/classify-issue.ts - - scripts/classify-issue.test.ts - - scripts/label-issue.ts - - scripts/label-issue.test.ts - - scripts/issue-labels.ts - - scripts/auto-close-duplicates.ts + - litellm/scripts/classify-issue.ts + - litellm/scripts/classify-issue.test.ts + - litellm/scripts/label-issue.ts + - litellm/scripts/label-issue.test.ts + - litellm/scripts/issue-labels.ts + - litellm/scripts/auto-close-duplicates.ts permissions: {} @@ -49,7 +49,7 @@ jobs: bun-version: "1.4.0" - name: Test the gate, the validation and the label step - run: bun test scripts/classify-issue.test.ts scripts/label-issue.test.ts + run: bun test litellm/scripts/classify-issue.test.ts litellm/scripts/label-issue.test.ts classify-issue: # An edit to a labelled issue is dropped here; the script decides the rest against the live labels @@ -108,7 +108,7 @@ jobs: ISSUE_CLASSIFIER_MODEL: ${{ vars.ISSUE_CLASSIFIER_MODEL }} run: | set -euo pipefail - bun run scripts/classify-issue.ts > classification.json + bun run litellm/scripts/classify-issue.ts > classification.json { echo 'verdict< /dev/null 2>&1 uv run --no-sync --with mutmut==3.5.0 mutmut results > mutmut-results.txt 2>&1 - uv run --no-sync python scripts/mutation_report.py + uv run --no-sync python litellm/scripts/mutation_report.py # The full report can be very long for big test files; the run-page # summary cuts off at 1 MB. Append the head of the report (summary # + survivor list) and link out to the artifact for the full body. diff --git a/.github/workflows/publish-lint-base-counts.yml b/.github/workflows/publish-lint-base-counts.yml index bbe9cadc893..b0a297932f7 100644 --- a/.github/workflows/publish-lint-base-counts.yml +++ b/.github/workflows/publish-lint-base-counts.yml @@ -3,7 +3,7 @@ name: Publish lint base counts # Every commit on main can become a future merge-base. # Publishing its per-rule counts for each lint gate (strict ruff, type discipline, # test quality, basedpyright) as an artifact lets the gates download them through -# scripts/lint_base_counts.py in seconds instead of scanning the merge-base tree in +# litellm/scripts/lint_base_counts.py in seconds instead of scanning the merge-base tree in # a throwaway worktree on every fresh checkout or moved merge-base. # No concurrency group on purpose: runs must never cancel each other, because # every sha's artifact matters (any of them can become a merge-base). @@ -30,15 +30,15 @@ jobs: matrix: include: - checker: ruff-strict - script: scripts/ruff_strict_gate.py + script: litellm/scripts/ruff_strict_gate.py - checker: type-discipline - script: scripts/type_discipline_gate.py + script: litellm/scripts/type_discipline_gate.py - checker: test-quality - script: scripts/test_quality_gate.py + script: litellm/scripts/test_quality_gate.py - checker: openapi-docs - script: scripts/openapi_docs_gate.py + script: litellm/scripts/openapi_docs_gate.py - checker: basedpyright - script: scripts/type_check_gate.py + script: litellm/scripts/type_check_gate.py steps: - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 @@ -91,7 +91,7 @@ jobs: - name: Emit basedpyright counts for HEAD if: matrix.checker == 'basedpyright' run: | - python scripts/type_check_gate.py --emit-counts-dir "$RUNNER_TEMP/lint-counts" + python litellm/scripts/type_check_gate.py --emit-counts-dir "$RUNNER_TEMP/lint-counts" - name: Name the counts artifact run: | diff --git a/.github/workflows/spend-logs-guard.yml b/.github/workflows/spend-logs-guard.yml index 26523450013..9405c37b9c4 100644 --- a/.github/workflows/spend-logs-guard.yml +++ b/.github/workflows/spend-logs-guard.yml @@ -42,8 +42,11 @@ jobs: MERGE_BASE: ${{ steps.revisions.outputs.merge_base }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - guard=scripts/ci_cd/spend_logs_guard.py + guard=litellm/scripts/ci_cd/spend_logs_guard.py # TODO: Drop the legacy path after this lands on main. + if [ ! -f "$guard" ]; then + guard=scripts/ci_cd/spend_logs_guard.py + fi if [ ! -f "$guard" ]; then guard=ci_cd/spend_logs_guard.py fi diff --git a/.github/workflows/sync-schema.yml b/.github/workflows/sync-schema.yml index 597ff592ee6..3a83444cb97 100644 --- a/.github/workflows/sync-schema.yml +++ b/.github/workflows/sync-schema.yml @@ -1,9 +1,9 @@ -name: Sync schema.prisma copies +name: Sync litellm/schema.prisma copies on: pull_request: paths: - - 'schema.prisma' + - 'litellm/schema.prisma' # Scoped to ONLY the permissions needed: # - contents:write to push the sync commit to the PR branch @@ -37,7 +37,7 @@ jobs: - name: Reject symlinked schema files run: | - for f in schema.prisma litellm/proxy/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma; do + for f in litellm/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 @@ -46,8 +46,8 @@ jobs: - 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 + cp litellm/schema.prisma litellm/proxy/schema.prisma + cp litellm/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma - name: Check for changes id: diff @@ -69,5 +69,5 @@ jobs: 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 commit -m "chore: sync litellm/schema.prisma copies from the canonical schema" git push origin "HEAD:$GITHUB_HEAD_REF" diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index 80502cc4c5f..d4e6b8eae6c 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -114,7 +114,7 @@ jobs: - name: Check ruff format if: steps.changes.outputs.decision != 'skip' run: | - git diff --name-only --diff-filter=ACMR "$GATE_BASE_SHA" HEAD -- ':(glob)litellm/**/*.py' | grep -Ev '^litellm/(enterprise|ui|vscode-extension|rust|proxy-extras|packaging|cli)/' > "$RUNNER_TEMP/ruff_format_files.txt" || true + git diff --name-only --diff-filter=ACMR "$GATE_BASE_SHA" HEAD -- ':(glob)litellm/**/*.py' | grep -Ev '^litellm/(enterprise|ui|vscode-extension|rust|proxy-extras|packaging|cli|scripts|runtime)/' > "$RUNNER_TEMP/ruff_format_files.txt" || true if [ ! -s "$RUNNER_TEMP/ruff_format_files.txt" ]; then echo "No changed litellm Python files to check with ruff format." exit 0 @@ -133,7 +133,7 @@ jobs: - name: Check MCP operation boundary if: steps.changes.outputs.decision != 'skip' - run: uv run --no-sync python scripts/check_mcp_operation_boundary.py + run: uv run --no-sync python litellm/scripts/check_mcp_operation_boundary.py - name: Run Ruff linting if: steps.changes.outputs.decision != 'skip' @@ -152,21 +152,21 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - uv run --no-sync python scripts/ruff_strict_gate.py --base "$GATE_BASE_SHA" + uv run --no-sync python litellm/scripts/ruff_strict_gate.py --base "$GATE_BASE_SHA" - name: Check type discipline (mutable collections / casts / type guards / kwargs / unexplained suppressions, delta vs merge-base counts) if: steps.changes.outputs.decision != 'skip' env: GH_TOKEN: ${{ github.token }} run: | - uv run --no-sync python scripts/type_discipline_gate.py --base "$GATE_BASE_SHA" + uv run --no-sync python litellm/scripts/type_discipline_gate.py --base "$GATE_BASE_SHA" - name: Check test quality (zero-assert / mock-echo tests, sys.path.insert, raw env writes, litellm global mutation, credential-gated skips, conftest snapshot inventory, delta vs merge-base counts) if: steps.changes.outputs.decision != 'skip' env: GH_TOKEN: ${{ github.token }} run: | - uv run --no-sync python scripts/test_quality_gate.py --base "$GATE_BASE_SHA" + uv run --no-sync python litellm/scripts/test_quality_gate.py --base "$GATE_BASE_SHA" - name: Check OpenAPI docs (POST routes without a request schema, description or 200 schema, delta vs merge-base counts) if: steps.changes.outputs.decision != 'skip' @@ -174,7 +174,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | uv sync --frozen --group proxy-dev --group e2e-dev --extra proxy - uv run --no-sync python scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA" + uv run --no-sync python litellm/scripts/openapi_docs_gate.py --base "$GATE_BASE_SHA" - name: Print OpenAI version if: steps.changes.outputs.decision != 'skip' @@ -186,7 +186,7 @@ jobs: env: GH_TOKEN: ${{ github.token }} run: | - uv run --no-sync python scripts/type_check_gate.py --base "$GATE_BASE_SHA" + uv run --no-sync python litellm/scripts/type_check_gate.py --base "$GATE_BASE_SHA" - name: Check tests/e2e basedpyright (zero errors) if: steps.changes.outputs.decision != 'skip' @@ -375,7 +375,7 @@ jobs: exit 0 fi files="$(git diff --name-only "$base" HEAD)" - if grep -Ev '^litellm/(ui|vscode-extension|rust|proxy-extras|packaging|cli)/' <<< "$files" | grep -E '^(litellm/|enterprise/|(pyproject\.toml|uv\.lock)$|\.github/workflows/test-linting\.yml$)' >/dev/null || grep -Eq '^litellm/ui/litellm-dashboard/(src/lib/http/schema\.d\.ts|scripts/gen-api-types\.mjs|package(-lock)?\.json)$' <<< "$files"; then + if grep -Ev '^litellm/(ui|vscode-extension|rust|proxy-extras|packaging|cli|scripts|runtime)/' <<< "$files" | grep -E '^(litellm/|enterprise/|(pyproject\.toml|uv\.lock)$|\.github/workflows/test-linting\.yml$)' >/dev/null || grep -Eq '^litellm/ui/litellm-dashboard/(src/lib/http/schema\.d\.ts|scripts/gen-api-types\.mjs|package(-lock)?\.json)$' <<< "$files"; then echo "relevant=true" >> "$GITHUB_OUTPUT" else echo "No litellm, enterprise, dependency or generator changes in this pull request, nothing to verify." @@ -524,7 +524,7 @@ jobs: run: uv run --no-sync python ./tests/code_coverage_tests/check_provider_folders_documented.py - name: check_provider_endpoints_support_generated - run: uv run --no-sync python scripts/generate_provider_endpoints_support.py --check + run: uv run --no-sync python litellm/scripts/generate_provider_endpoints_support.py --check - name: test_generate_provider_endpoints_support run: uv run --no-sync pytest -q --noconftest -p no:cacheprovider -c /dev/null tests/code_coverage_tests/test_generate_provider_endpoints_support.py diff --git a/.github/workflows/test-mcp-dependency-resolution.yml b/.github/workflows/test-mcp-dependency-resolution.yml index 0305ed7939e..49bd5719a2b 100644 --- a/.github/workflows/test-mcp-dependency-resolution.yml +++ b/.github/workflows/test-mcp-dependency-resolution.yml @@ -15,12 +15,12 @@ on: - "litellm/__init__.py" - "litellm/_version.py" - "litellm/_lazy_imports_registry.py" - - "scripts/build_core_distribution.py" + - "litellm/scripts/build_core_distribution.py" - "litellm/proxy/proxy_server.py" - "litellm/**/*mcp*" - "litellm/**/*mcp*/**" - "litellm/integrations/arize/**" - - "scripts/check_mcp_sdk_install.py" + - "litellm/scripts/check_mcp_sdk_install.py" - "tests/base_sdk_tests/**" - ".github/workflows/test-mcp-dependency-resolution.yml" - ".github/actions/detect-changes/**" @@ -91,7 +91,7 @@ jobs: checker=("$GITHUB_WORKSPACE/tests/base_sdk_tests/check_base_sdk_install.py") (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/.venv-$extra/bin/python" -I "$GITHUB_WORKSPACE/tests/base_sdk_tests/check_sdk_http.py") else - checker=("$GITHUB_WORKSPACE/scripts/check_mcp_sdk_install.py" --extra "$extra") + checker=("$GITHUB_WORKSPACE/litellm/scripts/check_mcp_sdk_install.py" --extra "$extra") fi (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/.venv-$extra/bin/python" "${checker[@]}") done @@ -115,7 +115,7 @@ jobs: if [ "$extra" = core ]; then checker=("$GITHUB_WORKSPACE/tests/base_sdk_tests/check_base_sdk_install.py") else - checker=("$GITHUB_WORKSPACE/scripts/check_mcp_sdk_install.py" --extra "$extra") + checker=("$GITHUB_WORKSPACE/litellm/scripts/check_mcp_sdk_install.py" --extra "$extra") fi (cd "$RUNNER_TEMP" && "$GITHUB_WORKSPACE/.venv-lowest-$extra/bin/python" "${checker[@]}") done @@ -147,15 +147,15 @@ jobs: - name: Build the core wheel and source distribution run: >- uv run --no-project --with coverage==7.14.0 python -m coverage run --rcfile=/dev/null - --branch --include="*/scripts/build_core_distribution.py" - scripts/build_core_distribution.py --out-dir dist/core + --branch --include="*/litellm/scripts/build_core_distribution.py" + litellm/scripts/build_core_distribution.py --out-dir dist/core - name: Verify metadata, resources, and independent source rebuild env: CORE_DISTRIBUTION_DIR: ${{ github.workspace }}/dist/core run: >- uv run --no-project --with pytest==9.0.3 --with pytest-cov==5.0.0 --with coverage==7.14.0 --with 'tomli==2.4.1; python_version < "3.11"' python -m pytest tests/base_sdk_tests/test_core_distribution.py -v - --cov-config=/dev/null --cov=scripts.build_core_distribution --cov-branch --cov-append --cov-report= + --cov-config=/dev/null --cov=litellm.scripts.build_core_distribution --cov-branch --cov-append --cov-report= - name: Verify isolated core installations run: | wheel=$(realpath dist/core/litellm_core-*.whl) diff --git a/.github/workflows/test-merge-smoke.yml b/.github/workflows/test-merge-smoke.yml index 1ac7c902aee..806e2d5f84c 100644 --- a/.github/workflows/test-merge-smoke.yml +++ b/.github/workflows/test-merge-smoke.yml @@ -229,7 +229,7 @@ jobs: "prisma==0.11.0" \ "openai==2.24.0" export PATH="/tmp/litellm-pip/bin:$PATH" - /tmp/litellm-pip/bin/prisma generate --schema=schema.prisma + /tmp/litellm-pip/bin/prisma generate --schema=litellm/schema.prisma - name: Start the fake OpenAI endpoint run: | diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 1e60b301ad1..386685f9c13 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -6,9 +6,9 @@ on: - "litellm/rust/**" - "model_prices_and_context_window.json" - "litellm/rust_bridge/**" - - "scripts/generate_trace_types.py" - - "scripts/trace_codegen/**" - - "scripts/lens_assets/**" + - "litellm/scripts/generate_trace_types.py" + - "litellm/scripts/trace_codegen/**" + - "litellm/scripts/lens_assets/**" - "litellm/tracing/**" - "tests/test_litellm_rust/**" - "litellm/integrations/custom_logger.py" @@ -41,9 +41,9 @@ on: - "litellm/rust/**" - "model_prices_and_context_window.json" - "litellm/rust_bridge/**" - - "scripts/generate_trace_types.py" - - "scripts/trace_codegen/**" - - "scripts/lens_assets/**" + - "litellm/scripts/generate_trace_types.py" + - "litellm/scripts/trace_codegen/**" + - "litellm/scripts/lens_assets/**" - "litellm/tracing/**" - "tests/test_litellm_rust/**" - "litellm/integrations/custom_logger.py" @@ -160,7 +160,7 @@ jobs: - name: Check generated trace contracts working-directory: . - run: uv run scripts/generate_trace_types.py --check + run: uv run litellm/scripts/generate_trace_types.py --check - run: cargo nextest run --workspace --locked diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 5fdc6360334..53ef8dd1e91 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -1043,7 +1043,7 @@ jobs: fi uv run --no-sync python -c 'import os, sys; print(sys.version); assert f"{sys.version_info.major}.{sys.version_info.minor}" == os.environ["UV_PYTHON"]' - - name: Check schema.prisma against the merge base + - name: Check litellm/schema.prisma against the merge base if: steps.changes.outputs.decision != 'skip' && matrix.shard == 'db-and-spend' && github.event_name == 'pull_request' timeout-minutes: 3 env: @@ -1056,7 +1056,7 @@ jobs: merge_base=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${base_ref}...${HEAD_SHA}?per_page=1" --jq '.merge_base_commit.sha') test -n "$merge_base" git fetch --no-tags --depth=1 origin "$merge_base" - uv run --no-sync python scripts/check_schema_compatibility.py --base-commit "$merge_base" + uv run --no-sync python litellm/scripts/check_schema_compatibility.py --base-commit "$merge_base" - name: Cache Prisma binaries if: steps.changes.outputs.decision != 'skip' diff --git a/.gitignore b/.gitignore index e7785febd94..0e20c40bc7f 100644 --- a/.gitignore +++ b/.gitignore @@ -106,7 +106,7 @@ litellm_config.yaml litellm/proxy/to_delete_loadtest_work/* update_model_cost_map.py tests/unit/proxy/mcp_server/test_mcp_server_manager.py -scripts/test_vertex_ai_search.py +litellm/scripts/test_vertex_ai_search.py LAZY_LOADING_IMPROVEMENTS.md STABILIZATION_TODO.md **/test-results diff --git a/AGENTS.md b/AGENTS.md index 77a3c065c42..6f33418e79f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ A test must only fail when litellm code changes. Never pin facts we don't own (a End-to-end tests belong in `tests/e2e/`. A rule about one directory's files, like its harness or which CI runs it, goes in that directory's `AGENTS.md`, never only in a README or here. Read a directory's `AGENTS.md` before editing files in it -When creating PRs, target the repository's current default branch for both internal and external / OSS contributions. Check it with `python3 scripts/default_branch.py --branch` instead of assuming a branch name or relying on cached `origin/HEAD` +When creating PRs, target the repository's current default branch for both internal and external / OSS contributions. Check it with `python3 litellm/scripts/default_branch.py --branch` instead of assuming a branch name or relying on cached `origin/HEAD` When writing a PR body, treat the comments and imperative instructions inside .github/PULL_REQUEST_TEMPLATE/general.md as rules to follow, not just layout. Agent harnesses may strip HTML comments from copies of that file injected into context, so read .github/PULL_REQUEST_TEMPLATE/general.md from disk before writing a PR body to make sure you see every comment rule. A section you have nothing to put in (Relevant issues, Affected release, Linear ticket, Caveats, QA runbook, and so on) is removed entirely, heading included, never left as an empty title @@ -56,11 +56,11 @@ Don't hesitate to use values in .env to get needed API keys and other secrets, a Python max line length is 120, not 88 -The five lint gates (`scripts/ruff_strict_gate.py`, `scripts/type_discipline_gate.py`, `scripts/test_quality_gate.py`, `scripts/openapi_docs_gate.py`, `scripts/type_check_gate.py`) compare each rule's codebase count on your branch against the count at its merge-base with the default branch, and a rule may not grow. There are no budget files to edit or ratchet: when a gate fails, fix the violations the branch introduced or remove at least as many of that rule elsewhere in the tree. The one exception is `reportAny` / `reportExplicitAny`, which share a fixed codebase-wide cap in `ANY_CAPS` in `scripts/type_check_gate.py` because Any spreads past the lines you touch. A branch may add Anys while the total stays under that cap. Only lower `ANY_CAPS`, and only in its own PR to the default branch, never raise it on a feature branch +The five lint gates (`litellm/scripts/ruff_strict_gate.py`, `litellm/scripts/type_discipline_gate.py`, `litellm/scripts/test_quality_gate.py`, `litellm/scripts/openapi_docs_gate.py`, `litellm/scripts/type_check_gate.py`) compare each rule's codebase count on your branch against the count at its merge-base with the default branch, and a rule may not grow. There are no budget files to edit or ratchet: when a gate fails, fix the violations the branch introduced or remove at least as many of that rule elsewhere in the tree. The one exception is `reportAny` / `reportExplicitAny`, which share a fixed codebase-wide cap in `ANY_CAPS` in `litellm/scripts/type_check_gate.py` because Any spreads past the lines you touch. A branch may add Anys while the total stays under that cap. Only lower `ANY_CAPS`, and only in its own PR to the default branch, never raise it on a feature branch `make check` (f.k.a. `make pre-commit`, which still works identically as an alias) saves its complete output to a log file in .git (overwriting previous logs) and prints that path as its first and last output lines. To inspect a run, read or grep that log instead of re-running the multi-minute checks just to see a different slice -`make check`, `make lint`, `scripts/pre_commit_lint.sh`, and the standalone lint gates (`scripts/ruff_strict_gate.py`, `scripts/type_discipline_gate.py`, `scripts/type_check_gate.py`) each hold one of 2 machine-wide slots, so when other sessions or worktrees on the same box are already running heavy work, yours prints "all N machine-wide slots are busy" followed by "queueing" and then stays quiet until a slot frees. Give the command a long timeout and let it wait rather than killing it, retrying it, or assuming it hung. Don't change the # of machine-wide slots or make it unlimited by setting `LITELLM_GATE_SLOTS=0` +`make check`, `make lint`, `litellm/scripts/pre_commit_lint.sh`, and the standalone lint gates (`litellm/scripts/ruff_strict_gate.py`, `litellm/scripts/type_discipline_gate.py`, `litellm/scripts/type_check_gate.py`) each hold one of 2 machine-wide slots, so when other sessions or worktrees on the same box are already running heavy work, yours prints "all N machine-wide slots are busy" followed by "queueing" and then stays quiet until a slot frees. Give the command a long timeout and let it wait rather than killing it, retrying it, or assuming it hung. Don't change the # of machine-wide slots or make it unlimited by setting `LITELLM_GATE_SLOTS=0` If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and pushing `reportAny` / `reportExplicitAny` closer to their cap in `ANY_CAPS`, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in diff --git a/Makefile b/Makefile index d30a9ab8293..8ce85098dd4 100644 --- a/Makefile +++ b/Makefile @@ -63,11 +63,11 @@ UV := uv UV_RUN := $(UV) run --no-sync BASE_REF ?= export BASE_REF -RESOLVE_BASE = python3 scripts/default_branch.py --base "$(BASE_REF)" +RESOLVE_BASE = python3 litellm/scripts/default_branch.py --base "$(BASE_REF)" # Machine-wide slot queue for the heavy targets below; python3 + stdlib only, so -# it runs before any venv exists. See scripts/gate_slot_lock.py. -GATE_SLOT_LOCK := python3 scripts/gate_slot_lock.py +# it runs before any venv exists. See litellm/scripts/gate_slot_lock.py. +GATE_SLOT_LOCK := python3 litellm/scripts/gate_slot_lock.py LINT_DEP_INSTALL ?= install-dev LINT_E2E_DEP_INSTALL ?= lint-install @@ -90,8 +90,8 @@ install-dev: # machine-wide slots the CPU-bound gates below share. bootstrap: $(UV) sync --inexact --frozen --extra proxy --group proxy-dev --group e2e-dev - $(UV_RUN) python scripts/prisma_generate_if_needed.py - cd litellm/ui/litellm-dashboard && ../../../scripts/with_dashboard_node.sh npm install --no-audit --no-fund + $(UV_RUN) python litellm/scripts/prisma_generate_if_needed.py + cd litellm/ui/litellm-dashboard && ../../scripts/with_dashboard_node.sh npm install --no-audit --no-fund @main_root=$$(git worktree list --porcelain | head -1 | sed 's/^worktree //'); \ if [ "$$main_root" != "$$(git rev-parse --show-toplevel)" ] && [ -f "$$main_root/.env" ] && [ ! -f .env ]; then \ cp "$$main_root/.env" .env && echo "bootstrap: copied .env from $$main_root"; \ @@ -123,7 +123,7 @@ install-helm-unittest: # Install git hooks that enforce Conventional Commits. # Opt-in: not chained into install-dev. install-hooks: - ./scripts/install_git_hooks.sh + ./litellm/scripts/install_git_hooks.sh # Formatting # Wrap width is pyproject.toml's single source of truth (line-length = 120), shared by the @@ -139,12 +139,12 @@ lint-fetch-base: # Mirror test-linting.yml's python job environment: the proxy-dev group plus a generated # Prisma client, so `basedpyright tests/e2e` resolves the same modules CI does. The -# basedpyright gate itself no longer measures here (scripts/type_check_gate.py provisions its +# basedpyright gate itself no longer measures here (litellm/scripts/type_check_gate.py provisions its # own .venv-typecheck). --inexact tops up the venv instead of pruning the proxy extras # gen:api and the running proxy need. lint-install: $(UV) sync --inexact --frozen --group proxy-dev --group e2e-dev - $(UV_RUN) python scripts/prisma_generate_if_needed.py + $(UV_RUN) python litellm/scripts/prisma_generate_if_needed.py # Diff-scoped format check, mirroring test-linting.yml's "Check ruff format" step: # only the litellm Python files changed vs the base are checked, so a pre-existing @@ -163,7 +163,7 @@ lint-format-check-changed: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) # Linting targets lint-ruff: $(LINT_DEP_INSTALL) - $(UV_RUN) python scripts/check_mcp_operation_boundary.py + $(UV_RUN) python litellm/scripts/check_mcp_operation_boundary.py cd litellm && $(UV_RUN) ruff check . && cd .. $(UV_RUN) ruff check --config .github/linters/ruff-tests.toml tests @@ -204,7 +204,7 @@ lint-ruff-FULL-dev: install-dev else echo "No changed .py files to check."; fi lint-basedpyright: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) - $(UV_RUN) python scripts/type_check_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/type_check_gate.py --base "$(BASE_REF)" lint-e2e-basedpyright: $(LINT_E2E_DEP_INSTALL) $(UV_RUN) basedpyright tests/e2e tests/e2e_harness @@ -212,29 +212,29 @@ lint-e2e-basedpyright: $(LINT_E2E_DEP_INSTALL) # Type-discipline gate (mutable collections / casts / type guards / kwargs / # unexplained suppressions), the test-linting.yml step `make lint` used to omit. lint-type-discipline: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) - $(UV_RUN) python scripts/type_discipline_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/type_discipline_gate.py --base "$(BASE_REF)" # Test-quality gate (zero-assert / mock-echo tests, sys.path.insert, raw env writes, # litellm module-global mutation, credential-gated skips, conftest snapshot # inventory), counted across tests/ the same delta-vs-base way. lint-test-quality: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) - $(UV_RUN) python scripts/test_quality_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/test_quality_gate.py --base "$(BASE_REF)" # OpenAPI docs gate (POST routes without a request body schema, a description or a # 200 schema in app.openapi()), counted the same delta-vs-base way so a new endpoint # has to show up documented in Swagger UI. lint-openapi-docs: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) - $(UV_RUN) python scripts/openapi_docs_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/openapi_docs_gate.py --base "$(BASE_REF)" lint-format: format-check lint-ruff-strict: install-dev - $(UV_RUN) python scripts/ruff_strict_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/ruff_strict_gate.py --base "$(BASE_REF)" # Strict gate, invoked the same way CI does in test-linting.yml so a local pass # means the CI check will pass too. lint-gate: $(LINT_DEP_INSTALL) $(LINT_DEP_BASE) - $(UV_RUN) python scripts/ruff_strict_gate.py --base "$(BASE_REF)" + $(UV_RUN) python litellm/scripts/ruff_strict_gate.py --base "$(BASE_REF)" check-circular-imports: $(LINT_DEP_INSTALL) cd litellm && $(UV_RUN) python ../tests/documentation_tests/test_circular_imports.py && cd .. @@ -272,7 +272,7 @@ check: @$(GATE_SLOT_LOCK) $(MAKE) check-inner check-inner: bootstrap - ./scripts/pre_commit_lint.sh + ./litellm/scripts/pre_commit_lint.sh pre-commit: @echo "make pre-commit is a legacy alias; use make check" >&2 diff --git a/README.md b/README.md index e0263e8c337..abe9b1b4b18 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ proxy, CLI, and optional extras Build and install core from this checkout while its release integration is pending: ```shell -python scripts/build_core_distribution.py --out-dir dist/core +python litellm/scripts/build_core_distribution.py --out-dir dist/core python -m pip install dist/core/litellm_core-*.whl ``` diff --git a/litellm/deploy/docker/Dockerfile b/litellm/deploy/docker/Dockerfile index ad41c7c0743..ac13b66533f 100644 --- a/litellm/deploy/docker/Dockerfile +++ b/litellm/deploy/docker/Dockerfile @@ -115,11 +115,11 @@ RUN uv sync --frozen --no-install-package litellm-proxy --no-default-groups --no RUN uv pip install --python /app/.venv/bin/python --no-deps /wheels/litellm_proxy-*.whl -RUN cp -r litellm/enterprise "$(.venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))')/litellm/enterprise" +RUN cp -r litellm/enterprise litellm/runtime "$(.venv/bin/python -c 'import sysconfig; print(sysconfig.get_path("purelib"))')/litellm/" RUN HOME=/opt/prisma XDG_CACHE_HOME=/opt/prisma/.cache PRISMA_BINARY_CACHE_DIR=/opt/prisma/binaries \ npm_config_cache=/root/.npm \ - prisma generate --schema=./schema.prisma + prisma generate --schema=litellm/schema.prisma RUN sed -i 's/\r$//' litellm/deploy/docker/docker-entrypoint.sh && chmod +x litellm/deploy/docker/docker-entrypoint.sh @@ -180,14 +180,12 @@ ENV PATH="/app/.venv/bin:${PATH}" \ COPY --from=builder /app/.venv /app/.venv COPY --from=liteadmin-builder /opt/liteadmin /opt/liteadmin COPY --from=builder /app/litellm/deploy/docker/docker-entrypoint.sh /app/docker-entrypoint.sh -COPY --from=builder /app/schema.prisma /app/schema.prisma +COPY --from=builder /app/litellm/schema.prisma /app/litellm/schema.prisma +COPY --from=builder /app/gateway /app/gateway COPY --from=builder /app/litellm/proxy/prisma_migration.py /app/litellm/proxy/prisma_migration.py # enterprise/ is imported by source path at runtime (proxy_cli puts the # working directory on sys.path; litellm/proxy/hooks resolves # litellm.enterprise.enterprise_hooks from it) -COPY --from=builder /app/gateway /app/gateway -COPY --from=builder /app/backend /app/backend -COPY --from=builder /app/migrations /app/migrations COPY --from=builder /opt/prisma /opt/prisma COPY --from=builder /var/lib/litellm /var/lib/litellm COPY litellm/ui/nginx.conf /etc/nginx/nginx.conf @@ -205,7 +203,7 @@ RUN find /app/.venv -depth -type d -path "*/tornado/test" -exec rm -rf {} + && \ ls /opt/prisma/binaries/node_modules/@prisma/engines/query-engine-* >/dev/null && \ python -c "from prisma.client import BINARY_PATHS; paths = list(BINARY_PATHS.query_engine.values()); assert paths and all(p.startswith('/opt/prisma/') for p in paths), paths" && \ python -c "from litellm.rust_bridge.loader import native_bridge_available; assert native_bridge_available()" && \ - python -c "import gateway.launch, backend.main" + python -c "import gateway.launch, litellm.runtime.backend.main" USER 65532:65532 diff --git a/litellm/deploy/docker/README.md b/litellm/deploy/docker/README.md index 9471fa8d992..a92835ffac4 100644 --- a/litellm/deploy/docker/README.md +++ b/litellm/deploy/docker/README.md @@ -30,9 +30,9 @@ Every LiteLLM container runs the same image. The first word of the container com |--------------|-----------------------------------------------------|------| | `proxy` | `litellm ...` (everything in one process, default) | 4000 | | `gateway` | `python -m gateway.launch ...` (inference routes) | 4000 | -| `backend` | `uvicorn backend.main:app ...` (management routes) | 4001 | +| `backend` | `uvicorn litellm.runtime.backend.main:app ...` (management routes) | 4001 | | `ui` | nginx serving the static admin UI | 3000 | -| `migrations` | `python migrations/run.py`, `prisma migrate deploy` then exit | | +| `migrations` | `python -m litellm.runtime.migrations.run`, `prisma migrate deploy` then exit | | | `metrics` | `python -m litellm.proxy.prometheus_metrics_server ...` | `--port` | | `collector` | `python -m litellm.proxy.collector ...` | | diff --git a/litellm/deploy/docker/docker-entrypoint.sh b/litellm/deploy/docker/docker-entrypoint.sh index 49ba2b38c78..360d1d1568b 100755 --- a/litellm/deploy/docker/docker-entrypoint.sh +++ b/litellm/deploy/docker/docker-entrypoint.sh @@ -8,9 +8,9 @@ # # proxy litellm ARGS everything in one process (default) # gateway python -m gateway.launch ARGS inference routes, 0.0.0.0:4000 -# backend uvicorn backend.main:app ARGS management routes, 0.0.0.0:4001 +# backend uvicorn litellm.runtime.backend.main:app ARGS management routes, 0.0.0.0:4001 # ui nginx serving the admin UI port 3000 -# migrations python migrations/run.py prisma migrate deploy, then exit +# migrations python litellm/runtime/migrations/run.py prisma migrate deploy, then exit # metrics python -m litellm.proxy.prometheus_metrics_server ARGS # collector python -m litellm.proxy.collector ARGS # --admin-agent the bundled LiteAdmin Slack agent (litellm-admin-agent --web ARGS) @@ -88,13 +88,13 @@ os.execvp("litellm", ["litellm", *sys.argv[1:]])' "$@" set -- python -m gateway.launch --workers "${NUM_WORKERS:-1}" --host 0.0.0.0 --port 4000 "$@" ;; backend) - set -- uvicorn backend.main:app --host 0.0.0.0 --port 4001 "$@" + set -- uvicorn litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001 "$@" ;; ui) exec nginx -g 'daemon off;' "$@" ;; migrations) - set -- python /app/migrations/run.py "$@" + set -- python -m litellm.runtime.migrations.run "$@" ;; metrics) set -- python -m litellm.proxy.prometheus_metrics_server "$@" diff --git a/litellm/deploy/helm/litellm/templates/_helpers.tpl b/litellm/deploy/helm/litellm/templates/_helpers.tpl index 13222d8284e..827c55d2aef 100644 --- a/litellm/deploy/helm/litellm/templates/_helpers.tpl +++ b/litellm/deploy/helm/litellm/templates/_helpers.tpl @@ -296,7 +296,7 @@ by the controller rather than declared, so nothing there can collide. LITELLM_MASTER_KEY, and PROXY_MASTER_KEY for configs copied from litellm-helm, for the app containers only. The migrations Job runs as a pre-install hook, before the chart's generated master key Secret exists, and -migrations/run.py never reads the key, so the Job must not reference it. +litellm/runtime/migrations/run.py never reads the key, so the Job must not reference it. */}} {{- define "litellm.masterKeyEnv" -}} {{- range list "LITELLM_MASTER_KEY" "PROXY_MASTER_KEY" }} @@ -507,7 +507,7 @@ The migrations Job (helm.sh/hook: pre-upgrade) is the single owner of Prisma schema-update on startup and contends with the Job — and with each other — for Prisma's Postgres advisory lock on the writer, which makes the Job's `migrate deploy` intermittently block until its per-attempt timeout -and retry-exhaust. The Job's entrypoint (migrations/run.py) does not import +and retry-exhaust. The Job's entrypoint (litellm/runtime/migrations/run.py) does not import proxy_server and never reads DISABLE_SCHEMA_UPDATE, so emitting it here is a harmless no-op for the Job and authoritative for the app pods. */}} diff --git a/litellm/packaging/litellm-core/pyproject.toml b/litellm/packaging/litellm-core/pyproject.toml index d3e1c97b9b1..121a902a730 100644 --- a/litellm/packaging/litellm-core/pyproject.toml +++ b/litellm/packaging/litellm-core/pyproject.toml @@ -66,6 +66,11 @@ include = [ "litellm/proxy/common_utils/codex_bedrock_runtime_models_0.166.0.json", ] exclude = [ + "litellm/scripts", + "litellm/scripts/**", + "litellm/runtime", + "litellm/runtime/**", + "litellm/schema.prisma", "litellm/enterprise", "litellm/enterprise/**", "litellm/proxy-extras", diff --git a/litellm/proxy-extras/litellm_proxy_extras/request_log_indexes.py b/litellm/proxy-extras/litellm_proxy_extras/request_log_indexes.py index 0991ab270f3..3a332ae1530 100644 --- a/litellm/proxy-extras/litellm_proxy_extras/request_log_indexes.py +++ b/litellm/proxy-extras/litellm_proxy_extras/request_log_indexes.py @@ -4,7 +4,7 @@ background, once it serves). A migration cannot build them: a plain `CREATE INDEX` blocks spend-log inserts for the whole build, and `CREATE INDEX CONCURRENTLY` is refused on a partitioned parent -(scripts/db/partition_spend_logs.sql). `REQUEST_LOG_INDEXES` is the one list to extend; +(litellm/scripts/db/partition_spend_logs.sql). `REQUEST_LOG_INDEXES` is the one list to extend; names match what Prisma derives from the `@@index` declarations in schema.prisma, so an index a database already has is recognized and never rebuilt. """ diff --git a/litellm/proxy-extras/litellm_proxy_extras/utils.py b/litellm/proxy-extras/litellm_proxy_extras/utils.py index 8d316e42ca5..f8625088d8e 100644 --- a/litellm/proxy-extras/litellm_proxy_extras/utils.py +++ b/litellm/proxy-extras/litellm_proxy_extras/utils.py @@ -147,7 +147,7 @@ _SPEND_LOGS_PK_CLAUSE_RE = re.compile( ) PARTITIONED_SPEND_LOGS_PUSH_ERROR = ( - "LiteLLM_SpendLogs is a partitioned table (see scripts/db/partition_spend_logs.sql), " + "LiteLLM_SpendLogs is a partitioned table (see litellm/scripts/db/partition_spend_logs.sql), " 'so its primary key must include the partition key ("startTime"). `prisma db push` ' "reconciles the database against schema.prisma, which declares the unpartitioned " 'primary key ("request_id"), and Postgres rejects that rewrite with: unique ' @@ -179,7 +179,7 @@ def _without_spend_logs_pk_clauses(statement: str) -> str | None: def filter_partitioned_spend_logs_diff(diff_sql: str) -> str: """Drop statements from a `prisma migrate diff` script that fight the - SpendLogs partitioning runbook (scripts/db/partition_spend_logs.sql): the + SpendLogs partitioning runbook (litellm/scripts/db/partition_spend_logs.sql): the primary-key rewrite on "LiteLLM_SpendLogs", which Postgres rejects on a partitioned table, and drops of runbook artifacts such as "LiteLLM_SpendLogs_legacy".""" @@ -679,7 +679,7 @@ class ProxyExtrasDBManager: """True when the connected database's LiteLLM_SpendLogs is a partitioned table in Prisma's target schema (the `schema` URL param, falling back to Prisma's default target, public), i.e. the operator - ran scripts/db/partition_spend_logs.sql. Returns False when psycopg is + ran litellm/scripts/db/partition_spend_logs.sql. Returns False when psycopg is unavailable or the database cannot be reached, preserving the pre-existing behavior in those cases.""" database_url = os.getenv("DATABASE_URL") @@ -692,7 +692,7 @@ class ProxyExtrasDBManager: logger.warning( "psycopg is not installed; skipping the LiteLLM_SpendLogs " "partition check. If this table is partitioned (see " - "scripts/db/partition_spend_logs.sql), schema reconciliation " + "litellm/scripts/db/partition_spend_logs.sql), schema reconciliation " "will try to rewrite its primary key and fail. Install the " "litellm[extra_proxy] extra, which now includes psycopg." ) diff --git a/litellm/proxy-extras/migration_runbook.md b/litellm/proxy-extras/migration_runbook.md index e49416ddb73..08c944a988d 100644 --- a/litellm/proxy-extras/migration_runbook.md +++ b/litellm/proxy-extras/migration_runbook.md @@ -10,7 +10,7 @@ Before doing anything else, make sure all `schema.prisma` files in the repo are | File | Purpose | |------|---------| -| `schema.prisma` (repo root) | Source of truth | +| `litellm/schema.prisma` (canonical) | Source of truth | | `litellm/proxy/schema.prisma` | Used by the proxy server | | `litellm/proxy-extras/litellm_proxy_extras/schema.prisma` | Used for migration generation | @@ -18,16 +18,16 @@ Before doing anything else, make sure all `schema.prisma` files in the repo are ```bash # 1. Diff all schema files against the root source of truth -diff schema.prisma litellm/proxy/schema.prisma -diff schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma +diff litellm/schema.prisma litellm/proxy/schema.prisma +diff litellm/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma # 2. If there are differences, copy the root schema to all locations -cp schema.prisma litellm/proxy/schema.prisma -cp schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma +cp litellm/schema.prisma litellm/proxy/schema.prisma +cp litellm/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma # 3. Verify all files are now identical -diff schema.prisma litellm/proxy/schema.prisma && echo "proxy schema in sync" || echo "MISMATCH" -diff schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma && echo "extras schema in sync" || echo "MISMATCH" +diff litellm/schema.prisma litellm/proxy/schema.prisma && echo "proxy schema in sync" || echo "MISMATCH" +diff litellm/schema.prisma litellm/proxy-extras/litellm_proxy_extras/schema.prisma && echo "extras schema in sync" || echo "MISMATCH" ``` > **Do NOT proceed to migration generation until all schema files are identical.** @@ -43,7 +43,7 @@ brew install postgresql@14 # macOS export PATH="/opt/homebrew/opt/postgresql@14/bin:$PATH" # Run migration -uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name" +uv run --with testing.postgresql python litellm/scripts/ci_cd/run_migration.py "your_migration_name" ``` ## What It Does @@ -69,7 +69,7 @@ When the guard fires: 1. Update your branch: ```bash - base_branch=$(python3 scripts/default_branch.py --branch) && + base_branch=$(python3 litellm/scripts/default_branch.py --branch) && git fetch origin "+refs/heads/$base_branch:refs/remotes/origin/$base_branch" && git rebase "origin/$base_branch" ``` @@ -89,7 +89,7 @@ When the guard fires: 4. Only if the drops are genuinely intentional, re-run with the flag: ```bash - uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name" --allow-destructive + uv run --with testing.postgresql python litellm/scripts/ci_cd/run_migration.py "your_migration_name" --allow-destructive ``` > **AI AGENTS / ASSISTANTS:** Do **not** automatically re-run the command with `--allow-destructive`. If the guard fires while you are driving the runbook for a human, stop, show them the error, and wait for their explicit confirmation before passing the flag. Auto-passing `--allow-destructive` is the exact failure mode this guard exists to prevent. @@ -98,7 +98,7 @@ When the guard fires: **Missing testing module:** ```bash -uv run --with testing.postgresql python scripts/ci_cd/run_migration.py "your_migration_name" +uv run --with testing.postgresql python litellm/scripts/ci_cd/run_migration.py "your_migration_name" ``` **initdb not found:** @@ -115,7 +115,7 @@ rm -rf litellm/proxy-extras/litellm_proxy_extras/migrations/[empty_dir] ## Rules - Sync all `schema.prisma` files first (Step 0) -- Update `schema.prisma` at the repo root first, then sync copies +- Update `litellm/schema.prisma` first, then sync copies - Review generated SQL before committing - Use descriptive migration names - Never edit existing migration files diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0de2bdc34e1..5c289051a02 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3004,7 +3004,7 @@ class ConfigGeneralSettings(LiteLLMPydanticObjectBase): ) use_spend_logs_partitioning: bool | None = Field( None, - description="If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.", + description="If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (litellm/scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False.", ) maximum_spend_logs_cleanup_batch_size: int | None = Field( None, diff --git a/litellm/proxy/client/cli/README.md b/litellm/proxy/client/cli/README.md index 1fdf8da7d2b..5f9faf4ee79 100644 --- a/litellm/proxy/client/cli/README.md +++ b/litellm/proxy/client/cli/README.md @@ -579,7 +579,7 @@ The Codex undo receipt is kept in a private `.litellm` directory beside the reso `lite configure claude` wires Claude Code up persistently with a long-lived virtual key, a pinned model and an undo, and `lite unconfigure claude` puts things back: ```bash -curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/scripts/install.sh | sh lite --base-url https://your-proxy.example.com configure claude --api-key sk-... --model claude-auto claude ``` @@ -615,7 +615,7 @@ After upgrading the CLI, run `lite configure claude` to refresh `~/.litellm/stat `lite autoroute start` builds and runs a throwaway litellm proxy locally, so unlike the rest of this CLI it needs the proxy server runtime, not just the thin `litellm[cli]` client. Install `litellm[proxy]` (which ships the `lite` command too) with a single curl command -- no existing Python tooling required, `uv` is bootstrapped automatically if missing: ```bash -curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/install.sh | sh +curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/scripts/install.sh | sh ``` To QA an unreleased branch or commit instead of the latest PyPI release, set `LITELLM_CLI_REF`: @@ -625,7 +625,7 @@ curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm// LITELLM_CLI_REF= sh ``` -The thin `scripts/install-cli.sh` installs only `litellm[cli]`, which is enough for `lite login`, `lite claude`, and `lite up`, but not for `lite autoroute start`; running it against a `litellm[cli]` install fails fast with a message telling you to install the proxy runtime. +The thin `litellm/scripts/install-cli.sh` installs only `litellm[cli]`, which is enough for `lite login`, `lite claude`, and `lite up`, but not for `lite autoroute start`; running it against a `litellm[cli]` install fails fast with a message telling you to install the proxy runtime. Point the CLI at your real proxy and key before running any `lite model-groups` or `lite autoroute` command -- like every other command in this CLI, they read `LITELLM_PROXY_URL`/`LITELLM_PROXY_API_KEY` (or `--base-url`/`--api-key`), no `lite login` required: diff --git a/litellm/proxy/db/create_views.py b/litellm/proxy/db/create_views.py index fd5c2356373..0b7fad88230 100644 --- a/litellm/proxy/db/create_views.py +++ b/litellm/proxy/db/create_views.py @@ -51,7 +51,7 @@ async def create_view_tolerating_race(db: SupportsExecuteRaw, view_name: str, dd async def create_missing_views(db: SupportsRawQueries) -> None: """ -------------------------------------------------- - NOTE: Copy of `scripts/db/create_views.py`. + NOTE: Copy of `litellm/scripts/db/create_views.py`. -------------------------------------------------- Checks if the LiteLLM_VerificationTokenView and MonthlyGlobalSpend exists in the user's db. diff --git a/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py index f916d53a63a..f9b0f8cf9d9 100644 --- a/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py +++ b/litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py @@ -9,7 +9,7 @@ to the OS immediately, with no tombstones and no vacuum. This manager only acts when use_spend_logs_partitioning is enabled in general_settings AND the table is already partitioned (set up via the -scripts/db/partition_spend_logs.sql runbook). Without both, the cleanup job +litellm/scripts/db/partition_spend_logs.sql runbook). Without both, the cleanup job keeps the batched-DELETE path, so existing deployments are untouched. """ diff --git a/backend/main.py b/litellm/runtime/backend/main.py similarity index 93% rename from backend/main.py rename to litellm/runtime/backend/main.py index e0cef90c979..5ceb4fcc51b 100644 --- a/backend/main.py +++ b/litellm/runtime/backend/main.py @@ -5,7 +5,7 @@ route table to just the management/admin surface used by the dashboard. Purely additive — no existing module is modified. Run with: - uvicorn backend.main:app --host 0.0.0.0 --port 4001 + uvicorn litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001 """ from collections.abc import AsyncGenerator, Mapping @@ -24,7 +24,7 @@ DatabaseURLSettings.from_env().apply_to_env() from litellm.proxy.proxy_server import app -from backend.routes.allowlist import ( +from litellm.runtime.backend.routes.allowlist import ( BACKEND_EXACT_PATHS, BACKEND_MOUNT_PATHS, BACKEND_PATH_PREFIXES, diff --git a/backend/routes/__init__.py b/litellm/runtime/backend/routes/__init__.py similarity index 100% rename from backend/routes/__init__.py rename to litellm/runtime/backend/routes/__init__.py diff --git a/backend/routes/allowlist.py b/litellm/runtime/backend/routes/allowlist.py similarity index 100% rename from backend/routes/allowlist.py rename to litellm/runtime/backend/routes/allowlist.py diff --git a/migrations/run.py b/litellm/runtime/migrations/run.py similarity index 100% rename from migrations/run.py rename to litellm/runtime/migrations/run.py diff --git a/schema.prisma b/litellm/schema.prisma similarity index 100% rename from schema.prisma rename to litellm/schema.prisma diff --git a/scripts/adaptive_router_demo/README.md b/litellm/scripts/adaptive_router_demo/README.md similarity index 98% rename from scripts/adaptive_router_demo/README.md rename to litellm/scripts/adaptive_router_demo/README.md index fc855fc24fe..a7a517cad97 100644 --- a/scripts/adaptive_router_demo/README.md +++ b/litellm/scripts/adaptive_router_demo/README.md @@ -76,7 +76,7 @@ The dashboard is a single static HTML file. Either: - **If your browser blocks `file://` fetches:** ```bash - cd scripts/adaptive_router_demo + cd litellm/scripts/adaptive_router_demo python3 -m http.server 8080 ``` @@ -95,7 +95,7 @@ Click **Connect**. The dashboard polls `GET /adaptive_router/state` every In a second terminal, replacing `` with the key printed in step 1: ```bash -uv run python scripts/adaptive_router_demo/traffic.py \ +uv run python litellm/scripts/adaptive_router_demo/traffic.py \ --proxy-url http://localhost:4000 \ --api-key \ --router smart-cheap-router \ diff --git a/scripts/adaptive_router_demo/chat.html b/litellm/scripts/adaptive_router_demo/chat.html similarity index 100% rename from scripts/adaptive_router_demo/chat.html rename to litellm/scripts/adaptive_router_demo/chat.html diff --git a/scripts/adaptive_router_demo/dashboard.html b/litellm/scripts/adaptive_router_demo/dashboard.html similarity index 100% rename from scripts/adaptive_router_demo/dashboard.html rename to litellm/scripts/adaptive_router_demo/dashboard.html diff --git a/scripts/adaptive_router_demo/eval.py b/litellm/scripts/adaptive_router_demo/eval.py similarity index 99% rename from scripts/adaptive_router_demo/eval.py rename to litellm/scripts/adaptive_router_demo/eval.py index a9a8a7bc71c..06f30efd905 100644 --- a/scripts/adaptive_router_demo/eval.py +++ b/litellm/scripts/adaptive_router_demo/eval.py @@ -9,7 +9,7 @@ For each test case: 4. Prints PASS or FAIL with one line of reasoning. Run: - uv run python scripts/adaptive_router_demo/eval.py \ + uv run python litellm/scripts/adaptive_router_demo/eval.py \ --proxy-url http://localhost:4000 \ --api-key "$LITELLM_MASTER_KEY" \ --router smart-cheap-router \ diff --git a/scripts/adaptive_router_demo/traffic.py b/litellm/scripts/adaptive_router_demo/traffic.py similarity index 99% rename from scripts/adaptive_router_demo/traffic.py rename to litellm/scripts/adaptive_router_demo/traffic.py index 5ff806ef5a6..b4f79a0267d 100644 --- a/scripts/adaptive_router_demo/traffic.py +++ b/litellm/scripts/adaptive_router_demo/traffic.py @@ -21,7 +21,7 @@ Why this shape: flow through routing + post-call hooks, so no API keys / no spend. Run: - uv run python scripts/adaptive_router_demo/traffic.py \\ + uv run python litellm/scripts/adaptive_router_demo/traffic.py \\ --proxy-url http://localhost:4000 \\ --api-key "$LITELLM_MASTER_KEY" \\ --router smart-cheap-router \\ diff --git a/scripts/auto-close-duplicates.test.ts b/litellm/scripts/auto-close-duplicates.test.ts similarity index 100% rename from scripts/auto-close-duplicates.test.ts rename to litellm/scripts/auto-close-duplicates.test.ts diff --git a/scripts/auto-close-duplicates.ts b/litellm/scripts/auto-close-duplicates.ts similarity index 100% rename from scripts/auto-close-duplicates.ts rename to litellm/scripts/auto-close-duplicates.ts diff --git a/scripts/benchmark_anthropic_messages_perf.py b/litellm/scripts/benchmark_anthropic_messages_perf.py similarity index 99% rename from scripts/benchmark_anthropic_messages_perf.py rename to litellm/scripts/benchmark_anthropic_messages_perf.py index 3639e5ef326..e1d8a7ed8ad 100644 --- a/scripts/benchmark_anthropic_messages_perf.py +++ b/litellm/scripts/benchmark_anthropic_messages_perf.py @@ -13,11 +13,11 @@ streaming SSE wire format (``message_start`` -> ``content_block_delta`` -> can be compared without depending on real provider latency. Example: - uv run python scripts/benchmark_anthropic_messages_perf.py \ + uv run python litellm/scripts/benchmark_anthropic_messages_perf.py \ --label baseline --proxy-command ".venv/bin/litellm" Compare an already-running proxy: - uv run python scripts/benchmark_anthropic_messages_perf.py \ + uv run python litellm/scripts/benchmark_anthropic_messages_perf.py \ --no-start-proxy --label current """ diff --git a/scripts/benchmark_chat_completions_perf.py b/litellm/scripts/benchmark_chat_completions_perf.py similarity index 99% rename from scripts/benchmark_chat_completions_perf.py rename to litellm/scripts/benchmark_chat_completions_perf.py index 61f04836cac..805d8fd486b 100644 --- a/scripts/benchmark_chat_completions_perf.py +++ b/litellm/scripts/benchmark_chat_completions_perf.py @@ -6,11 +6,11 @@ from any checkout. That makes it useful for comparing tags/commits without depending on real provider latency. Example: - uv run python scripts/benchmark_chat_completions_perf.py \ + uv run python litellm/scripts/benchmark_chat_completions_perf.py \ --label current --requests 500 --concurrency 100 Compare another checkout: - uv run python scripts/benchmark_chat_completions_perf.py \ + uv run python litellm/scripts/benchmark_chat_completions_perf.py \ --label v1.83.14-stable --litellm-dir /tmp/litellm-v1.83.14-stable """ diff --git a/scripts/benchmark_mock.py b/litellm/scripts/benchmark_mock.py similarity index 100% rename from scripts/benchmark_mock.py rename to litellm/scripts/benchmark_mock.py diff --git a/scripts/benchmark_model_response_creator.py b/litellm/scripts/benchmark_model_response_creator.py similarity index 96% rename from scripts/benchmark_model_response_creator.py rename to litellm/scripts/benchmark_model_response_creator.py index 881870d3854..d7e0199e6fc 100644 --- a/scripts/benchmark_model_response_creator.py +++ b/litellm/scripts/benchmark_model_response_creator.py @@ -6,8 +6,8 @@ isolate per-call cost. Driving the full wrapper adds threadpool logging, gc, and other noise that swamps microsecond-scale changes here. Example: - uv run python scripts/benchmark_model_response_creator.py --label baseline - uv run python scripts/benchmark_model_response_creator.py --label optimized + uv run python litellm/scripts/benchmark_model_response_creator.py --label baseline + uv run python litellm/scripts/benchmark_model_response_creator.py --label optimized """ from __future__ import annotations diff --git a/scripts/benchmark_ocr_callbacks.py b/litellm/scripts/benchmark_ocr_callbacks.py similarity index 99% rename from scripts/benchmark_ocr_callbacks.py rename to litellm/scripts/benchmark_ocr_callbacks.py index e2282356356..163f340b530 100644 --- a/scripts/benchmark_ocr_callbacks.py +++ b/litellm/scripts/benchmark_ocr_callbacks.py @@ -2,7 +2,7 @@ """Measure serial sync/async OCR latency through a loopback HTTP provider Run each callback mode in a fresh process against an installed release wheel: -python -I scripts/benchmark_ocr_callbacks.py --callbacks none --label before \ +python -I litellm/scripts/benchmark_ocr_callbacks.py --callbacks none --label before \ --expected-transport rust --iterations 200 --warmup 20 --output before-none.json Repeat with --callbacks noop and with the candidate wheel in a separate venv """ diff --git a/scripts/benchmark_proxy_vs_provider.py b/litellm/scripts/benchmark_proxy_vs_provider.py similarity index 98% rename from scripts/benchmark_proxy_vs_provider.py rename to litellm/scripts/benchmark_proxy_vs_provider.py index 28d9d0c9cc7..0d289ee1ae0 100755 --- a/scripts/benchmark_proxy_vs_provider.py +++ b/litellm/scripts/benchmark_proxy_vs_provider.py @@ -539,28 +539,28 @@ Examples: export PROVIDER_URL='https://api.openai.com/v1/chat/completions' export LITELLM_PROXY_API_KEY="$LITELLM_MASTER_KEY" export PROVIDER_API_KEY='sk-openai-key' - python scripts/benchmark_proxy_vs_provider.py + python litellm/scripts/benchmark_proxy_vs_provider.py # 2. Multiple runs for statistical accuracy (recommended) - python scripts/benchmark_proxy_vs_provider.py --runs 5 + python litellm/scripts/benchmark_proxy_vs_provider.py --runs 5 # 3. Realistic load testing with concurrency limit - python scripts/benchmark_proxy_vs_provider.py --max-concurrent 100 --requests 2000 + python litellm/scripts/benchmark_proxy_vs_provider.py --max-concurrent 100 --requests 2000 # 4. Quick test with fewer requests - python scripts/benchmark_proxy_vs_provider.py --requests 100 + python litellm/scripts/benchmark_proxy_vs_provider.py --requests 100 # 5. Parallel execution (not recommended - may affect accuracy) - python scripts/benchmark_proxy_vs_provider.py --parallel + python litellm/scripts/benchmark_proxy_vs_provider.py --parallel # 6. Custom timeout for slower endpoints - python scripts/benchmark_proxy_vs_provider.py --timeout 120 + python litellm/scripts/benchmark_proxy_vs_provider.py --timeout 120 # 7. Combined options for comprehensive testing - python scripts/benchmark_proxy_vs_provider.py --runs 3 --requests 500 --max-concurrent 50 + python litellm/scripts/benchmark_proxy_vs_provider.py --runs 3 --requests 500 --max-concurrent 50 # 8. Skip warmup (not recommended - may affect first request accuracy) - python scripts/benchmark_proxy_vs_provider.py --no-warmup + python litellm/scripts/benchmark_proxy_vs_provider.py --no-warmup """, ) parser.add_argument( diff --git a/scripts/benchmark_streaming_chunk_overhead.py b/litellm/scripts/benchmark_streaming_chunk_overhead.py similarity index 99% rename from scripts/benchmark_streaming_chunk_overhead.py rename to litellm/scripts/benchmark_streaming_chunk_overhead.py index 11fbea6a6a3..9a5fe751a23 100644 --- a/scripts/benchmark_streaming_chunk_overhead.py +++ b/litellm/scripts/benchmark_streaming_chunk_overhead.py @@ -7,7 +7,7 @@ and Bedrock Converse (ModelResponseStream). A full proxy benchmark adds FastAPI, HTTP, and TCP latency, which dilutes the per-chunk CPU signal. Example: - uv run python scripts/benchmark_streaming_chunk_overhead.py \\ + uv run python litellm/scripts/benchmark_streaming_chunk_overhead.py \\ --streams 500 --chunks 200 --warmup 50 --repeats 5 """ diff --git a/scripts/build_core_distribution.py b/litellm/scripts/build_core_distribution.py similarity index 94% rename from scripts/build_core_distribution.py rename to litellm/scripts/build_core_distribution.py index 62a4a3b37a1..e823bc9aeb6 100644 --- a/scripts/build_core_distribution.py +++ b/litellm/scripts/build_core_distribution.py @@ -8,7 +8,7 @@ import tempfile from pathlib import Path from typing import Final -ROOT: Final = Path(__file__).resolve().parents[1] +ROOT: Final = Path(__file__).resolve().parents[2] SOURCES: Final = ( "litellm", "litellm/rust", @@ -57,7 +57,10 @@ def stage_core_distribution(source: Path, destination: Path, *, package: str = " def ignored_sources(directory: str, names: list[str]) -> set[str]: workspace_sources: Final = ( - {"enterprise", "proxy-extras", "packaging", "rust", "cli", "ui", "vscode-extension", "deploy"} + { + "enterprise", "proxy-extras", "packaging", "rust", "cli", "scripts", "runtime", + "schema.prisma", "ui", "vscode-extension", "deploy", + } if Path(directory) == source / "litellm" else set() ) diff --git a/scripts/check_mcp_operation_boundary.py b/litellm/scripts/check_mcp_operation_boundary.py similarity index 100% rename from scripts/check_mcp_operation_boundary.py rename to litellm/scripts/check_mcp_operation_boundary.py diff --git a/scripts/check_mcp_sdk_install.py b/litellm/scripts/check_mcp_sdk_install.py similarity index 100% rename from scripts/check_mcp_sdk_install.py rename to litellm/scripts/check_mcp_sdk_install.py diff --git a/scripts/check_openapi_docs.py b/litellm/scripts/check_openapi_docs.py similarity index 96% rename from scripts/check_openapi_docs.py rename to litellm/scripts/check_openapi_docs.py index 219ef912952..e88594f5a51 100755 --- a/scripts/check_openapi_docs.py +++ b/litellm/scripts/check_openapi_docs.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """List the POST operations in the proxy's OpenAPI document that Swagger UI cannot explain. -Every finding is one line, `` ``, so scripts/openapi_docs_gate.py can +Every finding is one line, `` ``, so litellm/scripts/openapi_docs_gate.py can count them per rule the same delta-vs-base way as the other lint gates. The rules: OA001 no request body schema (the handler reads ``request.body()`` and never told FastAPI diff --git a/scripts/check_schema_compatibility.py b/litellm/scripts/check_schema_compatibility.py similarity index 86% rename from scripts/check_schema_compatibility.py rename to litellm/scripts/check_schema_compatibility.py index c56986a38c8..5d7ee790cb2 100644 --- a/scripts/check_schema_compatibility.py +++ b/litellm/scripts/check_schema_compatibility.py @@ -67,11 +67,18 @@ def main() -> int: options: Final = TypeAdapter(dict[str, str | None]).validate_python(vars(parser.parse_args())) base_commit: Final = options["base_commit"] base: Final = base_commit if base_commit else _merge_base(options["base_ref"] or "") - previous: Final = subprocess.run( - ("git", "show", f"{base}:schema.prisma"), capture_output=True, text=True, check=True - ).stdout + canonical: Final = subprocess.run( + ("git", "show", f"{base}:litellm/schema.prisma"), capture_output=True, text=True, check=False + ) + previous: Final = ( + canonical.stdout + if canonical.returncode == 0 + else subprocess.run( + ("git", "show", f"{base}:schema.prisma"), capture_output=True, text=True, check=True + ).stdout + ) changes: Final = check_breaking_changes( - parse_model_fields(previous), parse_model_fields(Path("schema.prisma").read_text()) + parse_model_fields(previous), parse_model_fields(Path("litellm/schema.prisma").read_text()) ) if changes: sys.stderr.write("\n".join(changes) + "\n") diff --git a/scripts/check_test_quality.py b/litellm/scripts/check_test_quality.py similarity index 99% rename from scripts/check_test_quality.py rename to litellm/scripts/check_test_quality.py index 6e82fe8c229..79a2ff57eeb 100644 --- a/scripts/check_test_quality.py +++ b/litellm/scripts/check_test_quality.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Test-quality checker: the test-suite smells no linter enforces. -Sibling of scripts/check_type_discipline.py, same output contract +Sibling of litellm/scripts/check_type_discipline.py, same output contract (``path:line: CODE message``) and same stdlib-only constraint, aimed at the test tree instead of the package. Each rule is a shape the testing-strategy audit -measured and named; scripts/test_quality_gate.py fails any change that grows the +measured and named; litellm/scripts/test_quality_gate.py fails any change that grows the codebase total of one past its merge-base count. Rules diff --git a/scripts/check_type_discipline.py b/litellm/scripts/check_type_discipline.py similarity index 99% rename from scripts/check_type_discipline.py rename to litellm/scripts/check_type_discipline.py index 9fee49e3b03..4b377d01259 100644 --- a/scripts/check_type_discipline.py +++ b/litellm/scripts/check_type_discipline.py @@ -1172,6 +1172,8 @@ def collect_paths(raw: Iterable[str]) -> Iterator[Path]: "litellm/packaging/", "litellm/rust/", "litellm/cli/", + "litellm/scripts/", + "litellm/runtime/", ) ) ) @@ -1186,6 +1188,8 @@ def collect_paths(raw: Iterable[str]) -> Iterator[Path]: "litellm/packaging/", "litellm/rust/", "litellm/cli/", + "litellm/scripts/", + "litellm/runtime/", ) ): yield p diff --git a/scripts/ci_cd/TEST_KEY_PATTERNS.md b/litellm/scripts/ci_cd/TEST_KEY_PATTERNS.md similarity index 100% rename from scripts/ci_cd/TEST_KEY_PATTERNS.md rename to litellm/scripts/ci_cd/TEST_KEY_PATTERNS.md diff --git a/scripts/ci_cd/check_file_length.py b/litellm/scripts/ci_cd/check_file_length.py similarity index 100% rename from scripts/ci_cd/check_file_length.py rename to litellm/scripts/ci_cd/check_file_length.py diff --git a/scripts/ci_cd/check_files_match.py b/litellm/scripts/ci_cd/check_files_match.py similarity index 100% rename from scripts/ci_cd/check_files_match.py rename to litellm/scripts/ci_cd/check_files_match.py diff --git a/scripts/ci_cd/cost_map_guard.py b/litellm/scripts/ci_cd/cost_map_guard.py similarity index 98% rename from scripts/ci_cd/cost_map_guard.py rename to litellm/scripts/ci_cd/cost_map_guard.py index 1ce49907b44..5c35ef7c093 100644 --- a/scripts/ci_cd/cost_map_guard.py +++ b/litellm/scripts/ci_cd/cost_map_guard.py @@ -2,7 +2,7 @@ Every pull request whose diff against its merge base touches one of the two cost map files or the pins file gets the file checks: the files parse, the backup copy matches the root file, and every capability pinned in -scripts/ci_cd/cost_map_pins.json (a value verified against a live provider call where the provider's model listing says +litellm/scripts/ci_cd/cost_map_pins.json (a value verified against a live provider call where the provider's model listing says otherwise) still holds in the tree that merging the pull request into its base branch produces, so a branch cut before a pin was added cannot lower it, and a pull request that deletes the pins file fails too. A pull request that leaves all three untouched skips them, since merging it keeps the base branch's copies and its head tree only carries @@ -27,7 +27,7 @@ from typing import Final COST_MAP_PATH: Final = "model_prices_and_context_window.json" BACKUP_PATH: Final = "litellm/model_prices_and_context_window_backup.json" -PINS_PATH: Final = "scripts/ci_cd/cost_map_pins.json" +PINS_PATH: Final = "litellm/scripts/ci_cd/cost_map_pins.json" PINS_PATHS: Final = ( "litellm/scripts/ci_cd/cost_map_pins.json", "scripts/ci_cd/cost_map_pins.json", diff --git a/scripts/ci_cd/cost_map_pins.json b/litellm/scripts/ci_cd/cost_map_pins.json similarity index 100% rename from scripts/ci_cd/cost_map_pins.json rename to litellm/scripts/ci_cd/cost_map_pins.json diff --git a/scripts/ci_cd/run_migration.py b/litellm/scripts/ci_cd/run_migration.py similarity index 99% rename from scripts/ci_cd/run_migration.py rename to litellm/scripts/ci_cd/run_migration.py index 17614663a78..92afecdf3e9 100644 --- a/scripts/ci_cd/run_migration.py +++ b/litellm/scripts/ci_cd/run_migration.py @@ -238,7 +238,7 @@ def create_migration( skip_freshness_check (bool): Skip the "branch is up to date" check. Only for intentional migrations against an older base. """ - root_dir = Path(__file__).resolve().parents[2] + root_dir = Path(__file__).resolve().parents[3] if skip_freshness_check: print( @@ -254,7 +254,7 @@ def create_migration( migrations_dir = ( root_dir / "litellm" / "proxy-extras" / "litellm_proxy_extras" / "migrations" ) - schema_path = root_dir / "schema.prisma" + schema_path = root_dir / "litellm/schema.prisma" # Create temporary PostgreSQL database with testing.postgresql.Postgresql() as postgresql: diff --git a/scripts/ci_cd/security_scans_readme.md b/litellm/scripts/ci_cd/security_scans_readme.md similarity index 100% rename from scripts/ci_cd/security_scans_readme.md rename to litellm/scripts/ci_cd/security_scans_readme.md diff --git a/scripts/ci_cd/spend_logs_guard.py b/litellm/scripts/ci_cd/spend_logs_guard.py similarity index 100% rename from scripts/ci_cd/spend_logs_guard.py rename to litellm/scripts/ci_cd/spend_logs_guard.py diff --git a/scripts/classify-issue.test.ts b/litellm/scripts/classify-issue.test.ts similarity index 100% rename from scripts/classify-issue.test.ts rename to litellm/scripts/classify-issue.test.ts diff --git a/scripts/classify-issue.ts b/litellm/scripts/classify-issue.ts similarity index 100% rename from scripts/classify-issue.ts rename to litellm/scripts/classify-issue.ts diff --git a/scripts/comment-fixed-issue.test.ts b/litellm/scripts/comment-fixed-issue.test.ts similarity index 100% rename from scripts/comment-fixed-issue.test.ts rename to litellm/scripts/comment-fixed-issue.test.ts diff --git a/scripts/comment-fixed-issue.ts b/litellm/scripts/comment-fixed-issue.ts similarity index 100% rename from scripts/comment-fixed-issue.ts rename to litellm/scripts/comment-fixed-issue.ts diff --git a/scripts/create_litellm_branch.ps1 b/litellm/scripts/create_litellm_branch.ps1 similarity index 100% rename from scripts/create_litellm_branch.ps1 rename to litellm/scripts/create_litellm_branch.ps1 diff --git a/scripts/create_litellm_branch.sh b/litellm/scripts/create_litellm_branch.sh similarity index 100% rename from scripts/create_litellm_branch.sh rename to litellm/scripts/create_litellm_branch.sh diff --git a/scripts/create_team_key_and_submit_guardrail.sh b/litellm/scripts/create_team_key_and_submit_guardrail.sh similarity index 100% rename from scripts/create_team_key_and_submit_guardrail.sh rename to litellm/scripts/create_team_key_and_submit_guardrail.sh diff --git a/scripts/db/backfill_daily_tool_spend.sql b/litellm/scripts/db/backfill_daily_tool_spend.sql similarity index 97% rename from scripts/db/backfill_daily_tool_spend.sql rename to litellm/scripts/db/backfill_daily_tool_spend.sql index 9c65be20ada..472fe696d55 100644 --- a/scripts/db/backfill_daily_tool_spend.sql +++ b/litellm/scripts/db/backfill_daily_tool_spend.sql @@ -26,7 +26,7 @@ -- such buckets keep the writer's numbers and skip the backfill's. -- -- Usage: --- psql "$DATABASE_URL" -v cutover="'2026-07-25T00:00:00Z'" -f scripts/db/backfill_daily_tool_spend.sql +-- psql "$DATABASE_URL" -v cutover="'2026-07-25T00:00:00Z'" -f litellm/scripts/db/backfill_daily_tool_spend.sql SET TIME ZONE 'UTC'; diff --git a/scripts/db/backfill_key_total_spend.sql b/litellm/scripts/db/backfill_key_total_spend.sql similarity index 92% rename from scripts/db/backfill_key_total_spend.sql rename to litellm/scripts/db/backfill_key_total_spend.sql index 8c3d75ef5e2..81123b4ace8 100644 --- a/scripts/db/backfill_key_total_spend.sql +++ b/litellm/scripts/db/backfill_key_total_spend.sql @@ -22,14 +22,14 @@ -- budget_id links to a LiteLLM_BudgetTable row whose budget_duration IS -- NOT NULL (a linked budget resets the key's spend each period too). For -- those keys this script only recovers the current period; --- scripts/db/backfill_key_total_spend_from_spend_logs.sql is an optional +-- litellm/scripts/db/backfill_key_total_spend_from_spend_logs.sql is an optional -- follow-up that rebuilds the earlier periods from LiteLLM_SpendLogs. -- -- 3. No proxy restart is needed. The proxy picks up the corrected values on -- its next read of each key. -- -- Usage: --- psql "$DATABASE_URL" -f scripts/db/backfill_key_total_spend.sql +-- psql "$DATABASE_URL" -f litellm/scripts/db/backfill_key_total_spend.sql UPDATE "LiteLLM_VerificationToken" SET total_spend = spend diff --git a/scripts/db/backfill_key_total_spend_from_spend_logs.sql b/litellm/scripts/db/backfill_key_total_spend_from_spend_logs.sql similarity index 95% rename from scripts/db/backfill_key_total_spend_from_spend_logs.sql rename to litellm/scripts/db/backfill_key_total_spend_from_spend_logs.sql index b76a5d1fb6d..5a26ca042b8 100644 --- a/scripts/db/backfill_key_total_spend_from_spend_logs.sql +++ b/litellm/scripts/db/backfill_key_total_spend_from_spend_logs.sql @@ -1,4 +1,4 @@ --- Optional follow-up to scripts/db/backfill_key_total_spend.sql. Run that +-- Optional follow-up to litellm/scripts/db/backfill_key_total_spend.sql. Run that -- script first; this one rebuilds earlier budget periods for the keys it -- can only partially fix: keys whose spend resets each period, because their own -- budget_duration IS NOT NULL or because their budget_id links to a @@ -38,7 +38,7 @@ -- its next read of each key. -- -- Usage: --- psql "$DATABASE_URL" -f scripts/db/backfill_key_total_spend_from_spend_logs.sql +-- psql "$DATABASE_URL" -f litellm/scripts/db/backfill_key_total_spend_from_spend_logs.sql -- Active keys whose spend resets (own budget_duration, or a linked -- LiteLLM_BudgetTable row with one). Rebuild from LiteLLM_SpendLogs, diff --git a/scripts/db/create_views.py b/litellm/scripts/db/create_views.py similarity index 100% rename from scripts/db/create_views.py rename to litellm/scripts/db/create_views.py diff --git a/scripts/db/migrate_keys.py b/litellm/scripts/db/migrate_keys.py similarity index 100% rename from scripts/db/migrate_keys.py rename to litellm/scripts/db/migrate_keys.py diff --git a/scripts/db/partition_spend_logs.sql b/litellm/scripts/db/partition_spend_logs.sql similarity index 99% rename from scripts/db/partition_spend_logs.sql rename to litellm/scripts/db/partition_spend_logs.sql index 217a7463b54..9c09b3626af 100644 --- a/scripts/db/partition_spend_logs.sql +++ b/litellm/scripts/db/partition_spend_logs.sql @@ -32,7 +32,7 @@ -- The spend-log cleanup job then verifies the table is partitioned and reclaims -- disk by dropping expired partitions instead of deleting rows. It also -- pre-creates upcoming partitions on each run. To roll back, see --- scripts/db/unpartition_spend_logs.sql. +-- litellm/scripts/db/unpartition_spend_logs.sql. BEGIN; diff --git a/scripts/db/strip_vector_store_internal_metadata.py b/litellm/scripts/db/strip_vector_store_internal_metadata.py similarity index 100% rename from scripts/db/strip_vector_store_internal_metadata.py rename to litellm/scripts/db/strip_vector_store_internal_metadata.py diff --git a/scripts/db/unpartition_spend_logs.sql b/litellm/scripts/db/unpartition_spend_logs.sql similarity index 97% rename from scripts/db/unpartition_spend_logs.sql rename to litellm/scripts/db/unpartition_spend_logs.sql index 3f2b9c768b0..9c0e8fb1905 100644 --- a/scripts/db/unpartition_spend_logs.sql +++ b/litellm/scripts/db/unpartition_spend_logs.sql @@ -1,4 +1,4 @@ --- Rolls back scripts/db/partition_spend_logs.sql: converts the native +-- Rolls back litellm/scripts/db/partition_spend_logs.sql: converts the native -- range-partitioned "LiteLLM_SpendLogs" table back into a plain, -- non-partitioned table matching the default LiteLLM schema. -- diff --git a/scripts/db/update_unassigned_teams.py b/litellm/scripts/db/update_unassigned_teams.py similarity index 100% rename from scripts/db/update_unassigned_teams.py rename to litellm/scripts/db/update_unassigned_teams.py diff --git a/scripts/default_branch.py b/litellm/scripts/default_branch.py similarity index 100% rename from scripts/default_branch.py rename to litellm/scripts/default_branch.py diff --git a/scripts/eval_compression.py b/litellm/scripts/eval_compression.py similarity index 99% rename from scripts/eval_compression.py rename to litellm/scripts/eval_compression.py index a169cc02d74..2365fea76b0 100644 --- a/scripts/eval_compression.py +++ b/litellm/scripts/eval_compression.py @@ -4,9 +4,9 @@ Prompt Compression Evaluation Harness Compare model performance on coding tasks with and without prompt compression. Usage: - python scripts/eval_compression.py --model gpt-4o --problems 5 - python scripts/eval_compression.py --model claude-sonnet-4-20250514 --problems 12 --runs 3 - python scripts/eval_compression.py --model gpt-4o-mini --padding-factor 50 + python litellm/scripts/eval_compression.py --model gpt-4o --problems 5 + python litellm/scripts/eval_compression.py --model claude-sonnet-4-20250514 --problems 12 --runs 3 + python litellm/scripts/eval_compression.py --model gpt-4o-mini --padding-factor 50 The harness runs each problem in two modes: 1. **baseline** — raw prompt sent directly to the model. diff --git a/scripts/flag-duplicate-issue.test.ts b/litellm/scripts/flag-duplicate-issue.test.ts similarity index 100% rename from scripts/flag-duplicate-issue.test.ts rename to litellm/scripts/flag-duplicate-issue.test.ts diff --git a/scripts/flag-duplicate-issue.ts b/litellm/scripts/flag-duplicate-issue.ts similarity index 100% rename from scripts/flag-duplicate-issue.ts rename to litellm/scripts/flag-duplicate-issue.ts diff --git a/scripts/gate_slot_lock.py b/litellm/scripts/gate_slot_lock.py similarity index 96% rename from scripts/gate_slot_lock.py rename to litellm/scripts/gate_slot_lock.py index c1ab00461e0..053fb6087e1 100644 --- a/scripts/gate_slot_lock.py +++ b/litellm/scripts/gate_slot_lock.py @@ -2,8 +2,8 @@ """Machine-wide slot lock for this repo's heavy entrypoints. `make check`, `make lint`, and the standalone lint gates -(scripts/ruff_strict_gate.py, scripts/type_discipline_gate.py, -scripts/type_check_gate.py) each hold one of N machine-wide slots while they +(litellm/scripts/ruff_strict_gate.py, litellm/scripts/type_discipline_gate.py, +litellm/scripts/type_check_gate.py) each hold one of N machine-wide slots while they run, so however many sessions and worktrees share one machine, at most N of them execute a basedpyright/pytest/prettier storm at a time instead of all thrashing it at once. Slots are fcntl.flock files (macOS ships no flock(1) @@ -22,7 +22,7 @@ itself. Any filesystem error fails open and the command runs unlocked: the lock is a courtesy to the machine, never a gate that may break a build (CI runs one job per machine, so there it only ever takes the instant path). -CLI: python3 scripts/gate_slot_lock.py [args...] +CLI: python3 litellm/scripts/gate_slot_lock.py [args...] """ from __future__ import annotations diff --git a/scripts/generate_provider_endpoints_support.py b/litellm/scripts/generate_provider_endpoints_support.py similarity index 98% rename from scripts/generate_provider_endpoints_support.py rename to litellm/scripts/generate_provider_endpoints_support.py index a2329ccded3..d9b3706bcd3 100644 --- a/scripts/generate_provider_endpoints_support.py +++ b/litellm/scripts/generate_provider_endpoints_support.py @@ -11,7 +11,7 @@ from typing import Final from pydantic import JsonValue, TypeAdapter -ROOT: Final = Path(__file__).resolve().parents[1] +ROOT: Final = Path(__file__).resolve().parents[2] COST_MAP_PATH: Final = ROOT / "model_prices_and_context_window.json" SOURCE_PATH: Final = ROOT / "provider_endpoints_support.json" OUTPUT_PATHS: Final = ( @@ -218,7 +218,7 @@ def generate_files( sys.stderr.write( "provider endpoint support is stale in " + ", ".join(str(path) for path in stale_paths) - + "\nRun `uv run --no-sync python scripts/generate_provider_endpoints_support.py` to regenerate it\n" + + "\nRun `uv run --no-sync python litellm/scripts/generate_provider_endpoints_support.py` to regenerate it\n" ) return 1 for path in output_paths: diff --git a/scripts/generate_trace_types.py b/litellm/scripts/generate_trace_types.py similarity index 97% rename from scripts/generate_trace_types.py rename to litellm/scripts/generate_trace_types.py index d1ab6e6bc3c..d3e6d8dd640 100644 --- a/scripts/generate_trace_types.py +++ b/litellm/scripts/generate_trace_types.py @@ -18,10 +18,10 @@ from typing import Final from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter -ROOT: Final = Path(__file__).resolve().parents[1] -TOOLING: Final = ROOT / "scripts/trace_codegen" +ROOT: Final = Path(__file__).resolve().parents[2] +TOOLING: Final = ROOT / "litellm/scripts/trace_codegen" GENERATED: Final = ROOT / "litellm/tracing/generated" -ASSETS: Final = ROOT / "scripts/lens_assets" +ASSETS: Final = ROOT / "litellm/scripts/lens_assets" SCHEMAS: Final = TypeAdapter(dict[str, dict[str, JsonValue]]) diff --git a/scripts/githooks/commit-msg b/litellm/scripts/githooks/commit-msg similarity index 100% rename from scripts/githooks/commit-msg rename to litellm/scripts/githooks/commit-msg diff --git a/scripts/health_check/benchmark_get_all_latest_health_checks.py b/litellm/scripts/health_check/benchmark_get_all_latest_health_checks.py similarity index 100% rename from scripts/health_check/benchmark_get_all_latest_health_checks.py rename to litellm/scripts/health_check/benchmark_get_all_latest_health_checks.py diff --git a/scripts/health_check/health_check_client.py b/litellm/scripts/health_check/health_check_client.py similarity index 100% rename from scripts/health_check/health_check_client.py rename to litellm/scripts/health_check/health_check_client.py diff --git a/scripts/health_check/health_check_client_README.md b/litellm/scripts/health_check/health_check_client_README.md similarity index 98% rename from scripts/health_check/health_check_client_README.md rename to litellm/scripts/health_check/health_check_client_README.md index 41c7ab7c35d..023fb6aba40 100644 --- a/scripts/health_check/health_check_client_README.md +++ b/litellm/scripts/health_check/health_check_client_README.md @@ -19,7 +19,7 @@ A health check tool for testing all configured models on a LiteLLM proxy. Tests ```bash export LITELLM_BASE_URL="https://litellm.example.com" export LITELLM_API_KEY="your-api-key" -python scripts/health_check/health_check_client.py +python litellm/scripts/health_check/health_check_client.py ``` **Option 2: Use YAML config file** @@ -27,7 +27,7 @@ python scripts/health_check/health_check_client.py export LITELLM_BASE_URL="https://litellm.example.com" export LITELLM_API_KEY="your-api-key" export LITELLM_MODELS_YAML="/path/to/config.yaml" -python scripts/health_check/health_check_client.py +python litellm/scripts/health_check/health_check_client.py ``` **Option 3: Use custom authentication header** @@ -35,7 +35,7 @@ python scripts/health_check/health_check_client.py export LITELLM_BASE_URL="https://litellm.example.com" export LITELLM_API_KEY="your-api-key" export LITELLM_CUSTOM_AUTH_HEADER="x-custom-auth-header" -python scripts/health_check/health_check_client.py +python litellm/scripts/health_check/health_check_client.py ``` ### As a Docker Container diff --git a/scripts/health_check/health_check_requirements.txt b/litellm/scripts/health_check/health_check_requirements.txt similarity index 100% rename from scripts/health_check/health_check_requirements.txt rename to litellm/scripts/health_check/health_check_requirements.txt diff --git a/scripts/health_check/run_parallel_health_checks.ps1 b/litellm/scripts/health_check/run_parallel_health_checks.ps1 similarity index 100% rename from scripts/health_check/run_parallel_health_checks.ps1 rename to litellm/scripts/health_check/run_parallel_health_checks.ps1 diff --git a/scripts/health_check/run_parallel_health_checks.sh b/litellm/scripts/health_check/run_parallel_health_checks.sh similarity index 100% rename from scripts/health_check/run_parallel_health_checks.sh rename to litellm/scripts/health_check/run_parallel_health_checks.sh diff --git a/scripts/install-cli.sh b/litellm/scripts/install-cli.sh similarity index 98% rename from scripts/install-cli.sh rename to litellm/scripts/install-cli.sh index 332bd559672..47584f8930a 100755 --- a/scripts/install-cli.sh +++ b/litellm/scripts/install-cli.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # LiteLLM CLI Installer (the thin `lite` client) -# Usage: curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/install-cli.sh | sh +# Usage: curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/scripts/install-cli.sh | sh # # Installs only litellm[cli]: the `lite` command for authenticating to a LiteLLM # proxy and running coding agents (lite claude / codex / opencode) through it. # None of the proxy server runtime is pulled in. To run a proxy server instead, -# use scripts/install.sh, which installs litellm[proxy]. +# use litellm/scripts/install.sh, which installs litellm[proxy]. # # Needs only curl: uv is bootstrapped if missing, and uv provisions a compatible # Python itself (honouring litellm's requires-python), downloading a managed one diff --git a/scripts/install.sh b/litellm/scripts/install.sh similarity index 99% rename from scripts/install.sh rename to litellm/scripts/install.sh index 275916d8a37..28c2ec5fe68 100755 --- a/scripts/install.sh +++ b/litellm/scripts/install.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # LiteLLM Installer -# Usage: curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/install.sh | sh +# Usage: curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/scripts/install.sh | sh # # Needs only curl: uv is bootstrapped if missing, and uv provisions a compatible # Python itself (reusing a suitable system one, else downloading a managed build). diff --git a/scripts/install_git_hooks.sh b/litellm/scripts/install_git_hooks.sh similarity index 81% rename from scripts/install_git_hooks.sh rename to litellm/scripts/install_git_hooks.sh index 35d5ce4c968..f8e53bc584b 100755 --- a/scripts/install_git_hooks.sh +++ b/litellm/scripts/install_git_hooks.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Install the repo's git hooks by pointing core.hooksPath at scripts/githooks. +# Install the repo's git hooks by pointing core.hooksPath at litellm/scripts/githooks. # # Idempotent: re-running just reaffirms the config and refreshes chmod bits. # Run from anywhere inside the repo. @@ -13,7 +13,7 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then fi repo_root=$(git rev-parse --show-toplevel) -hooks_dir="$repo_root/scripts/githooks" +hooks_dir="$repo_root/litellm/scripts/githooks" if [ ! -d "$hooks_dir" ]; then echo "install_git_hooks: $hooks_dir does not exist" >&2 @@ -24,11 +24,11 @@ fi # filesystems sometimes lose the exec bit; this normalizes it. chmod +x "$hooks_dir"/* 2>/dev/null || true -git config core.hooksPath scripts/githooks +git config core.hooksPath litellm/scripts/githooks cat < Iterator[bytes]: text = ( - "mock streaming: ok from scripts/mock_bedrock_passthrough_target.py " + "mock streaming: ok from litellm/scripts/mock_bedrock_passthrough_target.py " "(invoke-with-response-stream)." ) headers = { diff --git a/scripts/mock_grayswan_timeout_server.py b/litellm/scripts/mock_grayswan_timeout_server.py similarity index 96% rename from scripts/mock_grayswan_timeout_server.py rename to litellm/scripts/mock_grayswan_timeout_server.py index bc40e13dfa5..8fc20cff7dc 100644 --- a/scripts/mock_grayswan_timeout_server.py +++ b/litellm/scripts/mock_grayswan_timeout_server.py @@ -2,7 +2,7 @@ Minimal mock GraySwan monitor server that intentionally responds slowly. Usage: - python scripts/mock_grayswan_timeout_server.py --port 8787 --delay 35 + python litellm/scripts/mock_grayswan_timeout_server.py --port 8787 --delay 35 Point GRAYSWAN_API_BASE at http://127.0.0.1:8787 so the guardrail hits this endpoint and times out (the guardrail client has a 30s timeout). diff --git a/scripts/mutation_report.py b/litellm/scripts/mutation_report.py similarity index 99% rename from scripts/mutation_report.py rename to litellm/scripts/mutation_report.py index d0f9ddf0491..001318ccf9b 100644 --- a/scripts/mutation_report.py +++ b/litellm/scripts/mutation_report.py @@ -29,7 +29,7 @@ if sys.version_info >= (3, 11): else: import tomli as tomllib -ROOT = Path(__file__).resolve().parent.parent +ROOT = Path(__file__).resolve().parents[2] MUTMUT_INVOCATION = ["uv", "run", "--no-sync", "--with", "mutmut==3.5.0", "mutmut"] diff --git a/scripts/openapi_docs_gate.py b/litellm/scripts/openapi_docs_gate.py similarity index 91% rename from scripts/openapi_docs_gate.py rename to litellm/scripts/openapi_docs_gate.py index c53b96ae5d7..fb88f0ab361 100755 --- a/scripts/openapi_docs_gate.py +++ b/litellm/scripts/openapi_docs_gate.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 -"""Delta-vs-base gate for the OA* rules in scripts/check_openapi_docs.py. +"""Delta-vs-base gate for the OA* rules in litellm/scripts/check_openapi_docs.py. -Sibling of scripts/test_quality_gate.py, pointed at the proxy's OpenAPI document +Sibling of litellm/scripts/test_quality_gate.py, pointed at the proxy's OpenAPI document instead of a source tree. Each rule is counted over every POST operation the proxy serves at HEAD and at the merge-base with the branch this change merges into, and the gate fails only when a rule grew past the merge-base count, so a new endpoint has to ship with a request schema, a description and a 200 schema while the undocumented routes that already exist are only ever allowed to shrink. -The merge-base counts come from scripts/lint_base_counts.py: the disk cache, then the +The merge-base counts come from litellm/scripts/lint_base_counts.py: the disk cache, then the CI artifact published for that commit, then a pass of the current checker over a detached worktree at the merge-base with that tree first on PYTHONPATH. """ @@ -39,8 +39,8 @@ from lint_base_counts import ( sha256_of, ) -REPO_ROOT: Final = Path(__file__).resolve().parent.parent -CHECKER: Final = REPO_ROOT / "scripts" / "check_openapi_docs.py" +REPO_ROOT: Final = Path(__file__).resolve().parents[2] +CHECKER: Final = REPO_ROOT / "litellm/scripts" / "check_openapi_docs.py" TARGET: Final = "litellm" TERMINATION_SIGNALS: Final = (signal.SIGTERM, signal.SIGHUP) @@ -127,8 +127,8 @@ def base_counts(ref: str, repo_root: Path = REPO_ROOT, checker: Path = CHECKER) worktree: Final = parent / "wt" try: _run(["git", "worktree", "add", "--detach", str(worktree), ref], cwd=repo_root) - (worktree / "scripts").mkdir(parents=True, exist_ok=True) - base_checker: Final = worktree / "scripts" / "check_openapi_docs.py" + (worktree / "litellm/scripts").mkdir(parents=True, exist_ok=True) + base_checker: Final = worktree / "litellm/scripts" / "check_openapi_docs.py" shutil.copy(checker, base_checker) _install_base_native(worktree, repo_root) return count_by_rule(_check(worktree, base_checker)) @@ -171,7 +171,7 @@ def cmd_check(base: str) -> None: "`openapi_extra={'requestBody': inline_request_body(Model, example)}` when the handler reads " "`request.body()`), a docstring, and a 200 schema (`response_model=` or `responses={200: {'model': ...}}`), " "or document an equal number of existing routes; the ceiling is the merge-base count. Run " - "`python scripts/check_openapi_docs.py` to see every finding." + "`python litellm/scripts/check_openapi_docs.py` to see every finding." ) raise SystemExit(1) diff --git a/scripts/pre_commit_lint.sh b/litellm/scripts/pre_commit_lint.sh similarity index 94% rename from scripts/pre_commit_lint.sh rename to litellm/scripts/pre_commit_lint.sh index def604c88cb..e8fe0beb4d1 100755 --- a/scripts/pre_commit_lint.sh +++ b/litellm/scripts/pre_commit_lint.sh @@ -13,8 +13,8 @@ # - tests/e2e and tests/e2e_harness Python # -> `make lint-e2e-basedpyright` (test-linting.yml's e2e type-check step) # + raw HTTP client ban (test-linting.yml's check_e2e_no_raw_requests) -# - tests/ Python, .github/linters/ruff-tests.toml, scripts/check_test_quality.py, -# scripts/test_quality_gate.py +# - tests/ Python, .github/linters/ruff-tests.toml, litellm/scripts/check_test_quality.py, +# litellm/scripts/test_quality_gate.py # -> ruff over .github/linters/ruff-tests.toml + `make lint-test-quality` (test-linting.yml's # test-tree ruff and test-quality gate steps) # - dashboard -> prettier + eslint + lint budgets (test-linting.yml's ui job) @@ -23,14 +23,14 @@ # # Each block is skipped when no matching files are in scope, so unrelated commits # stay fast. This is intentionally not auto-installed as a git hook (see -# scripts/install_git_hooks.sh): the dashboard and basedpyright passes can take +# litellm/scripts/install_git_hooks.sh): the dashboard and basedpyright passes can take # minutes, so it's run on demand rather than firing on every human commit. It is # hook-compatible if you want that anyway: -# `ln -s ../../scripts/pre_commit_lint.sh .git/hooks/pre-commit`. +# `ln -s ../../litellm/scripts/pre_commit_lint.sh .git/hooks/pre-commit`. set -eu -# Queue for one of the machine-wide heavy-work slots (see scripts/gate_slot_lock.py) +# Queue for one of the machine-wide heavy-work slots (see litellm/scripts/gate_slot_lock.py) # before anything else, so N parallel `make check` runs across worktrees execute two # at a time instead of thrashing the machine. The wrapper exports # LITELLM_GATE_SLOT_HELD, so this re-exec happens exactly once and everything this @@ -98,21 +98,21 @@ existing_files() { litellm_py_pattern='^litellm/.*\.py$' e2e_py_pattern='^tests/e2e(_harness)?/.*\.py$' -test_tree_pattern='^(tests/.*\.py|\.github/linters/ruff-tests\.toml|pyproject\.toml|scripts/(check_test_quality|test_quality_gate)\.py)$' +test_tree_pattern='^(tests/.*\.py|\.github/linters/ruff-tests\.toml|pyproject\.toml|litellm/scripts/(check_test_quality|test_quality_gate)\.py)$' spec_pattern='^(litellm/.*|enterprise/.*|pyproject\.toml|uv\.lock|litellm/ui/litellm-dashboard/(scripts/gen-api-types\.mjs|package\.json|package-lock\.json|src/lib/http/schema\.d\.ts))$' ui_prettier_pattern='^litellm/ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs|json|css|scss|md|mdx|yml|yaml|html)$' ui_eslint_pattern='^litellm/ui/litellm-dashboard/.*\.(js|jsx|ts|tsx|mjs|cjs)$' -litellm_py_files=$(scope_match "$litellm_py_pattern" | grep -Ev '^litellm/(enterprise|ui|vscode-extension|rust|proxy-extras|packaging|cli)/' || true) -if [ -n "$(scope_match '^(litellm/proxy/mcp_server/|scripts/check_mcp_operation_boundary\.py)')" ]; then - uv run --no-sync python scripts/check_mcp_operation_boundary.py || exit 1 +litellm_py_files=$(scope_match "$litellm_py_pattern" | grep -Ev '^litellm/(enterprise|ui|vscode-extension|rust|proxy-extras|packaging|cli|scripts|runtime)/' || true) +if [ -n "$(scope_match '^(litellm/proxy/mcp_server/|litellm/scripts/check_mcp_operation_boundary\.py)')" ]; then + uv run --no-sync python litellm/scripts/check_mcp_operation_boundary.py || exit 1 fi e2e_py_files=$(scope_match "$e2e_py_pattern") test_tree_files=$(scope_match "$test_tree_pattern") # ruff format (and CI's format step) skip enterprise; the rest of make lint covers it. fmt_files=$(printf '%s\n' "$litellm_py_files" | grep -v '^litellm/enterprise/' | existing_files) spec_files=$({ - scope_match "$spec_pattern" | grep -Ev '^litellm/(ui|vscode-extension|rust|proxy-extras|packaging|cli)/' || true + scope_match "$spec_pattern" | grep -Ev '^litellm/(ui|vscode-extension|rust|proxy-extras|packaging|cli|scripts|runtime)/' || true scope_match '^litellm/ui/litellm-dashboard/(scripts/gen-api-types\.mjs|package\.json|package-lock\.json|src/lib/http/schema\.d\.ts)$' }) # CI's ui job runs prettier over a wider extension set than eslint; keep that @@ -271,7 +271,7 @@ genapi_checks() { echo "✗ The Python env lacks the proxy deps (orjson/prisma) that gen:api needs." >&2 bootstrap_hint status=1 - elif ! uv run --no-sync python scripts/prisma_generate_if_needed.py; then + elif ! uv run --no-sync python litellm/scripts/prisma_generate_if_needed.py; then echo "✗ Could not regenerate Prisma client (prisma generate failed)." >&2 status=1 elif ! uv run --no-sync python -m litellm.proxy._lazy_openapi_snapshot; then diff --git a/scripts/prisma_generate_if_needed.py b/litellm/scripts/prisma_generate_if_needed.py similarity index 98% rename from scripts/prisma_generate_if_needed.py rename to litellm/scripts/prisma_generate_if_needed.py index f2d2232b23f..fad9ad5d61d 100644 --- a/scripts/prisma_generate_if_needed.py +++ b/litellm/scripts/prisma_generate_if_needed.py @@ -26,7 +26,7 @@ import sys from collections.abc import Callable, Mapping from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parent.parent +REPO_ROOT = Path(__file__).resolve().parents[2] SCHEMA = REPO_ROOT / "litellm" / "proxy" / "schema.prisma" STAMP = Path(sys.prefix) / "litellm-prisma-schema.stamp" diff --git a/scripts/ruff_strict_gate.py b/litellm/scripts/ruff_strict_gate.py similarity index 97% rename from scripts/ruff_strict_gate.py rename to litellm/scripts/ruff_strict_gate.py index 248a76cea1d..3b73decce60 100644 --- a/scripts/ruff_strict_gate.py +++ b/litellm/scripts/ruff_strict_gate.py @@ -7,7 +7,7 @@ past the merge-base count, so a change is blamed for the violations it adds, never for drift that already sits in the base. There is no committed budget: the merge-base count is the ceiling, so it moves only when the base branch does. -The merge-base counts come from scripts/lint_base_counts.py: the disk cache, +The merge-base counts come from litellm/scripts/lint_base_counts.py: the disk cache, then the CI artifact published for that commit, then a ruff pass over a detached worktree at the merge-base under the current ruff configs. ``--emit-counts-dir`` writes HEAD's counts as the file that artifact is built @@ -36,7 +36,7 @@ from lint_base_counts import ( sha256_of, ) -REPO_ROOT: Final = Path(__file__).resolve().parent.parent +REPO_ROOT: Final = Path(__file__).resolve().parents[2] STRICT_CONFIG: Final = REPO_ROOT / ".github/linters/ruff-strict.toml" BASE_CONFIG: Final = REPO_ROOT / "pyproject.toml" TARGET: Final = "litellm" diff --git a/scripts/seed_request_logs.py b/litellm/scripts/seed_request_logs.py similarity index 99% rename from scripts/seed_request_logs.py rename to litellm/scripts/seed_request_logs.py index 1217a9fb7e4..d28aed18e10 100644 --- a/scripts/seed_request_logs.py +++ b/litellm/scripts/seed_request_logs.py @@ -15,7 +15,7 @@ from typing import TYPE_CHECKING, Final, Literal import httpx from pydantic import BaseModel, ConfigDict, JsonValue, TypeAdapter -from scripts.seed_tracing_fixtures import JSON_OBJECT, spend_fixtures +from litellm.scripts.seed_tracing_fixtures import JSON_OBJECT, spend_fixtures if TYPE_CHECKING: from prisma.types import LiteLLM_SpendLogsCreateWithoutRelationsInput diff --git a/scripts/seed_tracing_fixtures.py b/litellm/scripts/seed_tracing_fixtures.py similarity index 99% rename from scripts/seed_tracing_fixtures.py rename to litellm/scripts/seed_tracing_fixtures.py index c6f0570c17c..db64f7ff29c 100644 --- a/scripts/seed_tracing_fixtures.py +++ b/litellm/scripts/seed_tracing_fixtures.py @@ -35,9 +35,9 @@ if TYPE_CHECKING: from prisma import Prisma from prisma.types import LiteLLM_SpendLogsCreateWithoutRelationsInput -REPO_ROOT: Final = Path(__file__).resolve().parents[1] -TRACE_FIXTURES: Final = REPO_ROOT / "scripts/lens_assets/fixtures/traces" -SPEND_FIXTURES: Final = REPO_ROOT / "scripts/lens_assets/fixtures/spend" +REPO_ROOT: Final = Path(__file__).resolve().parents[2] +TRACE_FIXTURES: Final = REPO_ROOT / "litellm/scripts/lens_assets/fixtures/traces" +SPEND_FIXTURES: Final = REPO_ROOT / "litellm/scripts/lens_assets/fixtures/spend" JSON: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue) JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) SPEND_ROWS: Final = TypeAdapter(tuple[SpendLogRecord, ...]) diff --git a/scripts/sync-issue-labels.test.ts b/litellm/scripts/sync-issue-labels.test.ts similarity index 100% rename from scripts/sync-issue-labels.test.ts rename to litellm/scripts/sync-issue-labels.test.ts diff --git a/scripts/sync-issue-labels.ts b/litellm/scripts/sync-issue-labels.ts similarity index 100% rename from scripts/sync-issue-labels.ts rename to litellm/scripts/sync-issue-labels.ts diff --git a/scripts/test_agent_mcp_endpoints.sh b/litellm/scripts/test_agent_mcp_endpoints.sh similarity index 100% rename from scripts/test_agent_mcp_endpoints.sh rename to litellm/scripts/test_agent_mcp_endpoints.sh diff --git a/scripts/test_guardrails_register_endpoints.sh b/litellm/scripts/test_guardrails_register_endpoints.sh similarity index 100% rename from scripts/test_guardrails_register_endpoints.sh rename to litellm/scripts/test_guardrails_register_endpoints.sh diff --git a/scripts/test_quality_gate.py b/litellm/scripts/test_quality_gate.py similarity index 91% rename from scripts/test_quality_gate.py rename to litellm/scripts/test_quality_gate.py index a9ad60f088c..ea0785ba836 100644 --- a/scripts/test_quality_gate.py +++ b/litellm/scripts/test_quality_gate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -"""Delta-vs-base gate for the TQ* rules in scripts/check_test_quality.py. +"""Delta-vs-base gate for the TQ* rules in litellm/scripts/check_test_quality.py. -Sibling of scripts/type_discipline_gate.py, pointed at the test tree instead of +Sibling of litellm/scripts/type_discipline_gate.py, pointed at the test tree instead of the package. Each rule is counted across the whole `tests` tree at HEAD and at the merge-base with the branch this change merges into, and the gate fails only when a rule grew past the merge-base count, so a change is blamed for the @@ -9,7 +9,7 @@ violations it adds, never for drift that already exists in the base. There is no committed budget: the merge-base count is the ceiling, so it moves only when the base branch does. -The merge-base counts come from scripts/lint_base_counts.py: the disk cache, +The merge-base counts come from litellm/scripts/lint_base_counts.py: the disk cache, then the CI artifact published for that commit, then a pass of the current checker over a detached worktree at the merge-base, so a rule introduced on this branch is counted at the base too. ``--emit-counts-dir`` writes HEAD's @@ -41,8 +41,8 @@ from lint_base_counts import ( sha256_of, ) -REPO_ROOT: Final = Path(__file__).resolve().parent.parent -CHECKER: Final = REPO_ROOT / "scripts" / "check_test_quality.py" +REPO_ROOT: Final = Path(__file__).resolve().parents[2] +CHECKER: Final = REPO_ROOT / "litellm/scripts" / "check_test_quality.py" TARGET: Final = "tests" TERMINATION_SIGNALS: Final = (signal.SIGTERM, signal.SIGHUP) @@ -110,8 +110,8 @@ def base_counts(ref: str, repo_root: Path = REPO_ROOT, checker: Path = CHECKER) worktree: Final = parent / "wt" try: _run(["git", "worktree", "add", "--detach", str(worktree), ref], cwd=repo_root) - (worktree / "scripts").mkdir(parents=True, exist_ok=True) - base_checker: Final = worktree / "scripts" / "check_test_quality.py" + (worktree / "litellm/scripts").mkdir(parents=True, exist_ok=True) + base_checker: Final = worktree / "litellm/scripts" / "check_test_quality.py" shutil.copy(checker, base_checker) return count_by_rule(_check(worktree, base_checker)) finally: @@ -167,7 +167,7 @@ def cmd_check(base: str) -> None: print( "Fix the new violations, or give each one a reason (`# test-quality-ok: `), or remove an " "equal number elsewhere; the ceiling is the merge-base count. Run " - "`python scripts/check_test_quality.py tests/` to see every finding." + "`python litellm/scripts/check_test_quality.py tests/` to see every finding." ) raise SystemExit(1) diff --git a/scripts/test_tool_allowlist_script.py b/litellm/scripts/test_tool_allowlist_script.py similarity index 97% rename from scripts/test_tool_allowlist_script.py rename to litellm/scripts/test_tool_allowlist_script.py index 607a8c39473..13311c951ad 100644 --- a/scripts/test_tool_allowlist_script.py +++ b/litellm/scripts/test_tool_allowlist_script.py @@ -3,7 +3,7 @@ Standalone script to test tool allowlist enforcement and tool name extraction. Run from repo root: - uv run python scripts/test_tool_allowlist_script.py + uv run python litellm/scripts/test_tool_allowlist_script.py Or run the unit tests: uv run pytest tests/unit/proxy/test_tools_allowlist_enforcement.py -v @@ -14,7 +14,7 @@ import sys from pathlib import Path # Ensure repo root is on path -repo_root = Path(__file__).resolve().parent.parent +repo_root = Path(__file__).resolve().parents[2] if str(repo_root) not in sys.path: sys.path.insert(0, str(repo_root)) diff --git a/scripts/tpm_headline_test.sh b/litellm/scripts/tpm_headline_test.sh similarity index 98% rename from scripts/tpm_headline_test.sh rename to litellm/scripts/tpm_headline_test.sh index ef6750e2433..0ce8293bd15 100755 --- a/scripts/tpm_headline_test.sh +++ b/litellm/scripts/tpm_headline_test.sh @@ -15,7 +15,7 @@ # kubectl port-forward -n litellm svc/litellm-gateway 4000:4000 # # Run: -# bash scripts/tpm_headline_test.sh +# bash litellm/scripts/tpm_headline_test.sh set -u PROXY="${PROXY:-http://localhost:4000}" MASTER_KEY="${MASTER_KEY:-sk-perf-test-fixed-do-not-rotate}" diff --git a/scripts/trace_codegen/README.md b/litellm/scripts/trace_codegen/README.md similarity index 55% rename from scripts/trace_codegen/README.md rename to litellm/scripts/trace_codegen/README.md index 00ff931e4ce..f903491150e 100644 --- a/scripts/trace_codegen/README.md +++ b/litellm/scripts/trace_codegen/README.md @@ -1,8 +1,8 @@ -Run `uv run scripts/generate_trace_types.py` from the repository root to regenerate Python HTTP contracts. Run it with `--check` to verify the frozen Lens schemas and compare generated Python with committed files +Run `uv run litellm/scripts/generate_trace_types.py` from the repository root to regenerate Python HTTP contracts. Run it with `--check` to verify the frozen Lens schemas and compare generated Python with committed files -Lens owns the Rust contracts. `scripts/lens_assets/source.json` pins the Lens repository revision and SHA-256 of each schema and development fixture. The gateway consumes these data files offline; it does not compile or maintain a second Rust tracing implementation +Lens owns the Rust contracts. `litellm/scripts/lens_assets/source.json` pins the Lens repository revision and SHA-256 of each schema and development fixture. The gateway consumes these data files offline; it does not compile or maintain a second Rust tracing implementation -To update the pin, check out the intended Lens commit and run its `export-traces-schema` binary for the default, `--requests` and `--responses` modes, plus `export-traces-clickhouse-schema`, with the `schema` feature and locked dependencies. Import the consumed roots into the corresponding `scripts/lens_assets/schemas` group and copy fixtures from that same commit. Update the source revision and hashes together, then regenerate and run the gateway trace contract tests +To update the pin, check out the intended Lens commit and run its `export-traces-schema` binary for the default, `--requests` and `--responses` modes, plus `export-traces-clickhouse-schema`, with the `schema` feature and locked dependencies. Import the consumed roots into the corresponding `litellm/scripts/lens_assets/schemas` group and copy fixtures from that same commit. Update the source revision and hashes together, then regenerate and run the gateway trace contract tests The generator pins datamodel-code-generator through its inline dependency metadata. Neither it nor the schema exporter is a runtime dependency. The templates preserve frozen models, tuple conversion and bounded scalars. Request models retain each canonical schema's extra-field policy, including ignored unknown GET parameters and forbidden extra SQL body fields diff --git a/scripts/trace_codegen/config.json b/litellm/scripts/trace_codegen/config.json similarity index 89% rename from scripts/trace_codegen/config.json rename to litellm/scripts/trace_codegen/config.json index 03c0574eaef..7a1d10d624e 100644 --- a/scripts/trace_codegen/config.json +++ b/litellm/scripts/trace_codegen/config.json @@ -7,7 +7,7 @@ "3.10", "--disable-timestamp", "--custom-file-header", - "# @generated by scripts/generate_trace_types.py, do not edit", + "# @generated by litellm/scripts/generate_trace_types.py, do not edit", "--use-standard-collections", "--use-union-operator", "--enum-field-as-literal", diff --git a/scripts/trace_codegen/templates/ScalarTypeAliasType.jinja2 b/litellm/scripts/trace_codegen/templates/ScalarTypeAliasType.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/ScalarTypeAliasType.jinja2 rename to litellm/scripts/trace_codegen/templates/ScalarTypeAliasType.jinja2 diff --git a/scripts/trace_codegen/templates/TypeAliasType.jinja2 b/litellm/scripts/trace_codegen/templates/TypeAliasType.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/TypeAliasType.jinja2 rename to litellm/scripts/trace_codegen/templates/TypeAliasType.jinja2 diff --git a/scripts/trace_codegen/templates/TypedDictClass.jinja2 b/litellm/scripts/trace_codegen/templates/TypedDictClass.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/TypedDictClass.jinja2 rename to litellm/scripts/trace_codegen/templates/TypedDictClass.jinja2 diff --git a/scripts/trace_codegen/templates/pydantic_v2/BaseModel.jinja2 b/litellm/scripts/trace_codegen/templates/pydantic_v2/BaseModel.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/pydantic_v2/BaseModel.jinja2 rename to litellm/scripts/trace_codegen/templates/pydantic_v2/BaseModel.jinja2 diff --git a/scripts/trace_codegen/templates/pydantic_v2/types.jinja2 b/litellm/scripts/trace_codegen/templates/pydantic_v2/types.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/pydantic_v2/types.jinja2 rename to litellm/scripts/trace_codegen/templates/pydantic_v2/types.jinja2 diff --git a/scripts/trace_codegen/templates/types.jinja2 b/litellm/scripts/trace_codegen/templates/types.jinja2 similarity index 100% rename from scripts/trace_codegen/templates/types.jinja2 rename to litellm/scripts/trace_codegen/templates/types.jinja2 diff --git a/scripts/type_check_gate.py b/litellm/scripts/type_check_gate.py similarity index 97% rename from scripts/type_check_gate.py rename to litellm/scripts/type_check_gate.py index 874fda013be..ef5736a0709 100644 --- a/scripts/type_check_gate.py +++ b/litellm/scripts/type_check_gate.py @@ -32,7 +32,7 @@ process OOMs at the ~4 GB default, and when callers had to remember the flag, every hand-copied pipeline (Makefile, CI, a dev running the recipe by hand) was one forgotten env line away from an 80-second crash. The base pass is a second basedpyright run over a detached worktree at the merge-base, under the -same environment so import resolution matches, and scripts/lint_base_counts.py +same environment so import resolution matches, and litellm/scripts/lint_base_counts.py spares it whenever it can: the per-rule counts are cached under the repo's git common dir keyed by merge-base commit, ``pyproject.toml``, ``uv.lock``, the Prisma schema, and the dependency-group set, and on a disk-cache miss the @@ -76,7 +76,7 @@ from lint_base_counts import ( resolve_base_point, ) -REPO_ROOT = Path(__file__).resolve().parent.parent +REPO_ROOT = Path(__file__).resolve().parents[2] PYRIGHT_CONFIG = REPO_ROOT / "pyproject.toml" UV_LOCK = REPO_ROOT / "uv.lock" @@ -87,7 +87,7 @@ UV_LOCK = REPO_ROOT / "uv.lock" # matched. TYPECHECK_ENV_DIR = REPO_ROOT / ".venv-typecheck" TYPECHECK_DEP_GROUPS = ("proxy-dev", "e2e-dev") -PRISMA_GENERATE_SCRIPT = REPO_ROOT / "scripts" / "prisma_generate_if_needed.py" +PRISMA_GENERATE_SCRIPT = REPO_ROOT / "litellm/scripts" / "prisma_generate_if_needed.py" PRISMA_SCHEMA = REPO_ROOT / "litellm" / "proxy" / "schema.prisma" # basedpyright's node process needs more than the ~4 GB default heap on this @@ -310,7 +310,7 @@ def judge(head: Mapping[str, int], base: Mapping[str, int], base_point: str) -> print(f" {breach.rule}: total {breach.total} over ceiling {breach.ceiling} (this change added {breach.added})") print( "Reduce the new errors or remove an equal number elsewhere; the ceiling is the merge-base " - "count, or the cap in ANY_CAPS (scripts/type_check_gate.py) when that is higher." + "count, or the cap in ANY_CAPS (litellm/scripts/type_check_gate.py) when that is higher." ) summary: Final = "; ".join(f"{b.rule} {b.total}/{b.ceiling} (+{b.added})" for b in breaches) print(f"BREACHED RULES: {summary}") diff --git a/scripts/type_discipline_gate.py b/litellm/scripts/type_discipline_gate.py similarity index 93% rename from scripts/type_discipline_gate.py rename to litellm/scripts/type_discipline_gate.py index 5e6745c041d..f60b7602811 100644 --- a/scripts/type_discipline_gate.py +++ b/litellm/scripts/type_discipline_gate.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 -"""Delta-vs-base gate for the LIT* rules in scripts/check_type_discipline.py. +"""Delta-vs-base gate for the LIT* rules in litellm/scripts/check_type_discipline.py. -Sibling of scripts/ruff_strict_gate.py. Each rule is counted across the whole +Sibling of litellm/scripts/ruff_strict_gate.py. Each rule is counted across the whole `litellm` tree at HEAD and at the merge-base with the branch this change merges into, and the gate fails only when a rule grew past the merge-base count, so a change is blamed for the violations it adds, never for drift that already exists @@ -23,7 +23,7 @@ violating comprehension spanning it and to any single-line violating comprehension on that line), and LIT015 (pydantic model not frozen; suppress with `# frozen-ok: `). -The merge-base counts come from scripts/lint_base_counts.py: the disk cache, +The merge-base counts come from litellm/scripts/lint_base_counts.py: the disk cache, then the CI artifact published for that commit, then a pass of the current checker over a detached worktree at the merge-base, so a rule change on this branch is measured on both sides. ``--emit-counts-dir`` writes HEAD's counts @@ -51,8 +51,8 @@ from lint_base_counts import ( sha256_of, ) -REPO_ROOT: Final = Path(__file__).resolve().parent.parent -CHECKER: Final = REPO_ROOT / "scripts" / "check_type_discipline.py" +REPO_ROOT: Final = Path(__file__).resolve().parents[2] +CHECKER: Final = REPO_ROOT / "litellm/scripts" / "check_type_discipline.py" TARGET: Final = "litellm" _HUNK: Final = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") @@ -108,8 +108,8 @@ def base_counts(ref: str) -> dict[str, int]: try: _run(["git", "worktree", "add", "--detach", str(worktree), ref]) # Measure the base with the *current* rule logic, not whatever shipped at base. - (worktree / "scripts").mkdir(parents=True, exist_ok=True) - checker: Final = worktree / "scripts" / "check_type_discipline.py" + (worktree / "litellm/scripts").mkdir(parents=True, exist_ok=True) + checker: Final = worktree / "litellm/scripts" / "check_type_discipline.py" shutil.copy(CHECKER, checker) return count_by_rule(_check(worktree, checker)) finally: diff --git a/scripts/verify_adaptive_router.py b/litellm/scripts/verify_adaptive_router.py similarity index 99% rename from scripts/verify_adaptive_router.py rename to litellm/scripts/verify_adaptive_router.py index fde9dc51a15..aa2522b87d4 100644 --- a/scripts/verify_adaptive_router.py +++ b/litellm/scripts/verify_adaptive_router.py @@ -11,7 +11,7 @@ Requires: * "smart" (expensive, higher quality) Run: - uv run python scripts/verify_adaptive_router.py + uv run python litellm/scripts/verify_adaptive_router.py Optional env: LITELLM_PROXY_URL (default: http://localhost:4000) diff --git a/scripts/with_dashboard_node.sh b/litellm/scripts/with_dashboard_node.sh similarity index 97% rename from scripts/with_dashboard_node.sh rename to litellm/scripts/with_dashboard_node.sh index 106c3243ce7..934ce5e78a1 100755 --- a/scripts/with_dashboard_node.sh +++ b/litellm/scripts/with_dashboard_node.sh @@ -3,7 +3,7 @@ set -eu [ $# -gt 0 ] || { echo "usage: $0 [args...]" >&2; exit 2; } -repo_root=$(cd "$(dirname "$0")/.." && pwd) +repo_root=$(cd "$(dirname "$0")/../.." && pwd) dashboard="$repo_root/litellm/ui/litellm-dashboard" floor=$(sed -n 's/.*"node": *">=\([0-9][0-9.]*\)".*/\1/p' "$dashboard/package.json") pinned=$(tr -d '[:space:]' < "$dashboard/.nvmrc") diff --git a/litellm/tracing/AGENTS.md b/litellm/tracing/AGENTS.md index 397252be7df..7a23b694b06 100644 --- a/litellm/tracing/AGENTS.md +++ b/litellm/tracing/AGENTS.md @@ -3,7 +3,7 @@ - Lens owns trace normalization, schemas, storage, graph assembly and querying in `BerriAI/lens` - This package owns the HTTP client, gateway response validation and asynchronous gateway spend export. Keep the trace reader remote-only - Gateway endpoints preserve authenticated user/team scope, trace references, pagination and public error categories -- `generated/` comes from `scripts/generate_trace_types.py` using the Lens schemas pinned in `scripts/lens_assets/source.json`. Update the canonical Lens Rust contracts and import their schema outputs before regenerating. Never edit generated Python +- `generated/` comes from `litellm/scripts/generate_trace_types.py` using the Lens schemas pinned in `litellm/scripts/lens_assets/source.json`. Update the canonical Lens Rust contracts and import their schema outputs before regenerating. Never edit generated Python - Keep the pinned schema and fixture checksums current. CI verifies assets and regenerated Python without a Rust trace implementation in this repository - Independent gateway ClickHouse spend logging lives in `litellm/integrations/clickhouse`, using `litellm/rust_bridge/clickhouse.py` and the native spend writer - OTLP upload routes only return setup guidance. Keep their JSON/protobuf error encoding without restoring ingestion or a native trace fallback diff --git a/litellm/tracing/generated/models.py b/litellm/tracing/generated/models.py index af2fb3e2211..0273ca6dc67 100644 --- a/litellm/tracing/generated/models.py +++ b/litellm/tracing/generated/models.py @@ -1,4 +1,4 @@ -# @generated by scripts/generate_trace_types.py, do not edit +# @generated by litellm/scripts/generate_trace_types.py, do not edit from __future__ import annotations diff --git a/litellm/tracing/generated/requests.py b/litellm/tracing/generated/requests.py index f0c9cbc2d4e..331572b85c9 100644 --- a/litellm/tracing/generated/requests.py +++ b/litellm/tracing/generated/requests.py @@ -1,4 +1,4 @@ -# @generated by scripts/generate_trace_types.py, do not edit +# @generated by litellm/scripts/generate_trace_types.py, do not edit from __future__ import annotations diff --git a/litellm/tracing/generated/responses.py b/litellm/tracing/generated/responses.py index 38dee46b305..0b2527b477f 100644 --- a/litellm/tracing/generated/responses.py +++ b/litellm/tracing/generated/responses.py @@ -1,4 +1,4 @@ -# @generated by scripts/generate_trace_types.py, do not edit +# @generated by litellm/scripts/generate_trace_types.py, do not edit from __future__ import annotations diff --git a/litellm/tracing/generated/types.py b/litellm/tracing/generated/types.py index ed2f90391be..ca139e1dcf6 100644 --- a/litellm/tracing/generated/types.py +++ b/litellm/tracing/generated/types.py @@ -1,4 +1,4 @@ -# @generated by scripts/generate_trace_types.py, do not edit +# @generated by litellm/scripts/generate_trace_types.py, do not edit from __future__ import annotations diff --git a/litellm/types/interactions/README.md b/litellm/types/interactions/README.md index 45783d604db..e2b4369654b 100644 --- a/litellm/types/interactions/README.md +++ b/litellm/types/interactions/README.md @@ -31,7 +31,7 @@ uv tool run --from datamodel-code-generator==0.66.0 datamodel-codegen \ --base-class litellm.types.llms.base.LiteLLMBaseModel ``` -The pin is the same `datamodel-code-generator` version `scripts/generate_trace_types.py` uses. The generator names +The pin is the same `datamodel-code-generator` version `litellm/scripts/generate_trace_types.py` uses. The generator names inline enums by order of appearance (`Status`, `Status1`, `Status2`, ...), so a regeneration can rename them. The compliance tests resolve each status enum through its model's `status` field, but `tests/unit/interactions/test_background_cost_polling.py` and diff --git a/litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts b/litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts index 0266c03c619..d38408e085d 100644 --- a/litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/litellm/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -31476,7 +31476,7 @@ export interface components { use_spend_logs_ledger: boolean; /** * Use Spend Logs Partitioning - * @description If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False. + * @description If True and LiteLLM_SpendLogs has been converted to a range-partitioned table (litellm/scripts/db/partition_spend_logs.sql), retention cleanup drops expired partitions instead of deleting rows, and pre-creates upcoming partitions. Default is False. */ use_spend_logs_partitioning?: boolean | null; /** diff --git a/pyproject.toml b/pyproject.toml index a1dc7516747..f2586bfc0a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -354,6 +354,11 @@ include = [ { path = "model_prices_and_context_window.json", format = "sdist" }, ] exclude = [ + "litellm/scripts", + "litellm/scripts/**", + "litellm/runtime", + "litellm/runtime/**", + "litellm/schema.prisma", "litellm/enterprise", "litellm/enterprise/**", "litellm/proxy-extras", @@ -437,8 +442,8 @@ version_files = [ ] [tool.pytest.ini_options] -norecursedirs = ["*.egg", ".*", "_darcs", "build", "CVS", "dist", "node_modules", "venv", "{arch}", "**/litellm/ui", "**/litellm/vscode-extension", "**/litellm/enterprise", "**/litellm/proxy-extras", "**/litellm/packaging", "**/litellm/rust"] -pythonpath = ["scripts"] +norecursedirs = ["*.egg", ".*", "_darcs", "build", "CVS", "dist", "node_modules", "venv", "{arch}", "**/litellm/ui", "**/litellm/vscode-extension", "**/litellm/enterprise", "**/litellm/proxy-extras", "**/litellm/packaging", "**/litellm/rust", "**/litellm/scripts", "**/litellm/runtime"] +pythonpath = ["litellm/scripts"] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" markers = [ @@ -523,7 +528,7 @@ lint.extend-select = [ # so RUF100 leaves their directives alone: the strict gate (.github/linters/ruff-strict.toml) and upstream # litellm's own ruff config both rely on suppressions this config can't see. lint.external = [ - # Enforced by the strict-rule gate (scripts/ruff_strict_gate.py + .github/linters/ruff-strict.toml). + # Enforced by the strict-rule gate (litellm/scripts/ruff_strict_gate.py + .github/linters/ruff-strict.toml). # Family entries whose every strict rule graduated into extend-select above (FURB), and # standalone graduated codes (I001, RUF010, RUF022, RUF023, RUF051), are dropped so this # config's RUF100 polices their directives itself. @@ -535,7 +540,7 @@ lint.external = [ ] line-length = 120 -extend-exclude = ["litellm/deploy", "litellm/ui", "litellm/vscode-extension", "litellm/enterprise", "litellm/proxy-extras", "litellm/packaging", "litellm/rust", "litellm/cli", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"] +extend-exclude = ["litellm/scripts", "litellm/runtime", "litellm/deploy", "litellm/ui", "litellm/vscode-extension", "litellm/enterprise", "litellm/proxy-extras", "litellm/packaging", "litellm/rust", "litellm/cli", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"] format.exclude = ["**/enterprise/**"] @@ -567,7 +572,7 @@ banned-module-level-imports = ["prisma"] [tool.pyright] include = ["litellm"] ignore = [] -exclude = ["**/node_modules", "**/__pycache__", "tests/e2e/claude_code", "tests/e2e_harness/claude_code", "tests/e2e/ui", "litellm/types/utils.py", "litellm/proxy/_types.py", "litellm/deploy", "litellm/ui", "litellm/vscode-extension", "litellm/enterprise", "litellm/proxy-extras", "litellm/packaging", "litellm/rust", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"] +exclude = ["**/node_modules", "**/__pycache__", "tests/e2e/claude_code", "tests/e2e_harness/claude_code", "tests/e2e/ui", "litellm/types/utils.py", "litellm/proxy/_types.py", "litellm/scripts", "litellm/runtime", "litellm/deploy", "litellm/ui", "litellm/vscode-extension", "litellm/enterprise", "litellm/proxy-extras", "litellm/packaging", "litellm/rust", "litellm/proxy/discovery_endpoints/skill_scanner_adapter"] pythonVersion = "3.12" typeCheckingMode = "strict" enableTypeIgnoreComments = false diff --git a/scripts/ci_cd/baseline_db.py b/scripts/ci_cd/baseline_db.py deleted file mode 100644 index 3fb9203b6a7..00000000000 --- a/scripts/ci_cd/baseline_db.py +++ /dev/null @@ -1,60 +0,0 @@ -import subprocess -from pathlib import Path -from datetime import datetime - - -def create_baseline(): - """Create baseline migration in deploy/migrations""" - try: - # Get paths - root_dir = Path(__file__).resolve().parents[2] - deploy_dir = root_dir / "deploy" - migrations_dir = deploy_dir / "migrations" - schema_path = root_dir / "schema.prisma" - - # Create migrations directory - migrations_dir.mkdir(parents=True, exist_ok=True) - - # Create migration_lock.toml if it doesn't exist - lock_file = migrations_dir / "migration_lock.toml" - if not lock_file.exists(): - lock_file.write_text('provider = "postgresql"\n') - - # Create timestamp-based migration directory - timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - migration_dir = migrations_dir / f"{timestamp}_baseline" - migration_dir.mkdir(parents=True, exist_ok=True) - - # Generate migration SQL - result = subprocess.run( - [ - "prisma", - "migrate", - "diff", - "--from-empty", - "--to-schema-datamodel", - str(schema_path), - "--script", - ], - capture_output=True, - text=True, - check=True, - ) - - # Write the SQL to migration.sql - migration_file = migration_dir / "migration.sql" - migration_file.write_text(result.stdout) - - print(f"Created baseline migration in {migration_dir}") - return True - - except subprocess.CalledProcessError as e: - print(f"Error running prisma command: {e.stderr}") - return False - except Exception as e: - print(f"Error creating baseline migration: {str(e)}") - return False - - -if __name__ == "__main__": - create_baseline() diff --git a/scripts/quickstart.ps1 b/scripts/quickstart.ps1 deleted file mode 100644 index 9d1d65f959e..00000000000 --- a/scripts/quickstart.ps1 +++ /dev/null @@ -1,1322 +0,0 @@ -# LiteLLM Gateway quickstart for Windows: the gateway, Postgres, and the admin UI in one command. -# powershell -c "irm https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.ps1 | iex" -# On macOS and Linux, scripts/quickstart.sh does the same. -# -# To read it before running it: -# irm https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.ps1 -OutFile quickstart.ps1 -# notepad quickstart.ps1 -# powershell -ExecutionPolicy Bypass -File quickstart.ps1 -# -# Asks where to keep the files, then offers to copy the admin password and -# open the admin UI; every question has a default you accept by pressing Enter. -# Run it again for a menu to open, stop, start, update, or remove the install. -# It asks nothing when input is not a console, under CI or Claude Code, or -# with -Yes. -# -# It also takes an action, which the litellm-gateway command it installs takes too: -# & ([scriptblock]::Create((irm https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.ps1))) stop -# status, start (up), stop (down), restart, logs, open, update, uninstall -# -# -Yes, or LITELLM_YES=1 no questions: install to ~\litellm-gateway, don't open a browser, -# and let uninstall delete the database without asking -# LITELLM_DIR folder to install into (skips the folder question) -# LITELLM_PORT port for the gateway (default 4000, or the next free one) -# NO_COLOR plain output, which agents, CI, and log files always get -# -# New installs listen on this machine only (127.0.0.1). To reach the gateway -# from other machines, remove LITELLM_BIND from .env and put it behind TLS. -# -# Keys and the database password are random, written only to .env, which only -# your Windows account can read, and never printed. When you ask, the admin -# password goes straight to your clipboard. Needs Docker Desktop, Podman, or -# Rancher Desktop. -# Works in Windows PowerShell 5.1 and PowerShell 7. Everything runs inside -# Invoke-LiteLLMQuickstart, so a partial download runs nothing. -param([switch]$Yes) - -function Invoke-LiteLLMQuickstart { - param([switch]$Yes, [object[]]$Rest = @()) - $ErrorActionPreference = 'Stop' - # Windows PowerShell draws a progress bar for every web request, which makes them crawl. - $ProgressPreference = 'SilentlyContinue' - Set-StrictMode -Version 2 - - $composeUrl = 'https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/deploy/docker/docker-compose.quickstart.yml' - if ($env:LITELLM_COMPOSE_URL) { $composeUrl = $env:LITELLM_COMPOSE_URL } - $scriptUrl = 'https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.ps1' - if ($env:LITELLM_SCRIPT_URL) { $scriptUrl = $env:LITELLM_SCRIPT_URL } - $onWindows = ($PSVersionTable.PSEdition -eq 'Desktop') -or ((Test-Path variable:IsWindows) -and $IsWindows) - - # A step that cannot go on stops the whole run with exit code 1, from any depth. - $exitSignal = 'LITELLM_QUICKSTART_EXIT' - function Stop-Quickstart { throw $exitSignal } - - function Show-Usage([IO.TextWriter]$To) { - @( - 'Usage: quickstart.ps1 [-Yes] [action]' - '' - 'With no action it installs the LiteLLM Gateway, or shows a menu to manage the' - 'install it finds. Actions:' - ' status whether it is running, and where' - ' start start it (also: up)' - ' stop stop it; models, keys, and logs are kept (also: down)' - ' restart stop it and start it again' - " logs show the gateway's logs" - ' open open the admin UI in your browser' - ' update download the latest image and restart on it' - ' uninstall remove its containers, its database, and its files' - ' help show this help (also: -h, --help)' - '' - ' -Yes, -y ask nothing; without a console, uninstall needs it' - '' - 'From the install command:' - ' & ([scriptblock]::Create((irm https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.ps1))) status' - ) | ForEach-Object { $To.WriteLine($_) } - } - - # Windows PowerShell passes -y and --yes through as plain arguments. - $noQuestions = [bool]$Yes - # A port set by the caller is never moved; later steps set LITELLM_PORT too. - $portPinned = [bool]$env:LITELLM_PORT - $action = '' - foreach ($arg in $Rest) { - $a = "$arg" - if ($a -in '-y', '--yes', '-yes') { - $noQuestions = $true - } elseif ($a -in '-h', '--help', '-help', 'help') { - Show-Usage ([Console]::Out) - return 0 - } elseif ($a -in 'status', 'start', 'up', 'stop', 'down', 'restart', 'logs', 'open', 'update', 'uninstall') { - if ($action) { - [Console]::Error.WriteLine("Choose one action, not both $action and $a") - return 1 - } - $action = $a.ToLowerInvariant() - } else { - [Console]::Error.WriteLine("Unknown option: $a") - Show-Usage ([Console]::Error) - return 1 - } - } - if ($action -eq 'up') { $action = 'start' } elseif ($action -eq 'down') { $action = 'stop' } - if ($env:LITELLM_YES) { $noQuestions = $true } - - # ------------------------------------------------------------ output - - # A person at a console gets colors, step marks, and a spinner. Agents, CI, - # log files, and NO_COLOR get the same lines as plain text. - $style = (-not [Console]::IsOutputRedirected) -and (-not $env:NO_COLOR) -and (-not $env:CI) -and - (-not $env:CLAUDECODE) -and ($env:TERM -ne 'dumb') - # Escape sequences give the brand blue and bold; older consoles get the - # sixteen console colors instead. - $vt = $style -and $Host.UI.PSObject.Properties['SupportsVirtualTerminal'] -and $Host.UI.SupportsVirtualTerminal - # Symbols need a console font that has them: Windows Terminal, VS Code, or - # any macOS or Linux terminal. The classic console gets plain ASCII. - $unicode = $style -and ((-not $onWindows) -or $env:WT_SESSION -or ($env:TERM_PROGRAM -eq 'vscode')) - if ($unicode) { - $sym = @{ Ok = [char]0x2713; Warn = '!'; Err = [char]0x2717; Head = [char]0x25C6; Ask = '?'; Pointer = [char]0x276F - Frames = @([char]0x280B, [char]0x2819, [char]0x2839, [char]0x2838, [char]0x283C, [char]0x2834, [char]0x2826, [char]0x2827, [char]0x2807, [char]0x280F) - Hint = "$([char]0x2191)/$([char]0x2193) to move, Enter to choose"; Dot = [char]0x00B7 - TL = [char]0x256D; TR = [char]0x256E; BL = [char]0x2570; BR = [char]0x256F; H = [char]0x2500; V = [char]0x2502 } - } else { - $sym = @{ Ok = '+'; Warn = '!'; Err = 'x'; Head = '*'; Ask = '?'; Pointer = '>'; Frames = @('|', '/', '-', '\') - Hint = 'Up/Down to move, Enter to choose'; Dot = '-'; TL = '+'; TR = '+'; BL = '+'; BR = '+'; H = '-'; V = '|' } - } - $e = [char]27 - $ansi = @{ a = "$e[38;2;91;108;255m"; b = "$e[1m"; d = "$e[90m"; g = "$e[32m"; y = "$e[33m"; r = "$e[1;31m"; u = "$e[1;38;2;91;108;255m"; off = "$e[0m" } - $colors = @{ a = 'Blue'; b = $null; d = 'DarkGray'; g = 'Green'; y = 'Yellow'; r = 'Red'; u = 'Blue' } - - # Say "text with {a:accent} {b:bold} {d:dim} {g:green} {y:yellow} {r:red} {u:link} spans" - function Say { - param([string]$Text, [switch]$Err, [switch]$NoNewline) - $parts = [regex]::Split($Text, '(\{[abdgyru]:[^}]*\})') - # Errors go out plain when stderr is redirected, so a log file gets no escape codes. - if (-not $style -or ($Err -and [Console]::IsErrorRedirected)) { - $plain = ($parts | ForEach-Object { if ($_ -match '^\{[abdgyru]:(.*)\}$') { $Matches[1] } else { $_ } }) -join '' - $stream = if ($Err) { [Console]::Error } else { [Console]::Out } - if ($NoNewline) { $stream.Write($plain) } else { $stream.WriteLine($plain) } - return - } - if ($vt) { - $line = ($parts | ForEach-Object { if ($_ -match '^\{([abdgyru]):(.*)\}$') { $ansi[$Matches[1]] + $Matches[2] + $ansi.off } else { $_ } }) -join '' - Write-Host $line -NoNewline:$NoNewline - return - } - foreach ($p in $parts) { - if ($p -match '^\{([abdgyru]):(.*)\}$') { - $c = $colors[$Matches[1]] - if ($c) { Write-Host $Matches[2] -NoNewline -ForegroundColor $c } else { Write-Host $Matches[2] -NoNewline } - } elseif ($p) { Write-Host $p -NoNewline } - } - if (-not $NoNewline) { Write-Host '' } - } - # Markup characters in a value (a path, a URL) must not open a span. - function Lit([string]$s) { return $s.Replace('{', '(').Replace('}', ')') } - function Step([string]$Text) { if ($style) { Say ("{g:$($sym.Ok)} " + $Text) } else { Say $Text } } - function Warn([string]$Text) { if ($style) { Say ("{y:$($sym.Warn)} " + $Text) } else { Say $Text } } - function Fail([string]$Head, [string]$Rest = '') { - $tail = if ($Rest) { " $Rest" } else { '' } - if ($style) { Say ("{r:$($sym.Err) $Head}" + $tail) -Err } else { Say ($Head + $tail) -Err } - } - function Tildify([string]$Path) { - if ($Path -eq $HOME) { return '~' } - if ($Path.StartsWith($HOME + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { return '~' + $Path.Substring($HOME.Length) } - return $Path - } - function Elapsed([datetime]$Since) { - $s = [int]((Get-Date) - $Since).TotalSeconds - if ($s -lt 60) { return "${s}s" } else { return "$([math]::Floor($s / 60))m $($s % 60)s" } - } - function Clear-Line { - if ($vt) { Write-Host "`r$e[2K" -NoNewline } else { Write-Host ("`r" + (' ' * ([Console]::WindowWidth - 1)) + "`r") -NoNewline } - } - function Show-Cursor([bool]$Shown) { try { [Console]::CursorVisible = $Shown } catch { <# a host without a cursor #> } } - - # Run a native program; returns its exit code and stdout lines. stderr is - # dropped so Windows PowerShell does not turn it into a terminating error. - function Invoke-Native { - param([string]$File, [string[]]$Arguments) - $prev = $ErrorActionPreference - $ErrorActionPreference = 'Continue' - try { - $out = & $File @Arguments 2>$null - $code = $LASTEXITCODE - } catch { - $out = @(); $code = 1 - } finally { - $ErrorActionPreference = $prev - } - return [pscustomobject]@{ Code = $code; Out = @($out) } - } - - # Run a native program with its output on the console, never in this - # function's results; returns its exit code. - function Invoke-Shown { - param([string]$File, [string[]]$Arguments) - $prev = $ErrorActionPreference - $ErrorActionPreference = 'Continue' - try { & $File @Arguments 2>&1 | ForEach-Object { [Console]::Out.WriteLine("$_") }; return $LASTEXITCODE } - finally { $ErrorActionPreference = $prev } - } - - # Write a text file as UTF-8 without a byte order mark, with LF line endings, - # which is what Docker Compose and git expect. - function Write-PlainText([string]$Path, [string]$Text, [switch]$Append) { - $enc = New-Object System.Text.UTF8Encoding $false - if ($Append) { [IO.File]::AppendAllText($Path, $Text, $enc) } else { [IO.File]::WriteAllText($Path, $Text, $enc) } - } - - # The POSIX cksum of a string's UTF-8 bytes: CRC-32 over the bytes and then - # their length. quickstart.sh names projects with the same number, so both - # scripts find the same database for a folder. Int64 keeps the arithmetic - # unsigned; hex literals with the top bit set would be negative here. - function Get-Cksum([string]$Text) { - $bytes = New-Object System.Collections.Generic.List[byte] - $bytes.AddRange([Text.Encoding]::UTF8.GetBytes($Text)) - for ($n = [int64]$bytes.Count; $n -gt 0; $n = $n -shr 8) { $bytes.Add([byte]($n -band 255)) } - [int64]$crc = 0 - foreach ($b in $bytes) { - $crc = $crc -bxor ([int64]$b -shl 24) - for ($i = 0; $i -lt 8; $i++) { - $crc = if ($crc -band 2147483648) { (($crc -shl 1) -bxor 79764919) -band 4294967295 } else { ($crc -shl 1) -band 4294967295 } - } - } - return (-bnot $crc) -band 4294967295 - } - - # ------------------------------------------------------------ questions - - $interactive = (-not $noQuestions) -and (-not $env:CI) -and (-not $env:CLAUDECODE) -and - (-not [Console]::IsInputRedirected) -and [Environment]::UserInteractive - - # Menu "Question" Default @(@('label', 'note'), ...) -> the 1-based pick. - # ReadKey holds on to Ctrl+C, so take it as a key and stop the way PowerShell - # does; finally blocks still run and put the terminal back. - function Read-Key { - $saved = [Console]::TreatControlCAsInput - try { - [Console]::TreatControlCAsInput = $true - $key = [Console]::ReadKey($true) - } finally { - [Console]::TreatControlCAsInput = $saved - } - if ($key.Key -eq 'C' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { - throw [System.Management.Automation.PipelineStoppedException]::new() - } - return $key - } - - function Menu { - param([string]$Question, [int]$Default, [object[]]$Options) - if (-not $interactive) { return $Default } - $count = $Options.Count - $pad = ($Options | ForEach-Object { $_[0].Length } | Measure-Object -Maximum).Maximum - Write-Host '' - if ($style) { Say ("{a:$($sym.Ask)} {b:" + (Lit $Question) + '}') } else { Write-Host $Question } - $choice = $Default - try { - Show-Cursor $false - $first = $true - $top = 0 - while ($true) { - # Escape-code consoles move up relative to the cursor; the classic - # Windows console jumps back to the row the menu started on. - if (-not $first) { if ($vt) { Write-Host "$e[$($count + 1)A" -NoNewline } else { [Console]::SetCursorPosition(0, $top) } } - $first = $false - for ($i = 1; $i -le $count; $i++) { - $label = (Lit $Options[$i - 1][0]).PadRight($pad) - $note = if ($Options[$i - 1].Count -gt 1) { Lit $Options[$i - 1][1] } else { '' } - Clear-Line - if ($i -eq $choice) { Say " {u:$($sym.Pointer) $label} {d:$note}" } else { Say " $label {d:$note}" } - } - Clear-Line - Say " {d:$($sym.Hint)}" - if (-not $vt) { $top = [Console]::CursorTop - ($count + 1) } - $key = Read-Key - if ($key.Key -eq 'UpArrow' -or $key.KeyChar -eq 'k') { if ($choice -gt 1) { $choice-- } } - elseif ($key.Key -eq 'DownArrow' -or $key.KeyChar -eq 'j') { if ($choice -lt $count) { $choice++ } } - elseif ($key.KeyChar -match '^[1-9]$' -and [int]"$($key.KeyChar)" -le $count) { $choice = [int]"$($key.KeyChar)" } - elseif ($key.Key -eq 'Enter') { break } - } - if ($vt) { Write-Host "$e[1A" -NoNewline } else { [Console]::SetCursorPosition(0, $top + $count) } - Clear-Line - } catch [System.Management.Automation.PipelineStoppedException] { - throw - } catch { - # No cursor control here: fall back to a numbered list. - for ($i = 1; $i -le $count; $i++) { Write-Host (" $i) " + $Options[$i - 1][0]) } - $answer = Read-Host "Choose [$Default]" - if ($answer -match '^[1-9]$' -and [int]$answer -le $count) { $choice = [int]$answer } - } finally { - Show-Cursor $true - } - return $choice - } - - # ------------------------------------------------------------ steps - - function Test-PortFree([int]$Port) { - # Windows lets a second program bind a port that another one holds on a - # different address, so ask it what is listening first. - if ($onWindows -and (Get-Command Get-NetTCPConnection -ErrorAction SilentlyContinue)) { - try { - if (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue) { return $false } - } catch { <# no answer: binding below decides #> } - } - # Binding answers right away and is accurate. Probing with a connect is - # not: the WSL2 localhost relay (Podman, Rancher Desktop) swallows the - # refusal on closed ports, so every connect waits out its timeout and - # closed ports look taken. A listener on all addresses only blocks the - # second bind, so try both. - foreach ($address in [Net.IPAddress]::Loopback, [Net.IPAddress]::Any) { - $listener = New-Object System.Net.Sockets.TcpListener($address, $Port) - try { - $listener.Start() - } catch { - return $false - } finally { - $listener.Stop() - } - } - return $true - } - - function Test-Ready([int]$Port) { - try { - $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 2 -Uri "http://127.0.0.1:$Port/health/readiness" - return $r.StatusCode -eq 200 - } catch { return $false } - } - - # Run a program with a spinner and the elapsed time, keeping its output to - # show if it fails. Without styling it runs in the open. - function Invoke-WithSpinner { - param([string]$Label, [string]$Detail, [string]$File, [string[]]$Arguments) - if (-not $style) { return Invoke-Shown $File $Arguments } - $out = [IO.Path]::GetTempFileName(); $err = [IO.Path]::GetTempFileName() - try { - $p = Start-Process -FilePath $File -ArgumentList $Arguments -NoNewWindow -PassThru -RedirectStandardOutput $out -RedirectStandardError $err - $null = $p.Handle # keeps ExitCode available after exit in Windows PowerShell - $start = Get-Date - $n = 0 - Show-Cursor $false - try { - while (-not $p.HasExited) { - $s = [int]((Get-Date) - $start).TotalSeconds - $clock = '{0}:{1:00}' -f [math]::Floor($s / 60), ($s % 60) - $extra = if ($Detail) { "$Detail $($sym.Dot) " } else { '' } - Clear-Line - Say ("{a:$($sym.Frames[$n % $sym.Frames.Count])} $Label {d:$($sym.Dot) $extra$clock}") -NoNewline - $n++ - Start-Sleep -Milliseconds 100 - } - $p.WaitForExit() - Clear-Line - } finally { Show-Cursor $true } - $code = $p.ExitCode - if ($code -ne 0) { - Get-Content $out, $err -ErrorAction SilentlyContinue | ForEach-Object { [Console]::Error.WriteLine($_) } - } - return $code - } finally { - # Also on Ctrl+C, so a cancelled run leaves no temporary files. - Remove-Item -LiteralPath $out, $err -Force -ErrorAction SilentlyContinue - } - } - - function Wait-Ready([int]$Port) { - # Up to 3 minutes, like the shell version. A spinner when styled. - $start = Get-Date - $n = 0 - $nextCheck = Get-Date - if ($style) { Show-Cursor $false } - try { - while (((Get-Date) - $start).TotalSeconds -lt 180) { - if ((Get-Date) -ge $nextCheck) { - if (Test-Ready $Port) { if ($style) { Clear-Line }; return $true } - $nextCheck = (Get-Date).AddSeconds(2) - } - if ($style) { - $s = [int]((Get-Date) - $start).TotalSeconds - Clear-Line - Say ("{a:$($sym.Frames[$n % $sym.Frames.Count])} Waiting for the gateway to be ready {d:$($sym.Dot) $('{0}:{1:00}' -f [math]::Floor($s / 60), ($s % 60))}") -NoNewline - $n++ - } - Start-Sleep -Milliseconds 100 - } - if ($style) { Clear-Line } - return $false - } finally { if ($style) { Show-Cursor $true } } - } - - # The closing summary. Plain output, and a console too narrow for the frame, - # get the same lines without it. - function Open-Url([string]$Url) { - try { - if ($onWindows) { Start-Process $Url } elseif (Get-Command open -ErrorAction SilentlyContinue) { & open $Url } else { & xdg-open $Url } - Step "Opened {a:$Url} in your browser" - } catch { - # No browser to open; print the address instead. - Say " {d:Open} {a:$Url} {d:when you are ready.}" - } - } - - # The admin password is the master key. It goes straight to the clipboard, - # never to the screen. - function Copy-Password([string]$Folder) { - $pw = Get-Content -LiteralPath (Join-Path $Folder '.env') | - Where-Object { $_.StartsWith('LITELLM_MASTER_KEY=') } | Select-Object -First 1 - if (-not $pw) { Warn ('No LITELLM_MASTER_KEY in ' + (Lit (Join-Path (Tildify $Folder) '.env')) + '.'); return } - try { - Set-Clipboard -Value $pw.Substring('LITELLM_MASTER_KEY='.Length) -ErrorAction Stop - Step 'Copied the admin password. Paste it into the sign-in page.' - } catch { - # Another app can hold the Windows clipboard open. - Warn ('Could not copy it. The password is LITELLM_MASTER_KEY in ' + (Lit (Join-Path (Tildify $Folder) '.env')) + '.') - } finally { - $pw = $null - } - } - - function Show-Box([string]$Title, [object[]]$Rows) { - $width = $Title.Length - foreach ($r in $Rows) { if (11 + $r[1].Length -gt $width) { $width = 11 + $r[1].Length } } - $cols = 0 - try { $cols = [Console]::WindowWidth } catch { <# no console: print without the frame #> } - if (-not $style -or $cols -lt $width + 4) { - if ($style) { Say "{u:$Title}" } else { Say "$Title." } - foreach ($r in $Rows) { Say (' ' + ($r[0] + ':').PadRight(12) + (Lit $r[1])) } - return - } - $rule = "$($sym.H)" * ($width + 2) - Say "{a:$($sym.TL)$rule$($sym.TR)}" - Say ("{a:$($sym.V)} {u:" + $Title.PadRight($width) + "} {a:$($sym.V)}") - foreach ($r in $Rows) { - $value = (Lit $r[1]).PadRight($width - 11) - if ($r[0] -eq 'Admin UI') { $value = "{u:$value}" } - Say ("{a:$($sym.V)} {d:" + $r[0].PadRight(10) + "} $value {a:$($sym.V)}") - } - Say "{a:$($sym.BL)$rule$($sym.BR)}" - } - - # ------------------------------------------------------------ the install - - # The install this run manages, filled in by Use-Install. - $inst = @{ Dir = ''; Project = ''; Port = 0; Url = ''; Command = '' } - $cf = @('compose', '-f', 'docker-compose.quickstart.yml') - - function Read-EnvValue([string]$Name) { - $p = Join-Path (Get-Location).ProviderPath '.env' - if (-not (Test-Path -LiteralPath $p)) { return '' } - $m = Get-Content -LiteralPath $p | - Where-Object { $_.StartsWith("$Name=") } | Select-Object -Last 1 - if ($m) { return ($m -split '=', 2)[1] } - return '' - } - - # An existing install: LITELLM_DIR, this folder, .\litellm-gateway, or - # ~\litellm-gateway, whichever has a .env first. Empty when there is none. - function Find-Install { - $here = (Get-Location).ProviderPath - if ($env:LITELLM_DIR) { - if (Test-Path -LiteralPath (Join-Path $env:LITELLM_DIR '.env')) { return $env:LITELLM_DIR } - } elseif ((Test-Path -LiteralPath (Join-Path $here '.env')) -and (Test-Path -LiteralPath (Join-Path $here 'docker-compose.quickstart.yml'))) { - return $here - } elseif (Test-Path -LiteralPath (Join-Path $here 'litellm-gateway/.env')) { - return (Join-Path $here 'litellm-gateway') - } elseif (Test-Path -LiteralPath (Join-Path $HOME 'litellm-gateway/.env')) { - return (Join-Path $HOME 'litellm-gateway') - } - return '' - } - - # What holds a port: Owner describes it, FreeIt is the command that frees it, - # and Project is set when it is another quickstart install. All stay empty - # when the holder cannot be named, such as another user's process. - function Get-PortOwner([int]$Port) { - $o = @{ Owner = ''; FreeIt = ''; Project = '' } - $name = '' - foreach ($line in (Invoke-Native $docker @('ps', '--format', '{{.Names}} {{.Ports}}')).Out) { - if ("$line" -match "[:]$Port->") { $name = ("$line" -split ' ')[0]; break } - } - if ($name) { - # Labels come as JSON: Windows PowerShell mangles the quotes an index template needs. - $labels = Invoke-Native $docker @('inspect', '--format', '{{json .Config.Labels}}', $name) - if ($labels.Code -eq 0) { - try { - $json = ($labels.Out -join "`n") | ConvertFrom-Json - if ($json -and $json.PSObject.Properties['com.docker.compose.project']) { $o.Project = "$($json.'com.docker.compose.project')" } - } catch { <# unreadable labels: treat it as a plain container #> } - } - # Only another quickstart install keeps its project, since that is the one - # this script offers to stop. Whoever started the container set the label, - # so it must also be a name Compose would accept. - if ($o.Project -cmatch '^litellm-gateway[a-z0-9_-]*$') { - $o.Owner = "another LiteLLM quickstart install ($engineName project $($o.Project))" - $o.FreeIt = "$engine compose -p $($o.Project) stop" - } else { - $o.Project = '' - $o.Owner = "the $engineName container $name"; $o.FreeIt = "$engine stop $name" - } - return $o - } - $procId = 0; $name = ''; $args0 = '' - if ($onWindows) { - try { - $conn = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($conn) { - $procId = [int]$conn.OwningProcess - $name = (Get-Process -Id $procId -ErrorAction SilentlyContinue).ProcessName - $args0 = "$((Get-CimInstance Win32_Process -Filter "ProcessId=$procId" -ErrorAction SilentlyContinue).CommandLine)" - } - } catch { <# not allowed to look: leave it unnamed #> } - if ($procId) { $o.FreeIt = "Stop-Process -Id $procId" } - } elseif (Get-Command lsof -CommandType Application -ErrorAction SilentlyContinue) { - $found = (Invoke-Native lsof @('-nP', "-iTCP:$Port", '-sTCP:LISTEN', '-t')).Out | Select-Object -First 1 - if ($found) { - $procId = [int]"$found" - $name = "$((Invoke-Native ps @('-o', 'comm=', '-p', "$procId")).Out | Select-Object -First 1)" - if ($name) { $name = Split-Path -Leaf $name.Trim() } - $args0 = "$((Invoke-Native ps @('-o', 'args=', '-p', "$procId")).Out | Select-Object -First 1)" - $o.FreeIt = "kill $procId" - } - } - if (-not $procId) { return $o } - if ($args0 -match 'litellm') { - $o.Owner = "a litellm server (pid $procId), maybe one started after pip install litellm" - } else { - if (-not $name) { $name = 'a program' } - $o.Owner = "$name (pid $procId)" - } - return $o - } - - function Select-Port { - $saved = Read-EnvValue 'LITELLM_PORT' - if ($env:LITELLM_PORT) { - $port = [int]$env:LITELLM_PORT - } elseif ($saved) { - $port = [int]$saved - } elseif (Test-Path -LiteralPath '.env') { - $port = 4000 # an existing install without a saved port runs on the compose default - } else { - $port = 4000 - while (-not (Test-PortFree $port)) { - $port++ - if ($port -gt 4099) { - Fail 'Ports 4000 to 4099 are all in use.' 'Set LITELLM_PORT to a free port and run this again.' - Stop-Quickstart - } - } - if ($port -eq 4000) { - Step 'Port {b:4000} is free' - } else { - $o = Get-PortOwner 4000 - $by = if ($o.Owner) { ' by ' + (Lit $o.Owner) } else { '' } - Warn "Port 4000 is in use$by, so LiteLLM will use {b:$port}" - } - } - $env:LITELLM_PORT = "$port" - return $port - } - - # Compose prefers values already set in the environment over .env, so drop - # inherited ones: .env stays the only source for keys and the project name. - function Clear-ComposeEnv { - foreach ($name in 'LITELLM_MASTER_KEY', 'LITELLM_SALT_KEY', 'POSTGRES_PASSWORD', 'COMPOSE_PROJECT_NAME') { - Remove-Item "Env:$name" -ErrorAction SilentlyContinue - } - # The bind address follows .env when .env sets it. For an older .env without - # it, a value already in the environment is kept. - if (Get-Content -LiteralPath '.env' | Where-Object { $_ -match '^LITELLM_BIND=' }) { Remove-Item Env:LITELLM_BIND -ErrorAction SilentlyContinue } - } - - function Use-Install([string]$Dir) { - Set-Location -LiteralPath $Dir - $inst.Dir = (Get-Location).ProviderPath - $inst.Project = Read-EnvValue 'COMPOSE_PROJECT_NAME' - # Installs from before COMPOSE_PROJECT_NAME have Compose's default, the folder name. - if (-not $inst.Project) { $inst.Project = (Split-Path -Leaf $inst.Dir).ToLowerInvariant() -replace '[^a-z0-9_-]', '' } - $inst.Port = Select-Port - Clear-ComposeEnv - $inst.Url = "http://localhost:$($inst.Port)/ui" - } - - function Save-Compose { - Invoke-WebRequest -UseBasicParsing -Uri $composeUrl -OutFile (Join-Path (Get-Location).ProviderPath 'docker-compose.quickstart.yml') - } - - # Where a symbolic link points, or nothing when the path is not one. - function Get-LinkTarget([string]$Path) { - $item = Get-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue - if ($item -and $item.LinkType -eq 'SymbolicLink') { return "$(@($item.Target)[0])" } - return '' - } - - function Test-BinOnPath { - $bin = Join-Path $HOME '.local/bin' - return (-not $onWindows) -and (@($env:PATH -split ':') -contains $bin) -and (Test-Path -LiteralPath $bin -PathType Container) - } - - # The litellm-gateway command: the copy of this script kept beside .env, run - # against this folder, so up, down, status, and the rest work from anywhere. - function Install-Command { - $dir = $inst.Dir - $tmp = Join-Path $dir ('quickstart.ps1.' + [IO.Path]::GetRandomFileName()) - try { - Invoke-WebRequest -UseBasicParsing -Uri $scriptUrl -OutFile $tmp - if ([IO.File]::ReadAllText($tmp) -match '(?m)^\$code = Invoke-LiteLLMQuickstart') { - Move-Item -LiteralPath $tmp -Destination (Join-Path $dir 'quickstart.ps1') -Force - } - } catch { - # Offline, or a partial download: keep the copy already here, if any. - } finally { - Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue - } - if (-not (Test-Path -LiteralPath (Join-Path $dir 'quickstart.ps1'))) { return } - if ($onWindows) { - # cmd reads a batch file line by line as it runs, so the last line exits - # before uninstall has deleted the file under it. - $path = Join-Path $dir 'litellm-gateway.cmd' - $text = (@( - '@echo off' - 'setlocal' - 'rem Manages the LiteLLM Gateway in this folder. Run it for a menu, or with one of' - 'rem status, up, down, restart, logs, open, update, uninstall' - 'set "LITELLM_DIR=%~dp0."' - 'powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0quickstart.ps1" %* & exit /b' - ) -join "`r`n") + "`r`n" - } else { - $path = Join-Path $dir 'litellm-gateway' - $shell = if (Get-Command pwsh -CommandType Application -ErrorAction SilentlyContinue) { 'pwsh' } else { (Get-Process -Id $PID).Path } - $text = (@( - '#!/bin/sh' - '# Manages the LiteLLM Gateway in this folder. Run it for a menu, or with one of' - '# status, up, down, restart, logs, open, update, uninstall' - ("export LITELLM_DIR='" + $dir.Replace("'", "'\''") + "'") - ("exec '" + $shell.Replace("'", "'\''") + "' -NoProfile -File `"`$LITELLM_DIR/quickstart.ps1`" `"`$@`"") - ) -join "`n") + "`n" - } - $current = if (Test-Path -LiteralPath $path) { [IO.File]::ReadAllText($path) } else { '' } - if ($current -ne $text) { Write-PlainText $path $text } - if ($onWindows) { return } - $null = Invoke-Native chmod @('755', $path) - # Linked into ~/.local/bin when that is on PATH. A link another install made - # stays, unless that install is gone. - if (-not (Test-BinOnPath)) { return } - $bin = Join-Path $HOME '.local/bin' - $link = Join-Path $bin 'litellm-gateway' - $target = Get-LinkTarget $link - if ($target) { - if ($target -eq $path -or (Test-Path -LiteralPath ([IO.Path]::Combine($bin, $target)))) { return } - Remove-Item -LiteralPath $link -Force - } elseif (Test-Path -LiteralPath $link) { - return - } - try { $null = New-Item -ItemType SymbolicLink -Path $link -Target $path } catch { <# not allowed: the full path still works #> } - } - - # How this install's litellm-gateway command runs from anywhere, or nothing. - function Find-Command { - $name = if ($onWindows) { 'litellm-gateway.cmd' } else { 'litellm-gateway' } - $path = Join-Path $inst.Dir $name - if (-not (Test-Path -LiteralPath $path)) { return '' } - if ((Test-BinOnPath) -and (Get-LinkTarget (Join-Path $HOME '.local/bin/litellm-gateway')) -eq $path) { return 'litellm-gateway' } - $command = Tildify $path - if ($command -match ' ') { $command = "& `"$command`"" } - return $command - } - - # The command for an action: the litellm-gateway command, or Compose itself. - function Hint([string]$What) { - if ($inst.Command) { return "$($inst.Command) $What" } - $verb = switch ($What) { 'up' { 'up -d' } 'down' { 'stop' } 'uninstall' { 'down --volumes' } 'logs' { 'logs litellm' } default { $What } } - $where = Tildify $inst.Dir - if ($where -match ' ') { $where = "`"$where`"" } - return "cd $where; $engine compose -f docker-compose.quickstart.yml $verb" - } - - # The gateway container's status, such as "Up 2 hours", or nothing when there is none. - function Get-GatewayStatus { - $r = Invoke-Native $docker @('ps', '-a', '--filter', "label=com.docker.compose.project=$($inst.Project)", - '--filter', 'label=com.docker.compose.service=litellm', '--format', '{{.Status}}') - return "$($r.Out | Select-Object -First 1)" - } - - function Test-Running { return (Get-GatewayStatus) -like 'Up*' } - - function Test-HasContainers([switch]$All) { - $a = @('ps', '-q') - if ($All) { $a += '-a' } - $a += @('--filter', "label=com.docker.compose.project=$($inst.Project)") - return [bool](@((Invoke-Native $docker $a).Out | Where-Object { $_ }).Count) - } - - # The running version, from the API description: the first "version" in it is - # the gateway's, and it needs no key. - function Get-GatewayVersion { - try { - $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 -Uri "http://127.0.0.1:$($inst.Port)/openapi.json" - if ("$($r.Content)" -match '"version": *"([^"]*)"') { return $Matches[1] } - } catch { <# not up yet #> } - return '' - } - - # How many requests the gateway has handled, failed ones included, counted - # from its request logs. It needs the admin key. Empty when the gateway cannot say. - function Get-RequestCount { - $key = Read-EnvValue 'LITELLM_MASTER_KEY' - if (-not $key) { return '' } - $today = [DateTime]::UtcNow.ToString('yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture) - try { - $r = Invoke-WebRequest -UseBasicParsing -TimeoutSec 5 -Headers @{ Authorization = "Bearer $key" } ` - -Uri "http://127.0.0.1:$($inst.Port)/global/activity?start_date=2000-01-01&end_date=$today" - if ("$($r.Content)" -match '"sum_api_requests": *([0-9]+)') { return $Matches[1] } - } catch { <# an older gateway, or no database yet #> } - return '' - } - - function Get-Images { - return @((Invoke-Native $docker ($cf + @('config', '--images'))).Out | Where-Object { $_ } | ForEach-Object { "$_" }) - } - - # Before starting: if something else holds the port, say what and how to free it. - # Moves this install to another port and saves it in .env. Writing the file in - # place keeps its permissions, so only you can still read the keys. - function Move-Port([int]$Port) { - $p = Join-Path $inst.Dir '.env' - $lines = @(Get-Content -LiteralPath $p -ErrorAction Stop | Where-Object { -not $_.StartsWith('LITELLM_PORT=') }) - Write-PlainText $p ((($lines + "LITELLM_PORT=$Port") -join "`n") + "`n") - $env:LITELLM_PORT = "$Port" - $inst.Port = $Port - $inst.Url = "http://localhost:$Port/ui" - } - - # Before starting: if something else holds the port, stop it when it is another - # quickstart install, or move to a free port when it is anything else. - function Confirm-PortFree { - $port = $inst.Port - if ((Test-Running) -or (Test-PortFree $port)) { return } - $o = Get-PortOwner $port - $who = if ($o.Owner) { $o.Owner } else { 'another program' } - $next = 0 - if ($o.Project) { - if ($interactive) { - $choice = Menu "Port $port is in use by $who. Stop it and start this one?" 1 @(@('Yes, stop it', $o.FreeIt), @('No')) - if ($choice -eq 1 -and (Invoke-WithSpinner 'Stopping it' '' $docker @('compose', '-p', $o.Project, 'stop')) -eq 0) { - Step ('Stopped ' + (Lit $o.Owner)) - return - } - } - } elseif (-not $portPinned) { - foreach ($candidate in ($port + 1)..($port + 100)) { - if (Test-PortFree $candidate) { $next = $candidate; break } - } - if ($next -and $interactive) { - $where = 'saved in ' + (Join-Path (Tildify $inst.Dir) '.env') - $choice = Menu "Port $port is in use by $who. Use port $next instead?" 1 @(@("Yes, use $next", $where), @('No')) - if ($choice -eq 1) { - Move-Port $next - Step "LiteLLM moved from port $port to {b:$next}" - return - } - } - } - $instead = if ($next) { ", such as $next" } else { '' } - Fail (Lit "Port $port is in use by $who.") ('Free it, or set LITELLM_PORT in ' + (Lit (Join-Path (Tildify $inst.Dir) '.env')) + " to another port$instead.") - if ($o.FreeIt) { Say (' To free it: ' + (Lit $o.FreeIt)) -Err } - Stop-Quickstart - } - - function Start-Gateway { - Confirm-PortFree - if (-not (Test-Path -LiteralPath 'docker-compose.quickstart.yml')) { Save-Compose } - $started = Get-Date - $detail = '' - foreach ($image in Get-Images) { - if ((Invoke-Native $docker @('image', 'inspect', $image)).Code -ne 0) { $detail = 'downloading images, first run only' } - } - if (-not $style) { if ($detail) { Say "Starting LiteLLM and Postgres ($detail)..." } else { Say 'Starting LiteLLM and Postgres...' } } - if ((Invoke-WithSpinner 'Starting LiteLLM and Postgres' $detail $docker ($cf + @('up', '-d'))) -ne 0) { - Fail "$engineName could not start LiteLLM and Postgres." 'Its output is above.' - Stop-Quickstart - } - if (-not (Wait-Ready $inst.Port)) { - Fail 'The gateway did not become ready in 3 minutes.' 'See what it logged:' - Say (' ' + (Lit (Hint 'logs'))) -Err - Stop-Quickstart - } - Step "LiteLLM is up at {a:$($inst.Url)} {d:$($sym.Dot) $(Elapsed $started)}" - } - - function Stop-Gateway { - if (-not (Test-HasContainers)) { Step 'LiteLLM is already stopped'; return } - if ((Invoke-WithSpinner 'Stopping LiteLLM and Postgres' '' $docker ($cf + @('stop'))) -ne 0) { - Fail "$engineName could not stop LiteLLM and Postgres." 'Its output is above.' - Stop-Quickstart - } - Step ("Stopped LiteLLM. Your models, keys, and logs are kept {d:$($sym.Dot) start it again: " + (Lit (Hint 'up')) + '}') - } - - function Restart-Gateway { - if ((Test-HasContainers) -and (Invoke-WithSpinner 'Stopping LiteLLM and Postgres' '' $docker ($cf + @('stop'))) -ne 0) { - Fail "$engineName could not stop LiteLLM and Postgres." 'Its output is above.' - Stop-Quickstart - } - Start-Gateway - } - - function Update-Gateway { - $before = '' - if (Test-Running) { $before = Get-GatewayVersion } - Save-Compose - Install-Command - $inst.Command = Find-Command - $images = Get-Images - $old = @(foreach ($image in $images) { - $r = Invoke-Native $docker @('image', 'inspect', '--format', '{{.Id}}', $image) - if ($r.Code -eq 0) { "$($r.Out | Select-Object -First 1)" } - }) - if (-not $style) { Say 'Downloading the latest LiteLLM and Postgres images...' } - if ((Invoke-WithSpinner 'Downloading the latest images' '' $docker ($cf + @('pull'))) -ne 0) { - Fail "$engineName could not download the images." 'Its output is above.' - Stop-Quickstart - } - Start-Gateway - # Delete the images this replaced. Removal fails, and they stay, while any - # container still uses them or another tag points at them. - $now = @(foreach ($image in $images) { "$((Invoke-Native $docker @('image', 'inspect', '--format', '{{.Id}}', $image)).Out | Select-Object -First 1)" }) - foreach ($id in $old) { - if ($id -and $now -notcontains $id) { $null = Invoke-Native $docker @('image', 'rm', $id) } - } - $after = Get-GatewayVersion - if ($before -and $before -eq $after) { - Step "LiteLLM $after is already the latest" - } elseif ($before -and $after) { - Step "Updated LiteLLM from $before to $after" - } elseif ($after) { - Step "LiteLLM $after is on the latest image" - } else { - Step 'LiteLLM is on the latest image' - } - } - - function Show-Logs([switch]$Follow) { - if (-not (Test-HasContainers -All)) { Step 'No logs yet: start LiteLLM first'; return } - if ($Follow) { - Say '{d:The gateway logs. Press Ctrl+C to stop following them.}' - $null = Invoke-Shown $docker ($cf + @('logs', '-f', '--tail', '100', 'litellm')) - return - } - $null = Invoke-Shown $docker ($cf + @('logs', '--tail', '100', 'litellm')) - } - - function Get-DesktopNote { - if ($engine -eq 'docker' -and ((Invoke-Native $docker @('info', '--format', '{{.OperatingSystem}}')).Out -match 'Docker Desktop')) { - return "listed as $($inst.Project) in Docker Desktop" - } - return '' - } - - function Show-Status { - $status = Get-GatewayStatus - $image = "$(Get-Images | Where-Object { $_ -match 'litellm' } | Select-Object -First 1)" - $desktop = Get-DesktopNote - $rows = New-Object System.Collections.ArrayList - if ($status -like 'Up*') { - $uptime = ($status -replace '^Up ', '') -replace ' \(.*$', '' - $version = Get-GatewayVersion - if (-not $version) { $version = 'starting' } - $requests = Get-RequestCount - $null = $rows.Add(@('Admin UI', $inst.Url)) - $null = $rows.Add(@('Version', $version)) - $null = $rows.Add(@('Uptime', $uptime)) - if ($requests -eq '0') { $requests = 'none yet' } elseif ($requests) { $requests = "$requests handled" } - if ($requests) { $null = $rows.Add(@('Requests', $requests)) } - $null = $rows.Add(@('Folder', (Tildify $inst.Dir))) - $null = $rows.Add(@('Image', $image)) - if ($desktop) { $null = $rows.Add(@('Desktop', $desktop)) } - $null = $rows.Add(@('Stop it', (Hint 'down'))) - Show-Box 'LiteLLM is running' $rows.ToArray() - } else { - $null = $rows.Add(@('Folder', (Tildify $inst.Dir))) - $null = $rows.Add(@('Image', $image)) - if ($desktop) { $null = $rows.Add(@('Desktop', $desktop)) } - $null = $rows.Add(@('Start it', (Hint 'up'))) - $null = $rows.Add(@('Remove it', (Hint 'uninstall'))) - Show-Box 'LiteLLM is stopped' $rows.ToArray() - } - } - - function Open-UI { - if (Test-Running) { Open-Url $inst.Url } else { Warn ('LiteLLM is stopped. Start it with: ' + (Lit (Hint 'up'))) } - } - - # Returns $false when the person keeps the install, so the menu can carry on. - function Remove-Gateway { - $dir = $inst.Dir - $volume = "$($inst.Project)_postgres_data" - if (-not (Test-Path -LiteralPath 'docker-compose.quickstart.yml')) { Save-Compose } - $images = Get-Images - $link = '' - if (-not $onWindows) { - $bin = Join-Path $HOME '.local/bin' - if ((Get-LinkTarget (Join-Path $bin 'litellm-gateway')) -eq (Join-Path $dir 'litellm-gateway')) { $link = Join-Path $bin 'litellm-gateway' } - } - if ($interactive) { - Write-Host '' - Say ('{b:This removes LiteLLM from ' + (Lit (Tildify $dir)) + ':}') - Say " - its containers ($engineName project $($inst.Project))" - Say " - its database, the $engineName volume $volume, with your models, keys, and spend logs" - Say (' - its files in ' + (Lit (Tildify $dir)) + ', including .env with the admin password') - if ($link) { Say (' - the litellm-gateway command in ' + (Lit (Tildify $bin))) } - if ((Menu 'Remove LiteLLM and delete its database?' 2 @(@('Yes, remove it'), @('No, keep it'))) -ne 1) { - Step 'Kept LiteLLM and its data' - return $false - } - } elseif (-not $noQuestions) { - Fail "Removing LiteLLM deletes its database ($engineName volume $volume)." 'To confirm, run it again with -Yes.' - Stop-Quickstart - } - - if (-not $style) { Say 'Removing the LiteLLM and Postgres containers and the database...' } - if ((Invoke-WithSpinner 'Removing the containers and the database' '' $docker ($cf + @('down', '--volumes', '--remove-orphans'))) -ne 0) { - Fail "$engineName could not remove them." 'Its output is above.' - Stop-Quickstart - } - Step "Removed the containers and the database {d:($volume)}" - - $kept = New-Object System.Collections.ArrayList - foreach ($image in $images) { - if ((Invoke-Native $docker @('image', 'inspect', $image)).Code -eq 0) { $null = $kept.Add($image) } - } - if ($kept.Count -and $interactive) { - # The size on disk, as the images list shows it (1.68GB, or 1.23 GB on Podman). - $bytes = 0.0 - foreach ($image in $kept) { - $size = "$((Invoke-Native $docker @('images', '--format', '{{.Size}}', $image)).Out | Select-Object -First 1)" - if ($size -match '^([\d.]+)\s*([kKMGT]?)') { - $unit = switch -CaseSensitive ($Matches[2]) { 'k' { 1e3 } 'K' { 1e3 } 'M' { 1e6 } 'G' { 1e9 } 'T' { 1e12 } default { 1 } } - $bytes += [double]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture) * $unit - } - } - $total = if ($bytes -gt 0) { ' (' + ($bytes / 1e9).ToString('0.0', [Globalization.CultureInfo]::InvariantCulture) + ' GB)' } else { '' } - $choice = Menu "Also delete the LiteLLM and Postgres images$($total)?" 2 @(@('Yes, delete them'), @('No, keep them', 'a reinstall starts faster')) - if ($choice -eq 1) { - foreach ($image in @($kept.ToArray())) { - if ((Invoke-Native $docker @('image', 'rm', $image)).Code -eq 0) { - Step "Deleted the image $image" - $kept.Remove($image) - } else { - Warn "Kept the image $image, which other containers use" - } - } - } - } - - foreach ($name in '.env', 'docker-compose.quickstart.yml', 'quickstart.ps1', 'quickstart.sh', 'litellm-gateway', 'litellm-gateway.cmd') { - Remove-Item -LiteralPath (Join-Path $dir $name) -Force -ErrorAction SilentlyContinue - } - $ignore = Join-Path $dir '.gitignore' - if ((Test-Path -LiteralPath $ignore) -and - -not ([IO.File]::ReadAllText($ignore) -split "`r?`n" | Where-Object { $_ -and $_ -ne '*' -and $_ -ne '.env' })) { - Remove-Item -LiteralPath $ignore -Force - } - if ($link) { Remove-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue } - # Windows will not delete the folder a program is working in, this one included. - Set-Location -LiteralPath $HOME - [Environment]::CurrentDirectory = $HOME - try { - [IO.Directory]::Delete($dir, $false) - Step ('Deleted ' + (Lit (Tildify $dir))) - } catch { - $why = if (@(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue).Count) { 'it has other files' } else { 'a program is using it' } - Step ('Deleted the LiteLLM files in ' + (Lit (Tildify $dir)) + " {d:(the folder stays: $why)}") - } - - $left = New-Object System.Collections.ArrayList - if (Test-HasContainers -All) { $null = $left.Add('containers') } - if ((Invoke-Native $docker @('volume', 'inspect', $volume)).Code -eq 0) { $null = $left.Add("volume $volume") } - if (Test-Path -LiteralPath (Join-Path $dir '.env')) { $null = $left.Add((Join-Path (Tildify $dir) '.env')) } - if ($left.Count) { - Warn ('Still there: ' + (Lit ($left.ToArray() -join ', '))) - return $true - } - Step 'LiteLLM is removed. Nothing is left running, and its data is deleted' - if ($kept.Count) { - Say (" {d:The images stay, so a reinstall starts faster. To delete them:} $engine image rm " + ($kept.ToArray() -join ' ')) - } - return $true - } - - # Over SSH the clipboard would be the server's, not yours. On Linux, - # Set-Clipboard without xclip quietly keeps the text inside PowerShell, so - # only Windows and macOS, which always have a clipboard, get the option. - function Test-CanCopy { - return $interactive -and (-not $env:SSH_CONNECTION) -and (-not $env:SSH_TTY) -and - ($onWindows -or ((Test-Path variable:IsMacOS) -and $IsMacOS)) - } - - # The menu a rerun shows for an existing install. Its options follow whether - # LiteLLM is running, and come back after each choice until Done. - function Show-ManageMenu { - Show-Status - $canCopy = Test-CanCopy - $pick = 1 - while ($true) { - $options = New-Object System.Collections.ArrayList - if (Test-Running) { - $question = "LiteLLM is running at $($inst.Url). What next?" - $null = $options.Add(@('Open the admin UI in your browser')) - if ($canCopy) { $null = $options.Add(@('Copy the admin password', 'to the clipboard, ready to paste')) } - $null = $options.Add(@('Stop it', 'keeps your models, keys, and logs')) - $null = $options.Add(@('Restart it')) - $null = $options.Add(@('Show the logs', 'the last 100 lines')) - $null = $options.Add(@('Update it', 'download the latest image and restart')) - $null = $options.Add(@('Remove it', 'delete its containers, database, and files')) - $null = $options.Add(@('Done')) - } else { - $question = 'LiteLLM is stopped. What next?' - $null = $options.Add(@('Start it', "at $($inst.Url)")) - $null = $options.Add(@('Update it', 'download the latest image and start')) - $null = $options.Add(@('Show the logs', 'the last 100 lines')) - $null = $options.Add(@('Remove it', 'delete its containers, database, and files')) - $null = $options.Add(@('Done')) - } - if ($pick -gt $options.Count) { $pick = 1 } - $choice = Menu $question $pick $options.ToArray() - $picked = $options[$choice - 1][0] - $pick = 1 - switch -Wildcard ($picked) { - 'Open*' { Open-Url $inst.Url; $pick = $options.Count } - 'Copy*' { Copy-Password $inst.Dir } - 'Start*' { Start-Gateway } - 'Stop*' { Stop-Gateway } - 'Restart*' { Restart-Gateway } - 'Show*' { Show-Logs } - 'Update*' { Update-Gateway } - 'Remove*' { if (Remove-Gateway) { return } } - default { return } - } - } - } - - # ------------------------------------------------------------ main - - $savedEncoding = $null - $savedLocation = (Get-Location).ProviderPath - $savedPort = $env:LITELLM_PORT - try { - if ($unicode -and $onWindows) { - # Windows PowerShell writes the console in the OEM code page, which has no check marks. - $savedEncoding = [Console]::OutputEncoding - [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false - } - if ($PSVersionTable.PSEdition -eq 'Desktop') { - # Windows PowerShell can default to TLS 1.0, which GitHub refuses. - [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 - } - - if ($action) { - # An action prints only its own result. - } elseif ($style) { - Write-Host '' - Say "{u:$($sym.Head) LiteLLM quickstart}" - Say '{d: The gateway, Postgres, and the admin UI in one command}' - Write-Host '' - } else { - Say 'LiteLLM quickstart' - } - - # Podman and Rancher Desktop (nerdctl in containerd mode; its dockerd mode - # already provides a docker CLI) work the same way through Compose v2. - $engineCmd = foreach ($cand in 'docker', 'podman', 'nerdctl') { - $c = Get-Command $cand -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($c) { $c; break } - } - if (-not $engineCmd) { - Fail 'No container engine found (docker, podman, or nerdctl).' 'The LiteLLM Gateway runs in a container alongside a Postgres database.' - $install = if ($onWindows) { 'https://docs.docker.com/desktop/setup/install/windows-install/' } else { 'https://docs.docker.com/get-docker/' } - Say '' -Err - Say " Install Docker Desktop ($install)," -Err - Say ' Podman (https://podman.io), or Rancher Desktop (https://rancherdesktop.io), then run this again.' -Err - Say ' Or deploy in one click (Railway or Render): https://docs.litellm.ai/docs/proxy/docker_quick_start' -Err - Say ' Only need to call models from Python? pip install litellm' -Err - return 1 - } - $docker = $engineCmd.Source - $engine = $engineCmd.Name -replace '\.exe$', '' - $engineName = switch ($engine) { 'podman' { 'Podman' } 'nerdctl' { 'nerdctl' } default { 'Docker' } } - $compose = Invoke-Native $docker @('compose', 'version', '--short') - if ($compose.Code -ne 0) { Fail "Compose v2 ('$engine compose') is required."; return 1 } - if ((Invoke-Native $docker @('info')).Code -ne 0) { - $startHint = switch ($engine) { - 'podman' { 'Start it (podman machine start) and run this again.' } - 'nerdctl' { 'Start Rancher Desktop and run this again.' } - default { 'Start Docker Desktop and run this again.' } - } - Fail "$engineName is installed but not running." $startHint - return 1 - } - - if ($action) { - $existing = Find-Install - if (-not $existing) { - $s = [IO.Path]::DirectorySeparatorChar - $places = if ($env:LITELLM_DIR) { $env:LITELLM_DIR } else { "this folder, .${s}litellm-gateway, or ~${s}litellm-gateway" } - Fail 'No LiteLLM install found' ('in ' + (Lit $places) + '.') - $runner = if ($onWindows) { 'powershell' } else { 'pwsh' } - Say " Install it with: $runner -c `"irm $scriptUrl | iex`"" -Err - return 1 - } - Use-Install $existing - $inst.Command = Find-Command - switch ($action) { - 'status' { Show-Status } - 'start' { if (Test-Running) { Step "LiteLLM is already running at {a:$($inst.Url)}" } else { Start-Gateway } } - 'stop' { Stop-Gateway } - 'restart' { Restart-Gateway } - 'logs' { if ($interactive) { Show-Logs -Follow } else { Show-Logs } } - 'open' { Open-UI } - 'update' { Update-Gateway } - 'uninstall' { $null = Remove-Gateway } - } - return 0 - } - - $server = Invoke-Native $docker @('version', '--format', '{{.Server.Version}}') - $engineVersion = if ($server.Code -eq 0 -and $server.Out.Count) { "$($server.Out[0]) " } else { '' } - $composeVersion = "$($compose.Out[0])".TrimStart('v') - Step "$engineName ${engineVersion}with Compose $composeVersion is running" - - $homeDir = Join-Path $HOME 'litellm-gateway' - $hereDir = Join-Path (Get-Location).ProviderPath 'litellm-gateway' - $dir = Find-Install - $found = [bool]$dir - if ($found) { - # installed before - } elseif ($env:LITELLM_DIR) { - $dir = $env:LITELLM_DIR - } elseif ($hereDir -eq $homeDir) { - $dir = $homeDir - } else { - $pick = Menu 'Where should LiteLLM keep its files (.env with your keys, and the compose file)?' 1 @( - , @((Tildify $homeDir), 'recommended, reruns always find it') - , @((Tildify $hereDir), 'this folder')) - $dir = if ($pick -eq 2) { $hereDir } else { $homeDir } - } - $created = -not (Test-Path -LiteralPath $dir) - $null = New-Item -ItemType Directory -Force -Path $dir - $dir = (Resolve-Path -LiteralPath $dir).ProviderPath - Set-Location -LiteralPath $dir - if ($found) { Step ('Found your install in {b:' + (Lit (Tildify $dir)) + '}') } else { Step ('Files go in {b:' + (Lit (Tildify $dir)) + '}') } - - $git = Get-Command git -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 - $inRepo = $git -and (Invoke-Native $git.Source @('rev-parse', '--is-inside-work-tree')).Code -eq 0 - if ($inRepo -and -not (Test-Path -LiteralPath (Join-Path $dir '.env')) -and - (Invoke-Native $git.Source @('ls-files', '--error-unmatch', '.env')).Code -eq 0) { - # An ignore rule does not cover a tracked file, so new keys written here - # would show up as a change to commit. - Fail 'Git tracks a .env file in this folder,' 'so your keys could be committed.' - Say '' -Err - Say ' Install into another folder (set LITELLM_DIR), or stop tracking the file' -Err - Say ' first with: git rm --cached .env' -Err - return 1 - } - if ($created) { - # A folder this script made holds only its own files, so keep all of it out of git. - Write-PlainText (Join-Path $dir '.gitignore') "*`n" - } elseif ($inRepo -and (Invoke-Native $git.Source @('check-ignore', '-q', '.env')).Code -ne 0) { - # In a folder that already existed, such as a repository root, leave the - # tracked .gitignore alone and add only .env to this clone's local exclude - # list, so the generated keys cannot be committed. - $exclude = "$((Invoke-Native $git.Source @('rev-parse', '--git-path', 'info/exclude')).Out[0])" - $exclude = [IO.Path]::GetFullPath([IO.Path]::Combine($dir, $exclude)) - $null = New-Item -ItemType Directory -Force -Path (Split-Path $exclude) - $prefix = "$((Invoke-Native $git.Source @('rev-parse', '--show-prefix')).Out | Select-Object -First 1)" - Write-PlainText $exclude "/$prefix.env`n" -Append - Step ("Kept .env out of git {d:(added it to this repository's local exclude list, " + (Lit (Tildify $exclude)) + ')}') - } elseif (-not $inRepo) { - # An existing folder outside git: ignore only .env, so it stays out of - # commits if the folder becomes a repository later. - $ignore = Join-Path $dir '.gitignore' - $current = if (Test-Path -LiteralPath $ignore) { [IO.File]::ReadAllText($ignore) } else { '' } - if (($current -split "`r?`n") -notcontains '.env' -and ($current -split "`r?`n") -notcontains '*') { - # Start on a new line if the file does not end with one. - $lead = if ($current.Length -gt 0 -and -not $current.EndsWith("`n")) { "`n" } else { '' } - Write-PlainText $ignore "$lead.env`n" -Append - } - } - - if ($found -and $interactive) { - Use-Install $dir - if (-not (Test-Path -LiteralPath 'quickstart.ps1') -or -not (Find-Command)) { Install-Command } - $inst.Command = Find-Command - Show-ManageMenu - return 0 - } - - # The engine names containers and the database volume after the project, so - # an install outside the home folder gets its own name and never shares a - # database with another litellm-gateway folder. Windows paths ignore case, - # so there the name does too. - $envFile = Join-Path $dir '.env' - $project = 'litellm-gateway' - if ($dir -ne $homeDir) { - $key = if ($onWindows) { $dir.ToLowerInvariant() } else { $dir } - $project = "litellm-gateway-$(Get-Cksum $key)" - } - if (-not (Test-Path -LiteralPath $envFile)) { - # Postgres keeps the password it was created with, so a new password over an - # old database volume would lock the gateway out. Stop and explain instead. - if ((Invoke-Native $docker @('volume', 'inspect', "${project}_postgres_data")).Code -eq 0) { - Fail 'Found a database from an earlier install' "($engineName volume ${project}_postgres_data)" - Say ('but no ' + (Lit (Join-Path (Tildify $dir) '.env')) + ' with its password.') -Err - Say '' -Err - Say ' Restore that .env file and run this again to keep your models and keys, or' -Err - Say ' delete the old database and start fresh (this removes its models and keys):' -Err - Say " $engine volume rm ${project}_postgres_data" -Err - return 1 - } - } - - Save-Compose - Step 'Downloaded docker-compose.quickstart.yml' - - if (Test-Path -LiteralPath $envFile) { - Step 'Reusing .env, so existing keys and data keep working' - } else { - $port = Select-Port - $rng = [Security.Cryptography.RandomNumberGenerator]::Create() - $hex = { - param([int]$n) - $bytes = New-Object byte[] $n - $rng.GetBytes($bytes) - -join ($bytes | ForEach-Object { $_.ToString('x2') }) - } - # Lock a temporary file down before anything secret goes into it, then - # rename it, so a failed write never leaves a partial .env that a rerun - # would mistake for a finished install. - $tmp = "$envFile.tmp" - try { - Write-PlainText $tmp '' - if ($onWindows) { - $acl = New-Object System.Security.AccessControl.FileSecurity - $acl.SetAccessRuleProtection($true, $false) - $me = [Security.Principal.WindowsIdentity]::GetCurrent().User - $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($me, 'FullControl', 'Allow'))) - Set-Acl -LiteralPath $tmp -AclObject $acl - } else { - & chmod 600 $tmp - if ($LASTEXITCODE -ne 0) { throw "Could not restrict the permissions of $tmp." } - } - Write-PlainText $tmp (("LITELLM_MASTER_KEY=sk-{0}`nLITELLM_SALT_KEY=sk-{1}`nPOSTGRES_PASSWORD={2}`n" + - "LITELLM_PORT={3}`nLITELLM_BIND=127.0.0.1:`nCOMPOSE_PROJECT_NAME={4}`n") -f (& $hex 32), (& $hex 32), (& $hex 24), $port, $project) - [IO.File]::Move($tmp, $envFile) - } finally { - Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue - } - Step 'Generated .env with your master key, salt key, and database password {d:(keep this file; only you can read it)}' - } - - Use-Install $dir - Install-Command - $inst.Command = Find-Command - Start-Gateway - - $url = $inst.Url - Write-Host '' - $canCopy = Test-CanCopy - $password = "the LITELLM_MASTER_KEY value in $(Join-Path (Tildify $dir) '.env')" - if ($canCopy) { $password = "copy it below, or LITELLM_MASTER_KEY in $(Join-Path (Tildify $dir) '.env')" } - # A rerun finds ~\litellm-gateway on its own; any other folder needs naming. - $rerun = 'rerun the install command' - if ($dir -ne $homeDir) { $rerun += " with LITELLM_DIR=$(Tildify $dir)" } - $manage = if ($inst.Command) { "run $($inst.Command), or $rerun" } else { $rerun } - $rows = New-Object System.Collections.ArrayList - $null = $rows.Add(@('Admin UI', $url)) - $null = $rows.Add(@('Username', 'admin')) - $null = $rows.Add(@('Password', $password)) - $null = $rows.Add(@('Next', 'in the UI, open Models + Endpoints > Add Model and paste a provider API key')) - $null = $rows.Add(@('Stop it', (Hint 'down'))) - $null = $rows.Add(@('Start it', (Hint 'up'))) - $null = $rows.Add(@('Remove it', (Hint 'uninstall'))) - $desktop = Get-DesktopNote - if ($desktop) { $null = $rows.Add(@('Desktop', $desktop)) } - $null = $rows.Add(@('Manage', $manage)) - Show-Box 'LiteLLM is running' $rows.ToArray() - - if ($canCopy) { - # Come back to the menu after each choice, with the next step as the default. - $pick = 1 - while ($true) { - $pick = Menu 'What next?' $pick @( - , @('Copy the admin password', 'to the clipboard, ready to paste') - , @('Open the admin UI in your browser') - , @('Done')) - if ($pick -eq 1) { Copy-Password $dir; $pick = 2 } - elseif ($pick -eq 2) { Open-Url $url; $pick = 3 } - else { break } - } - } elseif ($interactive) { - if ((Menu 'Open the admin UI in your browser?' 1 @(@('Yes'), @('No'))) -eq 1) { Open-Url $url } - } - return 0 - } catch { - if ($_.Exception.Message -eq $exitSignal) { return 1 } - throw - } finally { - if ($savedEncoding) { [Console]::OutputEncoding = $savedEncoding } - if ($savedPort) { $env:LITELLM_PORT = $savedPort } else { Remove-Item Env:LITELLM_PORT -ErrorAction SilentlyContinue } - if (Test-Path -LiteralPath $savedLocation) { Set-Location -LiteralPath $savedLocation } - } -} - -$code = Invoke-LiteLLMQuickstart -Yes:$Yes -Rest $args | Select-Object -Last 1 -$global:LASTEXITCODE = $code -# Run from a file, or as `powershell -c "irm ... | iex"`, the exit code should -# reach the caller. Pasted into an open window, `exit` would close that window, -# so there it only sets $LASTEXITCODE. -$hostArgs = [Environment]::GetCommandLineArgs() -$oneShot = ($hostArgs -match '^-(c|command)$') -and -not ($hostArgs -match '^-noexit$') -if ($code -ne 0 -and ($PSCommandPath -or $oneShot)) { exit $code } diff --git a/scripts/quickstart.sh b/scripts/quickstart.sh deleted file mode 100755 index 53a6f39c74a..00000000000 --- a/scripts/quickstart.sh +++ /dev/null @@ -1,1184 +0,0 @@ -#!/bin/sh -# LiteLLM Gateway quickstart: the gateway, Postgres, and the admin UI in one command. -# curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh | sh -# On Windows, scripts/quickstart.ps1 does the same in PowerShell. -# -# To read it before running it: -# curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh -o quickstart.sh -# less quickstart.sh -# sh quickstart.sh -# -# Asks where to keep the files, then offers to copy the admin password and -# open the admin UI; every question has a default you accept by pressing Enter. -# Run it again for a menu to open, stop, start, update, or remove the install. -# It asks nothing when there is no terminal, under CI or Claude Code, or when -# run with --yes. -# -# It also takes an action, which the litellm-gateway command it installs takes too: -# curl -fsSL https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh | sh -s -- stop -# status, start (up), stop (down), restart, logs, open, update, uninstall -# -# --yes, -y no questions: install to ~/litellm-gateway, don't open a browser, -# and let uninstall delete the database without asking -# LITELLM_DIR folder to install into (skips the folder question) -# LITELLM_PORT port for the gateway (default 4000, or the next free one) -# NO_COLOR plain output, which agents, CI, and log files always get -# -# New installs listen on this machine only (127.0.0.1). To reach the gateway -# from other machines, remove LITELLM_BIND from .env and put it behind TLS. -# -# Keys and the database password are random (openssl rand), written only to -# .env with permissions 600, and never printed. When you ask, the admin -# password goes straight to your clipboard. Needs Docker, Podman, or Rancher -# Desktop, each with Compose v2. -# Everything runs inside main(), so a partial download runs nothing. -set -eu - -COMPOSE_URL="${LITELLM_COMPOSE_URL:-https://raw.githubusercontent.com/BerriAI/litellm/main/litellm/deploy/docker/docker-compose.quickstart.yml}" -SCRIPT_URL="${LITELLM_SCRIPT_URL:-https://raw.githubusercontent.com/BerriAI/litellm/main/scripts/quickstart.sh}" -# A port set by the caller is never moved; later steps export LITELLM_PORT too. -PORT_PINNED="${LITELLM_PORT:+1}" - -# ---------------------------------------------------------------- output - -# A person watching a terminal gets colors, step marks, and a spinner. Agents, -# CI, log files, and NO_COLOR get the same lines as plain text. -STYLE=0 -ERR_STYLE=0 # stderr is styled only when it is a terminal too -C_ACC='' C_OK='' C_WARN='' C_ERR='' C_DIM='' C_BOLD='' C_OFF='' -POINTER='>' S_OK='+' S_WARN='!' S_ERR='x' S_HEAD='*' S_ASK='?' -# shellcheck disable=SC1003 # the last frame is a backslash -SPIN_FRAMES='| / - \' -HINT='Up/Down to move, Enter to choose' -BOX_TL='+' BOX_TR='+' BOX_BL='+' BOX_BR='+' BOX_H='-' BOX_V='|' -SPIN_PID='' SPIN_LOG='' ENV_TMP='' - -setup_output() { - case "${LC_ALL:-${LC_CTYPE:-${LANG:-}}}" in - *UTF-8* | *utf-8* | *UTF8* | *utf8*) - POINTER='❯' S_OK='✓' S_WARN='!' S_ERR='✗' S_HEAD='◆' S_ASK='?' - SPIN_FRAMES='⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏' - HINT='↑/↓ to move, Enter to choose' - BOX_TL='╭' BOX_TR='╮' BOX_BL='╰' BOX_BR='╯' BOX_H='─' BOX_V='│' - ;; - esac - if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-dumb}" != "dumb" ] && - [ -z "${CI:-}" ] && [ -z "${CLAUDECODE:-}" ]; then - STYLE=1 - e="$(printf '\033')" - # The accent is the LiteLLM blue, lightened so it reads on dark and light themes. - case "${COLORTERM:-}" in - truecolor | 24bit) C_ACC="${e}[38;2;91;108;255m" ;; - *) C_ACC="${e}[94m" ;; - esac - C_OK="${e}[32m" C_WARN="${e}[33m" C_ERR="${e}[31m" C_DIM="${e}[90m" C_BOLD="${e}[1m" C_OFF="${e}[0m" - if [ -t 2 ]; then ERR_STYLE=1; fi - fi -} - -step() { - if [ "$STYLE" = 1 ]; then - printf '%s%s%s %s%s\n' "${C_OK}" "$S_OK" "${C_OFF}" "$1" "${C_OFF}" - else - printf '%s\n' "$1" - fi -} - -warn() { - if [ "$STYLE" = 1 ]; then - printf '%s%s%s %s%s\n' "${C_WARN}" "$S_WARN" "${C_OFF}" "$1" "${C_OFF}" - else - printf '%s\n' "$1" - fi -} - -fail() { - if [ "$ERR_STYLE" = 1 ]; then - printf '%s%s %s%s%s\n' "${C_ERR}${C_BOLD}" "$S_ERR" "$1" "${C_OFF}" "${2:+ $2}" >&2 - else - printf '%s%s\n' "$1" "${2:+ $2}" >&2 - fi -} - -tildify() { - case "$1" in - "$HOME") printf '~' ;; - "$HOME"/*) printf '~%s' "${1#"$HOME"}" ;; - *) printf '%s' "$1" ;; - esac -} - -elapsed_since() { - s=$(($(date +%s) - $1)) - if [ "$s" -lt 60 ]; then printf '%ss' "$s"; else printf '%sm %ss' "$((s / 60))" "$((s % 60))"; fi -} - -# spin "label" "detail" command...: run the command behind a spinner with the -# elapsed time, keeping its output to show if it fails. Without styling the -# command runs in the open, as before. -spin() { - label="$1" detail="$2" - shift 2 - if [ "$STYLE" != 1 ]; then - "$@" - return - fi - SPIN_LOG="$(mktemp)" - "$@" >"$SPIN_LOG" 2>&1 & - SPIN_PID=$! - start="$(date +%s)" s=0 n=0 - frames="$SPIN_FRAMES " - printf '\033[?25l' - while kill -0 "$SPIN_PID" 2>/dev/null; do - # Rotate the frames in the shell and read the clock once a second, so a - # redraw starts no process besides sleep. - frame="${frames%% *}" - frames="${frames#* }$frame " - if [ $((n % 10)) = 0 ]; then s=$(($(date +%s) - start)); fi - printf '\r\033[2K%s%s%s %s %s· %s%d:%02d%s' "${C_ACC}" "$frame" "${C_OFF}" "$label" "${C_DIM}" \ - "${detail:+$detail · }" "$((s / 60))" "$((s % 60))" "${C_OFF}" - n=$((n + 1)) - sleep 0.1 - done - rc=0 - wait "$SPIN_PID" || rc=$? - SPIN_PID='' - printf '\r\033[2K\033[?25h' - if [ "$rc" != 0 ]; then cat "$SPIN_LOG" >&2; fi - rm -f "$SPIN_LOG" - SPIN_LOG='' - return "$rc" -} - -# box "Title" "Label|value"...: the closing summary. Plain output, and a -# terminal too narrow for the frame, get the same lines without it. -box() { - title="$1" - shift - cols='' - if [ "$STYLE" = 1 ]; then cols="$( (stty size /dev/null | cut -d ' ' -f 2)" || cols=''; fi - width=${#title} - for row in "$@"; do - value="${row#*|}" - if [ $((11 + ${#value})) -gt "$width" ]; then width=$((11 + ${#value})); fi - done - if [ "$STYLE" != 1 ] || [ -z "$cols" ] || [ $((width + 4)) -gt "$cols" ]; then - if [ "$STYLE" = 1 ]; then printf '%s%s%s\n' "${C_ACC}${C_BOLD}" "$title" "${C_OFF}"; else printf '%s.\n' "$title"; fi - for row in "$@"; do - printf ' %-12s%s\n' "${row%%|*}:" "${row#*|}" - done - return 0 - fi - rule="$(printf "%$((width + 2))s" '' | sed "s/ /$BOX_H/g")" - printf '%s%s%s%s%s\n' "${C_ACC}" "$BOX_TL" "$rule" "$BOX_TR" "${C_OFF}" - printf '%s%s%s %s%-*s%s %s%s%s\n' "${C_ACC}" "$BOX_V" "${C_OFF}" "${C_ACC}${C_BOLD}" "$width" "$title" "${C_OFF}" \ - "${C_ACC}" "$BOX_V" "${C_OFF}" - for row in "$@"; do - label="${row%%|*}" value="${row#*|}" shade='' - if [ "$label" = "Admin UI" ]; then shade="${C_ACC}${C_BOLD}"; fi - printf '%s%s%s %s%-10s%s %s%-*s%s %s%s%s\n' "${C_ACC}" "$BOX_V" "${C_OFF}" "${C_DIM}" "$label" "${C_OFF}" \ - "$shade" "$((width - 11))" "$value" "${C_OFF}" "${C_ACC}" "$BOX_V" "${C_OFF}" - done - printf '%s%s%s%s%s\n' "${C_ACC}" "$BOX_BL" "$rule" "$BOX_BR" "${C_OFF}" -} - -# ---------------------------------------------------------------- terminal - -INTERACTIVE=0 # a person is at a terminal we can ask -ARROWS=0 # that terminal supports the arrow-key menu -STTY_SAVED="" - -detect_terminal() { - # Piped from curl, stdin is the script itself, so questions go to /dev/tty. - if (exec /dev/null && [ "${TERM:-dumb}" != "dumb" ]; then - INTERACTIVE=1 - if STTY_SAVED="$(stty -g /dev/null)" && [ -n "$STTY_SAVED" ]; then - ARROWS=1 - fi - fi -} - -restore_terminal() { - if [ -n "$STTY_SAVED" ]; then - stty "$STTY_SAVED" /dev/null || true - fi - if [ "$STYLE" = 1 ] || [ -n "$STTY_SAVED" ]; then - printf '\033[?25h' >/dev/tty 2>/dev/null || true - fi -} - -on_interrupt() { - if [ -n "$SPIN_PID" ]; then kill "$SPIN_PID" 2>/dev/null || true; fi - if [ -n "$SPIN_LOG" ]; then rm -f "$SPIN_LOG"; fi - if [ -n "$ENV_TMP" ]; then rm -f "$ENV_TMP"; fi - if [ "$STYLE" = 1 ]; then printf '\r\033[2K'; fi - restore_terminal - printf '\nCancelled.\n' >&2 - exit 130 -} - -read_key() { - # One keypress in raw mode. Enter comes back empty (command substitution - # drops the newline); arrows come back as "up" or "down". - k="$(dd bs=1 count=1 2>/dev/null sets CHOICE to the 1-based pick. -# An option may carry a note after a tab, shown dimmed and lined up. -menu() { - question="$1" - CHOICE="$2" - shift 2 - count=$# - if [ "$INTERACTIVE" != 1 ]; then return 0; fi - - tab="$(printf '\t')" - pad=0 - for opt in "$@"; do - label="${opt%%"$tab"*}" - if [ ${#label} -gt "$pad" ]; then pad=${#label}; fi - done - - if [ "$STYLE" = 1 ]; then - printf '\n%s%s%s %s%s%s\n' "${C_ACC}" "$S_ASK" "${C_OFF}" "${C_BOLD}" "$question" "${C_OFF}" >/dev/tty - else - printf '\n%s\n' "$question" >/dev/tty - fi - if [ "$ARROWS" = 1 ]; then - stty -icanon -echo min 1 time 0 /dev/tty - first=1 - while :; do - [ "$first" = 1 ] || printf '\033[%sA' "$((count + 1))" >/dev/tty - first=0 - i=1 - for opt in "$@"; do - label="${opt%%"$tab"*}" note='' - [ "$label" = "$opt" ] || note="${opt#*"$tab"}" - if [ "$i" = "$CHOICE" ]; then - printf '\033[2K %s%s %-*s%s %s%s%s\n' "${C_ACC}${C_BOLD}" "$POINTER" "$pad" "$label" "${C_OFF}" \ - "${C_DIM}" "$note" "${C_OFF}" >/dev/tty - else - printf '\033[2K %-*s %s%s%s\n' "$pad" "$label" "${C_DIM}" "$note" "${C_OFF}" >/dev/tty - fi - i=$((i + 1)) - done - printf '\033[2K %s%s%s\n' "${C_DIM}" "$HINT" "${C_OFF}" >/dev/tty - key="$(read_key)" - case "$key" in - up | k) [ "$CHOICE" -gt 1 ] && CHOICE=$((CHOICE - 1)) ;; - down | j) [ "$CHOICE" -lt "$count" ] && CHOICE=$((CHOICE + 1)) ;; - [1-9]) [ "$key" -le "$count" ] && CHOICE="$key" ;; - '' | "$(printf '\r')") break ;; - esac - done - printf '\033[1A\033[2K' >/dev/tty - stty "$STTY_SAVED" /dev/null || true - printf '\033[?25h' >/dev/tty - else - i=1 - for opt in "$@"; do - label="${opt%%"$tab"*}" note='' - [ "$label" = "$opt" ] || note=" ${opt#*"$tab"}" - printf ' %s) %s%s\n' "$i" "$label" "$note" >/dev/tty - i=$((i + 1)) - done - printf 'Choose [%s]: ' "$CHOICE" >/dev/tty - answer="" - read -r answer /dev/null | grep -E "[:]$1->" | head -n 1 | cut -d ' ' -f 1)" - if [ -n "$name" ]; then - OWNER_PROJECT="$("$ENGINE" inspect --format '{{index .Config.Labels "com.docker.compose.project"}}' "$name" 2>/dev/null)" || - OWNER_PROJECT='' - # Only another quickstart install keeps its project, since that is the one - # this script offers to stop. Whoever started the container set the label, - # so it must also be a name Compose would accept. - case "$OWNER_PROJECT" in - *[!a-z0-9_-]*) OWNER_PROJECT='' ;; - esac - case "$OWNER_PROJECT" in - litellm-gateway*) - OWNER="another LiteLLM quickstart install ($ENGINE_NAME project $OWNER_PROJECT)" - FREE_IT="$ENGINE compose -p $OWNER_PROJECT stop" - ;; - *) OWNER_PROJECT='' OWNER="the $ENGINE_NAME container $name" FREE_IT="$ENGINE stop $name" ;; - esac - return 0 - fi - command -v lsof >/dev/null 2>&1 || return 0 - pid="$(lsof -nP -iTCP:"$1" -sTCP:LISTEN -t 2>/dev/null | head -n 1)" - [ -n "$pid" ] || return 0 - FREE_IT="kill $pid" - case "$(ps -o args= -p "$pid" 2>/dev/null)" in - *litellm*) OWNER="a litellm server (pid $pid), maybe one started after pip install litellm" ;; - *) OWNER="$(basename "$(ps -o comm= -p "$pid" 2>/dev/null || echo a program)") (pid $pid)" ;; - esac -} - -# Sets DIR to an existing install: LITELLM_DIR, this folder, ./litellm-gateway, -# or ~/litellm-gateway, whichever has a .env first. -locate_install() { - DIR='' - if [ -n "${LITELLM_DIR:-}" ]; then - if [ -f "$LITELLM_DIR/.env" ]; then DIR="$LITELLM_DIR"; fi - elif [ -f .env ] && [ -f docker-compose.quickstart.yml ]; then - DIR="$(pwd)" - elif [ -f litellm-gateway/.env ]; then - DIR="$(pwd)/litellm-gateway" - elif [ -f "$HOME/litellm-gateway/.env" ]; then - DIR="$HOME/litellm-gateway" - fi - [ -n "$DIR" ] -} - -pick_folder() { - home_dir="$HOME/litellm-gateway" - here_dir="$(pwd)/litellm-gateway" - found=0 - if locate_install; then - found=1 - elif [ -n "${LITELLM_DIR:-}" ]; then - DIR="$LITELLM_DIR" - elif [ "$here_dir" = "$home_dir" ]; then - DIR="$home_dir" - else - tab="$(printf '\t')" - menu "Where should LiteLLM keep its files (.env with your keys, and the compose file)?" 1 \ - "$(tildify "$home_dir")${tab}recommended, reruns always find it" \ - "$(tildify "$here_dir")${tab}this folder" - if [ "$CHOICE" = 2 ]; then DIR="$here_dir"; else DIR="$home_dir"; fi - fi - created=0 - [ -d "$DIR" ] || created=1 - mkdir -p "$DIR" - cd "$DIR" - DIR="$(pwd)" - if [ "$found" = 1 ]; then - step "Found your install in ${C_BOLD}$(tildify "$DIR")${C_OFF}" - else - step "Files go in ${C_BOLD}$(tildify "$DIR")${C_OFF}" - fi - if [ ! -f .env ] && command -v git >/dev/null 2>&1 && - git ls-files --error-unmatch .env >/dev/null 2>&1; then - # An ignore rule does not cover a tracked file, so new keys written here - # would show up as a change to commit. - fail "Git tracks a .env file in this folder," "so your keys could be committed." - cat >&2 <<'MSG' - - Install into another folder (set LITELLM_DIR), or stop tracking the file - first with: git rm --cached .env -MSG - exit 1 - fi - if [ "$created" = 1 ]; then - # A folder this script made holds only its own files, so keep all of it out of git. - printf '*\n' >.gitignore - elif command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1 && - ! git check-ignore -q .env 2>/dev/null; then - # In a folder that already existed, such as a repository root, leave the - # tracked .gitignore alone and add only .env to this clone's local exclude - # list, so the generated keys cannot be committed. - exclude="$(git rev-parse --git-path info/exclude)" - mkdir -p "$(dirname "$exclude")" - exclude="$(cd "$(dirname "$exclude")" && pwd)/exclude" - printf '/%s.env\n' "$(git rev-parse --show-prefix)" >>"$exclude" - step "Kept .env out of git ${C_DIM}(added it to this repository's local exclude list, $(tildify "$exclude"))" - elif ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - # An existing folder outside git: ignore only .env, so it stays out of - # commits if the folder becomes a repository later. - if ! grep -qxE '\*|\.env' .gitignore 2>/dev/null; then - # Start on a new line if the file does not end with one. - if [ -s .gitignore ] && [ -n "$(tail -c 1 .gitignore)" ]; then printf '\n' >>.gitignore; fi - printf '.env\n' >>.gitignore - fi - fi -} - -pick_port() { - saved="" - [ -f .env ] && saved="$(sed -n 's/^LITELLM_PORT=//p' .env | tail -n 1)" - if [ -n "${LITELLM_PORT:-}" ]; then - PORT="$LITELLM_PORT" - elif [ -n "$saved" ]; then - PORT="$saved" - elif [ -f .env ]; then - # An existing install without a saved port runs on the compose default. - PORT=4000 - else - PORT=4000 - while ! port_free "$PORT"; do - PORT=$((PORT + 1)) - if [ "$PORT" -gt 4099 ]; then - fail "Ports 4000 to 4099 are all in use." "Set LITELLM_PORT to a free port and run this again." - exit 1 - fi - done - if [ "$PORT" = 4000 ]; then - step "Port ${C_BOLD}4000${C_OFF} is free" - else - port_owner 4000 - warn "Port 4000 is in use${OWNER:+ by $OWNER}, so LiteLLM will use ${C_BOLD}$PORT${C_OFF}" - fi - fi - export LITELLM_PORT="$PORT" -} - -# Docker names containers and the database volume after the project, so an -# install outside the home folder gets its own name and never shares a -# database with another litellm-gateway folder. -check_new_install() { - project=litellm-gateway - [ "$DIR" = "$HOME/litellm-gateway" ] || project="litellm-gateway-$(printf '%s' "$DIR" | cksum | cut -d ' ' -f 1)" - # Postgres keeps the password it was created with, so a new password over an - # old database volume would lock the gateway out. Stop and explain instead. - if "$ENGINE" volume inspect "${project}_postgres_data" >/dev/null 2>&1; then - fail "Found a database from an earlier install" "($ENGINE_NAME volume ${project}_postgres_data)" - cat >&2 </dev/null && grep -q '^main "\$@"$' "$tmp"; then - mv -f "$tmp" quickstart.sh - else - rm -f "$tmp" - fi - fi - [ -f quickstart.sh ] || return 0 - quoted="$(printf '%s' "$DIR" | sed "s/'/'\\\\''/g")" - { - printf '#!/bin/sh\n' - printf '# Manages the LiteLLM Gateway in this folder. Run it for a menu, or with one of\n' - printf '# status, up, down, restart, logs, open, update, uninstall\n' - printf "export LITELLM_DIR='%s'\n" "$quoted" - # shellcheck disable=SC2016 # expanded when the command runs - printf 'exec sh "$LITELLM_DIR/quickstart.sh" "$@"\n' - } >litellm-gateway - chmod 755 litellm-gateway - # Linked into ~/.local/bin when that is on PATH. A link another install made - # stays, unless that install is gone. - link="$HOME/.local/bin/litellm-gateway" - case ":$PATH:" in *":$HOME/.local/bin:"*) ;; *) return 0 ;; esac - [ -d "$HOME/.local/bin" ] || return 0 - if [ -L "$link" ]; then - if [ "$(readlink "$link")" = "$DIR/litellm-gateway" ] || [ -e "$link" ]; then return 0; fi - rm -f "$link" - elif [ -e "$link" ]; then - return 0 - fi - ln -s "$DIR/litellm-gateway" "$link" -} - -# Sets COMMAND to how this install's litellm-gateway command runs from anywhere. -find_command() { - COMMAND='' - [ -x "$DIR/litellm-gateway" ] || return 0 - COMMAND="$(tildify "$DIR")/litellm-gateway" - case ":$PATH:" in - *":$HOME/.local/bin:"*) - if [ "$(readlink "$HOME/.local/bin/litellm-gateway" 2>/dev/null)" = "$DIR/litellm-gateway" ]; then - COMMAND=litellm-gateway - fi - ;; - esac -} - -hint() { - if [ -n "$COMMAND" ]; then - printf '%s %s' "$COMMAND" "$1" - return 0 - fi - case "$1" in - up) set -- up -d ;; - down) set -- stop ;; - uninstall) set -- down --volumes ;; - logs) set -- logs litellm ;; - esac - printf 'cd %s && %s compose -f docker-compose.quickstart.yml %s' "$(tildify "$DIR")" "$ENGINE" "$*" -} - -# The gateway container's status, such as "Up 2 hours", or nothing when there is none. -gateway_status() { - "$ENGINE" ps -a --filter "label=com.docker.compose.project=$project" \ - --filter label=com.docker.compose.service=litellm --format '{{.Status}}' 2>/dev/null | head -n 1 -} - -is_running() { - case "$(gateway_status)" in - Up*) return 0 ;; - *) return 1 ;; - esac -} - -project_containers() { - "$ENGINE" ps -q "$@" --filter "label=com.docker.compose.project=$project" 2>/dev/null -} - -# The running version, from the API description: the first "version" in it is -# the gateway's, and it needs no key. -gateway_version() { - curl -fsS --max-time 5 "http://127.0.0.1:$PORT/openapi.json" 2>/dev/null | - grep -o '"version": *"[^"]*"' | head -n 1 | sed 's/.*"\([^"]*\)"$/\1/' -} - -# How many requests the gateway has handled, failed ones included, counted from -# its request logs. It needs the admin key, which curl reads from stdin so it -# stays off the command line. Empty when the gateway cannot say. -gateway_requests() { - key="$(sed -n 's/^LITELLM_MASTER_KEY=//p' .env | tail -n 1)" - [ -n "$key" ] || return 0 - printf 'header = "Authorization: Bearer %s"\n' "$key" | - curl -fsS --max-time 5 -K - \ - "http://127.0.0.1:$PORT/global/activity?start_date=2000-01-01&end_date=$(date -u +%Y-%m-%d)" 2>/dev/null | - grep -o '"sum_api_requests": *[0-9]*' | grep -o '[0-9]*$' -} - -# Stops the other quickstart install port_owner found. -stop_owner() { - "$ENGINE" compose -p "$OWNER_PROJECT" stop -} - -# Sets NEXT_PORT to the first free port after $1, or empty when the next 100 -# are all taken. -next_free_port() { - NEXT_PORT=$(($1 + 1)) - while ! port_free "$NEXT_PORT"; do - NEXT_PORT=$((NEXT_PORT + 1)) - if [ "$NEXT_PORT" -gt $(($1 + 100)) ]; then - NEXT_PORT='' - return 0 - fi - done -} - -# Moves this install to NEXT_PORT and saves it in .env, through a fresh mktemp -# file (mode 600) and a rename so the keys are never half written. -move_port() { - ENV_TMP="$(mktemp .env.XXXXXX)" || return 1 - if ! awk '!/^LITELLM_PORT=/' .env >"$ENV_TMP" || - ! printf 'LITELLM_PORT=%s\n' "$NEXT_PORT" >>"$ENV_TMP" || ! mv -f "$ENV_TMP" .env; then - rm -f "$ENV_TMP" - ENV_TMP='' - return 1 - fi - ENV_TMP='' - PORT="$NEXT_PORT" - export LITELLM_PORT="$PORT" - url="http://localhost:$PORT/ui" -} - -# Before starting: if something else holds the port, stop it when it is another -# quickstart install, or move to a free port when it is anything else. -check_port() { - if is_running || port_free "$PORT"; then return 0; fi - port_owner "$PORT" - tab="$(printf '\t')" NEXT_PORT='' - if [ -n "$OWNER_PROJECT" ]; then - if [ "$INTERACTIVE" = 1 ]; then - menu "Port $PORT is in use by $OWNER. Stop it and start this one?" 1 \ - "Yes, stop it${tab}$FREE_IT" "No" - if [ "$CHOICE" = 1 ] && spin "Stopping it" "" stop_owner; then - step "Stopped $OWNER" - return 0 - fi - fi - elif [ -z "$PORT_PINNED" ]; then - next_free_port "$PORT" - if [ -n "$NEXT_PORT" ] && [ "$INTERACTIVE" = 1 ]; then - menu "Port $PORT is in use by ${OWNER:-another program}. Use port $NEXT_PORT instead?" 1 \ - "Yes, use $NEXT_PORT${tab}saved in $(tildify "$DIR")/.env" "No" - if [ "$CHOICE" = 1 ]; then - old="$PORT" - if move_port; then - step "LiteLLM moved from port $old to ${C_BOLD}$PORT${C_OFF}" - return 0 - fi - fail "Could not write $(tildify "$DIR")/.env." - exit 1 - fi - fi - fi - fail "Port $PORT is in use by ${OWNER:-another program}." \ - "Free it, or set LITELLM_PORT in $(tildify "$DIR")/.env to another port${NEXT_PORT:+, such as $NEXT_PORT}." - if [ -n "$FREE_IT" ]; then printf ' To free it: %s\n' "$FREE_IT" >&2; fi - exit 1 -} - -wait_ready() { - i=0 - until curl -fsS "http://127.0.0.1:$PORT/health/readiness" >/dev/null 2>&1; do - i=$((i + 1)) - if [ "$i" -gt 90 ]; then return 1; fi - sleep 2 - done -} - -launch() { - check_port - [ -f docker-compose.quickstart.yml ] || fetch_compose - started="$(date +%s)" - detail='' - for image in $(compose config --images 2>/dev/null); do - "$ENGINE" image inspect "$image" >/dev/null 2>&1 || detail="downloading images, first run only" - done - [ "$STYLE" = 1 ] || echo "Starting LiteLLM and Postgres${detail:+ ($detail)}..." - if ! spin "Starting LiteLLM and Postgres" "$detail" compose up -d; then - fail "$ENGINE_NAME could not start LiteLLM and Postgres." "Its output is above." - exit 1 - fi - if ! spin "Waiting for the gateway to be ready" "" wait_ready; then - fail "The gateway did not become ready in 3 minutes." "See what it logged:" - echo " $(hint logs)" >&2 - exit 1 - fi - step "LiteLLM is up at ${C_ACC}$url${C_OFF} ${C_DIM}· $(elapsed_since "$started")" -} - -stop_stack() { - if [ -z "$(project_containers)" ]; then - step "LiteLLM is already stopped" - return 0 - fi - if ! spin "Stopping LiteLLM and Postgres" "" compose stop; then - fail "$ENGINE_NAME could not stop LiteLLM and Postgres." "Its output is above." - exit 1 - fi - step "Stopped LiteLLM. Your models, keys, and logs are kept ${C_DIM}· start it again: $(hint up)" -} - -restart_stack() { - if [ -n "$(project_containers)" ] && ! spin "Stopping LiteLLM and Postgres" "" compose stop; then - fail "$ENGINE_NAME could not stop LiteLLM and Postgres." "Its output is above." - exit 1 - fi - launch -} - -update_stack() { - before='' - if is_running; then before="$(gateway_version)"; fi - fetch_compose - install_command - find_command - images="$(compose config --images 2>/dev/null)" || images='' - old='' - for image in $images; do - old="$old $("$ENGINE" image inspect --format '{{.Id}}' "$image" 2>/dev/null)" - done - [ "$STYLE" = 1 ] || echo "Downloading the latest LiteLLM and Postgres images..." - if ! spin "Downloading the latest images" "" compose pull; then - fail "$ENGINE_NAME could not download the images." "Its output is above." - exit 1 - fi - launch - # Delete the images this replaced. Removal fails, and they stay, while any - # container still uses them or another tag points at them. - for id in $old; do - for image in $images; do - [ "$id" != "$("$ENGINE" image inspect --format '{{.Id}}' "$image" 2>/dev/null)" ] || continue 2 - done - "$ENGINE" image rm "$id" >/dev/null 2>&1 || true - done - after="$(gateway_version)" - if [ -n "$before" ] && [ "$before" = "$after" ]; then - step "LiteLLM $after is already the latest" - elif [ -n "$before" ] && [ -n "$after" ]; then - step "Updated LiteLLM from $before to $after" - else - step "LiteLLM ${after:+$after }is on the latest image" - fi -} - -show_logs() { - if [ -z "$(project_containers -a)" ]; then - step "No logs yet: start LiteLLM first" - return 0 - fi - if [ "${1:-}" = follow ]; then - printf '%sThe gateway logs. Press Ctrl+C to stop following them.%s\n' "${C_DIM}" "${C_OFF}" - exec "$ENGINE" compose -f docker-compose.quickstart.yml logs -f --tail 100 litellm - fi - compose logs --tail 100 litellm -} - -desktop_row() { - if [ "$ENGINE" = docker ] && docker info --format '{{.OperatingSystem}}' 2>/dev/null | grep -q 'Docker Desktop'; then - printf 'Desktop|listed as %s in Docker Desktop' "$project" - fi -} - -show_status() { - status="$(gateway_status)" - image="$(compose config --images 2>/dev/null | grep -m 1 litellm)" || image='' - desktop="$(desktop_row)" - case "$status" in - Up*) - uptime="${status#Up }" - version="$(gateway_version)" - requests="$(gateway_requests)" || requests='' - case "$requests" in - '') ;; - 0) requests='Requests|none yet' ;; - *) requests="Requests|$requests handled" ;; - esac - box "LiteLLM is running" "Admin UI|$url" "Version|${version:-starting}" "Uptime|${uptime%% (*}" \ - ${requests:+"$requests"} "Folder|$(tildify "$DIR")" "Image|$image" ${desktop:+"$desktop"} "Stop it|$(hint down)" - ;; - *) - box "LiteLLM is stopped" "Folder|$(tildify "$DIR")" "Image|$image" ${desktop:+"$desktop"} \ - "Start it|$(hint up)" "Remove it|$(hint uninstall)" - ;; - esac -} - -open_ui() { - if is_running; then - open_url "$url" - else - warn "LiteLLM is stopped. Start it with: $(hint up)" - fi -} - -# Returns 1 when the person keeps the install, so the menu can carry on. -uninstall() { - volume="${project}_postgres_data" - [ -f docker-compose.quickstart.yml ] || fetch_compose - images="$(compose config --images 2>/dev/null)" || images='' - link='' - if [ "$(readlink "$HOME/.local/bin/litellm-gateway" 2>/dev/null)" = "$DIR/litellm-gateway" ]; then - link="$HOME/.local/bin/litellm-gateway" - fi - if [ "$INTERACTIVE" = 1 ]; then - printf '\n%sThis removes LiteLLM from %s:%s\n' "${C_BOLD}" "$(tildify "$DIR")" "${C_OFF}" - printf ' - its containers (%s project %s)\n' "$ENGINE_NAME" "$project" - printf ' - its database, the %s volume %s, with your models, keys, and spend logs\n' "$ENGINE_NAME" "$volume" - printf ' - its files in %s, including .env with the admin password\n' "$(tildify "$DIR")" - if [ -n "$link" ]; then printf ' - the litellm-gateway command in %s\n' "$(tildify "$HOME/.local/bin")"; fi - menu "Remove LiteLLM and delete its database?" 2 "Yes, remove it" "No, keep it" - if [ "$CHOICE" != 1 ]; then - step "Kept LiteLLM and its data" - return 1 - fi - elif [ "$NO_QUESTIONS" != 1 ]; then - fail "Removing LiteLLM deletes its database ($ENGINE_NAME volume $volume)." "To confirm, run it again with --yes." - exit 1 - fi - - [ "$STYLE" = 1 ] || echo "Removing the LiteLLM and Postgres containers and the database..." - if ! spin "Removing the containers and the database" "" compose down --volumes --remove-orphans; then - fail "$ENGINE_NAME could not remove them." "Its output is above." - exit 1 - fi - step "Removed the containers and the database ${C_DIM}($volume)" - - kept='' - for image in $images; do - "$ENGINE" image inspect "$image" >/dev/null 2>&1 && kept="$kept $image" - done - if [ -n "$kept" ] && [ "$INTERACTIVE" = 1 ]; then - # The size on disk, as the images list shows it (1.68GB, or 1.23 GB on Podman). - size="$(for image in $kept; do "$ENGINE" images --format '{{.Size}}' "$image" 2>/dev/null | head -n 1; done | - awk '{ n = $1 + 0; u = $0; gsub(/[0-9. ]/, "", u); m = 1 - if (u ~ /^[kK]/) m = 1e3; else if (u ~ /^M/) m = 1e6; else if (u ~ /^G/) m = 1e9; else if (u ~ /^T/) m = 1e12 - s += n * m } END { if (s > 0) printf " (%.1f GB)", s / 1e9 }')" - tab="$(printf '\t')" - menu "Also delete the LiteLLM and Postgres images${size}?" 2 \ - "Yes, delete them" "No, keep them${tab}a reinstall starts faster" - if [ "$CHOICE" = 1 ]; then - for image in $kept; do - if "$ENGINE" image rm "$image" >/dev/null 2>&1; then - step "Deleted the image $image" - kept="$(printf '%s' "$kept" | sed "s| $image||")" - else - warn "Kept the image $image, which other containers use" - fi - done - fi - fi - - rm -f .env docker-compose.quickstart.yml quickstart.sh quickstart.ps1 litellm-gateway litellm-gateway.cmd - if [ -f .gitignore ] && ! grep -qvxE '\*|\.env' .gitignore; then rm -f .gitignore; fi - if [ -n "$link" ]; then rm -f "$link"; fi - cd "$HOME" - if rmdir "$DIR" 2>/dev/null; then - step "Deleted $(tildify "$DIR")" - else - step "Deleted the LiteLLM files in $(tildify "$DIR") ${C_DIM}(the folder stays: it has other files)" - fi - - left='' - if [ -n "$(project_containers -a)" ]; then left=" containers,"; fi - if "$ENGINE" volume inspect "$volume" >/dev/null 2>&1; then left="$left volume $volume,"; fi - if [ -f "$DIR/.env" ]; then left="$left $(tildify "$DIR")/.env,"; fi - if [ -n "$left" ]; then - warn "Still there:${left%,}" - return 0 - fi - step "LiteLLM is removed. Nothing is left running, and its data is deleted" - if [ -n "$kept" ]; then - printf ' %sThe images stay, so a reinstall starts faster. To delete them:%s %s image rm%s\n' \ - "${C_DIM}" "${C_OFF}" "$ENGINE" "$kept" - fi -} - -open_url() { - for opener in open xdg-open; do - if command -v "$opener" >/dev/null 2>&1 && "$opener" "$1" >/dev/null 2>&1; then - step "Opened ${C_ACC}$1${C_OFF} in your browser" - return 0 - fi - done - printf ' %sOpen%s %s%s%s %swhen you are ready.%s\n' "${C_DIM}" "${C_OFF}" "${C_ACC}" "$1" "${C_OFF}" "${C_DIM}" "${C_OFF}" -} - -# A clipboard to copy the admin password to, or nothing. Over SSH the -# clipboard would be the server's, not yours. -find_clipboard() { - CLIP='' - [ -z "${SSH_CONNECTION:-}${SSH_TTY:-}" ] || return 0 - if command -v pbcopy >/dev/null 2>&1; then CLIP=pbcopy - elif [ -n "${WAYLAND_DISPLAY:-}" ] && command -v wl-copy >/dev/null 2>&1; then CLIP=wl-copy - elif [ -n "${DISPLAY:-}" ] && command -v xclip >/dev/null 2>&1; then CLIP="xclip -selection clipboard" - elif [ -n "${DISPLAY:-}" ] && command -v xsel >/dev/null 2>&1; then CLIP="xsel --clipboard --input" - elif command -v clip.exe >/dev/null 2>&1; then CLIP=clip.exe - fi -} - -# The admin password is the master key. It goes through a pipe, so it is -# never on screen or in the process list. -copy_password() { - pw="$(sed -n 's/^LITELLM_MASTER_KEY=//p' .env | head -n 1)" - if [ -z "$pw" ]; then - warn "No LITELLM_MASTER_KEY in $(tildify "$DIR")/.env." - return 0 - fi - # shellcheck disable=SC2086 # CLIP is a command and its flags - if printf '%s' "$pw" | $CLIP >/dev/null 2>&1; then - step "Copied the admin password. Paste it into the sign-in page." - else - warn "Could not copy it. The password is LITELLM_MASTER_KEY in $(tildify "$DIR")/.env." - fi - pw='' -} - -# After the summary: copy the password, open the admin UI, or finish. The -# menu comes back after each choice with the next step as its default. -next_steps() { - [ "$INTERACTIVE" = 1 ] || return 0 - if [ -z "$CLIP" ]; then - menu "Open the admin UI in your browser?" 1 "Yes" "No" - if [ "$CHOICE" = 1 ]; then open_url "$url"; fi - return 0 - fi - tab="$(printf '\t')" - pick=1 - while :; do - menu "What next?" "$pick" \ - "Copy the admin password${tab}to the clipboard, ready to paste" \ - "Open the admin UI in your browser" \ - "Done" - case "$CHOICE" in - 1) copy_password; pick=2 ;; - 2) open_url "$url"; pick=3 ;; - *) return 0 ;; - esac - done -} - -# The menu a rerun shows for an existing install. Its options follow whether -# LiteLLM is running, and come back after each choice until Done. -manage() { - show_status - find_clipboard - tab="$(printf '\t')" - pick=1 - while :; do - if is_running; then - question="LiteLLM is running at $url. What next?" - set -- "Open the admin UI in your browser" - [ -z "$CLIP" ] || set -- "$@" "Copy the admin password${tab}to the clipboard, ready to paste" - set -- "$@" "Stop it${tab}keeps your models, keys, and logs" "Restart it" \ - "Show the logs${tab}the last 100 lines" "Update it${tab}download the latest image and restart" \ - "Remove it${tab}delete its containers, database, and files" "Done" - else - question="LiteLLM is stopped. What next?" - set -- "Start it${tab}at $url" "Update it${tab}download the latest image and start" \ - "Show the logs${tab}the last 100 lines" "Remove it${tab}delete its containers, database, and files" "Done" - fi - [ "$pick" -le $# ] || pick=1 - menu "$question" "$pick" "$@" - picked='' - eval "picked=\${$CHOICE}" - pick=1 - case "$picked" in - Open*) open_url "$url"; pick=$# ;; - Copy*) copy_password ;; - Start*) launch ;; - Stop*) stop_stack ;; - Restart*) restart_stack ;; - Show*) show_logs ;; - Update*) update_stack ;; - Remove*) if uninstall; then return 0; fi ;; - *) return 0 ;; - esac - done -} - -main() { - NO_QUESTIONS=0 ACTION='' - for arg in "$@"; do - case "$arg" in - -y | --yes) NO_QUESTIONS=1 ;; - -h | --help | help) usage; exit 0 ;; - status | start | up | stop | down | restart | logs | open | update | uninstall) - if [ -n "$ACTION" ]; then - echo "Choose one action, not both $ACTION and $arg" >&2 - exit 1 - fi - ACTION="$arg" - ;; - *) echo "Unknown option: $arg" >&2; usage >&2; exit 1 ;; - esac - done - case "$ACTION" in - up) ACTION=start ;; - down) ACTION=stop ;; - esac - - setup_output - detect_terminal - # Agents and CI get the defaults even inside a terminal, so nothing waits on a keypress. - if [ "$NO_QUESTIONS" = 1 ] || [ -n "${CI:-}" ] || [ -n "${CLAUDECODE:-}" ]; then INTERACTIVE=0; fi - trap restore_terminal EXIT - trap on_interrupt INT TERM - - if [ -n "$ACTION" ]; then - : - elif [ "$STYLE" = 1 ]; then - printf '\n%s%s LiteLLM quickstart%s\n' "${C_ACC}${C_BOLD}" "$S_HEAD" "${C_OFF}" - printf '%s The gateway, Postgres, and the admin UI in one command%s\n\n' "${C_DIM}" "${C_OFF}" - else - echo "LiteLLM quickstart" - fi - - # Podman and Rancher Desktop (nerdctl in containerd mode; its dockerd mode - # already provides a docker CLI) work the same way through Compose v2. - ENGINE='' - for cand in docker podman nerdctl; do - if command -v "$cand" >/dev/null 2>&1; then - ENGINE="$cand" - break - fi - done - if [ -z "$ENGINE" ]; then - fail "No container engine found (docker, podman, or nerdctl)." "The LiteLLM Gateway runs in a container alongside a Postgres database." - cat >&2 <<'MSG' - - Install Docker (https://docs.docker.com/get-docker/), Podman (https://podman.io), - or Rancher Desktop (https://rancherdesktop.io), then run this again. - Or deploy in one click (Railway or Render): https://docs.litellm.ai/docs/proxy/docker_quick_start - Only need to call models from Python? pip install litellm -MSG - exit 1 - fi - case "$ENGINE" in - podman) ENGINE_NAME=Podman ;; - nerdctl) ENGINE_NAME=nerdctl ;; - *) ENGINE_NAME=Docker ;; - esac - compose_version="$("$ENGINE" compose version --short 2>/dev/null)" || - { fail "Compose v2 ('$ENGINE compose') is required."; exit 1; } - if ! "$ENGINE" info >/dev/null 2>&1; then - if [ "$ENGINE" = podman ]; then - fail "Podman is installed but not running." "Start it (podman machine start) and run this again." - else - fail "$ENGINE_NAME is installed but not running." "Start it and run this again." - fi - exit 1 - fi - - if [ -n "$ACTION" ]; then - if ! locate_install; then - fail "No LiteLLM install found" "in ${LITELLM_DIR:-this folder, ./litellm-gateway, or ~/litellm-gateway}." - printf ' Install it with: curl -fsSL %s | sh\n' "$SCRIPT_URL" >&2 - exit 1 - fi - use_install - find_command - case "$ACTION" in - status) show_status ;; - start) if is_running; then step "LiteLLM is already running at ${C_ACC}$url${C_OFF}"; else launch; fi ;; - stop) stop_stack ;; - restart) restart_stack ;; - logs) if [ "$INTERACTIVE" = 1 ]; then show_logs follow; else show_logs; fi ;; - open) open_ui ;; - update) update_stack ;; - uninstall) uninstall || true ;; - esac - return 0 - fi - - engine_version="$("$ENGINE" version --format '{{.Server.Version}}' 2>/dev/null)" || engine_version='' - step "$ENGINE_NAME ${engine_version:+$engine_version }with Compose ${compose_version#v} is running" - - pick_folder - if [ "$found" = 1 ] && [ "$INTERACTIVE" = 1 ]; then - use_install - if [ ! -x litellm-gateway ] || [ ! -f quickstart.sh ]; then install_command; fi - find_command - manage - return 0 - fi - if [ "$found" = 0 ]; then - command -v openssl >/dev/null 2>&1 || { fail "openssl is required to generate keys."; exit 1; } - check_new_install - fi - fetch_compose - step "Downloaded docker-compose.quickstart.yml" - - if [ "$found" = 1 ]; then - step "Reusing .env, so existing keys and data keep working" - else - pick_port - master="$(openssl rand -hex 32)" - salt="$(openssl rand -hex 32)" - db_password="$(openssl rand -hex 24)" - # Write a fresh mktemp file (mode 600, never a reused one) and rename it, so a - # failed write never leaves a partial .env that a rerun would mistake for a - # finished install. - if ! ENV_TMP="$(mktemp .env.XXXXXX)" || - ! printf 'LITELLM_MASTER_KEY=sk-%s\nLITELLM_SALT_KEY=sk-%s\nPOSTGRES_PASSWORD=%s\nLITELLM_PORT=%s\nLITELLM_BIND=127.0.0.1:\nCOMPOSE_PROJECT_NAME=%s\n' \ - "$master" "$salt" "$db_password" "$PORT" "$project" >"$ENV_TMP" || ! mv -f "$ENV_TMP" .env; then - if [ -n "$ENV_TMP" ]; then rm -f "$ENV_TMP"; fi - fail "Could not write $(tildify "$DIR")/.env." - exit 1 - fi - ENV_TMP='' - unset master salt db_password - step "Generated .env with your master key, salt key, and database password ${C_DIM}(keep this file; only you can read it)" - fi - - use_install - install_command - find_command - launch - - password="the LITELLM_MASTER_KEY value in $(tildify "$DIR")/.env" - CLIP='' - if [ "$INTERACTIVE" = 1 ]; then find_clipboard; fi - if [ -n "$CLIP" ]; then - password="copy it below, or LITELLM_MASTER_KEY in $(tildify "$DIR")/.env" - fi - desktop="$(desktop_row)" - # A rerun finds ~/litellm-gateway on its own; any other folder needs naming. - rerun="rerun the install command" - [ "$DIR" = "$HOME/litellm-gateway" ] || rerun="$rerun with LITELLM_DIR=$(tildify "$DIR")" - echo - box "LiteLLM is running" \ - "Admin UI|$url" \ - "Username|admin" \ - "Password|$password" \ - "Next|in the UI, open Models + Endpoints > Add Model and paste a provider API key" \ - "Stop it|$(hint down)" \ - "Start it|$(hint up)" \ - "Remove it|$(hint uninstall)" \ - ${desktop:+"$desktop"} \ - "Manage|${COMMAND:+run $COMMAND, or }$rerun" - - next_steps -} - -main "$@" diff --git a/tests/base_sdk_tests/test_core_distribution.py b/tests/base_sdk_tests/test_core_distribution.py index adb60cbb478..f8618d606a4 100644 --- a/tests/base_sdk_tests/test_core_distribution.py +++ b/tests/base_sdk_tests/test_core_distribution.py @@ -31,7 +31,7 @@ ROOT: Final = Path(__file__).resolve().parents[2] def test_build_command_selects_requested_distributions( tmp_path: Path, sdist_only: bool, package_cli_args: tuple[str, ...], package: str ) -> None: - from scripts.build_core_distribution import main + from litellm.scripts.build_core_distribution import main output: Final = tmp_path / "dist" arguments: Final = ["build_core_distribution.py", *package_cli_args, "--out-dir", str(output)] + ( @@ -39,8 +39,8 @@ def test_build_command_selects_requested_distributions( ) with ( patch.object(sys, "argv", arguments), - patch("scripts.build_core_distribution.stage_core_distribution") as stage, - patch("scripts.build_core_distribution.subprocess.run") as run, + patch("litellm.scripts.build_core_distribution.stage_core_distribution") as stage, + patch("litellm.scripts.build_core_distribution.subprocess.run") as run, ): main() run.assert_called_once() @@ -67,7 +67,7 @@ def source_repository(tmp_path: Path) -> Path: def test_staging_stamps_release_version_without_modifying_sources( tmp_path: Path, source_repository: Path, package: str ) -> None: - from scripts.build_core_distribution import SOURCES, stage_core_distribution + from litellm.scripts.build_core_distribution import SOURCES, stage_core_distribution source: Final = source_repository for name in SOURCES: @@ -112,7 +112,7 @@ def test_staging_stamps_release_version_without_modifying_sources( def test_gateway_staging_refuses_manifest_without_in_tree_rust_path(tmp_path: Path, source_repository: Path) -> None: - from scripts.build_core_distribution import SOURCES, stage_core_distribution + from litellm.scripts.build_core_distribution import SOURCES, stage_core_distribution source: Final = source_repository for name in SOURCES: @@ -215,7 +215,7 @@ def test_core_sdist_rebuilds_without_repository(distributions: tuple[Path, Path] with tarfile.open(distributions[1]) as archive: archive.extractall(tmp_path, filter="data") source: Final = next(tmp_path.glob("litellm_core-*")) - assert not (source / "scripts/build_core_distribution.py").exists() + assert not (source / "litellm/scripts/build_core_distribution.py").exists() assert tomllib.loads((source / "pyproject.toml").read_text())["project"]["name"] == "litellm-core" result: Final = subprocess.run( ["uv", "build", "--python", sys.executable, "--wheel", "--out-dir", str(tmp_path / "rebuilt")], @@ -233,7 +233,7 @@ def test_core_sdist_rebuilds_without_repository(distributions: tuple[Path, Path] def test_staging_copies_sources_without_build_artifacts(tmp_path: Path, source_repository: Path) -> None: - from scripts.build_core_distribution import SOURCES, stage_core_distribution + from litellm.scripts.build_core_distribution import SOURCES, stage_core_distribution source: Final = source_repository for name in SOURCES: @@ -255,7 +255,7 @@ def test_staging_copies_sources_without_build_artifacts(tmp_path: Path, source_r def test_staging_preserves_gitignore_rules(tmp_path: Path, source_repository: Path) -> None: - from scripts.build_core_distribution import SOURCES, stage_core_distribution + from litellm.scripts.build_core_distribution import SOURCES, stage_core_distribution source: Final = source_repository for name in SOURCES: @@ -287,7 +287,7 @@ def test_staging_preserves_gitignore_rules(tmp_path: Path, source_repository: Pa def test_staging_rejects_missing_release_version(tmp_path: Path) -> None: - from scripts.build_core_distribution import stage_core_distribution + from litellm.scripts.build_core_distribution import stage_core_distribution manifest: Final = tmp_path / "litellm/packaging/litellm-core/pyproject.toml" manifest.parent.mkdir(parents=True) diff --git a/tests/code_coverage_tests/check_migrations_no_data_rewrites.py b/tests/code_coverage_tests/check_migrations_no_data_rewrites.py index 48c2c468b9f..43e776cd33d 100644 --- a/tests/code_coverage_tests/check_migrations_no_data_rewrites.py +++ b/tests/code_coverage_tests/check_migrations_no_data_rewrites.py @@ -27,7 +27,7 @@ table without a default is still free on every version. An index on a request-log table cannot ship as a migration at all. A plain `CREATE INDEX` blocks writes to the table until the build finishes, and `CREATE INDEX CONCURRENTLY` is refused by Postgres on a partitioned parent, which LiteLLM_SpendLogs -is wherever the operator ran scripts/db/partition_spend_logs.sql. The migration job builds +is wherever the operator ran litellm/scripts/db/partition_spend_logs.sql. The migration job builds those indexes after `migrate deploy`, concurrently and per partition, from the list in litellm_proxy_extras/request_log_indexes.py, so that list is where a new one goes. diff --git a/tests/code_coverage_tests/test_generate_provider_endpoints_support.py b/tests/code_coverage_tests/test_generate_provider_endpoints_support.py index c2ec6f56c3a..62782edb7f4 100644 --- a/tests/code_coverage_tests/test_generate_provider_endpoints_support.py +++ b/tests/code_coverage_tests/test_generate_provider_endpoints_support.py @@ -6,7 +6,7 @@ from typing import Final import pytest from pydantic import JsonValue -from scripts.generate_provider_endpoints_support import ( +from litellm.scripts.generate_provider_endpoints_support import ( generate_provider_endpoints_support, main, ) @@ -155,7 +155,7 @@ def test_check_reports_stale_output_without_writing(tmp_path: Path, capsys: pyte assert not (tmp_path / "litellm/provider_endpoints_support_backup.json").exists() error_output: Final = capsys.readouterr().err assert "provider endpoint support is stale" in error_output - assert "scripts/generate_provider_endpoints_support.py" in error_output + assert "litellm/scripts/generate_provider_endpoints_support.py" in error_output def test_check_reports_stale_backup_without_writing(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: diff --git a/tests/integration/README.md b/tests/integration/README.md index 442d7dcddbf..16a9d6f807d 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -26,9 +26,9 @@ Reuse the existing canned provider handlers through `_support/upstream.py`. It r Owned proxies disable `.env` loading by default. A contract testing dotenv behavior can explicitly set `PYTHON_DOTENV_DISABLED=0` in its owned environment. The runner drops inherited `*_API_KEY` variables, so a contract that needs one sets a synthetic value in its owned environment. An explicit scratch writer database drops an inherited reader with a different database name; pass both database URLs when the contract needs a paired reader. Database relays close listeners, active connections and their event loops on teardown. Upstream startup failures reap the child process before returning the failure. Custom subprocesses, direct SQL and background tasks still need an explicit owner in the test -Owned proxy workers configure a 32-thread executor before application startup, preserving additional worker startup hooks. Held synchronous provider calls must not consume a CPU-dependent executor limit before the rest of a fault burst can start. The proxy entrypoint imports LiteLLM inside `main`, so spawned workers begin supervision before importing the application. Worker healthchecks have a five-second deadline by default, so uvicorn kills a worker that stays paused longer than that. Tests never signal proxy processes themselves. `OwnedProxy.kill_worker` SIGKILLs a worker and returns once its replacement answers readiness with its own pid, `replacing_worker` does the same for a kill that has to land mid-burst, and `suspended` pauses a worker or the supervisor, which only works on a proxy launched with `healthcheck=WorkerHealthcheck.LENIENT`. The `kill()` that `replacing_worker` yields returns as soon as the worker is dead, so a test can release requests it holds on the surviving worker right away. Uvicorn may take a full worker healthcheck to notice the death, so the block waits for the supervisor to reap the worker and for the replacement only when it exits. Helper processes that are not the proxy stop through `stop_child`, `stop_processes` and `stop_process_group` and pause through `paused`. `scripts/check_test_quality.py` rule TQ010 rejects a raw `os.kill`, SIGKILL / SIGSTOP / SIGCONT, `.kill()`, `.terminate()`, `.suspend()` or `.send_signal()` anywhere under `tests/integration` outside `_support`, and TQ011 rejects an assertion that caps an elapsed time with a fixed number. Bound timings with `_support/timing.py` instead, relative to a measured control or to the injected delay. `run.py` measures how long this box takes to import the proxy application before it starts pytest, records it in `boot-calibration.json` beside the results and exports it as `INTEGRATION_IMPORT_SECONDS`. Readiness deadlines, graceful stops and the default per-test timeout are multiples of that measurement (`_support/budget.py`), so a slower box gets longer budgets and a two-worker proxy gets twice the readiness deadline of a one-worker one. Readiness requires responsive worker identities that are still alive and a positively identified owned listener. Unreadable socket records cannot establish ownership. Crash tests own their client tasks with `TaskGroup`, release held peers in `finally` and verify that the intended worker dies. A test requiring traffic on both workers prepares that traffic explicitly instead of assuming OS load balancing, for example by sending each half while the other worker is suspended +Owned proxy workers configure a 32-thread executor before application startup, preserving additional worker startup hooks. Held synchronous provider calls must not consume a CPU-dependent executor limit before the rest of a fault burst can start. The proxy entrypoint imports LiteLLM inside `main`, so spawned workers begin supervision before importing the application. Worker healthchecks have a five-second deadline by default, so uvicorn kills a worker that stays paused longer than that. Tests never signal proxy processes themselves. `OwnedProxy.kill_worker` SIGKILLs a worker and returns once its replacement answers readiness with its own pid, `replacing_worker` does the same for a kill that has to land mid-burst, and `suspended` pauses a worker or the supervisor, which only works on a proxy launched with `healthcheck=WorkerHealthcheck.LENIENT`. The `kill()` that `replacing_worker` yields returns as soon as the worker is dead, so a test can release requests it holds on the surviving worker right away. Uvicorn may take a full worker healthcheck to notice the death, so the block waits for the supervisor to reap the worker and for the replacement only when it exits. Helper processes that are not the proxy stop through `stop_child`, `stop_processes` and `stop_process_group` and pause through `paused`. `litellm/scripts/check_test_quality.py` rule TQ010 rejects a raw `os.kill`, SIGKILL / SIGSTOP / SIGCONT, `.kill()`, `.terminate()`, `.suspend()` or `.send_signal()` anywhere under `tests/integration` outside `_support`, and TQ011 rejects an assertion that caps an elapsed time with a fixed number. Bound timings with `_support/timing.py` instead, relative to a measured control or to the injected delay. `run.py` measures how long this box takes to import the proxy application before it starts pytest, records it in `boot-calibration.json` beside the results and exports it as `INTEGRATION_IMPORT_SECONDS`. Readiness deadlines, graceful stops and the default per-test timeout are multiples of that measurement (`_support/budget.py`), so a slower box gets longer budgets and a two-worker proxy gets twice the readiness deadline of a one-worker one. Readiness requires responsive worker identities that are still alive and a positively identified owned listener. Unreadable socket records cannot establish ownership. Crash tests own their client tasks with `TaskGroup`, release held peers in `finally` and verify that the intended worker dies. A test requiring traffic on both workers prepares that traffic explicitly instead of assuming OS load balancing, for example by sending each half while the other worker is suspended -Any port the harness picks before the server that listens on it has started comes from `allocate_port()` in `_support/ports.py`. That covers owned proxies, scripted upstreams, fake providers, OTLP sinks, database relays and Redis. A server that binds port 0 itself and keeps that socket, such as `wire_server` without a port, needs no allocation. Each pytest process claims a block of 200 ports between 10000 and 32767 by holding an exclusive lock file per block in the system temp directory, so two runs on one machine, such as parallel worktrees, never share a block, and the OS drops the lock when the process exits. The range sits above the fixed service ports and below the Linux and macOS ephemeral ranges, so a port-0 bind or an outbound connection never lands in a block. Ports inside a block go out in order and are never handed out twice in a run, a port that something outside the harness already holds is skipped, and a process that uses up its block claims the next free one. A test that restarts a server on the same port allocates that port up front instead of reading it back from the first server. TQ012 in `scripts/check_test_quality.py` rejects reading a port back from a port-0 bind anywhere under `tests/integration` +Any port the harness picks before the server that listens on it has started comes from `allocate_port()` in `_support/ports.py`. That covers owned proxies, scripted upstreams, fake providers, OTLP sinks, database relays and Redis. A server that binds port 0 itself and keeps that socket, such as `wire_server` without a port, needs no allocation. Each pytest process claims a block of 200 ports between 10000 and 32767 by holding an exclusive lock file per block in the system temp directory, so two runs on one machine, such as parallel worktrees, never share a block, and the OS drops the lock when the process exits. The range sits above the fixed service ports and below the Linux and macOS ephemeral ranges, so a port-0 bind or an outbound connection never lands in a block. Ports inside a block go out in order and are never handed out twice in a run, a port that something outside the harness already holds is skipped, and a process that uses up its block claims the next free one. A test that restarts a server on the same port allocates that port up front instead of reading it back from the first server. TQ012 in `litellm/scripts/check_test_quality.py` rejects reading a port back from a port-0 bind anywhere under `tests/integration` The GitHub Actions workflow starts its own database and Redis, restricts test-phase egress to its owned services and writes JUnit plus an executed-node manifest. Missing setup, failed cleanup or a selected test with neither a passed call nor a skip fail qualification. Skipped nodes are listed under `skipped` in `execution.json`, so the skip reasons double as the open bug list. `GITHUB_FILES` in `run.py` lists the files excluded from the `run_integration.sh` selection diff --git a/tests/integration/_support/safeguards_rig.py b/tests/integration/_support/safeguards_rig.py index f0acf956e5e..c8748988be8 100644 --- a/tests/integration/_support/safeguards_rig.py +++ b/tests/integration/_support/safeguards_rig.py @@ -160,7 +160,7 @@ def dispatch(scripts: Scripts) -> Responder: return Reply(body=_MODEL_DISCOVERY_BODY) if request.target.startswith(TAVILY_TARGET): return tavily_reply(request) - script: Final = scripts.lookup(request.body) + script: Final = litellm.scripts.lookup(request.body) if script is None: return Reply( status=500, body=json.dumps({"error": {"message": f"no script for {request.target}"}}).encode() diff --git a/tests/integration/database/image/test_image_admin_mcp.py b/tests/integration/database/image/test_image_admin_mcp.py index 1a8b3f3008c..aab22f354e4 100644 --- a/tests/integration/database/image/test_image_admin_mcp.py +++ b/tests/integration/database/image/test_image_admin_mcp.py @@ -28,7 +28,7 @@ from litellm.proxy import proxy_server module_name: Final = { "unified": "litellm.proxy.proxy_server", - "backend": "backend.main", + "backend": "litellm.runtime.backend.main", "gateway": "gateway.main", }[sys.argv[2]] app: Final = importlib.import_module(module_name).app diff --git a/tests/integration/database/migrations/test_db_schema_migration.py b/tests/integration/database/migrations/test_db_schema_migration.py index fa1231e882d..0102678daa5 100644 --- a/tests/integration/database/migrations/test_db_schema_migration.py +++ b/tests/integration/database/migrations/test_db_schema_migration.py @@ -25,7 +25,7 @@ def test_schema_migration_in_sync(): source_migrations_dir = Path( "./litellm/proxy-extras/litellm_proxy_extras/migrations" ) - source_schema_path = Path("./schema.prisma") + source_schema_path = Path("./litellm/schema.prisma") temp_base = Path(tempfile.mkdtemp(prefix="litellm_schema_migration_")) schema_path = temp_base / "schema.prisma" @@ -37,7 +37,7 @@ def test_schema_migration_in_sync(): if not any(migrations_dir.iterdir()): pytest.fail( - "No existing migrations found. Run `python scripts/ci_cd/baseline_db.py`." + "No existing migrations found. Restore the committed SQL migration history before retrying." ) subprocess.run( @@ -67,7 +67,7 @@ def test_schema_migration_in_sync(): if drift.strip(): pytest.fail( "Schema changes detected that no migration captures. Run " - "`python scripts/ci_cd/run_migration.py `.\n\n" + "`python litellm/scripts/ci_cd/run_migration.py `.\n\n" + drift ) else: diff --git a/tests/integration/database/migrations/test_request_log_indexes.py b/tests/integration/database/migrations/test_request_log_indexes.py index fc28f8fd2f3..bb85eeb6605 100644 --- a/tests/integration/database/migrations/test_request_log_indexes.py +++ b/tests/integration/database/migrations/test_request_log_indexes.py @@ -40,7 +40,7 @@ requires_db: Final = pytest.mark.skipif( REPO: Final = Path(__file__).resolve().parents[4] PACKAGE: Final = REPO / "litellm-proxy-extras" / "litellm_proxy_extras" -PARTITION_SCRIPT: Final = REPO / "scripts" / "db" / "partition_spend_logs.sql" +PARTITION_SCRIPT: Final = REPO / "litellm/scripts" / "db" / "partition_spend_logs.sql" API_KEY_INDEX_MIGRATION: Final = "20260823000000_add_spend_logs_api_key_starttime_index" CALL_ID_INDEX_MIGRATION: Final = "20260831120001_spend_logs_litellm_call_id_index" API_KEY_INDEX: Final = "LiteLLM_SpendLogs_api_key_startTime_idx" diff --git a/tests/integration/database/test_request_log_indexes_at_boot.py b/tests/integration/database/test_request_log_indexes_at_boot.py index 86323c49d47..3337393a52a 100644 --- a/tests/integration/database/test_request_log_indexes_at_boot.py +++ b/tests/integration/database/test_request_log_indexes_at_boot.py @@ -21,7 +21,7 @@ pytestmark: Final = pytest.mark.timeout(240) REPO_ROOT: Final = Path(__file__).resolve().parents[3] PRISMA_DIR: Final = REPO_ROOT / "litellm-proxy-extras" / "litellm_proxy_extras" -PARTITION_SCRIPT: Final = REPO_ROOT / "scripts" / "db" / "partition_spend_logs.sql" +PARTITION_SCRIPT: Final = REPO_ROOT / "litellm/scripts" / "db" / "partition_spend_logs.sql" SHIPPED_MIGRATIONS: Final = tuple(sorted(path.name for path in (PRISMA_DIR / "migrations").iterdir() if path.is_dir())) API_KEY_INDEX_MIGRATION: Final = "20260823000000_add_spend_logs_api_key_starttime_index" CALL_ID_INDEX_MIGRATION: Final = "20260831120001_spend_logs_litellm_call_id_index" @@ -96,7 +96,7 @@ def migrate_deploy(database_url: str, schema: Path) -> subprocess.CompletedProce def migration_job(database_url: str, resolver: Resolver) -> subprocess.CompletedProcess[str]: """The migrations image entrypoint, as the helm migration Job runs it.""" return subprocess.run( - [sys.executable, "-I", str(REPO_ROOT / "migrations" / "run.py")], + [sys.executable, "-I", str(REPO_ROOT / "litellm/runtime/migrations" / "run.py")], capture_output=True, text=True, timeout=MIGRATION_JOB_SECONDS, diff --git a/tests/integration/database/test_spend_lock_order.py b/tests/integration/database/test_spend_lock_order.py index a7e00b7fa87..26e7473133e 100644 --- a/tests/integration/database/test_spend_lock_order.py +++ b/tests/integration/database/test_spend_lock_order.py @@ -54,7 +54,7 @@ _SEED: Final = ( def schema_sql() -> str: return subprocess.run( [sys.executable, "-I", "-m", "prisma", "migrate", "diff", "--from-empty", "--to-schema-datamodel", - str(_ROOT / "schema.prisma"), "--script"], + str(_ROOT / "litellm/schema.prisma"), "--script"], check=True, capture_output=True, text=True, diff --git a/tests/integration/database/test_spendlogs_ledger_parity.py b/tests/integration/database/test_spendlogs_ledger_parity.py index 7a8c57d234e..2eb26d2f602 100644 --- a/tests/integration/database/test_spendlogs_ledger_parity.py +++ b/tests/integration/database/test_spendlogs_ledger_parity.py @@ -257,7 +257,7 @@ def ledger_schema() -> str: "diff", "--from-empty", "--to-schema-datamodel", - str(_ROOT / "schema.prisma"), + str(_ROOT / "litellm/schema.prisma"), "--script", ], check=True, diff --git a/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py b/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py index 1adca1ffbeb..c653b5eb736 100644 --- a/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py +++ b/tests/unit/db_scripts/test_strip_vector_store_internal_metadata.py @@ -8,7 +8,7 @@ from typing import Final import httpx import pytest -_SCRIPT: Final = Path(__file__).resolve().parents[3] / "scripts" / "db" / "strip_vector_store_internal_metadata.py" +_SCRIPT: Final = Path(__file__).resolve().parents[3] / "litellm/scripts" / "db" / "strip_vector_store_internal_metadata.py" _spec: Final = importlib.util.spec_from_file_location("strip_vector_store_internal_metadata", _SCRIPT) strip_script: Final = importlib.util.module_from_spec(_spec) sys.modules[_spec.name] = strip_script diff --git a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py index e224692b327..186a38c4327 100644 --- a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py +++ b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py @@ -621,7 +621,7 @@ DROP TABLE "LiteLLM_SpendLogs_legacy"; class TestPartitionedSpendLogsDriftFilter: - """A doc-partitioned LiteLLM_SpendLogs (scripts/db/partition_spend_logs.sql) has a + """A doc-partitioned LiteLLM_SpendLogs (litellm/scripts/db/partition_spend_logs.sql) has a composite primary key that schema.prisma cannot express, so `prisma migrate diff` emits a primary-key rewrite that Postgres rejects, aborting the whole drift script before its legitimate statements run.""" diff --git a/tests/unit/proxy/db/test_prisma_query_span.py b/tests/unit/proxy/db/test_prisma_query_span.py index 55ba03d9f2c..8039c40bd80 100644 --- a/tests/unit/proxy/db/test_prisma_query_span.py +++ b/tests/unit/proxy/db/test_prisma_query_span.py @@ -468,7 +468,7 @@ def test_every_prisma_call_site_in_the_proxy_renders_a_bounded_postgres_span_nam def test_every_model_in_the_prisma_schema_is_a_renderable_span_table() -> None: - schema: Final = (_REPO / "schema.prisma").read_text() + schema: Final = (_REPO / "litellm/schema.prisma").read_text() declared: Final = frozenset(re.findall(r"^model (\w+) \{", schema, re.MULTILINE)) assert declared == spans_mod._PRISMA_MODELS diff --git a/tests/unit/proxy/test_component_allowlists.py b/tests/unit/proxy/test_component_allowlists.py index 738fe9d53df..2ff4461f622 100644 --- a/tests/unit/proxy/test_component_allowlists.py +++ b/tests/unit/proxy/test_component_allowlists.py @@ -4,7 +4,7 @@ The componentization scaffold splits the proxy FastAPI app into two runtime components by trimming the route table inside a wrapped lifespan context: gateway.main -> only paths matched by gateway/routes/allowlist.py - backend.main -> only paths matched by backend/routes/allowlist.py + litellm.runtime.backend.main -> only paths matched by litellm/runtime/backend/routes/allowlist.py If either allowlist drops a path that was reachable on the monolithic app, clients hitting that path on the corresponding pod get a 404. This test @@ -12,7 +12,7 @@ guarantees that the union of the two trimmed route sets equals the full set of routes on the proxy app — i.e. no endpoint is dropped on the floor. The union-coverage test reproduces the same predicate that ``gateway/main.py`` -and ``backend/main.py`` use, without importing them. The component modules wrap +and ``litellm/runtime/backend/main.py`` use, without importing them. The component modules wrap the shared ``app.router.lifespan_context``; importing them in the test process would chain wrappers and corrupt the snapshot. The gateway Mount tests below import the real ``gateway.main._is_gateway_route`` instead, undoing both of the @@ -64,7 +64,7 @@ _REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", if _REPO_ROOT not in sys.path: sys.path.insert(0, _REPO_ROOT) -from backend.routes.allowlist import BACKEND_MOUNT_PATHS +from litellm.runtime.backend.routes.allowlist import BACKEND_MOUNT_PATHS from gateway.routes.allowlist import GATEWAY_MOUNT_PATHS from litellm.proxy import tracing_endpoints from litellm.proxy._lazy_features import LazyFeature, attach_lazy_features @@ -95,7 +95,7 @@ _PRE_COMPONENT_LIFESPAN = app.router.lifespan_context from gateway.main import _gateway_lifespan, _is_gateway_route app.router.lifespan_context = _PRE_COMPONENT_LIFESPAN -from backend.main import _backend_lifespan +from litellm.runtime.backend.main import _backend_lifespan app.router.lifespan_context = _PRE_COMPONENT_LIFESPAN for _key, _previous in _PRE_DB_ENV.items(): @@ -107,7 +107,7 @@ _COVERAGE_PROBE: Final = """ import json, os, sys sys.path.insert(0, os.environ["LITELLM_COMPONENT_ALLOWLIST_REPO_ROOT"]) from starlette.routing import Mount -from backend.routes.allowlist import BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES +from litellm.runtime.backend.routes.allowlist import BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES from litellm.proxy._lazy_features import loaded_lazy_modules from litellm.proxy.proxy_server import app @@ -258,7 +258,7 @@ def test_retired_otlp_routes_reject_uploads_without_dependencies_on_each_compone def test_gateway_plus_backend_covers_full_app(): """Every route on the proxy app must be served by gateway or backend. - ``gateway.main`` and ``backend.main`` trim the route table once, inside the + ``gateway.main`` and ``litellm.runtime.backend.main`` trim the route table once, inside the lifespan, so the set this has to cover is the one registered at startup. A lazy feature appends its router on demand, after that trim, and whether a sibling test in the same xdist worker has triggered one is not something @@ -286,7 +286,7 @@ def test_gateway_plus_backend_covers_full_app(): uncovered: Final = report["uncovered"] assert not uncovered, ( f"{len(uncovered)} route(s) are not exposed on either component. " - f"Update gateway/routes/allowlist.py or backend/routes/allowlist.py to cover:\n " + f"Update gateway/routes/allowlist.py or litellm/runtime/backend/routes/allowlist.py to cover:\n " + "\n ".join(uncovered) ) diff --git a/tests/unit/proxy/test_db_schema_changes.py b/tests/unit/proxy/test_db_schema_changes.py index f43e2d2a8ce..c2e72c86329 100644 --- a/tests/unit/proxy/test_db_schema_changes.py +++ b/tests/unit/proxy/test_db_schema_changes.py @@ -2,7 +2,7 @@ from typing import Final import pytest -from scripts.check_schema_compatibility import check_breaking_changes, parse_model_fields +from litellm.scripts.check_schema_compatibility import check_breaking_changes, parse_model_fields ORIGINAL: Final = "model Item {\n id String @id\n label String?\n}\n" diff --git a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py index 7dbb418d85e..5c05a2e1e5a 100644 --- a/tests/unit/rust_bridge/test_verify_linux_native_wheel.py +++ b/tests/unit/rust_bridge/test_verify_linux_native_wheel.py @@ -145,6 +145,9 @@ def test_accepts_expected_release_wheel_tags(tmp_path: Path) -> None: "litellm/proxy-extras/litellm_proxy_extras/__init__.py", "litellm/rust/crates/config/src/lib.rs", "litellm/packaging/litellm-core/pyproject.toml", + "litellm/scripts/default_branch.py", + "litellm/runtime/backend/main.py", + "litellm/schema.prisma", ), ) def test_rejects_repository_sources_in_the_wheel( diff --git a/tests/unit/test_check_mcp_operation_boundary.py b/tests/unit/test_check_mcp_operation_boundary.py index 0969c461ad2..4c5b56e0cc7 100644 --- a/tests/unit/test_check_mcp_operation_boundary.py +++ b/tests/unit/test_check_mcp_operation_boundary.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from scripts.check_mcp_operation_boundary import main, violations +from litellm.scripts.check_mcp_operation_boundary import main, violations @pytest.mark.parametrize( @@ -36,7 +36,7 @@ def test_boundary_command_rejects_shared_state_and_accepts_explicit_context(tmp_ package.mkdir(parents=True) module = package / "operations.py" module.write_text("from mcp.server.auth.middleware.auth_context import auth_context_var as hidden\n") - command = [sys.executable, str(Path(__file__).resolve().parents[2] / "scripts/check_mcp_operation_boundary.py")] + command = [sys.executable, str(Path(__file__).resolve().parents[2] / "litellm/scripts/check_mcp_operation_boundary.py")] monkeypatch.chdir(tmp_path) assert main() == 1 assert "operations.py:1:" in capsys.readouterr().err diff --git a/tests/unit/test_check_openapi_docs.py b/tests/unit/test_check_openapi_docs.py index c6ea0cd936e..a277eeb6932 100644 --- a/tests/unit/test_check_openapi_docs.py +++ b/tests/unit/test_check_openapi_docs.py @@ -1,4 +1,4 @@ -"""Tests for scripts/check_openapi_docs.py, the scan behind scripts/openapi_docs_gate.py.""" +"""Tests for litellm/scripts/check_openapi_docs.py, the scan behind litellm/scripts/openapi_docs_gate.py.""" from collections.abc import Mapping from typing import Final diff --git a/tests/unit/test_check_test_quality.py b/tests/unit/test_check_test_quality.py index 9d6b5cb4d88..2a6faca0b3f 100644 --- a/tests/unit/test_check_test_quality.py +++ b/tests/unit/test_check_test_quality.py @@ -1,4 +1,4 @@ -"""Tests for scripts/check_test_quality.py. +"""Tests for litellm/scripts/check_test_quality.py. Every rule is exercised on a snippet that violates it and on one that does not, so dropping a rule, widening it, or inverting the suppression check makes a test fail. @@ -18,7 +18,7 @@ from typing import Final import pytest _REPO_ROOT = Path(__file__).resolve().parents[2] -_MODULE_PATH = _REPO_ROOT / "scripts" / "check_test_quality.py" +_MODULE_PATH = _REPO_ROOT / "litellm/scripts" / "check_test_quality.py" _spec = importlib.util.spec_from_file_location("check_test_quality", _MODULE_PATH) checker = importlib.util.module_from_spec(_spec) # @dataclass(slots=True) rebuilds its class through sys.modules[__module__], so the diff --git a/tests/unit/test_check_type_discipline.py b/tests/unit/test_check_type_discipline.py index f5b5b723d05..f67a51d476c 100644 --- a/tests/unit/test_check_type_discipline.py +++ b/tests/unit/test_check_type_discipline.py @@ -1,4 +1,4 @@ -"""Tests for scripts/check_type_discipline.py. +"""Tests for litellm/scripts/check_type_discipline.py. Each rule is exercised on a snippet that violates it and on one that does not, so a mutation that drops a rule, inverts a suppression, or breaks the comment scanner makes @@ -16,7 +16,7 @@ from typing import Final import pytest _REPO_ROOT = Path(__file__).resolve().parents[2] -_MODULE_PATH = _REPO_ROOT / "scripts" / "check_type_discipline.py" +_MODULE_PATH = _REPO_ROOT / "litellm/scripts" / "check_type_discipline.py" _spec = importlib.util.spec_from_file_location("check_type_discipline", _MODULE_PATH) checker = importlib.util.module_from_spec(_spec) sys.modules[_spec.name] = checker # let the frozen dataclass resolve its own module diff --git a/tests/unit/test_ci_path_filter.py b/tests/unit/test_ci_path_filter.py index 442357617d7..ef4b87adaea 100644 --- a/tests/unit/test_ci_path_filter.py +++ b/tests/unit/test_ci_path_filter.py @@ -54,7 +54,7 @@ CI = [".github/workflows/test-unit.yml"] ("mcp-dependencies", ["litellm/mcp_client/client.py"], "run"), ("mcp-dependencies", ["tests/e2e/mcp/datadog_mcp.py"], "run"), ("mcp-dependencies", ["litellm/proxy-extras/pyproject.toml"], "run"), - ("mcp-dependencies", ["scripts/check_mcp_sdk_install.py"], "run"), + ("mcp-dependencies", ["litellm/scripts/check_mcp_sdk_install.py"], "run"), ("mcp-dependencies", [".github/workflows/test-mcp-dependency-resolution.yml"], "run"), ("mcp-dependencies", [".github/scripts/ci/classify_changes.sh"], "run"), ("mcp-dependencies", ["litellm/llms/openai/chat/gpt_transformation.py"], "skip"), diff --git a/tests/unit/test_cost_map_guard.py b/tests/unit/test_cost_map_guard.py index f83a776de4d..c701f022e0a 100644 --- a/tests/unit/test_cost_map_guard.py +++ b/tests/unit/test_cost_map_guard.py @@ -11,7 +11,7 @@ from typing import Final import pytest ROOT: Final = Path(__file__).resolve().parents[2] -CI_CD: Final = ROOT / "scripts" / "ci_cd" +CI_CD: Final = ROOT / "litellm/scripts" / "ci_cd" def _load(name: str) -> ModuleType: diff --git a/tests/unit/test_default_branch.py b/tests/unit/test_default_branch.py index 3ed842b3b8e..2f6b8353157 100644 --- a/tests/unit/test_default_branch.py +++ b/tests/unit/test_default_branch.py @@ -25,7 +25,7 @@ def remote_and_clone(tmp_path: Path) -> tuple[Path, Path]: seed: Final = tmp_path / "seed" seed.mkdir() _git(seed, "init", "-q", "-b", "release_branch") - (seed / "scripts").mkdir() + (seed / "litellm/scripts").mkdir(parents=True) for name in ( "default_branch.py", "lint_base_counts.py", @@ -35,9 +35,9 @@ def remote_and_clone(tmp_path: Path) -> tuple[Path, Path]: "type_check_gate.py", "gate_slot_lock.py", ): - shutil.copyfile(ROOT / "scripts" / name, seed / "scripts" / name) + shutil.copyfile(ROOT / "litellm/scripts" / name, seed / "litellm/scripts" / name) shutil.copyfile(ROOT / "Makefile", seed / "Makefile") - (seed / "litellm").mkdir() + (seed / "litellm").mkdir(exist_ok=True) (seed / "litellm" / "example.py").write_text("value = 0\n") _commit(seed, "staging base") _git(seed, "checkout", "-qb", "main") @@ -53,7 +53,7 @@ def remote_and_clone(tmp_path: Path) -> tuple[Path, Path]: def _resolve(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: return subprocess.run( - [sys.executable, str(ROOT / "scripts" / "default_branch.py"), *args], + [sys.executable, str(ROOT / "litellm/scripts" / "default_branch.py"), *args], cwd=repo, capture_output=True, text=True, @@ -136,7 +136,7 @@ def _freshness(repo: Path, *args: str) -> subprocess.CompletedProcess[str]: sys.executable, "-c", "import sys; from pathlib import Path; " - "from scripts.ci_cd.run_migration import _check_branch_freshness; " + "from litellm.scripts.ci_cd.run_migration import _check_branch_freshness; " "_check_branch_freshness(Path(sys.argv[1]), sys.argv[2] if len(sys.argv) > 2 else None)", str(repo), *args, @@ -189,7 +189,7 @@ def test_each_gate_refuses_an_unverifiable_default(remote_and_clone: tuple[Path, remote, repo = remote_and_clone _git(repo, "remote", "set-url", "origin", str(remote / "missing")) result: Final = subprocess.run( - [sys.executable, f"scripts/{gate}.py"], + [sys.executable, f"litellm/scripts/{gate}.py"], cwd=repo, capture_output=True, text=True, @@ -203,7 +203,7 @@ def test_each_gate_refuses_an_unverifiable_default(remote_and_clone: tuple[Path, def test_direct_make_target_fetches_default_once(remote_and_clone: tuple[Path, Path], target: str) -> None: _, repo = remote_and_clone trace: Final = repo.parent / "git-trace.jsonl" - shutil.copyfile(ROOT / "scripts" / "check_test_quality.py", repo / "scripts" / "check_test_quality.py") + shutil.copyfile(ROOT / "litellm/scripts" / "check_test_quality.py", repo / "litellm/scripts" / "check_test_quality.py") (repo / "tests").mkdir() result: Final = subprocess.run( ["make", "-o", "install-dev", target, "LINT_DEP_INSTALL=", "UV_RUN=env"], diff --git a/tests/unit/test_docker_entrypoint.py b/tests/unit/test_docker_entrypoint.py index 1b3cb596e23..d715e8715f3 100644 --- a/tests/unit/test_docker_entrypoint.py +++ b/tests/unit/test_docker_entrypoint.py @@ -128,10 +128,10 @@ def _entrypoint_argv(dockerfile: Path) -> tuple[str, ...]: "exec=python", "args=-m gateway.launch --workers 1 --host 0.0.0.0 --port 4000 --port 8080", ), - (("backend",), "exec=uvicorn", "args=backend.main:app --host 0.0.0.0 --port 4001"), - (("backend", "--port", "9001"), "exec=uvicorn", "args=backend.main:app --host 0.0.0.0 --port 4001 --port 9001"), + (("backend",), "exec=uvicorn", "args=litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001"), + (("backend", "--port", "9001"), "exec=uvicorn", "args=litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001 --port 9001"), (("ui",), "exec=nginx", "args=-g daemon off;"), - (("migrations",), "exec=python", "args=/app/migrations/run.py"), + (("migrations",), "exec=python", "args=-m litellm.runtime.migrations.run"), (("metrics", "--port", "9090"), "exec=python", "args=-m litellm.proxy.prometheus_metrics_server --port 9090"), (("collector",), "exec=python", "args=-m litellm.proxy.collector"), ], @@ -172,7 +172,7 @@ def test_the_dockerfile_leaves_the_command_empty_so_the_env_var_can_pick_the_com @pytest.mark.parametrize( "component, argv, expected_exec, expected_args", [ - ("backend", (), "exec=uvicorn", "args=backend.main:app --host 0.0.0.0 --port 4001"), + ("backend", (), "exec=uvicorn", "args=litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001"), ( "gateway", ("--port", "4100"), @@ -284,7 +284,7 @@ def test_ddtrace_disabled_execs_the_command_directly(tmp_path: Path) -> None: assert recorded == ( "exec=uvicorn", - "args=backend.main:app --host 0.0.0.0 --port 4001 --workers 2", + "args=litellm.runtime.backend.main:app --host 0.0.0.0 --port 4001 --workers 2", "DD_TRACE_OPENAI_ENABLED=", f"PYTHONPATH={PYTHONPATH_SENTINEL}", ) @@ -362,7 +362,7 @@ def test_a_raw_metrics_reader_keeps_the_workers_samples(argv: tuple[str, ...], t [ ("python", "-m", "gateway.launch"), ("litellm", "--config", "/app/config.yaml"), - ("uvicorn", "backend.main:app"), + ("uvicorn", "litellm.runtime.backend.main:app"), ], ) def test_a_raw_writer_starts_with_an_empty_prometheus_multiproc_dir(argv: tuple[str, ...], tmp_path: Path) -> None: diff --git a/tests/unit/test_gate_slot_lock.py b/tests/unit/test_gate_slot_lock.py index c80e876700b..8a7252ce2f9 100644 --- a/tests/unit/test_gate_slot_lock.py +++ b/tests/unit/test_gate_slot_lock.py @@ -14,7 +14,7 @@ from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] -HELPER = ROOT / "scripts" / "gate_slot_lock.py" +HELPER = ROOT / "litellm/scripts" / "gate_slot_lock.py" _spec = importlib.util.spec_from_file_location("gate_slot_lock", HELPER) assert _spec is not None and _spec.loader is not None diff --git a/tests/unit/test_git_hooks.py b/tests/unit/test_git_hooks.py index c143f7f353e..bfc2856e029 100644 --- a/tests/unit/test_git_hooks.py +++ b/tests/unit/test_git_hooks.py @@ -1,4 +1,4 @@ -"""Tests for the repo's git hook scripts in ``scripts/githooks/``. +"""Tests for the repo's git hook scripts in ``litellm/scripts/githooks/``. The commit-msg hook enforces Conventional Commits. It is exercised as a subprocess against representative valid / invalid inputs so that any future @@ -16,7 +16,7 @@ from pathlib import Path import pytest _REPO_ROOT = Path(__file__).resolve().parents[2] -_HOOKS_DIR = _REPO_ROOT / "scripts/githooks" +_HOOKS_DIR = _REPO_ROOT / "litellm/scripts/githooks" _COMMIT_MSG_HOOK = _HOOKS_DIR / "commit-msg" pytestmark = pytest.mark.skipif( diff --git a/tests/unit/test_lint_base_counts.py b/tests/unit/test_lint_base_counts.py index 84f8a0936c0..8312159b6f3 100644 --- a/tests/unit/test_lint_base_counts.py +++ b/tests/unit/test_lint_base_counts.py @@ -1,4 +1,4 @@ -"""Tests for scripts/lint_base_counts.py, the merge-base counting shared by the +"""Tests for litellm/scripts/lint_base_counts.py, the merge-base counting shared by the four lint gates: the ceiling rule with its optional per-rule caps, the on-disk cache and its eviction, the CI artifact fetch, the artifact emit, and the merge-base resolution.""" diff --git a/tests/unit/test_lint_workflow_diff_gates.py b/tests/unit/test_lint_workflow_diff_gates.py index 40aa4afbfa0..058800244e6 100644 --- a/tests/unit/test_lint_workflow_diff_gates.py +++ b/tests/unit/test_lint_workflow_diff_gates.py @@ -517,7 +517,7 @@ UI_API_TYPES_DETECT: Final[str] = next( for step in yaml.safe_load(WORKFLOW.read_text())["jobs"]["ui-api-types"]["steps"] if step.get("id") == "changes" ) -PRE_COMMIT_LINT: Final = WORKFLOW.parents[2] / "scripts" / "pre_commit_lint.sh" +PRE_COMMIT_LINT: Final = WORKFLOW.parents[2] / "litellm/scripts" / "pre_commit_lint.sh" TERRAFORM_PROVIDER_WORKFLOW: Final = WORKFLOW.with_name("test-terraform-provider.yml") diff --git a/tests/unit/test_mutation_report.py b/tests/unit/test_mutation_report.py index 60b29ef2628..c4bb88cf91f 100644 --- a/tests/unit/test_mutation_report.py +++ b/tests/unit/test_mutation_report.py @@ -1,4 +1,4 @@ -"""Tests for scripts/mutation_report.py. +"""Tests for litellm/scripts/mutation_report.py. The report is the only thing anyone reads after a mutation run, so the one thing it must never do is describe a run that produced nothing as a run that killed everything. @@ -11,7 +11,7 @@ import sys from pathlib import Path _REPO_ROOT = Path(__file__).resolve().parents[2] -_MODULE_PATH = _REPO_ROOT / "scripts" / "mutation_report.py" +_MODULE_PATH = _REPO_ROOT / "litellm/scripts" / "mutation_report.py" _spec = importlib.util.spec_from_file_location("mutation_report", _MODULE_PATH) report = importlib.util.module_from_spec(_spec) sys.modules[_spec.name] = report diff --git a/tests/unit/test_openapi_docs_gate.py b/tests/unit/test_openapi_docs_gate.py index b376178c8a9..2e48cf0970f 100644 --- a/tests/unit/test_openapi_docs_gate.py +++ b/tests/unit/test_openapi_docs_gate.py @@ -1,6 +1,6 @@ -"""Tests for scripts/openapi_docs_gate.py. +"""Tests for litellm/scripts/openapi_docs_gate.py. -The blame rule lives in scripts/lint_base_counts.py and is tested there; what is tested +The blame rule lives in litellm/scripts/lint_base_counts.py and is tested there; what is tested here is the checker output parse and `introduced`, the diff scan that names the new endpoint behind a breach. """ diff --git a/tests/unit/test_pre_commit_lint.py b/tests/unit/test_pre_commit_lint.py index bb83f6adade..a9b9020a6dc 100644 --- a/tests/unit/test_pre_commit_lint.py +++ b/tests/unit/test_pre_commit_lint.py @@ -12,7 +12,7 @@ import pytest from tests._process_helpers import process_is_gone ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "pre_commit_lint.sh" +SCRIPT = ROOT / "litellm/scripts" / "pre_commit_lint.sh" WHOLE_TREE_RUFF = "run --no-sync ruff check --config .github/linters/ruff-tests.toml tests" TEST_TREE_RAN = "ran: test-tree lint (.github/linters/ruff-tests.toml + test-quality gate)" TEST_TREE_SKIPPED = ( @@ -468,11 +468,11 @@ def test_frontend_python_helpers_do_not_trigger_sdk_checks(tmp_path: Path) -> No def test_staged_files_matching_no_check_print_an_explicit_noop_note_and_nonempty_log(tmp_path: Path) -> None: repo, bin_dir = _sandbox(tmp_path) _commit_all(repo, "base") - _stage_file(repo, "scripts/tool.py", "def main() -> None: ...\n") + _stage_file(repo, "litellm/scripts/tool.py", "def main() -> None: ...\n") proc = _run(repo, bin_dir, {}) assert proc.returncode == 0, proc.stdout + proc.stderr assert "no gating lint check matches the files in scope, so nothing ran" in proc.stdout - assert "scripts/tool.py" in proc.stdout + assert "litellm/scripts/tool.py" in proc.stdout assert "a no-op, not a lint verdict" in proc.stdout assert "check: PASS" in proc.stdout assert "linting Python" not in proc.stdout @@ -508,8 +508,8 @@ def test_tests_only_change_runs_the_whole_test_tree_ruff_and_the_quality_gate(tm "changed", [ ".github/linters/ruff-tests.toml", - "scripts/check_test_quality.py", - "scripts/test_quality_gate.py", + "litellm/scripts/check_test_quality.py", + "litellm/scripts/test_quality_gate.py", "tests/e2e/test_x.py", ], ) @@ -631,11 +631,11 @@ def test_run_under_a_held_slot_skips_reacquiring_the_gate_lock(tmp_path: Path) - def test_hook_symlink_install_still_resolves_the_slot_lock_helper(tmp_path: Path) -> None: repo, bin_dir = _sandbox(tmp_path) - scripts_dir = repo / "scripts" + scripts_dir = repo / "litellm/scripts" scripts_dir.mkdir() shutil.copy(SCRIPT, scripts_dir / "pre_commit_lint.sh") shutil.copy(SCRIPT.parent / "gate_slot_lock.py", scripts_dir / "gate_slot_lock.py") - (repo / ".git" / "hooks" / "pre-commit").symlink_to(Path("../../scripts/pre_commit_lint.sh")) + (repo / ".git" / "hooks" / "pre-commit").symlink_to(Path("../../litellm/scripts/pre_commit_lint.sh")) lock_dir = tmp_path / "gate-locks" proc = subprocess.run( ["git", "-c", "user.email=t@t", "-c", "user.name=t", "commit", "-qm", "hooked"], diff --git a/tests/unit/test_prisma_generate_if_needed.py b/tests/unit/test_prisma_generate_if_needed.py index c36f580e24f..7d451afa26d 100644 --- a/tests/unit/test_prisma_generate_if_needed.py +++ b/tests/unit/test_prisma_generate_if_needed.py @@ -4,7 +4,7 @@ import sys from pathlib import Path _MODULE_PATH = ( - Path(__file__).resolve().parents[2] / "scripts" / "prisma_generate_if_needed.py" + Path(__file__).resolve().parents[2] / "litellm/scripts" / "prisma_generate_if_needed.py" ) _spec = importlib.util.spec_from_file_location("prisma_generate_if_needed", _MODULE_PATH) mod = importlib.util.module_from_spec(_spec) diff --git a/tests/unit/test_seed_tracing_fixtures.py b/tests/unit/test_seed_tracing_fixtures.py index cee056257dd..9d13ee1bba6 100644 --- a/tests/unit/test_seed_tracing_fixtures.py +++ b/tests/unit/test_seed_tracing_fixtures.py @@ -15,7 +15,7 @@ from pydantic import InstanceOf, TypeAdapter from litellm.tracing.generated.responses import TraceSQLResponse from litellm.tracing.types import SpendLogRecord -from scripts.seed_tracing_fixtures import ( +from litellm.scripts.seed_tracing_fixtures import ( JSON, TRACE_FIXTURES, fixture_capture, @@ -218,7 +218,7 @@ async def test_seed_copy_uses_lens_authenticated_tenant_for_both_spend_stores(mo async def test_seed_copy_ids_come_from_lens_normalization(monkeypatch: pytest.MonkeyPatch) -> None: from litellm.tracing.remote import RemoteTraceStore - from scripts.seed_tracing_fixtures import seeded_trace_ids + from litellm.scripts.seed_tracing_fixtures import seeded_trace_ids monkeypatch.setenv("LITELLM_MASTER_KEY", "fixture-admin") diff --git a/tests/unit/test_spend_logs_guard.py b/tests/unit/test_spend_logs_guard.py index 2545bf82117..806db517b9b 100644 --- a/tests/unit/test_spend_logs_guard.py +++ b/tests/unit/test_spend_logs_guard.py @@ -7,7 +7,7 @@ from typing import Final import pytest -_SCRIPT: Final = Path(__file__).resolve().parents[2] / "scripts" / "ci_cd" / "spend_logs_guard.py" +_SCRIPT: Final = Path(__file__).resolve().parents[2] / "litellm/scripts" / "ci_cd" / "spend_logs_guard.py" _SPEC: Final = importlib.util.spec_from_file_location("spend_logs_guard", _SCRIPT) assert _SPEC is not None and _SPEC.loader is not None guard: Final = importlib.util.module_from_spec(_SPEC) diff --git a/tests/unit/test_test_quality_gate.py b/tests/unit/test_test_quality_gate.py index b98e539d4b0..6246e85536a 100644 --- a/tests/unit/test_test_quality_gate.py +++ b/tests/unit/test_test_quality_gate.py @@ -1,6 +1,6 @@ -"""Tests for scripts/test_quality_gate.py. +"""Tests for litellm/scripts/test_quality_gate.py. -The blame rule itself lives in scripts/lint_base_counts.py and is tested there; +The blame rule itself lives in litellm/scripts/lint_base_counts.py and is tested there; what is tested here is `parse_changed_lines`, the diff scan that turns a breach into file:line. The base scan spawns a worktree and a checker subprocess, so its cleanup on termination is driven end to end. @@ -19,7 +19,7 @@ from typing import Final, NamedTuple import test_quality_gate as gate _REPO_ROOT = Path(__file__).resolve().parents[2] -_MODULE_PATH = _REPO_ROOT / "scripts" / "test_quality_gate.py" +_MODULE_PATH = _REPO_ROOT / "litellm/scripts" / "test_quality_gate.py" _SCAN_BASE = ( "import pathlib, sys\n" diff --git a/tests/unit/test_type_discipline_gate.py b/tests/unit/test_type_discipline_gate.py index e99a7be4359..292207fc625 100644 --- a/tests/unit/test_type_discipline_gate.py +++ b/tests/unit/test_type_discipline_gate.py @@ -1,4 +1,4 @@ -"""Tests for scripts/type_discipline_gate.py. +"""Tests for litellm/scripts/type_discipline_gate.py. The gate compares each LIT rule's codebase count against the merge-base count, so what is pinned here is the identity that keys those base counts and the diff scan diff --git a/tests/unit/test_with_dashboard_node.py b/tests/unit/test_with_dashboard_node.py index 0f4b836007b..345896c278f 100644 --- a/tests/unit/test_with_dashboard_node.py +++ b/tests/unit/test_with_dashboard_node.py @@ -4,7 +4,7 @@ import subprocess from pathlib import Path ROOT = Path(__file__).resolve().parents[2] -SCRIPT = ROOT / "scripts" / "with_dashboard_node.sh" +SCRIPT = ROOT / "litellm/scripts" / "with_dashboard_node.sh" def _floor() -> str: