diff --git a/.github/workflows/test-linting.yml b/.github/workflows/test-linting.yml index 0a80a65cbe6..de7e1b68346 100644 --- a/.github/workflows/test-linting.yml +++ b/.github/workflows/test-linting.yml @@ -87,14 +87,9 @@ jobs: run: | uv run --no-sync python -c "import openai; print(f'OpenAI version: {openai.__version__}')" - - name: Run MyPy type checking - run: | - cd litellm - (uv run --no-sync mypy . || true) | uv run --no-sync python ../scripts/type_check_gate.py --tool mypy - - name: Run basedpyright type checking run: | - (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py --tool basedpyright + (uv run --no-sync basedpyright --outputjson || true) | uv run --no-sync python scripts/type_check_gate.py - name: Check for circular imports run: | @@ -133,56 +128,6 @@ jobs: run: | python scripts/budget_ratchet_check.py --base "$BASE_SHA" - any-discipline: - # Separate job: the first run cold-builds litellm's type cache (~2 min, ~3 GB), - # so keep it off the main lint job's time budget. Subsequent runs reuse the - # cached .mypy_cache_any and only re-type-check the changed files. - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - # Check out the PR head, not the default refs/pull/N/merge: the merge ref - # folds in newer base commits, which the diff-based gates (ruff delta, - # Any-discipline) would otherwise blame on this branch. - with: - ref: ${{ github.event.pull_request.head.sha }} - fetch-depth: 0 - clean: true - persist-credentials: false - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: "3.12" - - - name: Set up uv - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - with: - version: "0.10.9" - - - name: Install dependencies - run: | - uv sync --frozen - - # Keyed on deps + mypy config (which fix the type cache's validity), not on - # source content, so changed files always differ from the restored cache. - # The gate also defensively invalidates each target's cache entry, so - # correctness never depends on cache freshness -- this is purely for speed. - - name: Restore Any-gate type cache - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 - with: - path: .mypy_cache_any - key: any-mypy-cache-${{ runner.os }}-py3.12-${{ hashFiles('uv.lock', 'litellm/mypy.ini') }} - restore-keys: | - any-mypy-cache-${{ runner.os }}-py3.12- - - - name: Check Any discipline (per-file budget on changed files) - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - run: | - uv run --no-sync python scripts/check_any_discipline.py --changed --base "$BASE_SHA" - secret-scan: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/.gitignore b/.gitignore index 1be40c0f863..78951aee663 100644 --- a/.gitignore +++ b/.gitignore @@ -76,8 +76,6 @@ tests/local_testing/log.txt .codegpt litellm/proxy/_new_new_secret_config.yaml litellm/proxy/custom_guardrail.py -**/.mypy_cache/ -**/.mypy_cache_any/ litellm/proxy/application.log tests/llm_translation/vertex_test_account.json tests/llm_translation/test_vertex_key.json diff --git a/CLAUDE.md b/CLAUDE.md index 95904ef8abd..2070b6fcdd6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,11 +36,9 @@ Don't hesitate to use values in .env to get needed API keys and other secrets, a Run tests, format your code, and lint your code before each commit -When you fix violations gated by `ruff-strict-budget.json`, `mypy-code-budget.json`, `basedpyright-code-budget.json`, or `any-discipline-budget.json`, run `make lint-budget-update` and commit the lowered baselines so the ceilings ratchet down instead of leaving stale headroom +When you fix violations gated by `ruff-strict-budget.json` or `basedpyright-code-budget.json`, run `make lint-budget-update` and commit the lowered baselines so the ceilings ratchet down instead of leaving stale headroom -If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and bringing it closer to the max, 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 - -The Any-discipline gate (`make lint-any`, also a CI job) fails when a changed file under `litellm/` carries more `Any`-typed values than its grandfathered ceiling in `any-discipline-budget.json` (each file's captured count plus 50% headroom). It flags values whose inferred type *contains* `Any`, including the `X | Any` unions mypy/basedpyright accept. Editing a legacy file is fine as long as you don't push its `Any` count past the ceiling; a brand-new file must be `Any`-free. Fix a value by giving it a concrete type (if you're given untyped input, validate with Pydantic). Ideally `# any-ok: ` is never used; treat it as a last resort for a genuine typed/untyped boundary that Pydantic truly can't model +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 basedpyright ceilings, 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 If you get an LIT001 or LIT002 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally `# mutable-ok` is never used; reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9643a58742c..1080579d0fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -154,8 +154,7 @@ Individual linting commands: ```bash make format-check # Check Black formatting make lint-ruff # Run Ruff linting -make lint-mypy # Run MyPy type checking -make lint-any # Gate changed files against their per-file Any budget +make lint-basedpyright # Run basedpyright type checking make check-circular-imports # Check for circular imports make check-import-safety # Check import safety ``` @@ -217,7 +216,7 @@ LiteLLM follows the [Google Python Style Guide](https://google.github.io/stylegu Our automated quality checks include: - **Black** for consistent code formatting - **Ruff** for linting and code quality -- **MyPy** for static type checking +- **basedpyright** for static type checking - **Circular import detection** - **Import safety validation** @@ -231,7 +230,7 @@ If `make lint` fails: 1. **Formatting issues**: Run `make format` to auto-fix 2. **Ruff issues**: Check the output and fix manually -3. **MyPy issues**: Add proper type hints +3. **basedpyright issues**: Add proper type hints 4. **Circular imports**: Refactor import dependencies 5. **Import safety**: Fix any unprotected imports @@ -246,7 +245,7 @@ If `make test-unit` fails: ### 3. Common Development Tips -- **Use type hints**: MyPy requires proper type annotations +- **Use type hints**: basedpyright requires proper type annotations - **Write descriptive commit messages**: Help reviewers understand your changes - **Keep PRs focused**: One feature/fix per PR - **Test edge cases**: Don't just test the happy path diff --git a/Makefile b/Makefile index b89ca22f846..a35ff1855b4 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,8 @@ test-unit-integrations test-unit-core-utils test-unit-other test-unit-root \ test-proxy-unit-a test-proxy-unit-b test-integration test-unit-helm \ info lint lint-dev format \ - lint-mypy lint-mypy-budget-update lint-basedpyright lint-basedpyright-budget-update \ - lint-ruff-budget lint-any lint-ruff-budget-update lint-budget-update lint-any-budget-update \ + lint-basedpyright lint-basedpyright-budget-update \ + lint-ruff-budget lint-ruff-budget-update lint-budget-update \ install-dev install-proxy-dev install-test-deps install-hooks \ install-helm-unittest check-circular-imports check-import-safety @@ -22,18 +22,14 @@ help: @echo " make install-hooks - Install git hooks (Conventional Commits + Branches)" @echo " make format - Apply Black code formatting" @echo " make format-check - Check Black code formatting (matches CI)" - @echo " make lint - Run all linting (Ruff, MyPy, Black check, circular imports, import safety)" + @echo " make lint - Run all linting (Ruff, basedpyright, Black check, circular imports, import safety)" @echo " make lint-ruff - Run Ruff linting only" - @echo " make lint-mypy - Run MyPy (disallow_untyped_defs), gated by per-rule error counts" - @echo " make lint-mypy-budget-update - Re-capture the MyPy per-rule budget (ratchet)" @echo " make lint-basedpyright - Run basedpyright strict, gated by per-rule error counts" @echo " make lint-basedpyright-budget-update - Re-capture the basedpyright per-rule budget (ratchet)" @echo " make lint-black - Check Black formatting (matches CI)" @echo " make lint-ruff-budget - Gate the codebase total of each strict ruff rule against its ceiling" - @echo " make lint-any - Gate changed files under litellm/ against their per-file Any budget" @echo " make lint-ruff-budget-update - Re-capture per-rule baselines in ruff-strict-budget.json (ratchet)" - @echo " make lint-budget-update - Re-capture all four ratchet budgets (ruff + mypy + basedpyright + any)" - @echo " make lint-any-budget-update - Re-capture the per-file Any budget across the whole tree (ratchet)" + @echo " make lint-budget-update - Re-capture all ratchet budgets (ruff + basedpyright)" @echo " make check-circular-imports - Check for circular imports" @echo " make check-import-safety - Check import safety" @echo " make test - Run all tests" @@ -127,17 +123,11 @@ lint-ruff-FULL-dev: install-dev if [ -n "$$files" ]; then echo "$$files" | xargs $(UV_RUN) ruff check; \ else echo "No changed .py files to check."; fi -lint-mypy: install-dev - cd litellm && ($(UV_RUN) mypy . || true) | $(UV_RUN) python ../scripts/type_check_gate.py --tool mypy - -lint-mypy-budget-update: install-dev - cd litellm && ($(UV_RUN) mypy . || true) | $(UV_RUN) python ../scripts/type_check_gate.py --tool mypy --update - lint-basedpyright: install-dev - ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --tool basedpyright + ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py lint-basedpyright-budget-update: install-dev - ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --tool basedpyright --update + ($(UV_RUN) basedpyright --outputjson || true) | $(UV_RUN) python scripts/type_check_gate.py --update lint-black: format-check @@ -150,14 +140,8 @@ lint-ruff-budget: install-dev lint-ruff-budget-update: install-dev $(UV_RUN) python scripts/ruff_strict_gate.py --update -# Ratchet all four budgets in one shot (ruff strict + mypy + basedpyright + any) -lint-budget-update: lint-ruff-budget-update lint-mypy-budget-update lint-basedpyright-budget-update lint-any-budget-update - -lint-any: install-dev - $(UV_RUN) python scripts/check_any_discipline.py --changed - -lint-any-budget-update: install-dev - $(UV_RUN) python scripts/check_any_discipline.py --update +# Ratchet all budgets in one shot (ruff strict + basedpyright) +lint-budget-update: lint-ruff-budget-update lint-basedpyright-budget-update check-circular-imports: install-dev cd litellm && $(UV_RUN) python ../tests/documentation_tests/test_circular_imports.py && cd .. @@ -166,10 +150,10 @@ check-import-safety: install-dev @$(UV_RUN) python -c "from litellm import *; print('[from litellm import *] OK! no issues!');" || (echo '🚨 import failed, this means you introduced unprotected imports! 🚨'; exit 1) # Combined linting (matches test-linting.yml workflow) -lint: format-check lint-ruff lint-mypy lint-basedpyright check-circular-imports check-import-safety lint-ruff-budget lint-any +lint: format-check lint-ruff lint-basedpyright check-circular-imports check-import-safety lint-ruff-budget # Faster linting for local development (only checks changed code) -lint-dev: lint-format-changed lint-mypy lint-any check-circular-imports check-import-safety +lint-dev: lint-format-changed check-circular-imports check-import-safety # Testing targets test: install-test-deps diff --git a/any-discipline-budget.json b/any-discipline-budget.json deleted file mode 100644 index 477ad5cb95a..00000000000 --- a/any-discipline-budget.json +++ /dev/null @@ -1,5826 +0,0 @@ -{ - "litellm/__init__.py": { - "baseline": 740, - "slack": 370 - }, - "litellm/_lazy_imports.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/_logging.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/_redis.py": { - "baseline": 416, - "slack": 208 - }, - "litellm/_redis_credential_provider.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/_service_logger.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/_uuid.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/a2a_protocol/card_resolver.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/a2a_protocol/client.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/a2a_protocol/cost_calculator.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/a2a_protocol/exception_mapping_utils.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/a2a_protocol/litellm_completion_bridge/handler.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/a2a_protocol/litellm_completion_bridge/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/a2a_protocol/main.py": { - "baseline": 208, - "slack": 104 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/config.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/handler.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/a2a_protocol/providers/bedrock_agentcore/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/a2a_protocol/providers/langflow/config.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/config.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/handler.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/a2a_protocol/providers/pydantic_ai_agents/transformation.py": { - "baseline": 139, - "slack": 70 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/config.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/handler.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/a2a_protocol/providers/watsonx_orchestrate/transformation.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/a2a_protocol/streaming_iterator.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/a2a_protocol/utils.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/anthropic_beta_headers_manager.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/anthropic_interface/exceptions/exception_mapping_utils.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/anthropic_interface/exceptions/exceptions.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/anthropic_interface/messages/__init__.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/assistants/main.py": { - "baseline": 382, - "slack": 191 - }, - "litellm/assistants/utils.py": { - "baseline": 94, - "slack": 47 - }, - "litellm/batch_completion/main.py": { - "baseline": 178, - "slack": 89 - }, - "litellm/batches/batch_utils.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/batches/main.py": { - "baseline": 221, - "slack": 111 - }, - "litellm/budget_manager.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/caching/_internal_lru_cache.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/caching/azure_blob_cache.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/caching/base_cache.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/caching/caching.py": { - "baseline": 378, - "slack": 189 - }, - "litellm/caching/caching_handler.py": { - "baseline": 336, - "slack": 168 - }, - "litellm/caching/disk_cache.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/caching/dual_cache.py": { - "baseline": 189, - "slack": 95 - }, - "litellm/caching/gcs_cache.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/caching/in_memory_cache.py": { - "baseline": 172, - "slack": 86 - }, - "litellm/caching/llm_caching_handler.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/caching/qdrant_semantic_cache.py": { - "baseline": 357, - "slack": 179 - }, - "litellm/caching/redis_cache.py": { - "baseline": 583, - "slack": 292 - }, - "litellm/caching/redis_cluster_cache.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/caching/redis_semantic_cache.py": { - "baseline": 188, - "slack": 94 - }, - "litellm/caching/s3_cache.py": { - "baseline": 138, - "slack": 69 - }, - "litellm/completion_extras/litellm_responses_transformation/handler.py": { - "baseline": 177, - "slack": 89 - }, - "litellm/completion_extras/litellm_responses_transformation/transformation.py": { - "baseline": 561, - "slack": 281 - }, - "litellm/compression/compress.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/compression/content_detection.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/compression/message_stubbing.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/compression/retrieval_tool.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/compression/scoring/bm25.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/compression/scoring/embedding_scorer.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/constants.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/containers/endpoint_factory.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/containers/main.py": { - "baseline": 219, - "slack": 110 - }, - "litellm/containers/utils.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/cost_calculator.py": { - "baseline": 428, - "slack": 214 - }, - "litellm/endpoints/speech/speech_to_completion_bridge/handler.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/endpoints/speech/speech_to_completion_bridge/transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/evals/main.py": { - "baseline": 500, - "slack": 250 - }, - "litellm/exceptions.py": { - "baseline": 481, - "slack": 241 - }, - "litellm/experimental_mcp_client/client.py": { - "baseline": 172, - "slack": 86 - }, - "litellm/experimental_mcp_client/tools.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/files/main.py": { - "baseline": 241, - "slack": 121 - }, - "litellm/files/streaming.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/files/types.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/fine_tuning/main.py": { - "baseline": 157, - "slack": 79 - }, - "litellm/google_genai/adapters/handler.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/google_genai/adapters/transformation.py": { - "baseline": 324, - "slack": 162 - }, - "litellm/google_genai/main.py": { - "baseline": 171, - "slack": 86 - }, - "litellm/google_genai/streaming_iterator.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/images/main.py": { - "baseline": 311, - "slack": 156 - }, - "litellm/images/utils.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/integrations/SlackAlerting/batching_handler.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/integrations/SlackAlerting/hanging_request_check.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/integrations/SlackAlerting/slack_alerting.py": { - "baseline": 635, - "slack": 318 - }, - "litellm/integrations/SlackAlerting/utils.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/integrations/additional_logging_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/agentops/agentops.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/integrations/anthropic_cache_control_hook.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/integrations/argilla.py": { - "baseline": 204, - "slack": 102 - }, - "litellm/integrations/arize/__init__.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/arize/_utils.py": { - "baseline": 632, - "slack": 316 - }, - "litellm/integrations/arize/arize.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/integrations/arize/arize_phoenix.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/integrations/arize/arize_phoenix_client.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/integrations/arize/arize_phoenix_prompt_manager.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/integrations/athina.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/integrations/azure_sentinel/azure_sentinel.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/integrations/azure_storage/azure_storage.py": { - "baseline": 148, - "slack": 74 - }, - "litellm/integrations/bitbucket/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/bitbucket/bitbucket_client.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/integrations/bitbucket/bitbucket_prompt_manager.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/integrations/braintrust_logging.py": { - "baseline": 318, - "slack": 159 - }, - "litellm/integrations/braintrust_mock_client.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/integrations/cloudzero/cloudzero.py": { - "baseline": 200, - "slack": 100 - }, - "litellm/integrations/cloudzero/cz_resource_names.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/cloudzero/cz_stream_api.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/integrations/cloudzero/database.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/integrations/cloudzero/transform.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/integrations/compression_interception/handler.py": { - "baseline": 184, - "slack": 92 - }, - "litellm/integrations/custom_batch_logger.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/integrations/custom_guardrail.py": { - "baseline": 303, - "slack": 152 - }, - "litellm/integrations/custom_logger.py": { - "baseline": 197, - "slack": 99 - }, - "litellm/integrations/custom_prompt_management.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/custom_sso_handler.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/datadog/datadog.py": { - "baseline": 264, - "slack": 132 - }, - "litellm/integrations/datadog/datadog_cost_management.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/integrations/datadog/datadog_handler.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/datadog/datadog_llm_obs.py": { - "baseline": 314, - "slack": 157 - }, - "litellm/integrations/datadog/datadog_metrics.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/integrations/datadog/datadog_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/datadog/datadog_team_handler.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/deepeval/api.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/integrations/deepeval/deepeval.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/integrations/deepeval/types.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/integrations/dotprompt/__init__.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/integrations/dotprompt/dotprompt_manager.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/integrations/dotprompt/prompt_manager.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/integrations/dynamodb.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/integrations/email_alerting.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/integrations/focus/database.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/integrations/focus/destinations/factory.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/integrations/focus/destinations/gcs_destination.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/integrations/focus/destinations/mavvrik_destination.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/integrations/focus/destinations/s3_destination.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/integrations/focus/destinations/vantage_destination.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/integrations/focus/export_engine.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/integrations/focus/focus_logger.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/integrations/focus/schema.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/integrations/focus/serializers/csv.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/integrations/focus/serializers/parquet.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/focus/transformer.py": { - "baseline": 109, - "slack": 55 - }, - "litellm/integrations/galileo.py": { - "baseline": 370, - "slack": 185 - }, - "litellm/integrations/gcs_bucket/gcs_bucket.py": { - "baseline": 99, - "slack": 50 - }, - "litellm/integrations/gcs_bucket/gcs_bucket_base.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/integrations/gcs_bucket/gcs_bucket_mock_client.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/integrations/gcs_pubsub/pub_sub.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/integrations/generic_api/generic_api_callback.py": { - "baseline": 196, - "slack": 98 - }, - "litellm/integrations/generic_prompt_management/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/generic_prompt_management/generic_prompt_manager.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/integrations/gitlab/__init__.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/integrations/gitlab/gitlab_client.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/integrations/gitlab/gitlab_prompt_manager.py": { - "baseline": 137, - "slack": 69 - }, - "litellm/integrations/greenscale.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/integrations/helicone.py": { - "baseline": 191, - "slack": 96 - }, - "litellm/integrations/helicone_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/humanloop.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/integrations/lago.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/integrations/langfuse/langfuse.py": { - "baseline": 610, - "slack": 305 - }, - "litellm/integrations/langfuse/langfuse_handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/integrations/langfuse/langfuse_mock_client.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/integrations/langfuse/langfuse_otel.py": { - "baseline": 135, - "slack": 68 - }, - "litellm/integrations/langfuse/langfuse_otel_attributes.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/integrations/langfuse/langfuse_prompt_management.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/integrations/langsmith.py": { - "baseline": 244, - "slack": 122 - }, - "litellm/integrations/langsmith_mock_client.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/langtrace.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/integrations/levo/levo.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/litellm_agent/litellm_agent_model_resolver.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/integrations/literal_ai.py": { - "baseline": 281, - "slack": 141 - }, - "litellm/integrations/logfire_logger.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/integrations/lunary.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/integrations/mavvrik_focus/mavvrik_focus_logger.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/integrations/mlflow.py": { - "baseline": 239, - "slack": 120 - }, - "litellm/integrations/mock_client_factory.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/integrations/newrelic/newrelic.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/integrations/openmeter.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/integrations/opentelemetry.py": { - "baseline": 1474, - "slack": 737 - }, - "litellm/integrations/opentelemetry_utils/base_otel_llm_obs_attributes.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/opentelemetry_utils/gen_ai_semconv.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/integrations/opik/opik.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/integrations/opik/opik_payload_builder/api.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/integrations/opik/opik_payload_builder/extractors.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/integrations/opik/opik_payload_builder/payload_builders.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/integrations/opik/opik_payload_builder/types.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/opik/utils.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/integrations/otel/logger.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/integrations/otel/mappers/genai.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/mappers/langfuse.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/mappers/langtrace.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/mappers/openinference.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/integrations/otel/mappers/utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/integrations/otel/model/baggage.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/model/config.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/otel/model/metadata.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/integrations/otel/model/payloads.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/integrations/otel/model/utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/otel/mount.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/integrations/otel/plumbing/metrics.py": { - "baseline": 109, - "slack": 55 - }, - "litellm/integrations/otel/plumbing/providers.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/plumbing/routing.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/integrations/otel/presets/agentops.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/integrations/otel/presets/arize.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/integrations/otel/presets/langfuse.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/presets/langtrace.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/presets/levo.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/presets/phoenix.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/otel/presets/weave.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/integrations/otel/runtime.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/integrations/posthog.py": { - "baseline": 349, - "slack": 175 - }, - "litellm/integrations/posthog_mock_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/prometheus.py": { - "baseline": 1088, - "slack": 544 - }, - "litellm/integrations/prometheus_helpers/__init__.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/integrations/prometheus_helpers/bounded_prometheus_series_tracker.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/integrations/prometheus_helpers/prometheus_api.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/integrations/prometheus_services.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/integrations/prompt_layer.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/integrations/prompt_management_base.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/integrations/rubrik.py": { - "baseline": 203, - "slack": 102 - }, - "litellm/integrations/s3.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/integrations/s3_v2.py": { - "baseline": 241, - "slack": 121 - }, - "litellm/integrations/sqs.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/integrations/supabase.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/integrations/traceloop.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/integrations/vantage/vantage_logger.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/integrations/vector_store_integrations/vector_store_pre_call_hook.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/integrations/weave/weave_otel.py": { - "baseline": 108, - "slack": 54 - }, - "litellm/integrations/websearch_interception/handler.py": { - "baseline": 430, - "slack": 215 - }, - "litellm/integrations/websearch_interception/tools.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/integrations/websearch_interception/transformation.py": { - "baseline": 185, - "slack": 93 - }, - "litellm/integrations/weights_biases.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/interactions/agents/http_handler.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/interactions/agents/main.py": { - "baseline": 189, - "slack": 95 - }, - "litellm/interactions/http_handler.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/interactions/litellm_responses_transformation/handler.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/interactions/litellm_responses_transformation/streaming_iterator.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/interactions/litellm_responses_transformation/transformation.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/interactions/main.py": { - "baseline": 147, - "slack": 74 - }, - "litellm/interactions/streaming_iterator.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/interactions/utils.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/litellm_core_utils/app_crypto.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/asyncify.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/litellm_core_utils/audio_utils/utils.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/litellm_core_utils/cli_token_utils.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/litellm_core_utils/cloud_storage_security.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/litellm_core_utils/completion_timeout.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/litellm_core_utils/core_helpers.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/litellm_core_utils/coroutine_checker.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/litellm_core_utils/credential_accessor.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/custom_logger_registry.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/dd_tracing.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/litellm_core_utils/default_encoding.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/dot_notation_indexing.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/litellm_core_utils/duration_parser.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/litellm_core_utils/exception_mapping_utils.py": { - "baseline": 2076, - "slack": 1038 - }, - "litellm/litellm_core_utils/fallback_utils.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/litellm_core_utils/get_blog_posts.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/litellm_core_utils/get_litellm_params.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/litellm_core_utils/get_llm_provider_logic.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/litellm_core_utils/get_model_cost_map.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/litellm_core_utils/get_provider_specific_headers.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/litellm_core_utils/get_supported_openai_params.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/litellm_core_utils/health_check_helpers.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/litellm_core_utils/health_check_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/litellm_core_utils/initialize_dynamic_callback_params.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/litellm_core_utils/json_validation_rule.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/litellm_core_utils/litellm_logging.py": { - "baseline": 2347, - "slack": 1174 - }, - "litellm/litellm_core_utils/llm_cost_calc/tool_call_cost_tracking.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/litellm_core_utils/llm_cost_calc/usage_object_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/litellm_core_utils/llm_cost_calc/utils.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/litellm_core_utils/llm_request_utils.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/litellm_core_utils/llm_response_utils/convert_dict_to_response.py": { - "baseline": 335, - "slack": 168 - }, - "litellm/litellm_core_utils/llm_response_utils/get_api_base.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/litellm_core_utils/llm_response_utils/get_formatted_prompt.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/litellm_core_utils/llm_response_utils/get_headers.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/litellm_core_utils/llm_response_utils/response_metadata.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/litellm_core_utils/logging_callback_manager.py": { - "baseline": 90, - "slack": 45 - }, - "litellm/litellm_core_utils/logging_utils.py": { - "baseline": 181, - "slack": 91 - }, - "litellm/litellm_core_utils/logging_worker.py": { - "baseline": 91, - "slack": 46 - }, - "litellm/litellm_core_utils/model_param_helper.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/litellm_core_utils/model_response_utils.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/litellm_core_utils/prompt_templates/common_utils.py": { - "baseline": 361, - "slack": 181 - }, - "litellm/litellm_core_utils/prompt_templates/factory.py": { - "baseline": 1445, - "slack": 723 - }, - "litellm/litellm_core_utils/prompt_templates/huggingface_template_handler.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/litellm_core_utils/prompt_templates/image_handling.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/litellm_core_utils/realtime_streaming.py": { - "baseline": 614, - "slack": 307 - }, - "litellm/litellm_core_utils/redact_messages.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/litellm_core_utils/rules.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/litellm_core_utils/safe_json_dumps.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/litellm_core_utils/safe_json_loads.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/litellm_core_utils/sensitive_data_masker.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/litellm_core_utils/specialty_caches/dynamic_logging_cache.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/litellm_core_utils/streaming_chunk_builder_utils.py": { - "baseline": 313, - "slack": 157 - }, - "litellm/litellm_core_utils/streaming_handler.py": { - "baseline": 1019, - "slack": 510 - }, - "litellm/litellm_core_utils/token_counter.py": { - "baseline": 249, - "slack": 125 - }, - "litellm/litellm_core_utils/url_utils.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/a2a/chat/guardrail_translation/handler.py": { - "baseline": 155, - "slack": 78 - }, - "litellm/llms/a2a/chat/streaming_iterator.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/a2a/chat/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/a2a/common_utils.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/ai21/chat/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/aiml/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/aiml/image_generation/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/aiohttp_openai/chat/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/amazon_nova/chat/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/anthropic/batches/handler.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/anthropic/batches/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/anthropic/chat/guardrail_translation/handler.py": { - "baseline": 175, - "slack": 88 - }, - "litellm/llms/anthropic/chat/handler.py": { - "baseline": 388, - "slack": 194 - }, - "litellm/llms/anthropic/chat/transformation.py": { - "baseline": 770, - "slack": 385 - }, - "litellm/llms/anthropic/common_utils.py": { - "baseline": 278, - "slack": 139 - }, - "litellm/llms/anthropic/completion/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/anthropic/cost_calculation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/anthropic/count_tokens/handler.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/count_tokens/token_counter.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/anthropic/count_tokens/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/handler.py": { - "baseline": 225, - "slack": 113 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py": { - "baseline": 434, - "slack": 217 - }, - "litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py": { - "baseline": 328, - "slack": 164 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/dispatcher.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/editors/clear_tool_uses.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/editors/compact.py": { - "baseline": 413, - "slack": 207 - }, - "litellm/llms/anthropic/experimental_pass_through/context_management/placeholders.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/agentic_streaming_iterator.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/fake_stream_iterator.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/handler.py": { - "baseline": 148, - "slack": 74 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/interceptors/advisor.py": { - "baseline": 150, - "slack": 75 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/transformation.py": { - "baseline": 162, - "slack": 81 - }, - "litellm/llms/anthropic/experimental_pass_through/messages/utils.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/handler.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py": { - "baseline": 187, - "slack": 94 - }, - "litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py": { - "baseline": 250, - "slack": 125 - }, - "litellm/llms/anthropic/files/handler.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/anthropic/files/transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/anthropic/skills/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/apiserpent/search/defaults.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/apiserpent/search/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/aws_polly/text_to_speech/transformation.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/llms/azure/assistants.py": { - "baseline": 101, - "slack": 51 - }, - "litellm/llms/azure/audio_transcription/transformation.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/azure/audio_transcriptions.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/azure/azure.py": { - "baseline": 453, - "slack": 227 - }, - "litellm/llms/azure/batches/handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/azure/chat/gpt_5_transformation.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/azure/chat/gpt_transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/azure/chat/o_series_handler.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/azure/chat/o_series_transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/azure/common_utils.py": { - "baseline": 217, - "slack": 109 - }, - "litellm/llms/azure/completion/handler.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/llms/azure/completion/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/azure/containers/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/azure/exception_mapping.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/azure/files/handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/azure/fine_tuning/handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/azure/image_edit/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/azure/image_generation/http_utils.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/azure/passthrough/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/azure/realtime/handler.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/azure/realtime/http_transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/azure/responses/o_series_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure/responses/transformation.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/llms/azure/text_to_speech/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/azure/vector_stores/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/azure/videos/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/azure_ai/agents/handler.py": { - "baseline": 292, - "slack": 146 - }, - "litellm/llms/azure_ai/agents/transformation.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/handler.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/token_counter.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/azure_ai/anthropic/count_tokens/transformation.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/azure_ai/anthropic/handler.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/azure_ai/anthropic/messages_transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/azure_ai/anthropic/transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/azure_ai/azure_model_router/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/azure_ai/chat/transformation.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/azure_ai/embed/cohere_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure_ai/embed/handler.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/llms/azure_ai/image_edit/flux2_transformation.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/azure_ai/image_edit/mai_transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/azure_ai/image_edit/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/azure_ai/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/azure_ai/image_generation/mai_transformation.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/llms/azure_ai/ocr/document_intelligence/transformation.py": { - "baseline": 124, - "slack": 62 - }, - "litellm/llms/azure_ai/ocr/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/azure_ai/rerank/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/azure_ai/vector_stores/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/base.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/base_llm/agents/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/anthropic_messages/transformation.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/base_llm/audio_transcription/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/base_llm/base_model_iterator.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/base_llm/base_utils.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/llms/base_llm/batches/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/base_llm/chat/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/base_llm/completion/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/containers/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/base_llm/embedding/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/evals/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/files/azure_blob_storage_backend.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/base_llm/files/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/google_genai/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/base_llm/guardrail_translation/base_translation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/base_llm/guardrail_translation/utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/base_llm/image_edit/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/base_llm/image_generation/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/image_variations/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/interactions/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/base_llm/managed_resources/base_managed_resource.py": { - "baseline": 108, - "slack": 54 - }, - "litellm/llms/base_llm/managed_resources/isolation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/base_llm/managed_resources/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/base_llm/ocr/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/base_llm/passthrough/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/base_llm/realtime/http_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/realtime/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/base_llm/rerank/transformation.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/base_llm/responses/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/base_llm/search/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/base_llm/skills/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/text_to_speech/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/base_llm/vector_store/transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/base_llm/vector_store_files/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/base_llm/videos/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/baseten/chat.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/bedrock/base_aws_llm.py": { - "baseline": 338, - "slack": 169 - }, - "litellm/llms/bedrock/batches/handler.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/llms/bedrock/batches/transformation.py": { - "baseline": 144, - "slack": 72 - }, - "litellm/llms/bedrock/chat/agentcore/transformation.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/llms/bedrock/chat/converse_handler.py": { - "baseline": 148, - "slack": 74 - }, - "litellm/llms/bedrock/chat/converse_transformation.py": { - "baseline": 521, - "slack": 261 - }, - "litellm/llms/bedrock/chat/invoke_agent/transformation.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/llms/bedrock/chat/invoke_handler.py": { - "baseline": 631, - "slack": 316 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_ai21_transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_cohere_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_deepseek_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_llama_transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_mistral_transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_moonshot_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_nova_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_openai_transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen2_transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_qwen3_transformation.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_titan_transformation.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/bedrock/chat/invoke_transformations/amazon_twelvelabs_pegasus_transformation.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude2_transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py": { - "baseline": 137, - "slack": 69 - }, - "litellm/llms/bedrock/chat/invoke_transformations/base_invoke_transformation.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/llms/bedrock/chat/mantle/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/bedrock/claude_platform/common_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/bedrock/claude_platform/messages_transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/bedrock/claude_platform/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/common_utils.py": { - "baseline": 279, - "slack": 140 - }, - "litellm/llms/bedrock/count_tokens/bedrock_token_counter.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/bedrock/count_tokens/handler.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/bedrock/count_tokens/transformation.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/llms/bedrock/embed/amazon_nova_transformation.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/llms/bedrock/embed/amazon_titan_g1_transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/bedrock/embed/amazon_titan_multimodal_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/bedrock/embed/amazon_titan_v2_transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/bedrock/embed/cohere_transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock/embed/embedding.py": { - "baseline": 225, - "slack": 113 - }, - "litellm/llms/bedrock/embed/twelvelabs_marengo_transformation.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/bedrock/files/handler.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/bedrock/files/transformation.py": { - "baseline": 218, - "slack": 109 - }, - "litellm/llms/bedrock/image_edit/amazon_nova_canvas_image_edit_transformation.py": { - "baseline": 156, - "slack": 78 - }, - "litellm/llms/bedrock/image_edit/handler.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/llms/bedrock/image_edit/stability_transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/bedrock/image_generation/amazon_nova_canvas_transformation.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/llms/bedrock/image_generation/amazon_stability1_transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/bedrock/image_generation/amazon_stability3_transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/bedrock/image_generation/amazon_titan_transformation.py": { - "baseline": 64, - "slack": 32 - }, - "litellm/llms/bedrock/image_generation/cost_calculator.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/bedrock/image_generation/image_handler.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py": { - "baseline": 237, - "slack": 119 - }, - "litellm/llms/bedrock/messages/mantle_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/passthrough/guardrail_translation/handler.py": { - "baseline": 335, - "slack": 168 - }, - "litellm/llms/bedrock/passthrough/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/bedrock/realtime/handler.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/bedrock/realtime/transformation.py": { - "baseline": 293, - "slack": 147 - }, - "litellm/llms/bedrock/rerank/handler.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/bedrock/rerank/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/bedrock/vector_stores/transformation.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/llms/bedrock_mantle/chat/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/bedrock_mantle/responses/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/black_forest_labs/image_edit/handler.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/llms/black_forest_labs/image_edit/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/black_forest_labs/image_generation/handler.py": { - "baseline": 127, - "slack": 64 - }, - "litellm/llms/black_forest_labs/image_generation/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/brave/search/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/bytez/chat/transformation.py": { - "baseline": 128, - "slack": 64 - }, - "litellm/llms/cerebras/chat.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/chatgpt/authenticator.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/llms/chatgpt/chat/streaming_utils.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/chatgpt/chat/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/chatgpt/common_utils.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/chatgpt/responses/transformation.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/llms/clarifai/chat/transformation.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/cloudflare/chat/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/codestral/completion/handler.py": { - "baseline": 128, - "slack": 64 - }, - "litellm/llms/codestral/completion/transformation.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/cohere/chat/transformation.py": { - "baseline": 113, - "slack": 57 - }, - "litellm/llms/cohere/chat/v2_transformation.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/llms/cohere/common_utils.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/llms/cohere/embed/handler.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/cohere/embed/transformation.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/cohere/embed/v1_transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/cohere/rerank/guardrail_translation/handler.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/cohere/rerank/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/cohere/rerank_v2/transformation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/cometapi/chat/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/cometapi/embed/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/cometapi/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/cometapi/image_generation/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/compactifai/chat/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/custom_httpx/aiohttp_handler.py": { - "baseline": 183, - "slack": 92 - }, - "litellm/llms/custom_httpx/aiohttp_transport.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/custom_httpx/async_client_cleanup.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/custom_httpx/container_handler.py": { - "baseline": 169, - "slack": 85 - }, - "litellm/llms/custom_httpx/http_handler.py": { - "baseline": 320, - "slack": 160 - }, - "litellm/llms/custom_httpx/httpx_handler.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/custom_httpx/llm_http_handler.py": { - "baseline": 3836, - "slack": 1918 - }, - "litellm/llms/custom_httpx/mock_transport.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/custom_llm.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/dashscope/common_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/dashscope/cost_calculator.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/dashscope/embed/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/dashscope/image_generation/transformation.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/llms/dashscope/rerank/transformation.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/llms/databricks/chat/transformation.py": { - "baseline": 167, - "slack": 84 - }, - "litellm/llms/databricks/common_utils.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/databricks/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/databricks/embed/handler.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/databricks/embed/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/databricks/responses/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/databricks/streaming_utils.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/dataforseo/search/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/deepgram/audio_transcription/transformation.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/llms/deepinfra/chat/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/deepinfra/rerank/transformation.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/deepseek/chat/transformation.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/deepseek/messages/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/deprecated_providers/aleph_alpha.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/llms/deprecated_providers/palm.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/docker_model_runner/chat/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/duckduckgo/search/transformation.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/llms/elevenlabs/audio_transcription/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/elevenlabs/text_to_speech/transformation.py": { - "baseline": 93, - "slack": 47 - }, - "litellm/llms/exa_ai/search/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/fal_ai/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/fal_ai/image_generation/bria_transformation.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/fal_ai/image_generation/bytedance_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/flux_pro_v11_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/flux_pro_v11_ultra_transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/fal_ai/image_generation/flux_schnell_transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fal_ai/image_generation/ideogram_v3_transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/fal_ai/image_generation/imagen4_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/fal_ai/image_generation/nano_banana_transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/fal_ai/image_generation/recraft_v3_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/fal_ai/image_generation/stable_diffusion_transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/fal_ai/image_generation/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/fastcrw/search/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/featherless_ai/chat/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/firecrawl/search/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/fireworks_ai/chat/transformation.py": { - "baseline": 124, - "slack": 62 - }, - "litellm/llms/fireworks_ai/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/fireworks_ai/completion/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/fireworks_ai/cost_calculator.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/fireworks_ai/embed/fireworks_ai_transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/fireworks_ai/rerank/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/gemini/agents/transformation.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/gemini/chat/transformation.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/gemini/common_utils.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/llms/gemini/count_tokens/handler.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/gemini/files/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/gemini/google_genai/transformation.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/llms/gemini/image_edit/cost_calculator.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/gemini/image_edit/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/gemini/image_generation/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/gemini/image_generation/transformation.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/llms/gemini/image_usage_transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/gemini/interactions/transformation.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/llms/gemini/realtime/transformation.py": { - "baseline": 324, - "slack": 162 - }, - "litellm/llms/gemini/vector_stores/transformation.py": { - "baseline": 97, - "slack": 49 - }, - "litellm/llms/gemini/videos/transformation.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/llms/gigachat/authenticator.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/gigachat/chat/streaming.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/gigachat/chat/transformation.py": { - "baseline": 157, - "slack": 79 - }, - "litellm/llms/gigachat/embedding/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/gigachat/file_handler.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/github_copilot/authenticator.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/github_copilot/chat/transformation.py": { - "baseline": 87, - "slack": 44 - }, - "litellm/llms/github_copilot/common_utils.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/llms/github_copilot/embedding/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/github_copilot/responses/transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/google_pse/search/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/gradient_ai/chat/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/groq/chat/handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/groq/chat/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/groq/stt/transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/heroku/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/hosted_vllm/chat/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/hosted_vllm/embedding/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/hosted_vllm/rerank/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/hosted_vllm/responses/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/hosted_vllm/transcriptions/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/huggingface/chat/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/huggingface/common_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/huggingface/embedding/handler.py": { - "baseline": 156, - "slack": 78 - }, - "litellm/llms/huggingface/embedding/transformation.py": { - "baseline": 224, - "slack": 112 - }, - "litellm/llms/huggingface/rerank/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/hyperbolic/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/inception/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/inception/completion/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/infinity/common_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/infinity/embedding/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/infinity/rerank/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/jina_ai/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/jina_ai/embedding/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/jina_ai/rerank/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/langflow/a2a.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/llms/langflow/chat/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/langgraph/chat/sse_iterator.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/langgraph/chat/transformation.py": { - "baseline": 103, - "slack": 52 - }, - "litellm/llms/lemonade/chat/transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/linkup/search/transformation.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/litellm_proxy/chat/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/litellm_proxy/image_edit/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/litellm_proxy/image_generation/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/litellm_proxy/skills/code_execution.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/llms/litellm_proxy/skills/handler.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/litellm_proxy/skills/prompt_injection.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/litellm_proxy/skills/sandbox_executor.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/llms/litellm_proxy/skills/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/lm_studio/chat/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/lm_studio/embed/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/manus/files/transformation.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/manus/responses/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/maritalk.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/meta_llama/chat/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/milvus/vector_stores/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/minimax/chat/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/minimax/text_to_speech/transformation.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/llms/mistral/audio_transcription/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/mistral/chat/transformation.py": { - "baseline": 180, - "slack": 90 - }, - "litellm/llms/mistral/ocr/guardrail_translation/handler.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/llms/mistral/ocr/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/modelscope/chat/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/modelscope/image_generation/transformation.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/moonshot/chat/transformation.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/llms/morph/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/nebius/chat/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/nlp_cloud/chat/handler.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/nlp_cloud/chat/transformation.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/llms/nlp_cloud/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/novita/chat/transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/nscale/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/nvidia_nim/chat/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/nvidia_nim/embed.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/nvidia_nim/rerank/ranking_transformation.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/nvidia_nim/rerank/transformation.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/llms/nvidia_riva/audio_transcription/audio_utils.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/nvidia_riva/audio_transcription/handler.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/llms/nvidia_riva/audio_transcription/transformation.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/llms/nvidia_riva/common_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/oci/chat/cohere.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/llms/oci/chat/generic.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/oci/chat/transformation.py": { - "baseline": 159, - "slack": 80 - }, - "litellm/llms/oci/common_utils.py": { - "baseline": 217, - "slack": 109 - }, - "litellm/llms/oci/embed/transformation.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/llms/ollama/chat/transformation.py": { - "baseline": 173, - "slack": 87 - }, - "litellm/llms/ollama/common_utils.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/ollama/completion/handler.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/ollama/completion/transformation.py": { - "baseline": 143, - "slack": 72 - }, - "litellm/llms/oobabooga/chat/oobabooga.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/oobabooga/chat/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/oobabooga/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/openai/chat/gpt_5_transformation.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/llms/openai/chat/gpt_audio_transformation.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/llms/openai/chat/gpt_transformation.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/llms/openai/chat/guardrail_translation/handler.py": { - "baseline": 188, - "slack": 94 - }, - "litellm/llms/openai/chat/o_series_transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/openai/common_utils.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/openai/completion/guardrail_translation/handler.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/openai/completion/handler.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/llms/openai/completion/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/openai/completion/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/openai/containers/transformation.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/openai/cost_calculation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/openai/embeddings/guardrail_translation/handler.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/openai/evals/transformation.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/llms/openai/fine_tuning/handler.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/openai/image_edit/dalle2_transformation.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/openai/image_edit/transformation.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/llms/openai/image_generation/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/openai/image_generation/dall_e_2_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/dall_e_3_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/gpt_transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/openai/image_generation/guardrail_translation/handler.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/openai/image_variations/handler.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/openai/image_variations/transformation.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/openai/openai.py": { - "baseline": 626, - "slack": 313 - }, - "litellm/llms/openai/realtime/handler.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/openai/realtime/http_transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/openai/responses/count_tokens/handler.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/openai/responses/count_tokens/token_counter.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/openai/responses/count_tokens/transformation.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/llms/openai/responses/guardrail_translation/handler.py": { - "baseline": 247, - "slack": 124 - }, - "litellm/llms/openai/responses/transformation.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/llms/openai/speech/guardrail_translation/handler.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/openai/transcriptions/gpt_transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/openai/transcriptions/guardrail_translation/handler.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/openai/transcriptions/handler.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/llms/openai/transcriptions/whisper_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/openai/vector_store_files/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/openai/vector_stores/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/openai/videos/transformation.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/llms/openai_like/chat/handler.py": { - "baseline": 110, - "slack": 55 - }, - "litellm/llms/openai_like/chat/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/openai_like/common_utils.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/openai_like/dynamic_config.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/llms/openai_like/embedding/handler.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/openai_like/json_loader.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/openai_like/responses/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/openrouter/chat/transformation.py": { - "baseline": 74, - "slack": 37 - }, - "litellm/llms/openrouter/embedding/transformation.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/openrouter/image_edit/transformation.py": { - "baseline": 93, - "slack": 47 - }, - "litellm/llms/openrouter/image_generation/transformation.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/openrouter/responses/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/ovhcloud/audio_transcription/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/ovhcloud/chat/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/ovhcloud/embedding/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/parallel_ai/search/transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/pass_through/guardrail_translation/handler.py": { - "baseline": 82, - "slack": 41 - }, - "litellm/llms/perplexity/chat/transformation.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/perplexity/cost_calculator.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/perplexity/embedding/transformation.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/llms/perplexity/responses/transformation.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/perplexity/search/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/petals/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/petals/completion/handler.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/petals/completion/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/pg_vector/vector_stores/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/predibase/chat/handler.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/llms/predibase/chat/transformation.py": { - "baseline": 116, - "slack": 58 - }, - "litellm/llms/predibase/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/ragflow/chat/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/ragflow/vector_stores/transformation.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/llms/recraft/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/recraft/image_edit/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/recraft/image_generation/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/reducto/common.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/llms/reducto/ocr/transformation.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/replicate/chat/handler.py": { - "baseline": 136, - "slack": 68 - }, - "litellm/llms/replicate/chat/transformation.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/llms/replicate/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/runwayml/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/runwayml/image_generation/transformation.py": { - "baseline": 78, - "slack": 39 - }, - "litellm/llms/runwayml/text_to_speech/transformation.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/llms/runwayml/videos/transformation.py": { - "baseline": 125, - "slack": 63 - }, - "litellm/llms/s3_vectors/vector_stores/transformation.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/llms/sagemaker/chat/handler.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/sagemaker/chat/transformation.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/llms/sagemaker/common_utils.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/llms/sagemaker/completion/handler.py": { - "baseline": 301, - "slack": 151 - }, - "litellm/llms/sagemaker/completion/transformation.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/llms/sagemaker/embedding/cohere_transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/sagemaker/embedding/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/sagemaker/nova/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/sambanova/chat.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/sambanova/common_utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/sambanova/embedding/transformation.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/llms/sap/chat/handler.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/sap/chat/models.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/sap/chat/transformation.py": { - "baseline": 182, - "slack": 91 - }, - "litellm/llms/sap/credentials.py": { - "baseline": 58, - "slack": 29 - }, - "litellm/llms/sap/embed/transformation.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/llms/scaleway/audio_transcription/transformation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/llms/searchapi/search/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/searxng/search/transformation.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/llms/serper/search/transformation.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/llms/snowflake/chat/transformation.py": { - "baseline": 244, - "slack": 122 - }, - "litellm/llms/snowflake/common_utils.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/snowflake/embedding/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/snowflake/utils.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/llms/soniox/audio_transcription/handler.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/llms/soniox/audio_transcription/transformation.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/llms/soniox/common_utils.py": { - "baseline": 68, - "slack": 34 - }, - "litellm/llms/stability/image_edit/transformations.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/llms/stability/image_generation/transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/llms/tavily/search/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/together_ai/chat.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/together_ai/completion/transformation.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/together_ai/cost_calculator.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/together_ai/rerank/handler.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/together_ai/rerank/transformation.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/llms/topaz/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/topaz/image_variations/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/triton/common_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/triton/completion/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/triton/embedding/transformation.py": { - "baseline": 35, - "slack": 18 - }, - "litellm/llms/v0/chat/transformation.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/vercel_ai_gateway/chat/transformation.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/llms/vercel_ai_gateway/embedding/transformation.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/vertex_ai/agent_engine/sse_iterator.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/vertex_ai/agent_engine/transformation.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/vertex_ai/aws_credentials_supplier.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/vertex_ai/batches/handler.py": { - "baseline": 115, - "slack": 58 - }, - "litellm/llms/vertex_ai/batches/transformation.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/vertex_ai/common_utils.py": { - "baseline": 493, - "slack": 247 - }, - "litellm/llms/vertex_ai/context_caching/transformation.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/llms/vertex_ai/context_caching/vertex_ai_context_caching.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/llms/vertex_ai/cost_calculator.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/vertex_ai/count_tokens/handler.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/llms/vertex_ai/files/handler.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/vertex_ai/files/transformation.py": { - "baseline": 177, - "slack": 89 - }, - "litellm/llms/vertex_ai/fine_tuning/handler.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/vertex_ai/gemini/transformation.py": { - "baseline": 311, - "slack": 156 - }, - "litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py": { - "baseline": 907, - "slack": 454 - }, - "litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_handler.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/llms/vertex_ai/gemini_embeddings/batch_embed_content_transformation.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/llms/vertex_ai/google_genai/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/vertex_ai/image_edit/cost_calculator.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/vertex_ai/image_edit/vertex_gemini_transformation.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/llms/vertex_ai/image_edit/vertex_imagen_transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/vertex_ai/image_generation/image_generation_handler.py": { - "baseline": 71, - "slack": 36 - }, - "litellm/llms/vertex_ai/image_generation/vertex_gemini_transformation.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/llms/vertex_ai/image_generation/vertex_imagen_transformation.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/llms/vertex_ai/multimodal_embeddings/embedding_handler.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/llms/vertex_ai/multimodal_embeddings/transformation.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/llms/vertex_ai/ocr/deepseek_transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/vertex_ai/ocr/transformation.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/vertex_ai/rag_engine/ingestion.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/llms/vertex_ai/rag_engine/transformation.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/llms/vertex_ai/realtime/transformation.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/vertex_ai/rerank/transformation.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/llms/vertex_ai/text_to_speech/text_to_speech_handler.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/vertex_ai/text_to_speech/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/vertex_ai/vector_stores/rag_api/transformation.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/vertex_ai/vector_stores/search_api/transformation.py": { - "baseline": 88, - "slack": 44 - }, - "litellm/llms/vertex_ai/vertex_ai_aws_wif.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/llms/vertex_ai/vertex_ai_non_gemini.py": { - "baseline": 319, - "slack": 160 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/ai21/transformation.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/experimental_pass_through/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/output_params_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/count_tokens/handler.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/gpt_oss/transformation.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/llama3/transformation.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/llms/vertex_ai/vertex_ai_partner_models/main.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/vertex_ai/vertex_embeddings/bge.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/llms/vertex_ai/vertex_embeddings/embedding_handler.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/llms/vertex_ai/vertex_embeddings/transformation.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/llms/vertex_ai/vertex_gemma_models/main.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/llms/vertex_ai/vertex_gemma_models/transformation.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/llms/vertex_ai/vertex_llm_base.py": { - "baseline": 301, - "slack": 151 - }, - "litellm/llms/vertex_ai/vertex_model_garden/main.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/llms/vertex_ai/videos/transformation.py": { - "baseline": 164, - "slack": 82 - }, - "litellm/llms/vllm/common_utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/llms/vllm/completion/handler.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/llms/vllm/passthrough/transformation.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/llms/volcengine/chat/transformation.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/llms/volcengine/common_utils.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/llms/volcengine/embedding/transformation.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/llms/volcengine/responses/transformation.py": { - "baseline": 200, - "slack": 100 - }, - "litellm/llms/voyage/embedding/transformation.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/voyage/embedding/transformation_contextual.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/llms/voyage/embedding/transformation_multimodal.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/llms/voyage/rerank/transformation.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/llms/wandb/chat/transformation.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/llms/watsonx/audio_transcription/transformation.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/llms/watsonx/chat/handler.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/llms/watsonx/chat/transformation.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/llms/watsonx/common_utils.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/llms/watsonx/completion/transformation.py": { - "baseline": 116, - "slack": 58 - }, - "litellm/llms/watsonx/embed/transformation.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/llms/watsonx/passthrough/transformation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/llms/watsonx/rerank/transformation.py": { - "baseline": 89, - "slack": 45 - }, - "litellm/llms/xai/chat/transformation.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/llms/xai/common_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/llms/xai/cost_calculator.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/llms/xai/oauth.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/llms/xai/realtime/handler.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/llms/xai/responses/transformation.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/llms/xinference/image_generation/transformation.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/llms/you_com/search/transformation.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/main.py": { - "baseline": 3102, - "slack": 1551 - }, - "litellm/models/base.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/models/credentials.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/models/end_user.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/models/model.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/models/organization.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/models/organization_membership.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/models/spend_logs.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/models/tag.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/models/team.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/models/user.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/models/verification_token.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/ocr/main.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/passthrough/main.py": { - "baseline": 91, - "slack": 46 - }, - "litellm/passthrough/timeout_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/passthrough/utils.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/_experimental/mcp_server/auth/token_exchange.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/_experimental/mcp_server/cost_calculator.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/_experimental/mcp_server/db.py": { - "baseline": 424, - "slack": 212 - }, - "litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py": { - "baseline": 189, - "slack": 95 - }, - "litellm/proxy/_experimental/mcp_server/elicitation_handler.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/_experimental/mcp_server/guardrail_translation/handler.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/_experimental/mcp_server/mcp_debug.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/_experimental/mcp_server/mcp_server_manager.py": { - "baseline": 833, - "slack": 417 - }, - "litellm/proxy/_experimental/mcp_server/oauth2_token_cache.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/proxy/_experimental/mcp_server/oauth_utils.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/_experimental/mcp_server/openapi_to_mcp_generator.py": { - "baseline": 213, - "slack": 107 - }, - "litellm/proxy/_experimental/mcp_server/rest_endpoints.py": { - "baseline": 277, - "slack": 139 - }, - "litellm/proxy/_experimental/mcp_server/sampling_handler.py": { - "baseline": 533, - "slack": 267 - }, - "litellm/proxy/_experimental/mcp_server/semantic_tool_filter.py": { - "baseline": 96, - "slack": 48 - }, - "litellm/proxy/_experimental/mcp_server/server.py": { - "baseline": 914, - "slack": 457 - }, - "litellm/proxy/_experimental/mcp_server/sse_transport.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/proxy/_experimental/mcp_server/tool_registry.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/_experimental/mcp_server/toolset_db.py": { - "baseline": 46, - "slack": 23 - }, - "litellm/proxy/_experimental/mcp_server/ui_session_utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/_experimental/mcp_server/utils.py": { - "baseline": 99, - "slack": 50 - }, - "litellm/proxy/_lazy_features.py": { - "baseline": 79, - "slack": 40 - }, - "litellm/proxy/_lazy_openapi_snapshot.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/proxy/_logging.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/_types.py": { - "baseline": 501, - "slack": 251 - }, - "litellm/proxy/a2a/agent_card.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/a2a/discovery.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/a2a/endpoints.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/agent_endpoints/a2a_endpoints.py": { - "baseline": 324, - "slack": 162 - }, - "litellm/proxy/agent_endpoints/a2a_routing.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/agent_endpoints/agent_registry.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/proxy/agent_endpoints/auth/agent_permission_handler.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/agent_endpoints/databricks_oauth.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/agent_endpoints/endpoints.py": { - "baseline": 208, - "slack": 104 - }, - "litellm/proxy/agent_endpoints/model_list_helpers.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/analytics_endpoints/analytics_endpoints.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/anthropic_endpoints/claude_code_endpoints/claude_code_marketplace.py": { - "baseline": 225, - "slack": 113 - }, - "litellm/proxy/anthropic_endpoints/endpoints.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/proxy/anthropic_endpoints/skills_endpoints.py": { - "baseline": 103, - "slack": 52 - }, - "litellm/proxy/auth/auth_checks.py": { - "baseline": 601, - "slack": 301 - }, - "litellm/proxy/auth/auth_checks_organization.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/auth/auth_exception_handler.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/auth/auth_utils.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/proxy/auth/handle_jwt.py": { - "baseline": 359, - "slack": 180 - }, - "litellm/proxy/auth/ip_address_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/auth/litellm_license.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/proxy/auth/login_utils.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/auth/model_checks.py": { - "baseline": 52, - "slack": 26 - }, - "litellm/proxy/auth/oauth2_check.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/auth/oauth2_proxy_hook.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/auth/rds_iam_token.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/auth/route_checks.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/proxy/auth/trusted_proxy_utils.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/auth/user_api_key_auth.py": { - "baseline": 535, - "slack": 268 - }, - "litellm/proxy/batches_endpoints/endpoints.py": { - "baseline": 330, - "slack": 165 - }, - "litellm/proxy/caching_routes.py": { - "baseline": 105, - "slack": 53 - }, - "litellm/proxy/client/chat.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/proxy/client/cli/commands/agents.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/client/cli/commands/auth.py": { - "baseline": 236, - "slack": 118 - }, - "litellm/proxy/client/cli/commands/chat.py": { - "baseline": 101, - "slack": 51 - }, - "litellm/proxy/client/cli/commands/credentials.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/client/cli/commands/http.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/client/cli/commands/keys.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/proxy/client/cli/commands/models.py": { - "baseline": 145, - "slack": 73 - }, - "litellm/proxy/client/cli/commands/teams.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/client/cli/commands/users.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/client/cli/interface.py": { - "baseline": 95, - "slack": 48 - }, - "litellm/proxy/client/cli/main.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/client/credentials.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/client/health.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/client/http_client.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/client/keys.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/proxy/client/model_groups.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/client/models.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/proxy/client/teams.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/client/users.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/common_request_processing.py": { - "baseline": 720, - "slack": 360 - }, - "litellm/proxy/common_utils/admin_ui_utils.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/common_utils/banner.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/common_utils/cache_coordinator.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/common_utils/cache_pydantic_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/common_utils/callback_utils.py": { - "baseline": 255, - "slack": 128 - }, - "litellm/proxy/common_utils/custom_openapi_spec.py": { - "baseline": 119, - "slack": 60 - }, - "litellm/proxy/common_utils/debug_utils.py": { - "baseline": 366, - "slack": 183 - }, - "litellm/proxy/common_utils/encrypt_decrypt_utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/common_utils/expired_ui_session_key_cleanup_manager.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/proxy/common_utils/get_routes.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/common_utils/http_parsing_utils.py": { - "baseline": 176, - "slack": 88 - }, - "litellm/proxy/common_utils/key_rotation_manager.py": { - "baseline": 66, - "slack": 33 - }, - "litellm/proxy/common_utils/load_config_utils.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/proxy/common_utils/openai_endpoint_utils.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/common_utils/openapi_schema_compat.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/proxy/common_utils/performance_utils.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/proxy/common_utils/proxy_rate_limit_error.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/common_utils/proxy_state.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/common_utils/rbac_utils.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/common_utils/reset_budget_job.py": { - "baseline": 535, - "slack": 268 - }, - "litellm/proxy/common_utils/swagger_utils.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/common_utils/timezone_utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/common_utils/user_api_key_cache.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/compliance_checks.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/config_management_endpoints/pass_through_endpoints.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/container_endpoints/endpoints.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/proxy/container_endpoints/handler_factory.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/proxy/container_endpoints/ownership.py": { - "baseline": 164, - "slack": 82 - }, - "litellm/proxy/credential_endpoints/endpoints.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/proxy/custom_hooks/custom_ui_sso_hook.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/custom_prompt_management.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/custom_sso.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/db/check_migration.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/db/create_views.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/proxy/db/db_spend_update_writer.py": { - "baseline": 308, - "slack": 154 - }, - "litellm/proxy/db/db_transaction_queue/base_update_queue.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/proxy/db/db_transaction_queue/daily_spend_update_queue.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/db/db_transaction_queue/pod_lock_manager.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/db/db_transaction_queue/redis_update_buffer.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/db/db_transaction_queue/spend_logs_partition_manager.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/db/db_transaction_queue/spend_update_queue.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/proxy/db/db_url_settings.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/db/dynamo_db.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/proxy/db/exception_handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/db/log_db_metrics.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/db/prisma_client.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/proxy/db/routing_prisma_wrapper.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/db/spend_counter_reseed.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/proxy/db/spend_log_tool_index.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/db/tool_registry_writer.py": { - "baseline": 144, - "slack": 72 - }, - "litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/example_config_yaml/custom_auth.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/example_config_yaml/custom_callbacks.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/example_config_yaml/custom_callbacks1.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/example_config_yaml/custom_guardrail.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/example_config_yaml/custom_handler.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/example_config_yaml/pipeline_test_guardrails.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/fine_tuning_endpoints/endpoints.py": { - "baseline": 218, - "slack": 109 - }, - "litellm/proxy/google_endpoints/agents_endpoints.py": { - "baseline": 158, - "slack": 79 - }, - "litellm/proxy/google_endpoints/endpoints.py": { - "baseline": 131, - "slack": 66 - }, - "litellm/proxy/guardrails/_content_utils.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/proxy/guardrails/guardrail_endpoints.py": { - "baseline": 579, - "slack": 290 - }, - "litellm/proxy/guardrails/guardrail_helpers.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/guardrails/guardrail_hooks/aim/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/aim/aim.py": { - "baseline": 138, - "slack": 69 - }, - "litellm/proxy/guardrails/guardrail_hooks/akto/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/guardrails/guardrail_hooks/akto/akto.py": { - "baseline": 123, - "slack": 62 - }, - "litellm/proxy/guardrails/guardrail_hooks/aporia_ai/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/aporia_ai/aporia_ai.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/__init__.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/base.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/prompt_shield.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/proxy/guardrails/guardrail_hooks/azure/text_moderation.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py": { - "baseline": 260, - "slack": 130 - }, - "litellm/proxy/guardrails/guardrail_hooks/block_code_execution/__init__.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/guardrails/guardrail_hooks/block_code_execution/block_code_execution.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/guardrails/guardrail_hooks/cato_networks/__init__.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/cato_networks/cato_networks.py": { - "baseline": 397, - "slack": 199 - }, - "litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/__init__.py": { - "baseline": 49, - "slack": 25 - }, - "litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense.py": { - "baseline": 746, - "slack": 373 - }, - "litellm/proxy/guardrails/guardrail_hooks/cisco_ai_defense/cisco_ai_defense_mcp.py": { - "baseline": 319, - "slack": 160 - }, - "litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/__init__.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/crowdstrike_aidr/crowdstrike_aidr.py": { - "baseline": 99, - "slack": 50 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/__init__.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py": { - "baseline": 67, - "slack": 34 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/primitives.py": { - "baseline": 100, - "slack": 50 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_code/sandbox.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/custom_guardrail.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/guardrails/guardrail_hooks/deepkeep/__init__.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/deepkeep/deepkeep.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/proxy/guardrails/guardrail_hooks/dynamoai/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/dynamoai/dynamoai.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/proxy/guardrails/guardrail_hooks/enkryptai/__init__.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/enkryptai/enkryptai.py": { - "baseline": 75, - "slack": 38 - }, - "litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/__init__.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/proxy/guardrails/guardrail_hooks/generic_guardrail_api/generic_guardrail_api.py": { - "baseline": 107, - "slack": 54 - }, - "litellm/proxy/guardrails/guardrail_hooks/grayswan/__init__.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/proxy/guardrails/guardrail_hooks/grayswan/grayswan.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/__init__.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/guardrails_ai/guardrails_ai.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/__init__.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/hiddenlayer/hiddenlayer.py": { - "baseline": 192, - "slack": 96 - }, - "litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/__init__.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/guardrails/guardrail_hooks/ibm_guardrails/ibm_detector.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/proxy/guardrails/guardrail_hooks/javelin/__init__.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/javelin/javelin.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/proxy/guardrails/guardrail_hooks/lakera_ai.py": { - "baseline": 91, - "slack": 46 - }, - "litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/lasso/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/lasso/lasso.py": { - "baseline": 364, - "slack": 182 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/__init__.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/competitor_intent/airline.py": { - "baseline": 56, - "slack": 28 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/competitor_intent/base.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py": { - "baseline": 270, - "slack": 135 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/guardrail_benchmarks/test_eval.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/patterns.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/proxy/guardrails/guardrail_hooks/llm_as_a_judge/__init__.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/__init__.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_end_user_permission/mcp_end_user_permission.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/__init__.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_jwt_signer/mcp_jwt_signer.py": { - "baseline": 202, - "slack": 101 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_security/__init__.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/mcp_security/mcp_security_guardrail.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/__init__.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/base.py": { - "baseline": 159, - "slack": 80 - }, - "litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/proxy/guardrails/guardrail_hooks/model_armor/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/model_armor/model_armor.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/proxy/guardrails/guardrail_hooks/noma/__init__.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/proxy/guardrails/guardrail_hooks/noma/noma.py": { - "baseline": 191, - "slack": 96 - }, - "litellm/proxy/guardrails/guardrail_hooks/noma/noma_v2.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/proxy/guardrails/guardrail_hooks/onyx/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/onyx/onyx.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/proxy/guardrails/guardrail_hooks/openai/__init__.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/proxy/guardrails/guardrail_hooks/ovalix/__init__.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/proxy/guardrails/guardrail_hooks/ovalix/ovalix.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/proxy/guardrails/guardrail_hooks/pangea/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/pangea/pangea.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/__init__.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/panw_prisma_airs/panw_prisma_airs.py": { - "baseline": 651, - "slack": 326 - }, - "litellm/proxy/guardrails/guardrail_hooks/pillar/__init__.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/proxy/guardrails/guardrail_hooks/pillar/pillar.py": { - "baseline": 180, - "slack": 90 - }, - "litellm/proxy/guardrails/guardrail_hooks/presidio.py": { - "baseline": 435, - "slack": 218 - }, - "litellm/proxy/guardrails/guardrail_hooks/prompt_security/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/prompt_security/prompt_security.py": { - "baseline": 255, - "slack": 128 - }, - "litellm/proxy/guardrails/guardrail_hooks/promptguard/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/promptguard/promptguard.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/proxy/guardrails/guardrail_hooks/qohash/__init__.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/guardrails/guardrail_hooks/qohash/qohash.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/guardrails/guardrail_hooks/qualifire/__init__.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/proxy/guardrails/guardrail_hooks/qualifire/qualifire.py": { - "baseline": 126, - "slack": 63 - }, - "litellm/proxy/guardrails/guardrail_hooks/rubrik/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/semantic_guard/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/guardrails/guardrail_hooks/semantic_guard/route_loader.py": { - "baseline": 40, - "slack": 20 - }, - "litellm/proxy/guardrails/guardrail_hooks/semantic_guard/semantic_guard.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/guardrails/guardrail_hooks/tool_permission.py": { - "baseline": 210, - "slack": 105 - }, - "litellm/proxy/guardrails/guardrail_hooks/tool_policy/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/tool_policy/tool_policy_guardrail.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/proxy/guardrails/guardrail_hooks/unified_guardrail/unified_guardrail.py": { - "baseline": 144, - "slack": 72 - }, - "litellm/proxy/guardrails/guardrail_hooks/vigil_guard/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/vigil_guard/vigil_guard.py": { - "baseline": 117, - "slack": 59 - }, - "litellm/proxy/guardrails/guardrail_hooks/xecguard/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py": { - "baseline": 221, - "slack": 111 - }, - "litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/__init__.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/guardrails/guardrail_hooks/zscaler_ai_guard/zscaler_ai_guard.py": { - "baseline": 194, - "slack": 97 - }, - "litellm/proxy/guardrails/guardrail_initializers.py": { - "baseline": 77, - "slack": 39 - }, - "litellm/proxy/guardrails/guardrail_registry.py": { - "baseline": 228, - "slack": 114 - }, - "litellm/proxy/guardrails/init_guardrails.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/proxy/guardrails/tool_name_extraction.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/guardrails/usage_endpoints.py": { - "baseline": 414, - "slack": 207 - }, - "litellm/proxy/guardrails/usage_tracking.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/proxy/health_check.py": { - "baseline": 302, - "slack": 151 - }, - "litellm/proxy/health_check_utils/shared_health_check_manager.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/proxy/health_endpoints/_health_endpoints.py": { - "baseline": 676, - "slack": 338 - }, - "litellm/proxy/hooks/azure_content_safety.py": { - "baseline": 86, - "slack": 43 - }, - "litellm/proxy/hooks/batch_rate_limiter.py": { - "baseline": 99, - "slack": 50 - }, - "litellm/proxy/hooks/batch_redis_get.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/hooks/cache_control_check.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/hooks/dynamic_rate_limiter.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/proxy/hooks/dynamic_rate_limiter_v3.py": { - "baseline": 106, - "slack": 53 - }, - "litellm/proxy/hooks/key_management_event_hooks.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/proxy/hooks/litellm_skills/main.py": { - "baseline": 383, - "slack": 192 - }, - "litellm/proxy/hooks/max_budget_limiter.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/hooks/max_budget_per_session_limiter.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/hooks/max_iterations_limiter.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/hooks/mcp_semantic_filter/hook.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/proxy/hooks/model_max_budget_limiter.py": { - "baseline": 113, - "slack": 57 - }, - "litellm/proxy/hooks/parallel_request_limiter.py": { - "baseline": 408, - "slack": 204 - }, - "litellm/proxy/hooks/parallel_request_limiter_v3.py": { - "baseline": 593, - "slack": 297 - }, - "litellm/proxy/hooks/prompt_injection_detection.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/proxy/hooks/proxy_track_cost_callback.py": { - "baseline": 193, - "slack": 97 - }, - "litellm/proxy/hooks/rate_limiter_utils.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/hooks/responses_id_security.py": { - "baseline": 57, - "slack": 29 - }, - "litellm/proxy/hooks/sensitive_data_routing.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/hooks/user_management_event_hooks.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/proxy/image_endpoints/endpoints.py": { - "baseline": 118, - "slack": 59 - }, - "litellm/proxy/lambda.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/litellm_pre_call_utils.py": { - "baseline": 911, - "slack": 456 - }, - "litellm/proxy/management_endpoints/access_group_endpoints.py": { - "baseline": 256, - "slack": 128 - }, - "litellm/proxy/management_endpoints/budget_management_endpoints.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/management_endpoints/cache_settings_endpoints.py": { - "baseline": 153, - "slack": 77 - }, - "litellm/proxy/management_endpoints/callback_management_endpoints.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/proxy/management_endpoints/common_daily_activity.py": { - "baseline": 446, - "slack": 223 - }, - "litellm/proxy/management_endpoints/common_utils.py": { - "baseline": 146, - "slack": 73 - }, - "litellm/proxy/management_endpoints/compliance_endpoints.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/proxy/management_endpoints/config_override_endpoints.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/management_endpoints/cost_tracking_settings.py": { - "baseline": 104, - "slack": 52 - }, - "litellm/proxy/management_endpoints/customer_endpoints.py": { - "baseline": 195, - "slack": 98 - }, - "litellm/proxy/management_endpoints/fallback_management_endpoints.py": { - "baseline": 50, - "slack": 25 - }, - "litellm/proxy/management_endpoints/internal_user_endpoints.py": { - "baseline": 707, - "slack": 354 - }, - "litellm/proxy/management_endpoints/jwt_key_mapping_endpoints.py": { - "baseline": 83, - "slack": 42 - }, - "litellm/proxy/management_endpoints/key_management_endpoints.py": { - "baseline": 1499, - "slack": 750 - }, - "litellm/proxy/management_endpoints/mcp_management_endpoints.py": { - "baseline": 564, - "slack": 282 - }, - "litellm/proxy/management_endpoints/model_access_group_management_endpoints.py": { - "baseline": 154, - "slack": 77 - }, - "litellm/proxy/management_endpoints/model_management_endpoints.py": { - "baseline": 367, - "slack": 184 - }, - "litellm/proxy/management_endpoints/organization_endpoints.py": { - "baseline": 308, - "slack": 154 - }, - "litellm/proxy/management_endpoints/policy_endpoints/ai_policy_suggester.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/management_endpoints/policy_endpoints/endpoints.py": { - "baseline": 260, - "slack": 130 - }, - "litellm/proxy/management_endpoints/router_settings_endpoints.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/proxy/management_endpoints/scim/scim_transformations.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/proxy/management_endpoints/scim/scim_v2.py": { - "baseline": 600, - "slack": 300 - }, - "litellm/proxy/management_endpoints/sso/custom_microsoft_sso.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/management_endpoints/sso_helper_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/management_endpoints/tag_management_endpoints.py": { - "baseline": 198, - "slack": 99 - }, - "litellm/proxy/management_endpoints/team_callback_endpoints.py": { - "baseline": 122, - "slack": 61 - }, - "litellm/proxy/management_endpoints/team_endpoints.py": { - "baseline": 1179, - "slack": 590 - }, - "litellm/proxy/management_endpoints/tool_management_endpoints.py": { - "baseline": 157, - "slack": 79 - }, - "litellm/proxy/management_endpoints/types.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/proxy/management_endpoints/ui_sso.py": { - "baseline": 985, - "slack": 493 - }, - "litellm/proxy/management_endpoints/usage_endpoints/ai_usage_chat.py": { - "baseline": 141, - "slack": 71 - }, - "litellm/proxy/management_endpoints/usage_endpoints/endpoints.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/proxy/management_endpoints/user_agent_analytics_endpoints.py": { - "baseline": 156, - "slack": 78 - }, - "litellm/proxy/management_endpoints/workflow_management_endpoints.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/proxy/management_helpers/audit_logs.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/proxy/management_helpers/object_permission_utils.py": { - "baseline": 140, - "slack": 70 - }, - "litellm/proxy/management_helpers/team_member_permission_checks.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/management_helpers/user_invitation.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/proxy/management_helpers/utils.py": { - "baseline": 276, - "slack": 138 - }, - "litellm/proxy/mcp_tools.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/proxy/memory/memory_endpoints.py": { - "baseline": 172, - "slack": 86 - }, - "litellm/proxy/middleware/in_flight_requests_middleware.py": { - "baseline": 15, - "slack": 8 - }, - "litellm/proxy/middleware/prometheus_auth_middleware.py": { - "baseline": 26, - "slack": 13 - }, - "litellm/proxy/middleware/request_size_limit_middleware.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/ocr_endpoints/endpoints.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/proxy/openai_evals_endpoints/endpoints.py": { - "baseline": 256, - "slack": 128 - }, - "litellm/proxy/openai_files_endpoints/common_utils.py": { - "baseline": 201, - "slack": 101 - }, - "litellm/proxy/openai_files_endpoints/file_content_streaming_handler.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/openai_files_endpoints/files_endpoints.py": { - "baseline": 405, - "slack": 203 - }, - "litellm/proxy/openai_files_endpoints/storage_backend_service.py": { - "baseline": 20, - "slack": 10 - }, - "litellm/proxy/pass_through_endpoints/jsonpath_extractor.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py": { - "baseline": 361, - "slack": 181 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/anthropic_passthrough_logging_handler.py": { - "baseline": 164, - "slack": 82 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/assembly_passthrough_logging_handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/base_passthrough_logging_handler.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/cohere_passthrough_logging_handler.py": { - "baseline": 39, - "slack": 20 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/cursor_passthrough_logging_handler.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/gemini_passthrough_logging_handler.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/openai_passthrough_logging_handler.py": { - "baseline": 114, - "slack": 57 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_ai_live_passthrough_logging_handler.py": { - "baseline": 141, - "slack": 71 - }, - "litellm/proxy/pass_through_endpoints/llm_provider_handlers/vertex_passthrough_logging_handler.py": { - "baseline": 163, - "slack": 82 - }, - "litellm/proxy/pass_through_endpoints/managed_id_rewriter.py": { - "baseline": 302, - "slack": 151 - }, - "litellm/proxy/pass_through_endpoints/pass_through_endpoints.py": { - "baseline": 894, - "slack": 447 - }, - "litellm/proxy/pass_through_endpoints/passthrough_endpoint_router.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/proxy/pass_through_endpoints/passthrough_guardrails.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy/pass_through_endpoints/streaming_handler.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/proxy/pass_through_endpoints/success_handler.py": { - "baseline": 112, - "slack": 56 - }, - "litellm/proxy/policy_engine/attachment_registry.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/proxy/policy_engine/init_policies.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/proxy/policy_engine/pipeline_executor.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/proxy/policy_engine/policy_endpoints.py": { - "baseline": 65, - "slack": 33 - }, - "litellm/proxy/policy_engine/policy_registry.py": { - "baseline": 255, - "slack": 128 - }, - "litellm/proxy/policy_engine/policy_resolve_endpoints.py": { - "baseline": 185, - "slack": 93 - }, - "litellm/proxy/policy_engine/policy_validator.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/proxy/post_call_rules.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/proxy/prisma_migration.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/proxy/prometheus_cleanup.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/proxy/prompts/init_prompts.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/prompts/prompt_endpoints.py": { - "baseline": 176, - "slack": 88 - }, - "litellm/proxy/prompts/prompt_registry.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/proxy/proxy_cli.py": { - "baseline": 308, - "slack": 154 - }, - "litellm/proxy/proxy_server.py": { - "baseline": 5015, - "slack": 2508 - }, - "litellm/proxy/public_endpoints/public_endpoints.py": { - "baseline": 165, - "slack": 83 - }, - "litellm/proxy/rag_endpoints/endpoints.py": { - "baseline": 244, - "slack": 122 - }, - "litellm/proxy/realtime_endpoints/endpoints.py": { - "baseline": 237, - "slack": 119 - }, - "litellm/proxy/rerank_endpoints/endpoints.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/proxy/response_api_endpoints/endpoints.py": { - "baseline": 302, - "slack": 151 - }, - "litellm/proxy/response_polling/background_streaming.py": { - "baseline": 155, - "slack": 78 - }, - "litellm/proxy/response_polling/polling_handler.py": { - "baseline": 81, - "slack": 41 - }, - "litellm/proxy/route_llm_request.py": { - "baseline": 137, - "slack": 69 - }, - "litellm/proxy/search_endpoints/endpoints.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/proxy/search_endpoints/search_tool_management.py": { - "baseline": 85, - "slack": 43 - }, - "litellm/proxy/search_endpoints/search_tool_registry.py": { - "baseline": 53, - "slack": 27 - }, - "litellm/proxy/spend_tracking/budget_reservation.py": { - "baseline": 222, - "slack": 111 - }, - "litellm/proxy/spend_tracking/cloudzero_endpoints.py": { - "baseline": 120, - "slack": 60 - }, - "litellm/proxy/spend_tracking/cold_storage_handler.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/spend_tracking/spend_log_error_logger.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/proxy/spend_tracking/spend_management_endpoints.py": { - "baseline": 974, - "slack": 487 - }, - "litellm/proxy/spend_tracking/spend_tracking_utils.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/proxy/spend_tracking/vantage_endpoints.py": { - "baseline": 176, - "slack": 88 - }, - "litellm/proxy/types_utils/utils.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py": { - "baseline": 477, - "slack": 239 - }, - "litellm/proxy/utils.py": { - "baseline": 1683, - "slack": 842 - }, - "litellm/proxy/vector_store_endpoints/endpoints.py": { - "baseline": 161, - "slack": 81 - }, - "litellm/proxy/vector_store_endpoints/management_endpoints.py": { - "baseline": 234, - "slack": 117 - }, - "litellm/proxy/vector_store_endpoints/utils.py": { - "baseline": 32, - "slack": 16 - }, - "litellm/proxy/vector_store_files_endpoints/endpoints.py": { - "baseline": 274, - "slack": 137 - }, - "litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/proxy/video_endpoints/endpoints.py": { - "baseline": 229, - "slack": 115 - }, - "litellm/proxy/video_endpoints/utils.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/proxy_auth/credentials.py": { - "baseline": 14, - "slack": 7 - }, - "litellm/rag/__init__.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/rag/ingestion/base_ingestion.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/rag/ingestion/bedrock_ingestion.py": { - "baseline": 267, - "slack": 134 - }, - "litellm/rag/ingestion/file_parsers/pdf_parser.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/rag/ingestion/gemini_ingestion.py": { - "baseline": 60, - "slack": 30 - }, - "litellm/rag/ingestion/openai_ingestion.py": { - "baseline": 31, - "slack": 16 - }, - "litellm/rag/ingestion/s3_vectors_ingestion.py": { - "baseline": 250, - "slack": 125 - }, - "litellm/rag/ingestion/vertex_ai_ingestion.py": { - "baseline": 130, - "slack": 65 - }, - "litellm/rag/main.py": { - "baseline": 102, - "slack": 51 - }, - "litellm/rag/rag_query.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/realtime_api/main.py": { - "baseline": 160, - "slack": 80 - }, - "litellm/repositories/base_repository.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/repositories/budget_repository.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/repositories/config_repository.py": { - "baseline": 89, - "slack": 45 - }, - "litellm/repositories/credentials_repository.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/repositories/model_repository.py": { - "baseline": 72, - "slack": 36 - }, - "litellm/repositories/object_permission_repository.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/repositories/organization_repository.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/repositories/project_repository.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/repositories/table_repositories.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/repositories/team_repository.py": { - "baseline": 149, - "slack": 75 - }, - "litellm/repositories/user_repository.py": { - "baseline": 73, - "slack": 37 - }, - "litellm/repositories/verification_token_repository.py": { - "baseline": 108, - "slack": 54 - }, - "litellm/rerank_api/main.py": { - "baseline": 129, - "slack": 65 - }, - "litellm/rerank_api/rerank_utils.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/responses/file_search/emulated_handler.py": { - "baseline": 279, - "slack": 140 - }, - "litellm/responses/litellm_completion_transformation/handler.py": { - "baseline": 27, - "slack": 14 - }, - "litellm/responses/litellm_completion_transformation/session_handler.py": { - "baseline": 42, - "slack": 21 - }, - "litellm/responses/litellm_completion_transformation/streaming_iterator.py": { - "baseline": 151, - "slack": 76 - }, - "litellm/responses/litellm_completion_transformation/transformation.py": { - "baseline": 552, - "slack": 276 - }, - "litellm/responses/main.py": { - "baseline": 548, - "slack": 274 - }, - "litellm/responses/mcp/chat_completions_handler.py": { - "baseline": 367, - "slack": 184 - }, - "litellm/responses/mcp/litellm_proxy_mcp_handler.py": { - "baseline": 445, - "slack": 223 - }, - "litellm/responses/mcp/mcp_streaming_iterator.py": { - "baseline": 193, - "slack": 97 - }, - "litellm/responses/sse_output_recovery.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/responses/streaming_iterator.py": { - "baseline": 977, - "slack": 489 - }, - "litellm/responses/utils.py": { - "baseline": 294, - "slack": 147 - }, - "litellm/router.py": { - "baseline": 4303, - "slack": 2152 - }, - "litellm/router_strategy/adaptive_router/adaptive_router.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/router_strategy/adaptive_router/bandit.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/router_strategy/adaptive_router/hooks.py": { - "baseline": 142, - "slack": 71 - }, - "litellm/router_strategy/adaptive_router/signals.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/router_strategy/adaptive_router/update_queue.py": { - "baseline": 30, - "slack": 15 - }, - "litellm/router_strategy/auto_router/auto_router.py": { - "baseline": 44, - "slack": 22 - }, - "litellm/router_strategy/auto_router/litellm_encoder.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/router_strategy/base_routing_strategy.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/router_strategy/budget_limiter.py": { - "baseline": 342, - "slack": 171 - }, - "litellm/router_strategy/complexity_router/complexity_router.py": { - "baseline": 59, - "slack": 30 - }, - "litellm/router_strategy/complexity_router/evals/eval_complexity_router.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/router_strategy/least_busy.py": { - "baseline": 155, - "slack": 78 - }, - "litellm/router_strategy/lowest_cost.py": { - "baseline": 211, - "slack": 106 - }, - "litellm/router_strategy/lowest_latency.py": { - "baseline": 404, - "slack": 202 - }, - "litellm/router_strategy/lowest_tpm_rpm.py": { - "baseline": 168, - "slack": 84 - }, - "litellm/router_strategy/lowest_tpm_rpm_v2.py": { - "baseline": 351, - "slack": 176 - }, - "litellm/router_strategy/quality_router/config.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/router_strategy/quality_router/quality_router.py": { - "baseline": 76, - "slack": 38 - }, - "litellm/router_strategy/simple_shuffle.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/router_strategy/tag_based_routing.py": { - "baseline": 80, - "slack": 40 - }, - "litellm/router_utils/add_retry_fallback_headers.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/router_utils/batch_utils.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/router_utils/client_initalization_utils.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/router_utils/clientside_credential_handler.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/router_utils/common_utils.py": { - "baseline": 63, - "slack": 32 - }, - "litellm/router_utils/cooldown_cache.py": { - "baseline": 38, - "slack": 19 - }, - "litellm/router_utils/cooldown_callbacks.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/router_utils/cooldown_handlers.py": { - "baseline": 24, - "slack": 12 - }, - "litellm/router_utils/fallback_event_handlers.py": { - "baseline": 54, - "slack": 27 - }, - "litellm/router_utils/get_retry_from_policy.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/router_utils/handle_error.py": { - "baseline": 21, - "slack": 11 - }, - "litellm/router_utils/health_state_cache.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/router_utils/pattern_match_deployments.py": { - "baseline": 41, - "slack": 21 - }, - "litellm/router_utils/pre_call_checks/deployment_affinity_check.py": { - "baseline": 111, - "slack": 56 - }, - "litellm/router_utils/pre_call_checks/encrypted_content_affinity_check.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/router_utils/pre_call_checks/model_rate_limit_check.py": { - "baseline": 133, - "slack": 67 - }, - "litellm/router_utils/pre_call_checks/prompt_caching_deployment_check.py": { - "baseline": 34, - "slack": 17 - }, - "litellm/router_utils/pre_call_checks/responses_api_deployment_check.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/router_utils/prompt_caching_cache.py": { - "baseline": 43, - "slack": 22 - }, - "litellm/router_utils/router_callbacks/track_deployment_metrics.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/router_utils/search_api_router.py": { - "baseline": 61, - "slack": 31 - }, - "litellm/scheduler.py": { - "baseline": 47, - "slack": 24 - }, - "litellm/search/cost_calculator.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/search/main.py": { - "baseline": 55, - "slack": 28 - }, - "litellm/secret_managers/aws_secret_manager.py": { - "baseline": 28, - "slack": 14 - }, - "litellm/secret_managers/aws_secret_manager_v2.py": { - "baseline": 122, - "slack": 61 - }, - "litellm/secret_managers/base_secret_manager.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/secret_managers/custom_secret_manager_loader.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/secret_managers/cyberark_secret_manager.py": { - "baseline": 84, - "slack": 42 - }, - "litellm/secret_managers/get_azure_ad_token_provider.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/secret_managers/google_kms.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/secret_managers/google_secret_manager.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/secret_managers/hashicorp_secret_manager.py": { - "baseline": 220, - "slack": 110 - }, - "litellm/secret_managers/main.py": { - "baseline": 37, - "slack": 19 - }, - "litellm/secret_managers/secret_manager_handler.py": { - "baseline": 45, - "slack": 23 - }, - "litellm/setup_wizard.py": { - "baseline": 109, - "slack": 55 - }, - "litellm/skills/main.py": { - "baseline": 207, - "slack": 104 - }, - "litellm/timeout.py": { - "baseline": 70, - "slack": 35 - }, - "litellm/types/agents.py": { - "baseline": 23, - "slack": 12 - }, - "litellm/types/caching.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/completion.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/containers/main.py": { - "baseline": 62, - "slack": 31 - }, - "litellm/types/files.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/types/google_genai/main.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/types/guardrails.py": { - "baseline": 48, - "slack": 24 - }, - "litellm/types/integrations/anthropic_cache_control_hook.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/integrations/arize.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/integrations/custom_logger.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/integrations/datadog.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/integrations/langfuse_otel.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/integrations/prometheus.py": { - "baseline": 51, - "slack": 26 - }, - "litellm/types/interactions/generated.py": { - "baseline": 69, - "slack": 35 - }, - "litellm/types/llms/anthropic.py": { - "baseline": 11, - "slack": 6 - }, - "litellm/types/llms/anthropic_messages/anthropic_response.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/llms/azure_ai.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/llms/base.py": { - "baseline": 29, - "slack": 15 - }, - "litellm/types/llms/bedrock.py": { - "baseline": 36, - "slack": 18 - }, - "litellm/types/llms/custom_http.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/llms/databricks.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/llms/gemini.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/types/llms/oci.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/llms/openai.py": { - "baseline": 132, - "slack": 66 - }, - "litellm/types/llms/openai_evals.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/llms/vertex_ai.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/types/management_endpoints/router_settings_endpoints.py": { - "baseline": 18, - "slack": 9 - }, - "litellm/types/mcp.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/memory_management.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/prompts/init_prompts.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/proxy/claude_code_endpoints.py": { - "baseline": 13, - "slack": 7 - }, - "litellm/types/proxy/cloudzero_endpoints.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/azure/azure_text_moderation.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/base.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/block_code_execution.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/cisco_ai_defense.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/generic_guardrail_api.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/litellm_content_filter.py": { - "baseline": 7, - "slack": 4 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/tool_permission.py": { - "baseline": 22, - "slack": 11 - }, - "litellm/types/proxy/guardrails/guardrail_hooks/xecguard.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/litellm_pre_call_utils.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/proxy/management_endpoints/common_daily_activity.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/proxy/management_endpoints/config_overrides.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/proxy/management_endpoints/internal_user_endpoints.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/types/proxy/management_endpoints/scim_v2.py": { - "baseline": 25, - "slack": 13 - }, - "litellm/types/proxy/management_endpoints/ui_sso.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/proxy/policy_engine/pipeline_types.py": { - "baseline": 3, - "slack": 2 - }, - "litellm/types/proxy/policy_engine/policy_types.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/proxy/policy_engine/resolver_types.py": { - "baseline": 16, - "slack": 8 - }, - "litellm/types/proxy/policy_engine/validation_types.py": { - "baseline": 5, - "slack": 3 - }, - "litellm/types/proxy/vantage_endpoints.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/types/rag.py": { - "baseline": 1, - "slack": 1 - }, - "litellm/types/realtime.py": { - "baseline": 4, - "slack": 2 - }, - "litellm/types/rerank.py": { - "baseline": 8, - "slack": 4 - }, - "litellm/types/responses/main.py": { - "baseline": 12, - "slack": 6 - }, - "litellm/types/router.py": { - "baseline": 103, - "slack": 52 - }, - "litellm/types/services.py": { - "baseline": 10, - "slack": 5 - }, - "litellm/types/tool_management.py": { - "baseline": 6, - "slack": 3 - }, - "litellm/types/utils.py": { - "baseline": 637, - "slack": 319 - }, - "litellm/types/vector_stores.py": { - "baseline": 9, - "slack": 5 - }, - "litellm/types/videos/main.py": { - "baseline": 33, - "slack": 17 - }, - "litellm/types/videos/utils.py": { - "baseline": 2, - "slack": 1 - }, - "litellm/utils.py": { - "baseline": 3362, - "slack": 1681 - }, - "litellm/vector_store_files/main.py": { - "baseline": 232, - "slack": 116 - }, - "litellm/vector_store_files/utils.py": { - "baseline": 17, - "slack": 9 - }, - "litellm/vector_stores/main.py": { - "baseline": 260, - "slack": 130 - }, - "litellm/vector_stores/utils.py": { - "baseline": 19, - "slack": 10 - }, - "litellm/vector_stores/vector_store_registry.py": { - "baseline": 92, - "slack": 46 - }, - "litellm/videos/main.py": { - "baseline": 479, - "slack": 240 - }, - "litellm/videos/utils.py": { - "baseline": 54, - "slack": 27 - } -} diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 7ece944fd0e..d241c501797 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -5455,21 +5455,19 @@ class StandardLoggingPayloadSetup: error_information = StandardLoggingPayloadSetup.get_error_information( original_exception=original_exception, ) - if not metadata.get("client_disconnected"): # any-ok: untyped metadata + if not metadata.get("client_disconnected"): return error_information, error_str - client_disconnect_error = metadata.get( # any-ok: untyped metadata - "error_information" - ) - if isinstance(client_disconnect_error, dict): # any-ok: untyped metadata + client_disconnect_error = metadata.get("error_information") + if isinstance(client_disconnect_error, dict): error_information = cast( StandardLoggingPayloadErrorInformation, - client_disconnect_error, # any-ok: untyped metadata + client_disconnect_error, ) else: error_information = cast( StandardLoggingPayloadErrorInformation, - { # any-ok: untyped metadata + { "error_code": "499", "error_message": "Client disconnected the request", "error_class": "ClientDisconnected", @@ -5808,7 +5806,7 @@ def get_standard_logging_object_payload( error_information, error_str = ( StandardLoggingPayloadSetup.get_error_information_for_logging_payload( - metadata=metadata, # any-ok: untyped metadata + metadata=metadata, original_exception=original_exception, error_str=error_str, ) diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 658dbc27145..11b29b78f56 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -2203,7 +2203,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): inference_geo = _usage["inference_geo"] service_tier = cast( str | None, - _usage.get("service_tier"), # any-ok: untyped usage dict + _usage.get("service_tier"), ) iterations: list[Any] | None = _usage.get("iterations") diff --git a/litellm/llms/hosted_vllm/chat/transformation.py b/litellm/llms/hosted_vllm/chat/transformation.py index 40906e83a9d..7c42e6a9a00 100644 --- a/litellm/llms/hosted_vllm/chat/transformation.py +++ b/litellm/llms/hosted_vllm/chat/transformation.py @@ -205,53 +205,40 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): tool_calls: list[ChatCompletionAssistantToolCall] = [] content_blocks: list[object] = [] has_structured_content = False - for c in existing_content: # any-ok: untyped content - if ( - isinstance(c, dict) # any-ok: untyped content - and c.get("type") == "text" # any-ok: untyped content - ): - text_parts.append( # any-ok: untyped content - c.get("text", "") # any-ok: untyped content - ) - content_blocks.append(c) # any-ok: untyped content - elif ( - isinstance(c, dict) # any-ok: untyped content - and c.get("type") == "tool_use" # any-ok: untyped content - ): - tool_input = c.get("input", {}) # any-ok: untyped content + for c in existing_content: + if isinstance(c, dict) and c.get("type") == "text": + text_parts.append(c.get("text", "")) + content_blocks.append(c) + elif isinstance(c, dict) and c.get("type") == "tool_use": + tool_input = c.get("input", {}) tool_calls.append( ChatCompletionAssistantToolCall( - id=c.get("id"), # any-ok: untyped content + id=c.get("id"), type="function", function=ChatCompletionToolCallFunctionChunk( - name=c.get("name"), # any-ok: untyped content + name=c.get("name"), arguments=( tool_input if isinstance( - tool_input, # any-ok: untyped content - str, # any-ok: untyped content - ) - else json.dumps( - tool_input # any-ok: untyped content + tool_input, + str, ) + else json.dumps(tool_input) ), ), ) ) else: - content_blocks.append(c) # any-ok: untyped content + content_blocks.append(c) has_structured_content = True if tool_calls: existing_tool_calls = message.get("tool_calls") if isinstance(existing_tool_calls, list): existing_tool_call_ids = { - tool_call.get("id") # any-ok: untyped content + tool_call.get("id") for tool_call in existing_tool_calls - if isinstance( - tool_call, dict - ) # any-ok: untyped content - and tool_call.get("id") - is not None # any-ok: untyped content + if isinstance(tool_call, dict) + and tool_call.get("id") is not None } new_tool_calls = [ tool_call @@ -264,7 +251,7 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): ) else: message["tool_calls"] = tool_calls - content_str = "\n".join(text_parts) # any-ok: untyped content + content_str = "\n".join(text_parts) new_content = ( content_blocks if has_structured_content else content_str ) diff --git a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py index ba60267fb4d..97ed0e46e57 100644 --- a/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py +++ b/litellm/llms/vertex_ai/gemini/vertex_and_google_ai_studio_gemini.py @@ -252,9 +252,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if isinstance(response_format, dict): return response_format - if isinstance(response_format, type) and issubclass( - response_format, _BaseModel - ): + if isinstance(response_format, type) and issubclass(response_format, _BaseModel): schema = response_format.model_json_schema() return { "type": "json_schema", @@ -287,9 +285,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return False @staticmethod - def _forward_gemini_function_call_id( - model: str, custom_llm_provider: str | None = None - ) -> bool: + def _forward_gemini_function_call_id(model: str, custom_llm_provider: str | None = None) -> bool: """ Whether to include `id` on function_call / function_response parts. @@ -344,9 +340,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): supported_params.append("thinking") return supported_params - def map_tool_choice_values( - self, model: str, tool_choice: Union[str, dict] - ) -> ToolConfig | None: + def map_tool_choice_values(self, model: str, tool_choice: Union[str, dict]) -> ToolConfig | None: if tool_choice == "none": return ToolConfig(functionCallingConfig=FunctionCallingConfig(mode="NONE")) elif tool_choice == "required": @@ -356,11 +350,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif isinstance(tool_choice, dict): # only supported for anthropic + mistral models - https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ToolChoice.html name = tool_choice.get("function", {}).get("name", "") - return ToolConfig( - functionCallingConfig=FunctionCallingConfig( - mode="ANY", allowed_function_names=[name] - ) - ) + return ToolConfig(functionCallingConfig=FunctionCallingConfig(mode="ANY", allowed_function_names=[name])) else: raise litellm.utils.UnsupportedParamsError( message="VertexAI doesn't support tool_choice={}. Supported tool_choice values=['auto', 'required', json object]. To drop it from the call, set `litellm.drop_params = True.".format( @@ -408,16 +398,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return search_tool_keys = cls._search_tool_keys() - has_function_declarations = any( - isinstance(tool, dict) and tool.get("function_declarations") - for tool in tools - ) + has_function_declarations = any(isinstance(tool, dict) and tool.get("function_declarations") for tool in tools) if not has_function_declarations: return has_search_tools = any( - isinstance(tool, dict) and any(key in tool for key in search_tool_keys) - for tool in tools + isinstance(tool, dict) and any(key in tool for key in search_tool_keys) for tool in tools ) if not has_search_tools: return @@ -430,11 +416,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "send a request without function calling tools." ) optional_params["tools"] = [ - tool - for tool in tools - if not ( - isinstance(tool, dict) and any(key in tool for key in search_tool_keys) - ) + tool for tool in tools if not (isinstance(tool, dict) and any(key in tool for key in search_tool_keys)) ] def _map_service_tier_param(self, value: str, optional_params: dict) -> None: @@ -478,19 +460,13 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # Transform excluded_predefined_functions to camelCase if "excluded_predefined_functions" in computer_use_config: - transformed_config["excludedPredefinedFunctions"] = computer_use_config[ - "excluded_predefined_functions" - ] + transformed_config["excludedPredefinedFunctions"] = computer_use_config["excluded_predefined_functions"] elif "excludedPredefinedFunctions" in computer_use_config: - transformed_config["excludedPredefinedFunctions"] = computer_use_config[ - "excludedPredefinedFunctions" - ] + transformed_config["excludedPredefinedFunctions"] = computer_use_config["excludedPredefinedFunctions"] return transformed_config - def _extract_google_maps_retrieval_config( - self, google_maps_config: dict - ) -> tuple[dict, dict | None]: + def _extract_google_maps_retrieval_config(self, google_maps_config: dict) -> tuple[dict, dict | None]: """ Extract location configuration from googleMaps tool for Vertex AI toolConfig. @@ -523,9 +499,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # Remove location fields from tool definition cleaned_config = { - k: v - for k, v in google_maps_config.items() - if k not in ["latitude", "longitude", "languageCode"] + k: v for k, v in google_maps_config.items() if k not in ["latitude", "longitude", "languageCode"] } return cleaned_config, retrieval_config @@ -542,9 +516,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): Optional[dict]: The tool value if found, None otherwise """ # Convert camelCase to underscore_case - underscore_name = "".join( - ["_" + c.lower() if c.isupper() else c for c in tool_name] - ).lstrip("_") + underscore_name = "".join(["_" + c.lower() if c.isupper() else c for c in tool_name]).lstrip("_") # Try both camelCase and underscore_case variants if tool.get(tool_name) is not None: @@ -588,14 +560,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): urlContext, ] ) - server_side_tool_invocations = optional_params.get( - "include_server_side_tool_invocations", False - ) - if ( - gtool_func_declarations - and has_search_tools - and not server_side_tool_invocations - ): + server_side_tool_invocations = optional_params.get("include_server_side_tool_invocations", False) + if gtool_func_declarations and has_search_tools and not server_side_tool_invocations: verbose_logger.warning( "Vertex AI does not support mixing function declarations with " "search tools (googleSearch, enterpriseWebSearch, urlContext, " @@ -651,9 +617,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): and _openai_function_object["parameters"] is not None and isinstance(_openai_function_object["parameters"], dict) ): # OPENAI accepts JSON Schema, Google accepts OpenAPI schema. - _openai_function_object["parameters"] = _build_vertex_schema( - _openai_function_object["parameters"] - ) + _openai_function_object["parameters"] = _build_vertex_schema(_openai_function_object["parameters"]) openai_function_object = _openai_function_object @@ -669,68 +633,43 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): "web_search", "web_search_preview", ): - verbose_logger.info( - f"Gemini: Transforming OpenAI-style '{tool['type']}' tool to googleSearch" - ) + verbose_logger.info(f"Gemini: Transforming OpenAI-style '{tool['type']}' tool to googleSearch") tool = {VertexToolName.GOOGLE_SEARCH.value: {}} # Handle tools with 'type' field (OpenAI spec compliance) Ignore this field -> https://github.com/BerriAI/litellm/issues/14644#issuecomment-3342061838 elif "type" in tool: tool = {k: tool[k] for k in tool if k != "type"} tool_name = list(tool.keys())[0] if len(tool.keys()) == 1 else None if tool_name and ( - tool_name == "codeExecution" - or tool_name == VertexToolName.CODE_EXECUTION.value + tool_name == "codeExecution" or tool_name == VertexToolName.CODE_EXECUTION.value ): # code_execution maintained for backwards compatibility code_execution = self.get_tool_value(tool, "codeExecution") - elif tool_name and ( - tool_name == VertexToolName.GOOGLE_SEARCH.value - or tool_name == "google_search" - ): + elif tool_name and (tool_name == VertexToolName.GOOGLE_SEARCH.value or tool_name == "google_search"): googleSearch = self.get_tool_value(tool, tool_name) elif tool_name and ( - tool_name == VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value - or tool_name == "google_search_retrieval" + tool_name == VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value or tool_name == "google_search_retrieval" ): googleSearchRetrieval = self.get_tool_value(tool, tool_name) elif tool_name and ( - tool_name == VertexToolName.ENTERPRISE_WEB_SEARCH.value - or tool_name == "enterprise_web_search" + tool_name == VertexToolName.ENTERPRISE_WEB_SEARCH.value or tool_name == "enterprise_web_search" ): enterpriseWebSearch = self.get_tool_value(tool, tool_name) - elif tool_name and ( - tool_name == VertexToolName.URL_CONTEXT.value - or tool_name == "urlContext" - ): + elif tool_name and (tool_name == VertexToolName.URL_CONTEXT.value or tool_name == "urlContext"): urlContext = self.get_tool_value(tool, tool_name) - elif tool_name and ( - tool_name == VertexToolName.GOOGLE_MAPS.value - or tool_name == "google_maps" - ): - google_maps_value = self.get_tool_value( - tool, VertexToolName.GOOGLE_MAPS.value - ) + elif tool_name and (tool_name == VertexToolName.GOOGLE_MAPS.value or tool_name == "google_maps"): + google_maps_value = self.get_tool_value(tool, VertexToolName.GOOGLE_MAPS.value) # Extract and transform location configuration for toolConfig if google_maps_value is not None: ( googleMaps, google_maps_retrieval_config, - ) = self._extract_google_maps_retrieval_config( - google_maps_config=google_maps_value - ) - elif tool_name and ( - tool_name == VertexToolName.COMPUTER_USE.value - or tool_name == "computer_use" - ): - computer_use_value = self.get_tool_value( - tool, VertexToolName.COMPUTER_USE.value - ) + ) = self._extract_google_maps_retrieval_config(google_maps_config=google_maps_value) + elif tool_name and (tool_name == VertexToolName.COMPUTER_USE.value or tool_name == "computer_use"): + computer_use_value = self.get_tool_value(tool, VertexToolName.COMPUTER_USE.value) # Transform Computer Use configuration to Gemini API format if computer_use_value is not None: - computerUse = self._transform_computer_use_config( - computer_use_config=computer_use_value - ) + computerUse = self._transform_computer_use_config(computer_use_config=computer_use_value) else: # Empty config - Gemini will use defaults computerUse = {} @@ -786,15 +725,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): _tools_list.append(search_tool) if googleSearchRetrieval is not None: retrieval_tool = Tools() - retrieval_tool[VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value] = ( - googleSearchRetrieval - ) + retrieval_tool[VertexToolName.GOOGLE_SEARCH_RETRIEVAL.value] = googleSearchRetrieval _tools_list.append(retrieval_tool) if enterpriseWebSearch is not None: enterprise_tool = Tools() - enterprise_tool[VertexToolName.ENTERPRISE_WEB_SEARCH.value] = ( - enterpriseWebSearch - ) + enterprise_tool[VertexToolName.ENTERPRISE_WEB_SEARCH.value] = enterpriseWebSearch _tools_list.append(enterprise_tool) if code_execution is not None: code_tool = Tools() @@ -817,9 +752,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if google_maps_retrieval_config is not None: if "toolConfig" not in optional_params: optional_params["toolConfig"] = {} - optional_params["toolConfig"][ - "retrievalConfig" - ] = google_maps_retrieval_config + optional_params["toolConfig"]["retrievalConfig"] = google_maps_retrieval_config return _tools_list @@ -828,19 +761,13 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if isinstance(old_schema, list): for item in old_schema: if isinstance(item, dict): - item = _build_vertex_schema( - parameters=item, add_property_ordering=True - ) + item = _build_vertex_schema(parameters=item, add_property_ordering=True) elif isinstance(old_schema, dict): - old_schema = _build_vertex_schema( - parameters=old_schema, add_property_ordering=True - ) + old_schema = _build_vertex_schema(parameters=old_schema, add_property_ordering=True) return old_schema - def apply_response_schema_transformation( - self, value: dict, optional_params: dict, model: str - ): + def apply_response_schema_transformation(self, value: dict, optional_params: dict, model: str): new_value = deepcopy(value) # remove 'strict' from json schema (not supported by Gemini) new_value = _remove_strict_from_schema(new_value) @@ -876,17 +803,13 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # - Standard JSON Schema format (lowercase types) # - Supports additionalProperties # - No propertyOrdering needed - optional_params["response_json_schema"] = _build_json_schema( - deepcopy(schema) - ) + optional_params["response_json_schema"] = _build_json_schema(deepcopy(schema)) else: # Use responseSchema (default, backwards compatible) # - OpenAPI-style format (uppercase types) # - No additionalProperties support # - Requires propertyOrdering - optional_params["response_schema"] = self._map_response_schema( - value=schema - ) + optional_params["response_schema"] = self._map_response_schema(value=schema) @staticmethod def _map_reasoning_effort_to_thinking_budget( @@ -901,9 +824,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif model and "gemini-2.5-pro" in model.lower(): budget = DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_PRO elif model and "gemini-2.5-flash" in model.lower(): - budget = ( - DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH - ) + budget = DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET_GEMINI_2_5_FLASH else: budget = DEFAULT_REASONING_EFFORT_MINIMAL_THINKING_BUDGET @@ -956,9 +877,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # Check if this is gemini-3-flash which supports MINIMAL thinking level # Covers gemini-3-flash, gemini-3-flash-preview, gemini-3.1-flash, gemini-3.1-flash-lite-preview, # gemini-3.5-flash, and any future 3.x-flash variants. - is_gemini3flash = model and ( - "flash" in model.lower() and "gemini-3" in model.lower() - ) + is_gemini3flash = model and ("flash" in model.lower() and "gemini-3" in model.lower()) is_gemini31pro = model and ("gemini-3.1-pro-preview" in model.lower()) if reasoning_effort == "minimal": if is_gemini3flash: @@ -1052,20 +971,14 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): params["includeThoughts"] = True # Follow provider defaults unless explicitly opted into legacy behavior. if litellm.enable_gemini_default_thinking_level_low is True: - is_gemini3flash = ( - "gemini-3" in model.lower() and "flash" in model.lower() - ) - params["thinkingLevel"] = ( - "minimal" if is_gemini3flash else "low" - ) + is_gemini3flash = "gemini-3" in model.lower() and "flash" in model.lower() + params["thinkingLevel"] = "minimal" if is_gemini3flash else "low" else: # Thinking disabled params["includeThoughts"] = False else: # For older Gemini models, use thinkingBudget - if thinking_enabled and not VertexGeminiConfig._is_thinking_budget_zero( - thinking_budget - ): + if thinking_enabled and not VertexGeminiConfig._is_thinking_budget_zero(thinking_budget): params["includeThoughts"] = True if thinking_budget is not None and isinstance(thinking_budget, int): params["thinkingBudget"] = thinking_budget @@ -1172,9 +1085,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): model: str, drop_params: bool, ) -> dict: - self._apply_include_server_side_tool_invocations( - non_default_params, optional_params - ) + self._apply_include_server_side_tool_invocations(non_default_params, optional_params) gemini_sampling_params_warned: bool = False for param, value in non_default_params.items(): if param == "temperature": @@ -1195,10 +1106,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): gemini_sampling_params_warned = True optional_params["temperature"] = value elif param == "top_p": - if ( - VertexGeminiConfig._is_gemini_3_or_newer(model) - and not gemini_sampling_params_warned - ): + if VertexGeminiConfig._is_gemini_3_or_newer(model) and not gemini_sampling_params_warned: verbose_logger.warning( "DeprecationWarning: `temperature`, `top_p`, and `top_k` continue to " f"function for Gemini 3+ ({model}) but are planned for removal in a " @@ -1208,10 +1116,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): gemini_sampling_params_warned = True optional_params["top_p"] = value elif param == "top_k": - if ( - VertexGeminiConfig._is_gemini_3_or_newer(model) - and not gemini_sampling_params_warned - ): + if VertexGeminiConfig._is_gemini_3_or_newer(model) and not gemini_sampling_params_warned: verbose_logger.warning( "DeprecationWarning: `temperature`, `top_p`, and `top_k` continue to " f"function for Gemini 3+ ({model}) but are planned for removal in a " @@ -1236,9 +1141,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): elif param == "max_tokens" or param == "max_completion_tokens": optional_params["max_output_tokens"] = value elif param == "response_format" and isinstance(value, dict): # type: ignore - self.apply_response_schema_transformation( - value=value, optional_params=optional_params, model=model - ) + self.apply_response_schema_transformation(value=value, optional_params=optional_params, model=model) elif param == "frequency_penalty": if self._supports_penalty_parameters(model): optional_params["frequency_penalty"] = value @@ -1249,21 +1152,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): optional_params["responseLogprobs"] = value elif param == "top_logprobs": optional_params["logprobs"] = value - elif ( - (param == "tools" or param == "functions") - and isinstance(value, list) - and value - ): + elif (param == "tools" or param == "functions") and isinstance(value, list) and value: # Pass optional_params so _map_function can add toolConfig if needed - mapped_tools = self._map_function( - value=value, optional_params=optional_params - ) - optional_params = self._add_tools_to_optional_params( - optional_params, mapped_tools - ) - elif param == "tool_choice" and ( - isinstance(value, str) or isinstance(value, dict) - ): + mapped_tools = self._map_function(value=value, optional_params=optional_params) + optional_params = self._add_tools_to_optional_params(optional_params, mapped_tools) + elif param == "tool_choice" and (isinstance(value, str) or isinstance(value, dict)): _tool_choice_value = self.map_tool_choice_values( model=model, tool_choice=value, # type: ignore @@ -1271,9 +1164,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if _tool_choice_value is not None: optional_params["tool_choice"] = _tool_choice_value elif param == "parallel_tool_calls": - tools_list = non_default_params.get( - "tools", non_default_params.get("functions") - ) + tools_list = non_default_params.get("tools", non_default_params.get("functions")) num_tools = len(tools_list) if isinstance(tools_list, list) else 0 # Gemini does not support parallel_tool_calls=False with multiple # tools. Drop the param instead of failing — Responses API clients @@ -1299,16 +1190,12 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): param_description="thinking_budget", ) if VertexGeminiConfig._is_gemini_3_or_newer(model): - optional_params["thinkingConfig"] = ( - VertexGeminiConfig._map_reasoning_effort_to_thinking_level( - effort_value, model - ) + optional_params["thinkingConfig"] = VertexGeminiConfig._map_reasoning_effort_to_thinking_level( + effort_value, model ) else: - optional_params["thinkingConfig"] = ( - VertexGeminiConfig._map_reasoning_effort_to_thinking_budget( - effort_value, model - ) + optional_params["thinkingConfig"] = VertexGeminiConfig._map_reasoning_effort_to_thinking_budget( + effort_value, model ) elif param == "thinking": # Validate no conflict with thinking_level @@ -1317,20 +1204,16 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): param_name="thinking", param_description="thinking_budget", ) - optional_params["thinkingConfig"] = ( - VertexGeminiConfig._map_thinking_param( - cast(AnthropicThinkingParam, value), - model=model, - ) + optional_params["thinkingConfig"] = VertexGeminiConfig._map_thinking_param( + cast(AnthropicThinkingParam, value), + model=model, ) elif param == "modalities" and isinstance(value, list): response_modalities = self.map_response_modalities(value) optional_params["responseModalities"] = response_modalities elif param == "web_search_options" and isinstance(value, dict): _tools = self._map_web_search_options(value) - optional_params = self._add_tools_to_optional_params( - optional_params, [_tools] - ) + optional_params = self._add_tools_to_optional_params(optional_params, [_tools]) elif param == "service_tier" and isinstance(value, str): self._map_service_tier_param(value, optional_params) elif param == "include_server_side_tool_invocations" and value is True: @@ -1473,11 +1356,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): """ from litellm.litellm_core_utils.core_helpers import _FINISH_REASON_MAP - return { - k: v - for k, v in _FINISH_REASON_MAP.items() - if k in VertexGeminiConfig._GEMINI_FINISH_REASON_KEYS - } + return {k: v for k, v in _FINISH_REASON_MAP.items() if k in VertexGeminiConfig._GEMINI_FINISH_REASON_KEYS} def translate_exception_str(self, exception_string: str): if ( @@ -1489,9 +1368,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ) return exception_string - def get_assistant_content_message( - self, parts: list[HttpxPartType] - ) -> tuple[str | None, str | None]: + def get_assistant_content_message(self, parts: list[HttpxPartType]) -> tuple[str | None, str | None]: content_str: str | None = None reasoning_content_str: str | None = None @@ -1503,9 +1380,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if text_content.startswith("data:audio") and ";base64," in text_content: try: if is_base64_encoded(text_content): - media_type, _ = text_content.split("data:")[1].split( - ";base64," - ) + media_type, _ = text_content.split("data:")[1].split(";base64,") if media_type.startswith("audio/"): continue except (ValueError, IndexError): @@ -1534,9 +1409,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return content_str, reasoning_content_str - def _extract_thinking_blocks_from_parts( - self, parts: list[HttpxPartType] - ) -> list[ChatCompletionThinkingBlock]: + def _extract_thinking_blocks_from_parts(self, parts: list[HttpxPartType]) -> list[ChatCompletionThinkingBlock]: """Extract thinking blocks from parts if present. Per Google's docs (https://ai.google.dev/gemini-api/docs/thinking): @@ -1559,9 +1432,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): thinking_blocks.append(block) return thinking_blocks - def _extract_thought_signatures_from_parts( - self, parts: list[HttpxPartType] - ) -> list[str] | None: + def _extract_thought_signatures_from_parts(self, parts: list[HttpxPartType]) -> list[str] | None: """Extract thoughtSignature values from parts. Per Google's docs, thoughtSignature is returned for multi-turn context preservation @@ -1639,9 +1510,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): return invocations if invocations else None - def _extract_image_response_from_parts( - self, parts: list[HttpxPartType] - ) -> list[ImageURLListItem] | None: + def _extract_image_response_from_parts(self, parts: list[HttpxPartType]) -> list[ImageURLListItem] | None: """Extract image response from parts if present""" images: list[ImageURLListItem] = [] for part in parts: @@ -1661,9 +1530,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ) return images - def _extract_audio_response_from_parts( - self, parts: list[HttpxPartType] - ) -> ChatCompletionAudioResponse | None: + def _extract_audio_response_from_parts(self, parts: list[HttpxPartType]) -> ChatCompletionAudioResponse | None: """Extract audio response from parts if present""" for part in parts: if "text" in part: @@ -1672,9 +1539,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if text_content.startswith("data:audio") and ";base64," in text_content: try: if is_base64_encoded(text_content): - media_type, audio_data = text_content.split("data:")[ - 1 - ].split(";base64,") + media_type, audio_data = text_content.split("data:")[1].split(";base64,") if media_type.startswith("audio/"): expires_at = int(time.time()) + (24 * 60 * 60) @@ -1697,9 +1562,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): expires_at = int(time.time()) + (24 * 60 * 60) transcript = "" # Gemini doesn't provide transcript - return ChatCompletionAudioResponse( - data=data, expires_at=expires_at, transcript=transcript - ) + return ChatCompletionAudioResponse(data=data, expires_at=expires_at, transcript=transcript) return None @@ -1719,9 +1582,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if "functionCall" in part: _function_chunk: ChatCompletionToolCallFunctionChunk = { "name": part["functionCall"]["name"], - "arguments": json.dumps( - part["functionCall"]["args"], ensure_ascii=False - ), + "arguments": json.dumps(part["functionCall"]["args"], ensure_ascii=False), } # Extract thought signature if present thought_signature = part.get("thoughtSignature") @@ -1735,9 +1596,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if thought_signature: if "provider_specific_fields" not in function_dict: function_dict["provider_specific_fields"] = {} - function_dict["provider_specific_fields"][ - "thought_signature" - ] = thought_signature + function_dict["provider_specific_fields"]["thought_signature"] = thought_signature function = cast(ChatCompletionToolCallFunctionChunk, function_dict) else: _tool_response_chunk: ChatCompletionToolCallChunk = { @@ -1756,10 +1615,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): _tool_response_chunk["provider_specific_fields"] = { # type: ignore "thought_signature": thought_signature } - _tool_response_chunk["id"] = ( - _encode_tool_call_id_with_signature( - _tool_response_chunk["id"] or "", thought_signature - ) + _tool_response_chunk["id"] = _encode_tool_call_id_with_signature( + _tool_response_chunk["id"] or "", thought_signature ) _tools.append(_tool_response_chunk) cumulative_tool_call_idx += 1 @@ -1780,19 +1637,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): logprobs_list: list[ChatCompletionTokenLogprob] = [] for index, candidate in enumerate(logprobs_result["chosenCandidates"]): top_logprobs: list[TopLogprob] = [] - if "topCandidates" in logprobs_result and index < len( - logprobs_result["topCandidates"] - ): - top_candidates_for_index = logprobs_result["topCandidates"][index][ - "candidates" - ] + if "topCandidates" in logprobs_result and index < len(logprobs_result["topCandidates"]): + top_candidates_for_index = logprobs_result["topCandidates"][index]["candidates"] for options in top_candidates_for_index: - top_logprobs.append( - TopLogprob( - token=options["token"], logprob=options["logProbability"] - ) - ) + top_logprobs.append(TopLogprob(token=options["token"], logprob=options["logProbability"])) logprobs_list.append( ChatCompletionTokenLogprob( token=candidate["token"], @@ -1827,12 +1676,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ## GET USAGE ## usage = Usage( - prompt_tokens=completion_response["usageMetadata"].get( - "promptTokenCount", 0 - ), - completion_tokens=completion_response["usageMetadata"].get( - "candidatesTokenCount", 0 - ), + prompt_tokens=completion_response["usageMetadata"].get("promptTokenCount", 0), + completion_tokens=completion_response["usageMetadata"].get("candidatesTokenCount", 0), total_tokens=completion_response["usageMetadata"].get("totalTokenCount", 0), ) @@ -1865,12 +1710,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ## GET USAGE ## usage = Usage( - prompt_tokens=completion_response["usageMetadata"].get( - "promptTokenCount", 0 - ), - completion_tokens=completion_response["usageMetadata"].get( - "candidatesTokenCount", 0 - ), + prompt_tokens=completion_response["usageMetadata"].get("promptTokenCount", 0), + completion_tokens=completion_response["usageMetadata"].get("candidatesTokenCount", 0), total_tokens=completion_response["usageMetadata"].get("totalTokenCount", 0), ) @@ -1898,17 +1739,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): @staticmethod def _calculate_usage( - completion_response: Union[ - GenerateContentResponseBody, BidiGenerateContentServerMessage - ], + completion_response: Union[GenerateContentResponseBody, BidiGenerateContentServerMessage], ) -> Usage: - if ( - completion_response is not None - and "usageMetadata" not in completion_response - ): - raise ValueError( - f"usageMetadata not found in completion_response. Got={completion_response}" - ) + if completion_response is not None and "usageMetadata" not in completion_response: + raise ValueError(f"usageMetadata not found in completion_response. Got={completion_response}") cached_tokens: int | None = None # Separate variables for prompt tokens by modality prompt_audio_tokens: int | None = None @@ -1937,17 +1771,11 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): modality = str(detail.get("modality", "")).upper() token_count = _get_token_count(detail) if modality == "TEXT": - response_tokens_details.text_tokens = ( - response_tokens_details.text_tokens or 0 - ) + token_count + response_tokens_details.text_tokens = (response_tokens_details.text_tokens or 0) + token_count elif modality == "AUDIO": - response_tokens_details.audio_tokens = ( - response_tokens_details.audio_tokens or 0 - ) + token_count + response_tokens_details.audio_tokens = (response_tokens_details.audio_tokens or 0) + token_count elif modality == "DOCUMENT": - response_tokens_details.text_tokens = ( - response_tokens_details.text_tokens or 0 - ) + token_count + response_tokens_details.text_tokens = (response_tokens_details.text_tokens or 0) + token_count ######################################################### @@ -1959,25 +1787,15 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): modality = str(detail.get("modality", "")).upper() token_count = _get_token_count(detail) if modality == "TEXT": - response_tokens_details.text_tokens = ( - response_tokens_details.text_tokens or 0 - ) + token_count + response_tokens_details.text_tokens = (response_tokens_details.text_tokens or 0) + token_count elif modality == "AUDIO": - response_tokens_details.audio_tokens = ( - response_tokens_details.audio_tokens or 0 - ) + token_count + response_tokens_details.audio_tokens = (response_tokens_details.audio_tokens or 0) + token_count elif modality == "IMAGE": - response_tokens_details.image_tokens = ( - response_tokens_details.image_tokens or 0 - ) + token_count + response_tokens_details.image_tokens = (response_tokens_details.image_tokens or 0) + token_count elif modality == "VIDEO": - response_tokens_details.video_tokens = ( - response_tokens_details.video_tokens or 0 - ) + token_count + response_tokens_details.video_tokens = (response_tokens_details.video_tokens or 0) + token_count elif modality == "DOCUMENT": - response_tokens_details.text_tokens = ( - response_tokens_details.text_tokens or 0 - ) + token_count + response_tokens_details.text_tokens = (response_tokens_details.text_tokens or 0) + token_count # Calculate text_tokens if not explicitly provided in candidatesTokensDetails # candidatesTokenCount includes all modalities, so: text = total - (image + audio + video) @@ -1990,10 +1808,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): completion_audio_tokens = response_tokens_details.audio_tokens or 0 completion_video_tokens = response_tokens_details.video_tokens or 0 calculated_text_tokens = ( - candidates_token_count - - completion_image_tokens - - completion_audio_tokens - - completion_video_tokens + candidates_token_count - completion_image_tokens - completion_audio_tokens - completion_video_tokens ) response_tokens_details.text_tokens = calculated_text_tokens ######################################################### @@ -2074,13 +1889,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): video_tokens=prompt_video_tokens, ) - completion_tokens = response_tokens or completion_response["usageMetadata"].get( - "candidatesTokenCount", 0 - ) - if ( - not VertexGeminiConfig.is_candidate_token_count_inclusive(usage_metadata) - and reasoning_tokens - ): + completion_tokens = response_tokens or completion_response["usageMetadata"].get("candidatesTokenCount", 0) + if not VertexGeminiConfig.is_candidate_token_count_inclusive(usage_metadata) and reasoning_tokens: completion_tokens = reasoning_tokens + completion_tokens ## GET USAGE ## usage = Usage( @@ -2161,11 +1971,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): def _calculate_web_search_requests(grounding_metadata: list[dict]) -> int | None: web_search_requests: int | None = None - if ( - grounding_metadata - and isinstance(grounding_metadata, list) - and len(grounding_metadata) > 0 - ): + if grounding_metadata and isinstance(grounding_metadata, list) and len(grounding_metadata) > 0: for grounding_metadata_item in grounding_metadata: web_search_queries = grounding_metadata_item.get("webSearchQueries") if web_search_queries and web_search_requests: @@ -2279,14 +2085,10 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ) -> None: setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata) # type: ignore if grounding_metadata: - model_response._hidden_params["vertex_ai_grounding_metadata"] = ( - grounding_metadata - ) + model_response._hidden_params["vertex_ai_grounding_metadata"] = grounding_metadata setattr(model_response, "vertex_ai_url_context_metadata", url_context_metadata) # type: ignore if url_context_metadata: - model_response._hidden_params["vertex_ai_url_context_metadata"] = ( - url_context_metadata - ) + model_response._hidden_params["vertex_ai_url_context_metadata"] = url_context_metadata setattr(model_response, "vertex_ai_safety_ratings", safety_ratings) # type: ignore setattr(model_response, "vertex_ai_safety_results", safety_ratings) # type: ignore if safety_ratings: @@ -2294,9 +2096,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): model_response._hidden_params["vertex_ai_safety_results"] = safety_ratings setattr(model_response, "vertex_ai_citation_metadata", citation_metadata) # type: ignore if citation_metadata: - model_response._hidden_params["vertex_ai_citation_metadata"] = ( - citation_metadata - ) + model_response._hidden_params["vertex_ai_citation_metadata"] = citation_metadata def apply_assembled_streaming_response_metadata( self, @@ -2429,51 +2229,35 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ( content, reasoning_content, - ) = VertexGeminiConfig().get_assistant_content_message( + ) = VertexGeminiConfig().get_assistant_content_message(parts=candidate["content"]["parts"]) + + audio_response = VertexGeminiConfig()._extract_audio_response_from_parts( + parts=candidate["content"]["parts"] + ) + image_response = VertexGeminiConfig()._extract_image_response_from_parts( parts=candidate["content"]["parts"] ) - audio_response = ( - VertexGeminiConfig()._extract_audio_response_from_parts( - parts=candidate["content"]["parts"] - ) - ) - image_response = ( - VertexGeminiConfig()._extract_image_response_from_parts( - parts=candidate["content"]["parts"] - ) - ) - - thinking_blocks = ( - VertexGeminiConfig()._extract_thinking_blocks_from_parts( - parts=candidate["content"]["parts"] - ) + thinking_blocks = VertexGeminiConfig()._extract_thinking_blocks_from_parts( + parts=candidate["content"]["parts"] ) # Extract thoughtSignatures from parts (can exist without thought: true) - thought_signatures = ( - VertexGeminiConfig()._extract_thought_signatures_from_parts( - parts=candidate["content"]["parts"] - ) + thought_signatures = VertexGeminiConfig()._extract_thought_signatures_from_parts( + parts=candidate["content"]["parts"] ) # Extract server-side tool invocations (context circulation) - server_side_tool_invocations = ( - VertexGeminiConfig._extract_server_side_tool_invocations( - parts=candidate["content"]["parts"] - ) + server_side_tool_invocations = VertexGeminiConfig._extract_server_side_tool_invocations( + parts=candidate["content"]["parts"] ) if audio_response is not None: - cast(dict[str, Any], chat_completion_message)[ - "audio" - ] = audio_response + cast(dict[str, Any], chat_completion_message)["audio"] = audio_response chat_completion_message["content"] = None # OpenAI spec if image_response is not None: # Handle image response - combine with text content into structured format - cast(dict[str, Any], chat_completion_message)[ - "images" - ] = image_response + cast(dict[str, Any], chat_completion_message)["images"] = image_response if content is not None: chat_completion_message["content"] = content @@ -2481,11 +2265,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): chat_completion_message["reasoning_content"] = reasoning_content if candidate_grounding_metadata: - annotations = ( - VertexGeminiConfig._convert_grounding_metadata_to_annotations( - grounding_metadata=candidate_grounding_metadata, - content_text=content, - ) + annotations = VertexGeminiConfig._convert_grounding_metadata_to_annotations( + grounding_metadata=candidate_grounding_metadata, + content_text=content, ) if annotations: chat_completion_message["annotations"] = annotations # type: ignore @@ -2515,10 +2297,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): # Convert thinking_blocks to reasoning_content for streaming # This ensures reasoning_content is available in streaming responses - if ( - isinstance(model_response, ModelResponseStream) - and reasoning_content is None - ): + if isinstance(model_response, ModelResponseStream) and reasoning_content is None: reasoning_content_parts = [] for block in thinking_blocks: thinking_text = block.get("thinking") @@ -2539,9 +2318,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): if server_side_tool_invocations is not None: if "provider_specific_fields" not in chat_completion_message: chat_completion_message["provider_specific_fields"] = {} - chat_completion_message["provider_specific_fields"][ - "server_side_tool_invocations" - ] = server_side_tool_invocations # type: ignore + chat_completion_message["provider_specific_fields"]["server_side_tool_invocations"] = ( + server_side_tool_invocations # type: ignore + ) if isinstance(model_response, ModelResponseStream): choice = VertexGeminiConfig._create_streaming_choice( @@ -2634,10 +2413,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): model_response.model = model ## CHECK IF RESPONSE FLAGGED - if ( - "promptFeedback" in completion_response - and "blockReason" in completion_response["promptFeedback"] - ): + if "promptFeedback" in completion_response and "blockReason" in completion_response["promptFeedback"]: return self._handle_blocked_response( model_response=model_response, completion_response=completion_response, @@ -2645,13 +2421,8 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): _candidates = completion_response.get("candidates") if _candidates and len(_candidates) > 0: - content_policy_violations = ( - VertexGeminiConfig().get_flagged_finish_reasons() - ) - if ( - "finishReason" in _candidates[0] - and _candidates[0]["finishReason"] in content_policy_violations.keys() - ): + content_policy_violations = VertexGeminiConfig().get_flagged_finish_reasons() + if "finishReason" in _candidates[0] and _candidates[0]["finishReason"] in content_policy_violations.keys(): return self._handle_content_policy_violation( model_response=model_response, completion_response=completion_response, @@ -2673,38 +2444,24 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): safety_ratings, citation_metadata, _, # cumulative_tool_call_index not needed in non-streaming - ) = VertexGeminiConfig._process_candidates( - _candidates, model_response, logging_obj.optional_params - ) + ) = VertexGeminiConfig._process_candidates(_candidates, model_response, logging_obj.optional_params) - usage = VertexGeminiConfig._calculate_usage( - completion_response=completion_response - ) + usage = VertexGeminiConfig._calculate_usage(completion_response=completion_response) - web_search_requests = VertexGeminiConfig._calculate_web_search_requests( - grounding_metadata - ) + web_search_requests = VertexGeminiConfig._calculate_web_search_requests(grounding_metadata) if web_search_requests is not None: - cast( - PromptTokensDetailsWrapper, usage.prompt_tokens_details - ).web_search_requests = web_search_requests + cast(PromptTokensDetailsWrapper, usage.prompt_tokens_details).web_search_requests = web_search_requests setattr(model_response, "usage", usage) ## ADD METADATA TO RESPONSE ## setattr(model_response, "vertex_ai_grounding_metadata", grounding_metadata) - model_response._hidden_params["vertex_ai_grounding_metadata"] = ( - grounding_metadata - ) + model_response._hidden_params["vertex_ai_grounding_metadata"] = grounding_metadata - setattr( - model_response, "vertex_ai_url_context_metadata", url_context_metadata - ) + setattr(model_response, "vertex_ai_url_context_metadata", url_context_metadata) - model_response._hidden_params["vertex_ai_url_context_metadata"] = ( - url_context_metadata - ) + model_response._hidden_params["vertex_ai_url_context_metadata"] = url_context_metadata setattr(model_response, "vertex_ai_safety_results", safety_ratings) model_response._hidden_params["vertex_ai_safety_results"] = ( @@ -2718,13 +2475,9 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): ) ## ADD TRAFFIC TYPE ## - traffic_type = completion_response.get("usageMetadata", {}).get( - "trafficType" - ) + traffic_type = completion_response.get("usageMetadata", {}).get("trafficType") if traffic_type: - model_response._hidden_params.setdefault( - "provider_specific_fields", {} - )["traffic_type"] = traffic_type + model_response._hidden_params.setdefault("provider_specific_fields", {})["traffic_type"] = traffic_type ## ADD SERVICE TIER ## if getattr(raw_response, "headers", None): @@ -2761,9 +2514,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): def get_error_class( self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers] ) -> BaseLLMException: - return VertexAIError( - message=error_message, status_code=status_code, headers=headers - ) + return VertexAIError(message=error_message, status_code=status_code, headers=headers) def transform_request( self, @@ -2773,9 +2524,7 @@ class VertexGeminiConfig(VertexAIBaseConfig, BaseConfig): litellm_params: dict, headers: dict, ) -> dict: - raise NotImplementedError( - "Vertex AI has a custom implementation of transform_request. Needs sync + async." - ) + raise NotImplementedError("Vertex AI has a custom implementation of transform_request. Needs sync + async.") def validate_environment( self, @@ -2818,9 +2567,7 @@ async def make_call( ) try: - response = await client.post( - api_base, headers=headers, data=data, stream=True, logging_obj=logging_obj - ) + response = await client.post(api_base, headers=headers, data=data, stream=True, logging_obj=logging_obj) response.raise_for_status() except httpx.HTTPStatusError as e: exception_string = str(await e.response.aread()) @@ -2841,7 +2588,7 @@ async def make_call( sync_stream=False, logging_obj=logging_obj, response_headers=response.headers, - response=response, # any-ok: untyped stream + response=response, ) # LOGGING logging_obj.post_call( @@ -2869,9 +2616,7 @@ def make_sync_call( if client is None: client = HTTPHandler() # Create a new client if none provided - response = client.post( - api_base, headers=headers, data=data, stream=True, logging_obj=logging_obj - ) + response = client.post(api_base, headers=headers, data=data, stream=True, logging_obj=logging_obj) if response.status_code != 200 and response.status_code != 201: raise VertexAIError( @@ -2885,7 +2630,7 @@ def make_sync_call( sync_stream=True, logging_obj=logging_obj, response_headers=response.headers, - response=response, # any-ok: untyped stream + response=response, ) # LOGGING @@ -2928,9 +2673,7 @@ class VertexLLM(VertexBase): gemini_api_key: str | None = None, extra_headers: dict | None = None, ) -> CustomStreamWrapper: - should_use_v1beta1_features = self.is_using_v1beta1_features( - optional_params=optional_params - ) + should_use_v1beta1_features = self.is_using_v1beta1_features(optional_params=optional_params) _auth_header, vertex_project = await self._ensure_access_token_async( credentials=vertex_credentials, @@ -2987,11 +2730,7 @@ class VertexLLM(VertexBase): completion_stream=None, make_call=partial( make_call, - gemini_client=( - client - if client is not None and isinstance(client, AsyncHTTPHandler) - else None - ), + gemini_client=(client if client is not None and isinstance(client, AsyncHTTPHandler) else None), api_base=api_base, headers=headers, data=request_body_str, @@ -3030,9 +2769,7 @@ class VertexLLM(VertexBase): gemini_api_key: str | None = None, extra_headers: dict | None = None, ) -> Union[ModelResponse, CustomStreamWrapper]: - should_use_v1beta1_features = self.is_using_v1beta1_features( - optional_params=optional_params - ) + should_use_v1beta1_features = self.is_using_v1beta1_features(optional_params=optional_params) _auth_header, vertex_project = await self._ensure_access_token_async( credentials=vertex_credentials, @@ -3077,9 +2814,7 @@ class VertexLLM(VertexBase): if timeout: _async_client_params["timeout"] = timeout if client is None or not isinstance(client, AsyncHTTPHandler): - client = get_async_httpx_client( - params=_async_client_params, llm_provider=litellm.LlmProviders.VERTEX_AI - ) + client = get_async_httpx_client(params=_async_client_params, llm_provider=litellm.LlmProviders.VERTEX_AI) else: client = client # type: ignore ## LOGGING @@ -3218,9 +2953,7 @@ class VertexLLM(VertexBase): extra_headers=extra_headers, ) - should_use_v1beta1_features = self.is_using_v1beta1_features( - optional_params=optional_params - ) + should_use_v1beta1_features = self.is_using_v1beta1_features(optional_params=optional_params) _auth_header, vertex_project = self._ensure_access_token( credentials=vertex_credentials, @@ -3279,11 +3012,7 @@ class VertexLLM(VertexBase): completion_stream=None, make_call=partial( make_sync_call, - gemini_client=( - client - if client is not None and isinstance(client, HTTPHandler) - else None - ), + gemini_client=(client if client is not None and isinstance(client, HTTPHandler) else None), api_base=url, data=request_data_str, model=model, @@ -3413,11 +3142,7 @@ class ModelResponseIterator: # to correctly set finish_reason="tool_calls" per the OpenAI spec. if not self.has_seen_tool_calls: for choice in model_response.choices: - if ( - hasattr(choice, "delta") - and choice.delta - and choice.delta.tool_calls - ): + if hasattr(choice, "delta") and choice.delta and choice.delta.tool_calls: self.has_seen_tool_calls = True break @@ -3437,9 +3162,7 @@ class ModelResponseIterator: if self.has_seen_tool_calls: mapped_finish_reason = "tool_calls" else: - mapped_finish_reason = VertexGeminiConfig._check_finish_reason( - None, finish_reason_str - ) + mapped_finish_reason = VertexGeminiConfig._check_finish_reason(None, finish_reason_str) choice = StreamingChoices( finish_reason=mapped_finish_reason, index=candidate.get("index", 0), @@ -3487,19 +3210,13 @@ class ModelResponseIterator: completion_response=processed_chunk, ) - web_search_requests = VertexGeminiConfig._calculate_web_search_requests( - grounding_metadata - ) + web_search_requests = VertexGeminiConfig._calculate_web_search_requests(grounding_metadata) if web_search_requests is not None: - cast( - PromptTokensDetailsWrapper, usage.prompt_tokens_details - ).web_search_requests = web_search_requests + cast(PromptTokensDetailsWrapper, usage.prompt_tokens_details).web_search_requests = web_search_requests traffic_type = processed_chunk.get("usageMetadata", {}).get("trafficType") if traffic_type: - model_response._hidden_params.setdefault("provider_specific_fields", {})[ - "traffic_type" - ] = traffic_type + model_response._hidden_params.setdefault("provider_specific_fields", {})["traffic_type"] = traffic_type service_tier = self.response_headers.get("x-gemini-service-tier") if service_tier: @@ -3546,9 +3263,7 @@ class ModelResponseIterator: citation_metadata, ) = self._apply_stream_candidates(_candidates, model_response) - usage = self._apply_stream_usage_metadata( - processed_chunk, model_response, grounding_metadata - ) + usage = self._apply_stream_usage_metadata(processed_chunk, model_response, grounding_metadata) setattr(model_response, "usage", usage) # type: ignore @@ -3580,9 +3295,7 @@ class ModelResponseIterator: return self.chunk_parser(chunk=json_chunk) - def handle_accumulated_json_chunk( - self, chunk: str - ) -> Optional["ModelResponseStream"]: + def handle_accumulated_json_chunk(self, chunk: str) -> Optional["ModelResponseStream"]: chunk = litellm.CustomStreamWrapper._strip_sse_data_from_chunk(chunk) or "" message = chunk.replace("\n\n", "") @@ -3598,9 +3311,7 @@ class ModelResponseIterator: # If it's not valid JSON yet, continue to the next event return None - def _common_chunk_parsing_logic( - self, chunk: str - ) -> Optional["ModelResponseStream"]: + def _common_chunk_parsing_logic(self, chunk: str) -> Optional["ModelResponseStream"]: try: chunk = litellm.CustomStreamWrapper._strip_sse_data_from_chunk(chunk) or "" if len(chunk) > 0: @@ -3658,49 +3369,31 @@ class ModelResponseIterator: raise RuntimeError(f"Error parsing chunk: {e},\nReceived chunk: {chunk}") async def aclose(self) -> None: - iterator = getattr( # any-ok: untyped stream + iterator = getattr( self, "async_response_iterator", - self.streaming_response, # any-ok: untyped stream + self.streaming_response, ) - if iterator is not None and hasattr( # any-ok: untyped stream - iterator, - "aclose", # any-ok: untyped stream - ): + if iterator is not None and hasattr(iterator, "aclose"): try: - await iterator.aclose() # any-ok: untyped stream + await iterator.aclose() except Exception as e: # noqa: BLE001 - verbose_logger.debug( - "ModelResponseIterator.aclose: error closing iterator: %s", e - ) + verbose_logger.debug("ModelResponseIterator.aclose: error closing iterator: %s", e) if self.response is not None: try: await self.response.aclose() except Exception as e: # noqa: BLE001 - verbose_logger.debug( - "ModelResponseIterator.aclose: error closing response: %s", e - ) + verbose_logger.debug("ModelResponseIterator.aclose: error closing response: %s", e) def close(self) -> None: - iterator = getattr( # any-ok: untyped stream - self, - "response_iterator", - self.streaming_response, # any-ok: untyped stream - ) - if iterator is not None and hasattr( # any-ok: untyped stream - iterator, - "close", # any-ok: untyped stream - ): + iterator = getattr(self, "response_iterator", self.streaming_response) + if iterator is not None and hasattr(iterator, "close"): try: - iterator.close() # any-ok: untyped stream + iterator.close() except Exception as e: # noqa: BLE001 - verbose_logger.debug( - "ModelResponseIterator.close: error closing iterator: %s", e - ) + verbose_logger.debug("ModelResponseIterator.close: error closing iterator: %s", e) if self.response is not None: try: self.response.close() except Exception as e: # noqa: BLE001 - verbose_logger.debug( - "ModelResponseIterator.close: error closing response: %s", e - ) + verbose_logger.debug("ModelResponseIterator.close: error closing response: %s", e) diff --git a/litellm/mypy.ini b/litellm/mypy.ini deleted file mode 100644 index b65e11bab42..00000000000 --- a/litellm/mypy.ini +++ /dev/null @@ -1,22 +0,0 @@ -[mypy] -warn_return_any = True -ignore_missing_imports = True -disallow_untyped_defs = True -mypy_path = litellm/stubs -namespace_packages = True -disable_error_code = - annotation-unchecked, - import-untyped - -[mypy-litellm.*] -ignore_missing_imports = False - -[mypy-google.*] -ignore_missing_imports = True - -[mypy-cryptography.hazmat.bindings._rust.x509] -ignore_errors = True - -[mypy-fastuuid.*] -ignore_missing_imports = True -ignore_errors = True \ No newline at end of file diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 7cccae6e761..2a0e8402f17 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -110,46 +110,32 @@ async def _record_streaming_client_disconnect_if_needed( if not disconnected: return False - logging_obj = request_data.get("litellm_logging_obj") # any-ok: untyped request - if logging_obj is not None: # any-ok: untyped request - litellm_params = ( - logging_obj.model_call_details.setdefault( # any-ok: untyped request - "litellm_params", {} - ) - ) + logging_obj = request_data.get("litellm_logging_obj") + if logging_obj is not None: + litellm_params = logging_obj.model_call_details.setdefault("litellm_params", {}) + _apply_client_disconnect_metadata(litellm_params.setdefault("metadata", {})) _apply_client_disconnect_metadata( - litellm_params.setdefault("metadata", {}) # any-ok: untyped request - ) - _apply_client_disconnect_metadata( - logging_obj.model_call_details.setdefault( # any-ok: untyped request - "metadata", {} - ) + logging_obj.model_call_details.setdefault("metadata", {}) ) - _apply_client_disconnect_metadata( - request_data.setdefault("metadata", {}) # any-ok: untyped request - ) - litellm_params = request_data.setdefault( # any-ok: untyped request - "litellm_params", {} # any-ok: untyped request - ) - _apply_client_disconnect_metadata( - litellm_params.setdefault("metadata", {}) # any-ok: untyped request - ) + _apply_client_disconnect_metadata(request_data.setdefault("metadata", {})) + litellm_params = request_data.setdefault("litellm_params", {}) + _apply_client_disconnect_metadata(litellm_params.setdefault("metadata", {})) verbose_proxy_logger.debug( "Recorded streaming client disconnect with error_code=499 for litellm_call_id=%s", - request_data.get("litellm_call_id"), # any-ok: untyped request + request_data.get("litellm_call_id"), ) return True async def _cancel_pending_gather_tasks(tasks: list["asyncio.Task[Any]"]) -> None: - pending_tasks = [task for task in tasks if not task.done()] # any-ok: untyped task - for task in pending_tasks: # any-ok: untyped task - task.cancel() # any-ok: untyped task - for task in pending_tasks: # any-ok: untyped task + pending_tasks = [task for task in tasks if not task.done()] + for task in pending_tasks: + task.cancel() + for task in pending_tasks: try: - await task # any-ok: untyped request + await task except (asyncio.CancelledError, Exception): # noqa: BLE001 pass @@ -1401,24 +1387,22 @@ class ProxyBaseLLMRequestProcessing: user_model=user_model, user_api_key_dict=user_api_key_dict, ) - llm_call_task = asyncio.create_task(llm_call) # any-ok: untyped task - tasks.append(llm_call_task) # any-ok: untyped task + llm_call_task = asyncio.create_task(llm_call) + tasks.append(llm_call_task) llm_responses = asyncio.gather( *tasks ) # run the moderation check in parallel to the actual llm api call try: - if general_settings.get( # any-ok: untyped request - "cancel_on_disconnect", False - ): - responses = await _await_llm_call_cancelling_on_disconnect( # any-ok: untyped request - request, llm_responses # any-ok: untyped task + if general_settings.get("cancel_on_disconnect", False): + responses = await _await_llm_call_cancelling_on_disconnect( + request, llm_responses ) else: - responses = await llm_responses # any-ok: untyped request + responses = await llm_responses finally: - await _cancel_pending_gather_tasks(tasks) # any-ok: untyped task + await _cancel_pending_gather_tasks(tasks) response = responses[1] @@ -2477,18 +2461,16 @@ class ProxyBaseLLMRequestProcessing: recorded_client_disconnect = ( await _record_streaming_client_disconnect_if_needed( request, - request_data, # any-ok: untyped request - client_disconnected, # any-ok: untyped request + request_data, + client_disconnected, ) ) if recorded_client_disconnect: - ProxyLogging._fire_deferred_stream_logging( - request_data # any-ok: untyped request - ) + ProxyLogging._fire_deferred_stream_logging(request_data) - if hasattr(response, "aclose"): # any-ok: untyped request + if hasattr(response, "aclose"): try: - await response.aclose() # any-ok: untyped request + await response.aclose() except BaseException as e: # noqa: BLE001 verbose_proxy_logger.debug( "async_streaming_data_generator: error closing response stream: %s", @@ -2624,8 +2606,8 @@ class ProxyBaseLLMRequestProcessing: finally: await ProxyBaseLLMRequestProcessing._finalize_streaming_generator_cleanup( request=request, - request_data=request_data, # any-ok: untyped request - response=response, # any-ok: untyped request + request_data=request_data, + response=response, stream_completed=stream_completed, client_disconnected=client_disconnected, ) diff --git a/litellm/proxy/common_utils/model_listing_utils.py b/litellm/proxy/common_utils/model_listing_utils.py new file mode 100644 index 00000000000..3a70377037d --- /dev/null +++ b/litellm/proxy/common_utils/model_listing_utils.py @@ -0,0 +1,167 @@ +"""Team-scoped (BYOK) model-name translation for the model listing endpoints. + +`/v1/models`, `/models`, and `GET /v1/models/{id}` should surface the public +`team_public_model_name` rather than the internal routing key +`model_name_{team_id}_{uuid}`, consistent with `/v1/model/info`. The internal +key still routes regardless; this is a presentation-layer swap only and does not +touch access-group or auth semantics (see issue #28382). Operators can pin the +legacy internal names with `general_settings.use_team_public_model_name: false`. +""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from litellm.router import Router + + +class TeamModelNameTranslator: + """Translates internal team routing keys to their public names for the model + listing/retrieve responses. Stateless; the live router and general_settings + are injected per call so the unit tests can drive it without globals. + """ + + @staticmethod + def _internal_public_pair(model: object) -> tuple[str, str] | None: + """`(internal_routing_key, public_name)` for a team-scoped row, else None.""" + if not isinstance(model, dict): + return None + model_dict = cast(dict[str, object], model) # any-ok: checked + model_info_raw: object = model_dict.get("model_info") + if not isinstance(model_info_raw, Mapping): + return None + model_info = cast(Mapping[str, object], model_info_raw) # any-ok: checked + team_id = model_info.get("team_id") + team_public = model_info.get("team_public_model_name") + name = model_dict.get("model_name") + if ( + isinstance(team_id, str) + and isinstance(team_public, str) + and isinstance(name, str) + and team_id + and team_public + and name.startswith(f"model_name_{team_id}_") + ): + return name, team_public + return None + + @staticmethod + def _is_enabled(general_settings: Mapping[str, object]) -> bool: + return general_settings.get("use_team_public_model_name", True) is not False + + @staticmethod + def build_internal_to_public_map( + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> dict[str, str]: + """Internal team routing key -> public `team_public_model_name`. + + Empty when disabled via the legacy flag, the router is absent, or the + router model list is malformed. + """ + if llm_router is None or not TeamModelNameTranslator._is_enabled( + general_settings + ): + return {} + router_model_list = llm_router.get_model_list() + if not isinstance(router_model_list, list): + return {} + return dict( + pair + for pair in ( + TeamModelNameTranslator._internal_public_pair(model) + for model in router_model_list + ) + if pair is not None + ) + + @staticmethod + def _response_to_lookup_map( + model_names: list[str], + internal_to_public: dict[str, str], + ) -> dict[str, str]: + """Map each public response id to the first internal lookup id seen in + `model_names`, preserving first-occurrence order. First-wins keeps list + and retrieve in agreement on which accessible deployment a shared public + id resolves to: a global iterated before a colliding team alias stays + the listed entry, and sibling team rows collapse to their first + occurrence. + """ + result: dict[str, str] = {} + for name in model_names: + result.setdefault(internal_to_public.get(name, name), name) + return result + + @staticmethod + def listing_entries( + model_names: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> list[tuple[str, str]]: + """`(response_id, metadata_lookup_id)` for each listed model, de-duplicated + by response_id while preserving order. + + For team-scoped rows `response_id` is the public name shown to the client, + while `metadata_lookup_id` stays the internal routing key so downstream + metadata/fallback lookups (keyed by the routing name) still resolve. The + lookup id is always one of `model_names` (the caller's accessible set), so + a public name shared across teams never resolves to another team's + internal key. Both ids are identical for unmapped names (globals, + access-group keys). + """ + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, general_settings + ) + if not internal_to_public: + return [(name, name) for name in model_names] + return list( + TeamModelNameTranslator._response_to_lookup_map( + model_names, internal_to_public + ).items() + ) + + @staticmethod + def translate_listing( + model_names: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> list[str]: + """Public-name view of `model_names` (the `response_id` of each listing + entry). Sibling deployments sharing a public name collapse to one entry + while preserving order; unmapped names pass through. + """ + return [ + entry[0] + for entry in TeamModelNameTranslator.listing_entries( + model_names, llm_router, general_settings + ) + ] + + @staticmethod + def resolve_public_name( + model_id: str, + available_models: list[str], + llm_router: "Router | None", + general_settings: Mapping[str, object], + ) -> str: + """Resolve a public team name back to the internal routing key the router + indexes by, so `GET /v1/models/{id}` accepts the name the listing returns. + + Resolution is restricted to `available_models` (the caller's accessible + set) so colliding public names across teams never resolve across an access + boundary. Uses the same first-occurrence dedup as `listing_entries` so a + public id advertised by `/v1/models` resolves to the same internal + deployment that the listing's metadata was built from. Returns `model_id` + unchanged when it is not an accessible public team name (already-internal + names and globals pass through). + """ + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, general_settings + ) + if not internal_to_public: + return model_id + return TeamModelNameTranslator._response_to_lookup_map( + available_models, internal_to_public + ).get(model_id, model_id) diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py index 726e71e307c..0e7e67aa37f 100644 --- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -25,15 +25,9 @@ async def get_ui_config(): or general_settings.get("auto_redirect_ui_login_to_sso", False) is True ) admin_ui_disabled = os.getenv("DISABLE_ADMIN_UI", "false").lower() == "true" - hide_default_credentials_hint = bool( # any-ok: untyped settings - os.getenv( # any-ok: untyped settings - "LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false" - ).lower() - == "true" - or general_settings.get( # any-ok: untyped settings - "hide_default_credentials_hint", False - ) - is True + hide_default_credentials_hint = bool( + os.getenv("LITELLM_HIDE_DEFAULT_CREDENTIALS_HINT", "false").lower() == "true" + or general_settings.get("hide_default_credentials_hint", False) is True ) sso_configured = _has_user_setup_sso() @@ -48,7 +42,7 @@ async def get_ui_config(): auto_redirect_to_sso=sso_configured and auto_redirect_ui_login_to_sso, admin_ui_disabled=admin_ui_disabled, sso_configured=sso_configured, - hide_default_credentials_hint=hide_default_credentials_hint, # any-ok: untyped settings + hide_default_credentials_hint=hide_default_credentials_hint, is_control_plane=is_control_plane, workers=proxy_config.worker_registry if is_control_plane else [], ) diff --git a/litellm/proxy/google_endpoints/endpoints.py b/litellm/proxy/google_endpoints/endpoints.py index 4234c433f22..6427835c250 100644 --- a/litellm/proxy/google_endpoints/endpoints.py +++ b/litellm/proxy/google_endpoints/endpoints.py @@ -107,7 +107,7 @@ async def google_stream_generate_content( data["stream"] = True # google-genai SDK (?alt=sse) must not receive OpenAI's data: [DONE] terminator. data["_litellm_skip_openai_stream_done"] = True - data["_litellm_raw_sse_stream"] = True # any-ok: untyped request + data["_litellm_raw_sse_stream"] = True processor = ProxyBaseLLMRequestProcessing(data=data) try: diff --git a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py index 7e6f3dac008..b3b8fbdb2a5 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py +++ b/litellm/proxy/guardrails/guardrail_hooks/openai/moderations.py @@ -25,7 +25,11 @@ from litellm.llms.custom_httpx.http_handler import ( httpxSpecialProvider, ) from litellm.types.guardrails import GuardrailEventHooks -from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus +from litellm.types.utils import ( + GenericGuardrailAPIInputs, + GuardrailStatus, + GuardrailTracingDetail, +) from .base import OpenAIGuardrailBase @@ -287,6 +291,7 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): start_time=start_time, end_time=end_time, event_type=event_type, + tracing_detail=self._build_tracing_detail(guardrail_response), ) return response @@ -328,9 +333,36 @@ class OpenAIModerationGuardrail(OpenAIGuardrailBase, CustomGuardrail): start_time=start_time, end_time=end_time, event_type=event_type, + tracing_detail=self._build_tracing_detail(guardrail_response), ) raise e + @staticmethod + def _build_tracing_detail( + guardrail_response: Union[dict, str, Exception], + ) -> Optional[GuardrailTracingDetail]: + """ + Pull the flagged category names out of the moderation response so trace + backends can index a short, queryable ``guardrail_violation_categories`` + attribute instead of the full ``guardrail_response`` blob, whose + ``category_scores`` map (one float per category) blows past indexed-field + length limits on backends like ELK (1024 chars). + """ + if not isinstance(guardrail_response, dict): + return None + + results = guardrail_response.get("results") or [] + violation_categories = [ + category + for result in results + if isinstance(result, dict) + for category, is_flagged in (result.get("categories") or {}).items() + if is_flagged + ] + if not violation_categories: + return None + return GuardrailTracingDetail(violation_categories=violation_categories) + @staticmethod def get_config_model() -> Optional[Type["GuardrailConfigModel"]]: """ diff --git a/litellm/proxy/guardrails/guardrail_hooks/presidio.py b/litellm/proxy/guardrails/guardrail_hooks/presidio.py index 5efb5966262..a8afe4efe2a 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/presidio.py +++ b/litellm/proxy/guardrails/guardrail_hooks/presidio.py @@ -747,12 +747,12 @@ class _OPTIONAL_PresidioPIIMasking(CustomGuardrail): # to the model would carry anonymization tokens and the response would echo them. if ( self.should_run_guardrail( - data=data, # any-ok: untyped request - event_type=GuardrailEventHooks.pre_call, # any-ok: untyped request + data=data, + event_type=GuardrailEventHooks.pre_call, ) is not True ): - return data # any-ok: untyped request + return data try: content_safety = data.get("content_safety", None) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 6e567a428e4..d6ecc59f263 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -3239,10 +3239,8 @@ async def _get_model_max_budget_current_spend( f"{VIRTUAL_KEY_SPEND_CACHE_KEY_PREFIX}:" f"{api_key_hash}:{model}:{budget_config.budget_duration}" ) - current_spend: float | None = ( - await user_api_key_cache.async_get_cache( # any-ok: untyped dump - key=virtual_key_model_spend_cache_key, - ) + current_spend: float | None = await user_api_key_cache.async_get_cache( + key=virtual_key_model_spend_cache_key, ) if current_spend is None: model_without_prefix = model.split("/")[-1] if "/" in model else model @@ -3250,13 +3248,11 @@ async def _get_model_max_budget_current_spend( f"{VIRTUAL_KEY_SPEND_CACHE_KEY_PREFIX}:" f"{api_key_hash}:{model_without_prefix}:{budget_config.budget_duration}" ) - current_spend = ( - await user_api_key_cache.async_get_cache( # any-ok: untyped dump - key=virtual_key_model_spend_cache_key, - ) + current_spend = await user_api_key_cache.async_get_cache( + key=virtual_key_model_spend_cache_key, ) try: - return float(current_spend or 0.0) # any-ok: untyped dump + return float(current_spend or 0.0) except (TypeError, ValueError): return 0.0 @@ -3365,27 +3361,17 @@ async def info_key_fn_v2( k_dict = k.model_dump() except Exception: k_dict = k.dict() - k_token_hash = k_dict.pop("token", None) # any-ok: untyped dump + k_token_hash = k_dict.pop("token", None) - model_max_budget = ( - k_dict.get("model_max_budget") or {} # any-ok: untyped dump - ) - budget_table = ( - k_dict.get("litellm_budget_table") or {} # any-ok: untyped dump - ) - if not model_max_budget and isinstance( # any-ok: untyped dump - budget_table, dict # any-ok: untyped dump - ): - model_max_budget = ( - budget_table.get("model_max_budget") or {} # any-ok: untyped dump - ) - if model_max_budget and k_token_hash: # any-ok: untyped dump - k_dict["model_max_budget_usage"] = ( # any-ok: untyped dump - await _build_model_max_budget_usage( # any-ok: untyped dump - api_key_hash=k_token_hash, # any-ok: untyped dump - model_max_budget=model_max_budget, # any-ok: untyped dump - user_api_key_cache=user_api_key_cache, - ) + model_max_budget = k_dict.get("model_max_budget") or {} + budget_table = k_dict.get("litellm_budget_table") or {} + if not model_max_budget and isinstance(budget_table, dict): + model_max_budget = budget_table.get("model_max_budget") or {} + if model_max_budget and k_token_hash: + k_dict["model_max_budget_usage"] = await _build_model_max_budget_usage( + api_key_hash=k_token_hash, + model_max_budget=model_max_budget, + user_api_key_cache=user_api_key_cache, ) filtered_key_info.append(k_dict) @@ -3470,27 +3456,17 @@ async def info_key_fn( except Exception: # if using pydantic v1 key_info = key_info.dict() - key_token_hash = key_info.pop("token") # any-ok: untyped dump + key_token_hash = key_info.pop("token") - model_max_budget = ( - key_info.get("model_max_budget") or {} # any-ok: untyped dump - ) - budget_table = ( - key_info.get("litellm_budget_table") or {} # any-ok: untyped dump - ) - if not model_max_budget and isinstance( # any-ok: untyped dump - budget_table, dict # any-ok: untyped dump - ): - model_max_budget = ( - budget_table.get("model_max_budget") or {} # any-ok: untyped dump - ) - if model_max_budget and key_token_hash: # any-ok: untyped dump - key_info["model_max_budget_usage"] = ( # any-ok: untyped dump - await _build_model_max_budget_usage( # any-ok: untyped dump - api_key_hash=key_token_hash, # any-ok: untyped dump - model_max_budget=model_max_budget, # any-ok: untyped dump - user_api_key_cache=user_api_key_cache, - ) + model_max_budget = key_info.get("model_max_budget") or {} + budget_table = key_info.get("litellm_budget_table") or {} + if not model_max_budget and isinstance(budget_table, dict): + model_max_budget = budget_table.get("model_max_budget") or {} + if model_max_budget and key_token_hash: + key_info["model_max_budget_usage"] = await _build_model_max_budget_usage( + api_key_hash=key_token_hash, + model_max_budget=model_max_budget, + user_api_key_cache=user_api_key_cache, ) # Attach object_permission if object_permission_id is set diff --git a/litellm/proxy/management_endpoints/ui_sso.py b/litellm/proxy/management_endpoints/ui_sso.py index 91a5c109acf..427c87e0f44 100644 --- a/litellm/proxy/management_endpoints/ui_sso.py +++ b/litellm/proxy/management_endpoints/ui_sso.py @@ -194,11 +194,7 @@ def _is_valid_cli_sso_user_code(user_code: str | None) -> bool: def _cli_sso_verification_uri_complete_enabled() -> bool: from litellm.proxy.proxy_server import general_settings - return bool( - general_settings.get( # any-ok: operator opt-in read from the untyped general_settings dict - "allow_cli_sso_verification_uri_complete", False - ) - ) + return bool(general_settings.get("allow_cli_sso_verification_uri_complete", False)) def _cli_sso_start_response_body( diff --git a/litellm/proxy/openai_files_endpoints/common_utils.py b/litellm/proxy/openai_files_endpoints/common_utils.py index 2ba1d937c04..bb3033e2a6c 100644 --- a/litellm/proxy/openai_files_endpoints/common_utils.py +++ b/litellm/proxy/openai_files_endpoints/common_utils.py @@ -14,6 +14,8 @@ from litellm.types.utils import SpecialEnums if TYPE_CHECKING: from fastapi import Request + from litellm.router import Router + def _is_base64_encoded_unified_file_id(b64_uid: str) -> Union[str, Literal[False]]: # Ensure b64_uid is a string and not a mock object @@ -300,6 +302,92 @@ def get_credentials_for_model( return credentials +def get_team_provider_credentials( + llm_router: Optional["Router"], + team_models: List[str], + custom_llm_provider: str, + team_id: Optional[str] = None, +) -> Optional[dict]: + """ + Resolve upstream credentials for a provider-scoped file operation + (e.g. GET /v1/files), which doesn't pin a model. + + Priority: + 1. The team's own (BYOK) deployment for this provider — a deployment whose + ``model_info.team_id`` matches ``team_id``. This keeps team-scoped listings + on the team's own provider account/key instead of a shared global one. + 2. Fallback: any deployment the team is granted access to for this provider, + expanding wildcard routes and the all-proxy-models sentinel. + + Credential lookup is always scoped to the team's allowlist, so a team can + never resolve a provider key for a deployment it isn't authorized to use. + Returns None when the router is unavailable or no authorized deployment + matches, so the caller can fall back to default credential resolution. + """ + if llm_router is None: + return None + + def _provider_credentials(model_id: str) -> Optional[dict]: + credentials = llm_router.get_deployment_credentials_with_provider( + model_id=model_id + ) + if ( + credentials is not None + and credentials.get("custom_llm_provider") == custom_llm_provider + ): + return credentials + return None + + # 1. Prefer the team's own BYOK deployment, matched by model_info.team_id. + if team_id is not None: + for deployment in llm_router.model_list or []: + model_info = deployment.get("model_info") or {} + if model_info.get("team_id") != team_id: + continue + deployment_id = model_info.get("id") + if deployment_id is None: + continue + credentials = _provider_credentials(deployment_id) + if credentials is not None: + return credentials + + # 2. Fall back to deployments the team is allowed to access. The + # all-proxy-models sentinel isn't expanded by get_complete_model_list, so + # normalize it to an empty allowlist, which defers to the team-scoped + # proxy model list. A team with a restricted allowlist (e.g. anthropic + # only) therefore never resolves another provider's key. + from litellm.proxy._types import SpecialModelNames + from litellm.proxy.auth.model_checks import get_complete_model_list + + grants_all_models = SpecialModelNames.all_proxy_models.value in team_models + effective_team_models = [] if grants_all_models else team_models + + proxy_model_list = llm_router.get_model_names(team_id=team_id) + model_access_groups = llm_router.get_model_access_groups() + models_to_try = list( + dict.fromkeys( + get_complete_model_list( + key_models=[], + team_models=effective_team_models, + proxy_model_list=proxy_model_list, + user_model=None, + infer_model_from_keys=False, + return_wildcard_routes=True, + llm_router=llm_router, + model_access_groups=model_access_groups, + include_model_access_groups=True, + team_id=team_id, + ) + ) + ) + for model_name in models_to_try: + credentials = _provider_credentials(model_name) + if credentials is not None: + return credentials + + return None + + def prepare_data_with_credentials( data: dict, credentials: dict, diff --git a/litellm/proxy/openai_files_endpoints/files_endpoints.py b/litellm/proxy/openai_files_endpoints/files_endpoints.py index f43e876d111..d7dab350154 100644 --- a/litellm/proxy/openai_files_endpoints/files_endpoints.py +++ b/litellm/proxy/openai_files_endpoints/files_endpoints.py @@ -43,6 +43,7 @@ from litellm.proxy.openai_files_endpoints.common_utils import ( encode_file_id_with_model, extract_file_creation_params, get_credentials_for_model, + get_team_provider_credentials, handle_model_based_routing, prepare_data_with_credentials, validate_managed_files_requirement, @@ -1351,14 +1352,20 @@ async def list_files( status_code=400, detail="target_model_names on list files must be a list of one model name. Example: ['gpt-4o']", ) - ## Use router to list fine-tuning jobs for that model if llm_router is None: raise HTTPException( status_code=500, detail="LLM Router not initialized. Ensure models added to proxy.", ) - data["model"] = target_model_names_list[0] - response = await llm_router.afile_list( + credentials = get_credentials_for_model( + llm_router=llm_router, + model_id=target_model_names_list[0], + operation_context="file list", + ) + prepare_data_with_credentials(data=data, credentials=credentials) + response = await litellm.afile_list( + custom_llm_provider=credentials["custom_llm_provider"], + purpose=purpose, **data, ) else: @@ -1370,6 +1377,18 @@ async def list_files( or "openai" ) + # No model/target_model_names pinned: resolve upstream credentials from + # the team's deployment for this provider so the call is authenticated + # against the team's own account (e.g. the team's openai deployment). + team_credentials = get_team_provider_credentials( + llm_router=llm_router, + team_models=user_api_key_dict.team_models or [], + custom_llm_provider=custom_llm_provider, + team_id=user_api_key_dict.team_id, + ) + if team_credentials is not None: + prepare_data_with_credentials(data=data, credentials=team_credentials) + response = await litellm.afile_list( custom_llm_provider=custom_llm_provider, purpose=purpose, **data # type: ignore ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index cbd9a888a93..cb15848d085 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -15,6 +15,7 @@ import threading import time import traceback import warnings +from collections.abc import Mapping from datetime import datetime, timedelta, timezone from typing import ( TYPE_CHECKING, @@ -301,6 +302,7 @@ from litellm.proxy.common_utils.load_config_utils import ( get_config_file_contents_from_gcs, get_file_contents_from_s3, ) +from litellm.proxy.common_utils.model_listing_utils import TeamModelNameTranslator from litellm.proxy.common_utils.openai_endpoint_utils import ( remove_sensitive_info_from_deployment, ) @@ -894,7 +896,7 @@ async def proxy_startup_event(app: FastAPI): if transaction_buffer_redis_cache is None: transaction_buffer_redis_cache = ( ProxyStartupEvent._get_transaction_buffer_redis_cache( - general_settings=general_settings # any-ok: untyped stream + general_settings=general_settings ) ) @@ -7080,9 +7082,7 @@ async def async_data_generator( # happened to ship a streaming-iterator override (the default). needs_iterator_wrap = proxy_logging_obj.needs_iterator_wrap() needs_per_chunk_hook = proxy_logging_obj.needs_per_chunk_streaming_hook() - is_raw_sse_stream = bool( - request_data.get("_litellm_raw_sse_stream") # any-ok: untyped stream - ) + is_raw_sse_stream = bool(request_data.get("_litellm_raw_sse_stream")) raw_sse_buffer = "" if needs_iterator_wrap: @@ -7121,26 +7121,26 @@ async def async_data_generator( frame, raw_sse_buffer = _pop_complete_sse_frame(raw_sse_buffer) if frame is None: break - yield frame # any-ok: untyped stream + yield frame if len(raw_sse_buffer) > _MAX_RAW_SSE_BUFFER_CHARS: raise ValueError( "Raw SSE stream exceeded maximum buffered size without a frame delimiter" ) continue if chunk.startswith(("data:", "event:", ":")): - yield ( # any-ok: untyped stream + yield ( chunk if chunk.endswith(_SSE_FRAME_DELIMITERS) else chunk + "\n\n" ) continue - elif isinstance(chunk, str) and is_raw_sse_stream: # any-ok: untyped stream + elif isinstance(chunk, str) and is_raw_sse_stream: raw_sse_buffer += chunk while True: frame, raw_sse_buffer = _pop_complete_sse_frame(raw_sse_buffer) if frame is None: break - yield frame # any-ok: untyped stream + yield frame if len(raw_sse_buffer) > _MAX_RAW_SSE_BUFFER_CHARS: raise ValueError( "Raw SSE stream exceeded maximum buffered size without a frame delimiter" @@ -7163,7 +7163,7 @@ async def async_data_generator( ProxyLogging._fire_deferred_stream_logging(request_data) if raw_sse_buffer: - yield ( # any-ok: untyped stream + yield ( raw_sse_buffer if raw_sse_buffer.endswith(_SSE_FRAME_DELIMITERS) else raw_sse_buffer + "\n\n" @@ -7229,8 +7229,8 @@ async def async_data_generator( await ProxyBaseLLMRequestProcessing._finalize_streaming_generator_cleanup( request=request, - request_data=request_data, # any-ok: untyped stream - response=response, # any-ok: untyped stream + request_data=request_data, + response=response, stream_completed=stream_completed, client_disconnected=client_disconnected, ) @@ -7351,10 +7351,8 @@ class ProxyStartupEvent: from litellm._redis import _redis_kwargs_from_environment from litellm.secret_managers.main import str_to_bool - _use_redis_transaction_buffer: bool | str | None = ( - general_settings.get( # any-ok: untyped stream - "use_redis_transaction_buffer", False - ) + _use_redis_transaction_buffer: bool | str | None = general_settings.get( + "use_redis_transaction_buffer", False ) if isinstance(_use_redis_transaction_buffer, str): _use_redis_transaction_buffer = str_to_bool(_use_redis_transaction_buffer) @@ -7362,14 +7360,11 @@ class ProxyStartupEvent: if not _use_redis_transaction_buffer: return None - redis_env_kwargs = _redis_kwargs_from_environment() # any-ok: untyped stream - if ( - "host" not in redis_env_kwargs # any-ok: untyped stream - and "url" not in redis_env_kwargs # any-ok: untyped stream - ): + redis_env_kwargs = _redis_kwargs_from_environment() + if "host" not in redis_env_kwargs and "url" not in redis_env_kwargs: return None - return RedisCache(**redis_env_kwargs) # any-ok: untyped stream + return RedisCache(**redis_env_kwargs) @classmethod async def _initialize_semantic_tool_filter( @@ -8376,6 +8371,8 @@ async def model_list( """ global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj + settings = cast(dict[str, object], general_settings) # any-ok: legacy settings + from litellm.proxy.management_endpoints.common_utils import ( _user_has_admin_privileges, ) @@ -8455,16 +8452,21 @@ async def model_list( if hidden_names: all_models = [m for m in all_models if m not in hidden_names] - # Build response data with all proxy models + # Surface the public team name by default; legacy internal keys via flag. + # The internal routing key drives the metadata/fallback lookup, while the + # public name is what the client sees as the model id. model_data = [] - for model in all_models: + for response_id, lookup_id in TeamModelNameTranslator.listing_entries( + all_models, llm_router, settings + ): model_info = create_model_info_response( - model_id=model, + model_id=lookup_id, provider="openai", include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, ) + model_info["id"] = response_id model_data.append(model_info) return dict( @@ -8492,16 +8494,21 @@ async def model_list( if hidden_names: all_models = [m for m in all_models if m not in hidden_names] - # Build response data + # Surface the public team name by default; legacy internal keys via flag. + # The internal routing key drives the metadata/fallback lookup, while the + # public name is what the client sees as the model id. model_data = [] - for model in all_models: + for response_id, lookup_id in TeamModelNameTranslator.listing_entries( + all_models, llm_router, settings + ): model_info = create_model_info_response( - model_id=model, + model_id=lookup_id, provider="openai", include_metadata=include_metadata or False, fallback_type=fallback_type, llm_router=llm_router, ) + model_info["id"] = response_id model_data.append(model_info) return dict( @@ -8523,6 +8530,8 @@ async def model_list( async def model_info( model_id: str, user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), + team_id: Optional[str] = None, + healthy_only: Optional[bool] = False, ): """ Retrieve information about a specific model accessible to your API key. @@ -8532,16 +8541,21 @@ async def model_info( Follows OpenAI API specification for individual model retrieval. https://platform.openai.com/docs/api-reference/models/retrieve + + Query parameters mirror `/v1/models` so the same caller context (team + scoping, health filtering, paused deployments) drives both endpoints; the + listing's public id must resolve to the same internal deployment here. """ global llm_model_list, general_settings, llm_router, prisma_client, user_api_key_cache, proxy_logging_obj + settings = cast(dict[str, object], general_settings) # any-ok: legacy settings + from litellm.proxy.utils import ( create_model_info_response, get_available_models_for_user, validate_model_access, ) - # Get available models for the user all_models = await get_available_models_for_user( user_api_key_dict=user_api_key_dict, llm_router=llm_router, @@ -8549,21 +8563,43 @@ async def model_info( user_model=user_model, prisma_client=prisma_client, proxy_logging_obj=proxy_logging_obj, - team_id=None, + team_id=team_id, include_model_access_groups=False, only_model_access_groups=False, return_wildcard_routes=False, user_api_key_cache=user_api_key_cache, ) + # Mirror /v1/models' visibility filter so first-occurrence resolution + # cannot land on a deployment the listing had hidden. + blocked_names = ( + llm_router.get_fully_blocked_model_names() if llm_router is not None else set() + ) + unhealthy_names: set[str] = set() + if healthy_only and llm_router is not None: + unhealthy_names = await llm_router.async_get_fully_unhealthy_model_names() + hidden_names = blocked_names | unhealthy_names + if hidden_names: + all_models = [m for m in all_models if m not in hidden_names] + + internal_to_public = TeamModelNameTranslator.build_internal_to_public_map( + llm_router, settings + ) + resolved_model_id = TeamModelNameTranslator.resolve_public_name( + model_id=model_id, + available_models=all_models, + llm_router=llm_router, + general_settings=settings, + ) + # Validate that the requested model is accessible - validate_model_access(model_id=model_id, available_models=all_models) + validate_model_access(model_id=resolved_model_id, available_models=all_models) # Get provider information from the router deployment if llm_router is None: raise HTTPException(status_code=500, detail="Router not initialized") - deployment = llm_router.get_deployment_by_model_group_name(model_id) + deployment = llm_router.get_deployment_by_model_group_name(resolved_model_id) if deployment is None: raise HTTPException( status_code=404, @@ -8573,9 +8609,9 @@ async def model_info( # Use the actual litellm model from the deployment to get provider info _, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model) - # Return the model information in the same format as the list endpoint + response_id = internal_to_public.get(resolved_model_id, model_id) return create_model_info_response( - model_id=model_id, + model_id=response_id, provider=provider, include_metadata=False, fallback_type=None, diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 7e225c6cd1c..451c32b334d 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -46,6 +46,7 @@ from litellm.proxy._types import ( ) from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error from litellm.types.guardrails import GuardrailEventHooks +from litellm.types.proxy.model_listing import ModelInfoResponse from litellm.types.utils import CallTypes, CallTypesLiteral try: @@ -6311,56 +6312,39 @@ def create_model_info_response( include_metadata: bool = False, fallback_type: Optional[str] = None, llm_router: Optional["Router"] = None, -) -> dict: +) -> ModelInfoResponse: """ - Create a standardized model info response. + Create a standardized OpenAI-compatible model object. - Args: - model_id: The model ID - provider: The model provider - include_metadata: Whether to include metadata - fallback_type: Type of fallbacks to include - llm_router: LiteLLM router instance - - Returns: - Dictionary containing model information + When include_metadata is true, attaches the model's configured fallbacks + (resolved via the router under fallback_type, defaulting to "general"). + Raises HTTPException(400) for an unknown fallback_type. """ from litellm.proxy.auth.model_checks import get_all_fallbacks - model_info = { + base: ModelInfoResponse = { "id": model_id, "object": "model", "created": DEFAULT_MODEL_CREATED_AT_TIME, "owned_by": provider, } + if not include_metadata: + return base - # Add metadata if requested - if include_metadata: - metadata = {} - - # Default fallback_type to "general" if include_metadata is true - effective_fallback_type = ( - fallback_type if fallback_type is not None else "general" + effective_fallback_type = fallback_type if fallback_type is not None else "general" + valid_fallback_types = ("general", "context_window", "content_policy") + if effective_fallback_type not in valid_fallback_types: + raise HTTPException( + status_code=400, + detail=f"Invalid fallback_type. Must be one of: {list(valid_fallback_types)}", ) - # Validate fallback_type - valid_fallback_types = ["general", "context_window", "content_policy"] - if effective_fallback_type not in valid_fallback_types: - raise HTTPException( - status_code=400, - detail=f"Invalid fallback_type. Must be one of: {valid_fallback_types}", - ) - - fallbacks = get_all_fallbacks( - model=model_id, - llm_router=llm_router, - fallback_type=effective_fallback_type, - ) - metadata["fallbacks"] = fallbacks - - model_info["metadata"] = metadata - - return model_info + fallbacks = get_all_fallbacks( + model=model_id, + llm_router=llm_router, + fallback_type=effective_fallback_type, + ) + return {**base, "metadata": {"fallbacks": fallbacks}} def validate_model_access( diff --git a/litellm/router_utils/fallback_event_handlers.py b/litellm/router_utils/fallback_event_handlers.py index bc01a894e1d..eb756e3cf8b 100644 --- a/litellm/router_utils/fallback_event_handlers.py +++ b/litellm/router_utils/fallback_event_handlers.py @@ -244,16 +244,12 @@ def _check_non_standard_fallback_format(fallbacks: Optional[List[Any]]) -> bool: if all(isinstance(item, str) for item in fallbacks): return True elif all(isinstance(item, dict) for item in fallbacks): - for item in fallbacks: # any-ok: untyped config - for ( - key - ) in ( - LiteLLMParamsTypedDict.__annotations__.keys() # any-ok: untyped config - ): - if key in item: # any-ok: untyped config + for item in fallbacks: + for key in LiteLLMParamsTypedDict.__annotations__.keys(): + if key in item: # If the value is a list, it's likely a standard fallback model group mapping # (e.g. {"model": ["backup"]}) rather than a parameter override. - if not isinstance(item[key], list): # any-ok: untyped config + if not isinstance(item[key], list): return True return False diff --git a/litellm/secret_managers/aws_secret_manager_v2.py b/litellm/secret_managers/aws_secret_manager_v2.py index 299217f14b2..ef3c821caf1 100644 --- a/litellm/secret_managers/aws_secret_manager_v2.py +++ b/litellm/secret_managers/aws_secret_manager_v2.py @@ -321,13 +321,13 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): ) try: - response = await async_client.post( # any-ok: untyped httpx + response = await async_client.post( url=endpoint_url, - headers=headers, # any-ok: untyped httpx - data=body.decode("utf-8"), # any-ok: untyped httpx + headers=headers, + data=body.decode("utf-8"), ) - response.raise_for_status() # any-ok: untyped httpx - create_response = response.json() # any-ok: untyped httpx + response.raise_for_status() + create_response = response.json() except httpx.HTTPStatusError as err: raise ValueError(f"HTTP error occurred: {err.response.text}") except httpx.TimeoutException: @@ -338,7 +338,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): await self.async_replicate_secret( secret_name=secret_name, replica_regions=self.replica_regions, - optional_params=optional_params, # any-ok: untyped httpx + optional_params=optional_params, timeout=timeout, ) verbose_logger.debug( @@ -354,7 +354,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): str(replication_err), ) - return create_response # any-ok: untyped httpx + return create_response async def async_replicate_secret( self, @@ -392,7 +392,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): "AddReplicaRegions": [{"Region": r} for r in replica_regions], } - endpoint_url, headers, body = self._prepare_request( # any-ok: untyped httpx + endpoint_url, headers, body = self._prepare_request( action="ReplicateSecretToRegions", secret_name=secret_name, optional_params=optional_params, @@ -401,7 +401,7 @@ class AWSSecretsManagerV2(BaseAWSLLM, BaseSecretManager): async_client = get_async_httpx_client( llm_provider=httpxSpecialProvider.SecretManager, - params={"timeout": timeout}, # any-ok: untyped httpx + params={"timeout": timeout}, ) try: diff --git a/litellm/types/proxy/model_listing.py b/litellm/types/proxy/model_listing.py new file mode 100644 index 00000000000..c3330da0d66 --- /dev/null +++ b/litellm/types/proxy/model_listing.py @@ -0,0 +1,21 @@ +"""Response types for the model listing/retrieve endpoints (/v1/models, /models).""" + +from typing import Literal + +from typing_extensions import NotRequired, TypedDict + + +class ModelInfoMetadata(TypedDict): + fallbacks: list[str] + + +class ModelInfoResponse(TypedDict): + """OpenAI-compatible model object. `metadata` is present only when the + endpoint is called with include_metadata=true. + """ + + id: str + object: Literal["model"] + created: int + owned_by: str + metadata: NotRequired[ModelInfoMetadata] diff --git a/litellm/utils.py b/litellm/utils.py index 30b5691a140..916260cab5a 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -6043,13 +6043,13 @@ def _get_model_info_helper( cache_read_input_token_cost_above_200k_tokens=_model_info.get( "cache_read_input_token_cost_above_200k_tokens", None ), - cache_read_input_token_cost_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + cache_read_input_token_cost_above_200k_tokens_priority=_model_info.get( "cache_read_input_token_cost_above_200k_tokens_priority", None ), cache_read_input_token_cost_above_272k_tokens=_model_info.get( "cache_read_input_token_cost_above_272k_tokens", None ), - cache_read_input_token_cost_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + cache_read_input_token_cost_above_272k_tokens_priority=_model_info.get( "cache_read_input_token_cost_above_272k_tokens_priority", None ), cache_read_input_token_cost_above_512k_tokens=_model_info.get( @@ -6073,13 +6073,13 @@ def _get_model_info_helper( input_cost_per_token_above_200k_tokens=_model_info.get( "input_cost_per_token_above_200k_tokens", None ), - input_cost_per_token_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + input_cost_per_token_above_200k_tokens_priority=_model_info.get( "input_cost_per_token_above_200k_tokens_priority", None ), input_cost_per_token_above_272k_tokens=_model_info.get( "input_cost_per_token_above_272k_tokens", None ), - input_cost_per_token_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + input_cost_per_token_above_272k_tokens_priority=_model_info.get( "input_cost_per_token_above_272k_tokens_priority", None ), input_cost_per_token_above_512k_tokens=_model_info.get( @@ -6137,13 +6137,13 @@ def _get_model_info_helper( output_cost_per_token_above_200k_tokens=_model_info.get( "output_cost_per_token_above_200k_tokens", None ), - output_cost_per_token_above_200k_tokens_priority=_model_info.get( # any-ok: untyped cost map + output_cost_per_token_above_200k_tokens_priority=_model_info.get( "output_cost_per_token_above_200k_tokens_priority", None ), output_cost_per_token_above_272k_tokens=_model_info.get( "output_cost_per_token_above_272k_tokens", None ), - output_cost_per_token_above_272k_tokens_priority=_model_info.get( # any-ok: untyped cost map + output_cost_per_token_above_272k_tokens_priority=_model_info.get( "output_cost_per_token_above_272k_tokens_priority", None ), output_cost_per_token_above_512k_tokens=_model_info.get( diff --git a/mypy-code-budget.json b/mypy-code-budget.json deleted file mode 100644 index 2cae0d661e9..00000000000 --- a/mypy-code-budget.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "import-not-found": { - "baseline": 8, - "slack": 3 - }, - "no-any-return": { - "baseline": 902, - "slack": 10 - }, - "no-untyped-def": { - "baseline": 4888, - "slack": 10 - }, - "valid-type": { - "baseline": 1, - "slack": 3 - } -} diff --git a/pyproject.toml b/pyproject.toml index 8b1386aaf87..8ee2840b573 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -148,7 +148,6 @@ dev = [ "diff-cover==9.7.2", "flake8==7.3.0", "black==26.3.1", - "mypy==1.19.0", "basedpyright==1.39.7", "pytest==9.0.3", "pytest-mock==3.15.1", @@ -261,8 +260,6 @@ source-exclude = [ "litellm/proxy/enterprise", "**/__pycache__", "**/__pycache__/**", - "**/.mypy_cache", - "**/.mypy_cache/**", "**/.pytest_cache", "**/.pytest_cache/**", "**/.ruff_cache", @@ -278,9 +275,6 @@ version_files = [ "pyproject.toml:^version", ] -[tool.mypy] -plugins = "pydantic.mypy" - [tool.pytest.ini_options] asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "session" diff --git a/scripts/budget_ratchet_check.py b/scripts/budget_ratchet_check.py index 6406b0d888e..861d65489e8 100644 --- a/scripts/budget_ratchet_check.py +++ b/scripts/budget_ratchet_check.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Non-gating ratchet guard: budget ceilings may only fall, never rise. -Every `*-budget.json` file (ruff-strict, type-discipline, mypy-code, basedpyright-code, -any-discipline) is a one-way ratchet: each rule's ceiling is `baseline + slack`, and the whole point is +Every `*-budget.json` file (ruff-strict, type-discipline, basedpyright-code) is a +one-way ratchet: each rule's ceiling is `baseline + slack`, and the whole point is to drive that number DOWN over time. This check compares every budget file against its own content at the merge-base with the target branch and fails (exits 1, red) if: @@ -12,12 +12,6 @@ its own content at the merge-base with the target branch and fails (exits 1, red New rules and lowered/equal ceilings are fine. -The any-discipline budget is keyed by file rather than rule: its gate treats an -absent file as ceiling 0 (the file must be Any-free), so an entry vanishing means -that file was cleaned to zero -- a tightening, and exactly the cleanup this -ratchet exists to encourage. Such a budget is therefore exempt from the -dropped-entry rule (a raised ceiling is still caught). - This is deliberately NOT a gating check. It should turn the run red so that a loosening is impossible to miss in review, but it must stay OUT of the branch-protection required-checks list: a justified bump (e.g. banning a new API, @@ -44,17 +38,9 @@ DEFAULT_BASE = "origin/litellm_internal_staging" DEFAULT_BUDGETS: tuple[str, ...] = ( "ruff-strict-budget.json", "type-discipline-budget.json", - "mypy-code-budget.json", "basedpyright-code-budget.json", - "any-discipline-budget.json", ) -# File-keyed budgets whose gate treats an absent entry as ceiling 0 (the file -# must stay clean). Dropping an entry there is a tightening, not the "untracked, -# now unbounded" loosening a vanished rule is for the rule-keyed budgets, so a -# dropped entry must not read as a regression. -ZERO_FLOOR_BUDGETS: frozenset[str] = frozenset({"any-discipline-budget.json"}) - class Regression(NamedTuple): budget: str @@ -112,15 +98,17 @@ def regressions_for(rel: str, base: dict | None, head: dict | None) -> list[Regr base_caps = _caps(base) head_caps = _caps(head) - drop_floors_to_zero = rel in ZERO_FLOOR_BUDGETS - out: list[Regression] = [] - for rule, base_cap in sorted(base_caps.items()): - if rule not in head_caps: - if not drop_floors_to_zero: - out.append(Regression(rel, rule, f"rule dropped (ceiling {base_cap} -> removed)")) - elif head_caps[rule] > base_cap: - out.append(Regression(rel, rule, f"ceiling raised {base_cap} -> {head_caps[rule]}")) - return out + return [ + Regression( + rel, + rule, + f"rule dropped (ceiling {base_cap} -> removed)" + if rule not in head_caps + else f"ceiling raised {base_cap} -> {head_caps[rule]}", + ) + for rule, base_cap in sorted(base_caps.items()) + if rule not in head_caps or head_caps[rule] > base_cap + ] def main() -> int: diff --git a/scripts/check_any_discipline.py b/scripts/check_any_discipline.py deleted file mode 100644 index 5b8c83e63e0..00000000000 --- a/scripts/check_any_discipline.py +++ /dev/null @@ -1,778 +0,0 @@ -#!/usr/bin/env python3 -"""Any-discipline gate: fail when a changed file exceeds its `Any` budget. - -Where ruff, `mypy --strict`, and even basedpyright's `reportAny` stop short, this -catches the case that actually bites: a *union* hiding an `Any`. For example -`re.Match.group()` -> `str | Any`, `json.loads()` -> `Any`, and bare `list`/`dict` --> `list[Any]`/`dict[..., Any]`. Any value whose inferred type *contains* `Any` -(recursively, through unions / generics / tuples) is reported. - -Scope: changed files, per-file budget -------------------------------------- -litellm carries a large amount of pre-existing `Any` (a single legacy file can -have >100 findings). Rather than force every touched line clean (the original -changed-lines rule, which tripped on merely *editing* a legacy `X | Any` line), -this gate grandfathers each file: `any-discipline-budget.json` records every -file's current count of Any-typed values, and a file fails only when its count -exceeds `baseline + slack`, where `slack` is 50% headroom (rounded up). New or -unbudgeted files have baseline 0, so they stay airtight. - -Only *changed* files (vs the merge-base with `--base`) are re-type-checked -- an -unchanged file's count can't move from edits this branch didn't make -- so the -per-PR cost equals re-checking just those files, exactly like the original -changed-lines gate. The whole-tree scan needed to (re)capture the budget -(~2 min, ~3 GB) runs only under `--update`. - -The budget is a one-way ratchet (the same `{baseline, slack}` shape as the -ruff / mypy / basedpyright budgets) guarded by `scripts/budget_ratchet_check.py`: -a file's ceiling may fall but never rise. Drive a file's count down and rerun -`--update` (`make lint-any-budget-update`) to lock in the lower ceiling. - -How it works ------------- -It loads `litellm/mypy.ini` (the same config `make lint-mypy` uses, so findings -match what developers already see), builds the changed files with mypy asking for -its exported expression->type map, and walks each file's AST applying a recursive -"contains Any" predicate -- the test `mypy --disallow-any-expr` uses internally -but applies inconsistently (python/mypy#12856). - -mypy only re-exports types for modules it re-type-checks, so for each target we -invalidate just its cached hash (deps stay warm) to force a fast re-check against -a persisted incremental cache (.mypy_cache_any). - -Rules ------ -Codes share the `LIT***` namespace with `scripts/check_type_discipline.py` (PR -#30500), which owns LIT001/002/003/004/006/007/008. This gate claims the rest: -LIT009 A value expression's inferred type is, or contains, `Any`. Budgeted - per file (a file fails when its count exceeds `baseline + slack`). - Suppress an individual line with `# any-ok: `. -LIT005 An `# any-ok` suppression without a reason (the shared - suppression-needs-a-reason code, same as `# cast-ok` / `# guard-ok`). -LIT000 Setup failure: mypy could not build, or a target file could not be read. - -`Any`s produced purely by an already-reported error, and the special-form / -implementation-artifact internal `Any`s, are ignored. A bound method *reference* -whose signature mentions `Any` is not flagged -- only the value its call produces. - -Usage ------ - # gate mode (CI / pre-push): per-file Any budget on changed files - uv run --no-sync python scripts/check_any_discipline.py --changed --base origin/litellm_internal_staging - - # re-capture the per-file budget across the whole tree (ratchet) - uv run --no-sync python scripts/check_any_discipline.py --update - - # whole-file spot-check (no budget, no line filter), paths relative to repo root - uv run --no-sync python scripts/check_any_discipline.py litellm/budget_manager.py - -Exit code 1 if a file is over budget (or a hard rule trips), 2 on a setup error. -""" - -from __future__ import annotations - -import argparse -import json -import os -import re -import subprocess -import sys -import tokenize -from collections.abc import Callable, Iterable, Sequence -from pathlib import Path -from typing import NamedTuple - -try: - from mypy import build - from mypy.config_parser import parse_config_file - from mypy.find_sources import create_source_list - from mypy.fscache import FileSystemCache - from mypy.modulefinder import BuildSource - from mypy.nodes import AssignmentStmt, Expression, NameExpr, Node, TempNode - from mypy.options import Options - from mypy.types import ( - AnyType, - CallableType, - Instance, - Overloaded, - TupleType, - Type, - TypeOfAny, - UnionType, - get_proper_type, - ) -except ImportError: # pragma: no cover - environment guard - sys.stderr.write( - "check_any_discipline: mypy is not importable in this interpreter.\n" - "Run it through the project environment, e.g.\n" - " uv run --no-sync python scripts/check_any_discipline.py --changed\n" - ) - raise SystemExit(2) - - -REPO_ROOT = Path(__file__).resolve().parent.parent -LITELLM_DIR = REPO_ROOT / "litellm" -MYPY_INI = LITELLM_DIR / "mypy.ini" -CACHE_DIR = REPO_ROOT / ".mypy_cache_any" -PY_TAG = f"{sys.version_info.major}.{sys.version_info.minor}" -DEFAULT_BASE = "origin/litellm_internal_staging" -BUDGET_PATH = REPO_ROOT / "any-discipline-budget.json" - -MIN_REASON_LEN = 3 -ANY_OK_RE = re.compile(r"#\s*any-ok(?::\s*(?P.*))?") -_HUNK_RE = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") - -# Files allowed to surface `Any` (the typed/untyped boundary). A finding is -# skipped if any fragment below is a substring of the file's posix path. Keep -# this tight -- prefer a line-level `# any-ok: ` over a blanket exemption. -BOUNDARY_PATHS: frozenset[str] = frozenset() - -# `Any` kinds that are not actionable: produced by an already-reported error, or -# an internal placeholder that never corresponds to a concrete runtime value. -# NOTE: `special_form` is deliberately NOT here. In mypy 1.19 the `Any` in -# typeshed unions like `re.Match.group() -> str | Any` is tagged `special_form`, -# and that union is the headline case this gate exists to catch. -_HARMLESS_ANY = frozenset( - kind - for kind in ( - TypeOfAny.from_error, - getattr(TypeOfAny, "implementation_artifact", None), - ) - if kind is not None -) - -# AST attributes that point OUTSIDE the syntactic subtree (a RefExpr's resolved -# definition, a node's TypeInfo). Skipping exactly these two makes a generic -# child-walk equivalent to mypy's TraverserVisitor -- validated to the node -# against ExtendedTraverserVisitor across the full grammar (see commit notes). -_NON_SYNTACTIC_ATTRS = frozenset({"node", "info"}) - -# Awaitable / coroutine / generator instances carry synthetic `Any` in their -# send (and, for coroutines, yield) protocol slots: `async def f() -> float` -# produces `Coroutine[Any, Any, float]`, so the bare call expression `f()` would -# be flagged even though the awaited value is a clean `float`. Only the args that -# hold a value the caller observes (the awaited result, the yielded item) are -# meaningful; a real `Any` there -- e.g. a coroutine that returns `Any` -- is -# still caught because that index is still checked. -_SYNTHETIC_SEND_YIELD_VALUE_ARGS: dict[str, tuple[int, ...]] = { - "typing.Coroutine": (2,), - "typing.Generator": (0, 2), - "typing.AsyncGenerator": (0,), -} - - -class Violation(NamedTuple): - path: Path - line: int - col: int - code: str - message: str - - def render(self) -> str: - return f"{self.path}:{self.line}:{self.col}: {self.code} {self.message}" - - -# --------------------------------------------------------------------------- # -# The "contains Any" predicate -# --------------------------------------------------------------------------- # - - -# Recursive type aliases (e.g. a JSON-like `T = Union[..., list[T], dict[str, T]]`) -# make `get_proper_type` yield a fresh object at every unfold, so an id()-based -# cycle guard never trips and a naive recursion overflows the stack. We walk -# iteratively and cap the depth: a real `Any` lives at shallow depth in the -# alias's definition, so a deep alias that has not produced one by `_MAX_DEPTH` -# never will. (The changed-lines gate never hit this; a whole-tree scan does.) -_MAX_DEPTH = 100 - - -def contains_any(t: Type) -> bool: - """True if a *value* of type ``t`` carries `Any` anywhere meaningful.""" - seen: set[int] = set() - stack: list[tuple[Type, int]] = [(t, 0)] - while stack: - cur, depth = stack.pop() - if depth > _MAX_DEPTH: - continue - p = get_proper_type(cur) - if id(p) in seen: - continue - seen.add(id(p)) - - # A function/method *reference* whose signature mentions Any is not itself - # an unsafe value -- only its eventual call result is. Don't recurse in. - if isinstance(p, (CallableType, Overloaded)): - continue - if isinstance(p, AnyType): - if p.type_of_any not in _HARMLESS_ANY: - return True - continue - if isinstance(p, UnionType): - stack.extend((item, depth + 1) for item in p.items) - elif isinstance(p, Instance): - value_arg_indices = _SYNTHETIC_SEND_YIELD_VALUE_ARGS.get(p.type.fullname) - if value_arg_indices is None: - stack.extend((arg, depth + 1) for arg in p.args) - else: - stack.extend( - (p.args[index], depth + 1) - for index in value_arg_indices - if index < len(p.args) - ) - elif isinstance(p, TupleType): - stack.extend((item, depth + 1) for item in p.items) - return False - - -# --------------------------------------------------------------------------- # -# Generic, leak-free AST walk (works under a mypyc-compiled mypy, which forbids -# subclassing TraverserVisitor) -# --------------------------------------------------------------------------- # - - -def _walk_file(tree: Node) -> tuple[list[Expression], set[int]]: - """Return (every Expression in `tree`, ids of simple assignment-target names). - - The walk follows only syntactic children (every attribute except the two - non-syntactic back-references), so it never escapes the module. Simple - ``x = `` name targets are collected separately so we don't double-report - the assigned name as an echo of an Any rvalue. - """ - exprs: list[Expression] = [] - skip_lvalues: set[int] = set() - stack: list[object] = [tree] - seen: set[int] = set() - while stack: - n = stack.pop() - if isinstance(n, Node): - if id(n) in seen: - continue - seen.add(id(n)) - if isinstance(n, Expression): - exprs.append(n) - if isinstance(n, AssignmentStmt): - for lvalue in n.lvalues: - if isinstance(lvalue, NameExpr): - skip_lvalues.add(id(lvalue)) - for name in dir(n): - if name.startswith("__") or name in _NON_SYNTACTIC_ATTRS: - continue - try: - val = getattr(n, name) - except Exception: - continue - if callable(val): - continue - if isinstance(val, (Node, list, tuple)): - stack.append(val) - elif isinstance(n, (list, tuple)): - stack.extend(n) - return exprs, skip_lvalues - - -def find_any_in_tree(tree: Node, idmap: dict[int, Type]) -> list[tuple[int, int, str]]: - exprs, skip_lvalues = _walk_file(tree) - findings: list[tuple[int, int, str]] = [] - for expr in exprs: - # A TempNode is mypy's synthetic placeholder for a position with no real - # expression -- e.g. the rvalue of an annotation-only `field: T` in a - # TypedDict / class body, whose `special_form` `Any` is not a value the - # author wrote. It never corresponds to a runtime value, so skip it. - if id(expr) in skip_lvalues or isinstance(expr, TempNode): - continue - t = idmap.get(id(expr)) - if t is not None and contains_any(t): - findings.append((expr.line, expr.column, str(get_proper_type(t)))) - - out: list[tuple[int, int, str]] = [] - seen_pos: set[tuple[int, int]] = set() - for line, col, typ in sorted(findings): - if line < 1 or (line, col) in seen_pos: - continue - seen_pos.add((line, col)) - out.append((line, col, typ)) - return out - - -# --------------------------------------------------------------------------- # -# Comment scanning (LIT005 + any-ok suppression) -# --------------------------------------------------------------------------- # - - -def _reason_ok(reason: str | None) -> bool: - return reason is not None and len(reason.strip()) >= MIN_REASON_LEN - - -def scan_any_ok( - path: Path, source: str -) -> tuple[frozenset[int], tuple[Violation, ...]]: - """Return (lines with a valid any-ok suppression, LIT005 violations).""" - try: - tokens = tokenize.generate_tokens( - iter(source.splitlines(keepends=True)).__next__ - ) - comments = tuple( - (t.start[0], t.string) for t in tokens if t.type == tokenize.COMMENT - ) - except tokenize.TokenError: - return frozenset(), () - - ok_lines: set[int] = set() - violations: list[Violation] = [] - for line, text in comments: - m = ANY_OK_RE.search(text) - if m is None: - continue - if _reason_ok(m.group("reason")): - ok_lines.add(line) - else: - violations.append( - Violation( - path, - line, - 0, - "LIT005", - "any-ok requires a reason: `# any-ok: `", - ) - ) - return frozenset(ok_lines), tuple(violations) - - -# --------------------------------------------------------------------------- # -# mypy build (parity with `make lint-mypy`) + forced target re-check -# --------------------------------------------------------------------------- # - - -def _build_options() -> Options: - opts = Options() - if MYPY_INI.exists(): - parse_config_file(opts, lambda: None, str(MYPY_INI), sys.stdout, sys.stderr) - opts.export_types = True - opts.preserve_asts = True - opts.incremental = True - opts.cache_dir = str(CACHE_DIR) - opts.show_traceback = False - return opts - - -def _meta_path(module: str) -> Path: - return CACHE_DIR / PY_TAG / (module.replace(".", os.sep) + ".meta.json") - - -def _force_recheck(sources: Sequence[BuildSource]) -> None: - """Invalidate each target's cached entry so mypy re-type-checks (and thus - re-exports types + preserves the AST for) exactly these modules, while their - dependencies stay warm. A missing entry is a cold build for that module. - - mypy trusts a cache entry whenever the source mtime matches the cached one - (it never re-hashes on that fast path), so we must break BOTH: zero the - cached mtime to force a re-hash, and corrupt the cached hash so the re-hash - mismatches and the module is treated as changed.""" - for src in sources: - if not src.module: - continue - meta = _meta_path(src.module) - if not meta.exists(): - continue - try: - data = json.loads(meta.read_text()) - data["hash"] = "0" * 40 - data["mtime"] = 0 - meta.write_text(json.dumps(data)) - except (OSError, ValueError): - continue - - -def check_files(rel_paths: Sequence[str]) -> tuple[Violation, ...]: - """`rel_paths` are relative to the litellm package dir (the build cwd).""" - prev_cwd = Path.cwd() - os.chdir(LITELLM_DIR) - try: - opts = _build_options() - fscache = FileSystemCache() - sources = create_source_list(list(rel_paths), opts, fscache) - _force_recheck(sources) - try: - res = build.build(sources, options=opts, fscache=fscache) - except build.CompileError as exc: - joined = "; ".join(exc.messages[:3]) or "blocking error" - return ( - Violation( - Path(rel_paths[0]), - 0, - 0, - "LIT000", - f"mypy could not build: {joined}", - ), - ) - idmap = {id(expr): t for expr, t in res.types.items()} - # Resolve trees to absolute source paths while cwd is the build dir, since - # mypy stores the paths it was given (relative to this cwd). - trees: dict[str, Node] = {} - for state in res.graph.values(): - if state.path and state.tree is not None: - trees[os.path.realpath(state.path)] = state.tree - finally: - os.chdir(prev_cwd) - - out: list[Violation] = [] - for rel in rel_paths: - abs_path = (LITELLM_DIR / rel).resolve() - report_path = abs_path.relative_to(REPO_ROOT) - if _is_boundary(report_path): - continue - try: - source = abs_path.read_text(encoding="utf-8") - except (OSError, UnicodeDecodeError) as exc: - out.append( - Violation(report_path, 0, 0, "LIT000", f"could not read file: {exc}") - ) - continue - - ok_lines, ok_violations = scan_any_ok(report_path, source) - out.extend(ok_violations) - tree = trees.get(os.path.realpath(abs_path)) - if tree is None: - continue - for line, col, typ in find_any_in_tree(tree, idmap): - if line in ok_lines: - continue - out.append( - Violation( - report_path, - line, - col, - "LIT009", - f"value type contains Any -> {typ}", - ) - ) - return tuple(out) - - -# --------------------------------------------------------------------------- # -# File selection (changed-only, changed-lines) + driver -# --------------------------------------------------------------------------- # - - -class _AllLines: - """Sentinel: a wholly new / untracked file -- every line is in scope. - - A distinct object, not None, so that `line_map.get(path)` returning None for - a path absent from the map is never mistaken for "whole file in scope".""" - - -# A changed file's in-scope lines: a specific set, or every line. -LineScope = set[int] | _AllLines -ALL_LINES = _AllLines() - - -def _is_boundary(path: Path) -> bool: - posix = path.as_posix() - return any(frag in posix for frag in BOUNDARY_PATHS) - - -def _git(*args: str) -> list[str]: - result = subprocess.run( - ["git", "-C", str(REPO_ROOT), *args], - capture_output=True, - text=True, - check=True, - ) - return result.stdout.splitlines() - - -def _parse_added_lines(diff_text: str) -> dict[str, set[int]]: - """Map repo-relative path -> set of new-file line numbers the diff adds/edits.""" - changed: dict[str, set[int]] = {} - path: str | None = None - for line in diff_text.splitlines(): - if line.startswith("+++ b/"): - path = line[6:] - elif path and (m := _HUNK_RE.match(line)): - start = int(m.group(1)) - count = int(m.group(2)) if m.group(2) is not None else 1 - if count: - changed.setdefault(path, set()).update(range(start, start + count)) - return changed - - -def changed_line_map(base: str) -> dict[str, LineScope] | None: - """Repo-relative `.py` path under litellm/ -> changed line numbers (or - ALL_LINES for untracked files). Compares the working tree to the merge-base - with `base`, so it covers committed-on-branch + unstaged edits. None if git - is unavailable / not a repo.""" - try: - merge_base = _git("merge-base", base, "HEAD") - point = merge_base[0].strip() if merge_base else base - diff = "\n".join( - _git( - "diff", - "--unified=0", - "--no-color", - "--diff-filter=d", - point, - "--", - "litellm", - ) - ) - untracked = _git("ls-files", "--others", "--exclude-standard", "--", "litellm") - except (subprocess.CalledProcessError, FileNotFoundError): - return None - - out: dict[str, LineScope] = {} - for name, lines in _parse_added_lines(diff).items(): - if name.endswith(".py") and (REPO_ROOT / name).exists(): - out[name] = lines - for name in untracked: - if name.endswith(".py") and (REPO_ROOT / name).exists(): - out[name] = ALL_LINES - return out - - -def _to_litellm_relative(paths: Iterable[Path]) -> list[str]: - rels: list[str] = [] - for p in sorted(paths): - try: - rels.append(p.resolve().relative_to(LITELLM_DIR).as_posix()) - except ValueError: - continue - return rels - - -def _in_scope(v: Violation, line_map: dict[str, LineScope] | None) -> bool: - """A finding survives if line filtering is off (explicit paths), it's a build - error, or its line is one the diff added/edited.""" - if line_map is None or v.code == "LIT000": - return True - lines = line_map.get(v.path.as_posix()) - return lines is ALL_LINES or (isinstance(lines, set) and v.line in lines) - - -# --------------------------------------------------------------------------- # -# Per-file Any budget (one-way ratchet, 50% headroom; ratchet-checked) -# --------------------------------------------------------------------------- # - - -def _slack_for(baseline: int) -> int: - """50% headroom, rounded up so even a 1-Any file gets a little room.""" - return (baseline + 1) // 2 - - -def _ceiling(spec: dict[str, int]) -> int: - """A file's ceiling: ``baseline + slack`` (0 for an absent/empty entry).""" - return int(spec.get("baseline", 0)) + int(spec.get("slack", 0)) - - -def load_budget() -> dict[str, dict[str, int]]: - """Read ``any-discipline-budget.json`` ({path: {baseline, slack}}); {} if absent.""" - if not BUDGET_PATH.exists(): - return {} - try: - data = json.loads(BUDGET_PATH.read_text()) - except (OSError, ValueError): - return {} - return data if isinstance(data, dict) else {} - - -def save_budget(counts: dict[str, int]) -> None: - """Write a fresh budget from per-file counts, with 50% headroom each. - - Files with zero Any are omitted: an absent entry means baseline 0, so a - file's first Any always trips the gate until it is deliberately baselined.""" - budget = { - path: {"baseline": n, "slack": _slack_for(n)} - for path, n in counts.items() - if n > 0 - } - BUDGET_PATH.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") - - -def lit009_counts(violations: Iterable[Violation]) -> dict[str, int]: - """Count LIT009 (Any-typed value) findings per repo-relative file path.""" - counts: dict[str, int] = {} - for v in violations: - if v.code == "LIT009": - key = v.path.as_posix() - counts[key] = counts.get(key, 0) + 1 - return counts - - -def all_litellm_py_files() -> list[str] | None: - """Every tracked ``.py`` under litellm/, as litellm-package-relative paths; - None if git is unavailable / not a repo (mirrors ``changed_line_map``).""" - try: - tracked = _git("ls-files", "--", "litellm") - except (subprocess.CalledProcessError, FileNotFoundError): - return None - return _to_litellm_relative( - REPO_ROOT / name for name in tracked if name.endswith(".py") - ) - - -def update_budget( - list_files: Callable[[], list[str] | None] = all_litellm_py_files, -) -> int: - """Whole-tree scan: recapture every file's Any count into the budget.""" - rel_paths = list_files() - if rel_paths is None: - print( - "check_any_discipline: not a git repository; cannot capture the budget", - file=sys.stderr, - ) - return 2 - if not rel_paths: - print("check_any_discipline: no litellm/*.py files found", file=sys.stderr) - return 2 - violations = check_files(rel_paths) - build_errors = [v for v in violations if v.code == "LIT000"] - if build_errors: - for v in build_errors: - print(v.render(), file=sys.stderr) - print( - "FAIL: mypy could not build the tree; budget left unchanged.", - file=sys.stderr, - ) - return 2 - counts = lit009_counts(violations) - save_budget(counts) - print( - f"Wrote {BUDGET_PATH.name}: " - f"{sum(1 for n in counts.values() if n > 0)} file(s), " - f"{sum(counts.values())} Any-typed value(s) baselined (50% headroom each)." - ) - return 0 - - -def _report_over_budget( - path: str, - count: int, - spec: dict[str, int] | None, - lit009: list[Violation], - line_map: dict[str, LineScope], -) -> None: - """Print one over-budget file plus the Any findings on its changed lines.""" - ceiling = _ceiling(spec or {}) - if spec: - why = f"baseline {spec['baseline']} + 50% slack {spec['slack']} = ceiling {ceiling}" - else: - why = "no budget entry -> baseline 0 (a new/unbudgeted file must be Any-free)" - print(f"{path}: {count} Any-typed value(s) total, over budget ({why})") - # Surface the findings on changed lines first: the ones this branch most - # likely just added, and the cheapest path back under the ceiling. - scope = line_map.get(path) - for v in sorted(lit009): - if scope is ALL_LINES or (isinstance(scope, set) and v.line in scope): - print(f" changed-line Any {v.line}:{v.col} {v.message}") - - -def run_gate(base: str) -> int: - """Gate changed files under litellm/ against the committed per-file budget.""" - line_map = changed_line_map(base) - if line_map is None: - print( - "check_any_discipline: not a git repository; nothing to check", - file=sys.stderr, - ) - return 0 - rel_paths = _to_litellm_relative((REPO_ROOT / name).resolve() for name in line_map) - if not rel_paths: - print("OK: no changed Python files under litellm/ to check") - return 0 - - violations = check_files(rel_paths) - budget = load_budget() - - # Hard rules, independent of the budget: a build/read failure (always), and a - # reasonless `# any-ok` on a line this branch touched. - hard = sorted( - v - for v in violations - if v.code == "LIT000" or (v.code == "LIT005" and _in_scope(v, line_map)) - ) - - # Per-file Any budget: a changed file fails when its total Any count exceeds - # its ceiling. Unchanged files keep their committed baseline (never re-scanned). - counts = lit009_counts(violations) - lit009_by_file: dict[str, list[Violation]] = {} - for v in violations: - if v.code == "LIT009": - lit009_by_file.setdefault(v.path.as_posix(), []).append(v) - over_budget = [ - (path, count) - for path, count in sorted(counts.items()) - if count > _ceiling(budget.get(path, {})) - ] - - if not hard and not over_budget: - print( - f"OK: {len(rel_paths)} changed file(s) under litellm/ are within their Any budget" - ) - return 0 - - for v in hard: - print(v.render()) - for path, count in over_budget: - _report_over_budget( - path, count, budget.get(path), lit009_by_file.get(path, []), line_map - ) - - print( - f"\nFAIL: {len(hard)} hard violation(s), {len(over_budget)} file(s) over their Any budget.\n" - "Give the new values concrete types (validate untyped input with Pydantic) to get back\n" - "under the file's ceiling, or annotate a genuine boundary line `# any-ok: `.\n" - "Re-baseline with `make lint-any-budget-update` only to lock in a reduction.", - file=sys.stderr, - ) - return 1 - - -def spot_check(rel_paths: Sequence[str]) -> int: - """Explicit-paths mode: report every finding in the files (no budget).""" - violations = sorted(check_files(rel_paths)) - for v in violations: - print(v.render()) - if violations: - print(f"\nFAIL: {len(violations)} Any-discipline finding(s).", file=sys.stderr) - return 1 - print(f"OK: {len(rel_paths)} file(s) have no Any-typed values") - return 0 - - -def main(argv: Sequence[str]) -> int: - parser = argparse.ArgumentParser( - description="Any-discipline gate (changed files, per-file Any budget)." - ) - parser.add_argument( - "paths", - nargs="*", - help="explicit files (repo-root relative); whole-file spot-check, no budget", - ) - parser.add_argument( - "--changed", - action="store_true", - help="gate changed files under litellm/ vs --base against the per-file budget", - ) - parser.add_argument( - "--update", - action="store_true", - help="recapture the whole-tree per-file budget (any-discipline-budget.json)", - ) - parser.add_argument("--base", default=os.environ.get("ANY_GATE_BASE", DEFAULT_BASE)) - args = parser.parse_args(list(argv)) - - if args.update: - return update_budget() - if args.changed: - return run_gate(args.base) - if args.paths: - rel_paths = _to_litellm_relative((REPO_ROOT / p).resolve() for p in args.paths) - if not rel_paths: - print("check_any_discipline: no litellm/*.py paths given", file=sys.stderr) - return 2 - return spot_check(rel_paths) - parser.error("pass --changed, --update, or explicit file paths") - return 2 - - -if __name__ == "__main__": - raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/check_type_discipline.py b/scripts/check_type_discipline.py index d83a1a7512f..6e152541863 100644 --- a/scripts/check_type_discipline.py +++ b/scripts/check_type_discipline.py @@ -25,10 +25,8 @@ LIT003 noqa suppression without rule codes or without a reason. Required shape: `# noqa: TID251 # ` LIT004 type/pyright/mypy ignore without bracketed codes or without a reason. Required shape: `# pyright: ignore[reportArgumentType] # ` -LIT005 A `# mutable-ok` / `# cast-ok` / `# guard-ok` / `# kwargs-ok` / `# any-ok` - suppression without a reason. (`any-ok` belongs to check_any_discipline.py; - it is enumerated here so the reason requirement holds even when only this - stdlib checker runs.) +LIT005 A `# mutable-ok` / `# cast-ok` / `# guard-ok` / `# kwargs-ok` + suppression without a reason. LIT006 `cast(...)` call. typing.cast is an unchecked assertion (the moral equivalent of TypeScript's `as`); it lies to the type checker with zero runtime guarantee. Validate into a concrete frozen type at the boundary instead. @@ -41,9 +39,8 @@ LIT008 `**kwargs` parameter. The keyword contract is erased and everything it c syntax. Declare explicit keyword params, or accept one frozen payload. `*args`, by contrast, is fine when typed (it's just a tuple). Suppress: `# kwargs-ok: `. -LIT000 and LIT009 are the sibling Any gate's (check_any_discipline.py, #30379): a mypy -build/read failure and an Any-typed value. They share this LIT namespace but are emitted -by that checker, not this one. +LIT000 Setup failure: a target file could not be read, or contains a syntax error. + Reported as a violation rather than crashing the run. Usage ----- @@ -105,17 +102,13 @@ MUTABLE_OK_RE = re.compile(r"#\s*mutable-ok(?::\s*(?P.*))?") CAST_OK_RE = re.compile(r"#\s*cast-ok(?::\s*(?P.*))?") GUARD_OK_RE = re.compile(r"#\s*guard-ok(?::\s*(?P.*))?") KWARGS_OK_RE = re.compile(r"#\s*kwargs-ok(?::\s*(?P.*))?") -ANY_OK_RE = re.compile(r"#\s*any-ok(?::\s*(?P.*))?") -# Suppression tokens that must each carry a reason (LIT005). `any-ok` is owned by -# check_any_discipline.py but listed here so the reason requirement is enforced even -# when only this stdlib checker runs. +# Suppression tokens that must each carry a reason (LIT005). OK_SUPPRESSIONS: tuple[tuple[str, re.Pattern[str]], ...] = ( ("mutable-ok", MUTABLE_OK_RE), ("cast-ok", CAST_OK_RE), ("guard-ok", GUARD_OK_RE), ("kwargs-ok", KWARGS_OK_RE), - ("any-ok", ANY_OK_RE), ) diff --git a/scripts/type_check_gate.py b/scripts/type_check_gate.py index 5ff485f0b0f..0f9a44703f9 100644 --- a/scripts/type_check_gate.py +++ b/scripts/type_check_gate.py @@ -1,47 +1,38 @@ #!/usr/bin/env python3 -"""Per-rule count gate for mypy and basedpyright. +"""Per-rule count gate for basedpyright. -Each tool's output is reduced to a count of errors per *rule* (mypy error codes -like ``arg-type``, basedpyright rules like ``reportAny``) and checked against a -committed budget of the form ``{rule: {baseline, slack}}``, the same shape as +basedpyright's ``--outputjson`` is reduced to a count of errors per *rule* +(``reportAny``, ``reportArgumentType``, ...) and checked against a committed +budget of the form ``{rule: {baseline, slack}}``, the same shape as ``ruff-strict-budget.json``. A rule fails when its codebase-wide total exceeds ``baseline + slack``. Counts ignore file, line, and column, so a violation moving anywhere in the tree is invisible; only the per-rule total moves the needle. Unlike ``ruff_strict_gate.py`` this does *not* re-run the tool on the merge base -to compute a delta: a second mypy/basedpyright pass is minutes and gigabytes, -whereas ruff is milliseconds. The committed budget is the baseline instead -- -exactly how the previous per-file gate worked -- so keep it fresh with -``--update`` (ratchet), which re-captures every rule's count from the current -tree while preserving each rule's slack. Tool output is read from stdin, so the -caller decides how to invoke the tool (and from which cwd). +to compute a delta: a second basedpyright pass is minutes and gigabytes, whereas +ruff is milliseconds. The committed budget is the baseline instead -- exactly +how the previous per-file gate worked -- so keep it fresh with ``--update`` +(ratchet), which re-captures every rule's count from the current tree while +preserving each rule's slack. Tool output is read from stdin, so the caller +decides how to invoke basedpyright (and from which cwd). -mypy is parsed from its text output (one error per line, the rule code in a -trailing ``[bracket]``). basedpyright is parsed from ``--outputjson``: its text -diagnostics routinely wrap across lines, leaving the ``(reportRule)`` on a -continuation line away from the ``- error:`` marker, so line parsing -mis-attributes ~60% of errors -- the JSON carries an unambiguous ``rule`` field. +``--outputjson`` is used rather than text diagnostics because the latter wrap +across lines, leaving the ``(reportRule)`` on a continuation line away from the +``- error:`` marker, so line parsing mis-attributes ~60% of errors -- the JSON +carries an unambiguous ``rule`` field. """ import argparse import json -import re import sys from collections import Counter from pathlib import Path -from typing import Iterable, Mapping, NamedTuple +from typing import Mapping, NamedTuple REPO_ROOT = Path(__file__).resolve().parent.parent -# mypy: one error per line, e.g. `path:12: error: msg [arg-type]`. ERROR_LINE -# recognizes the line; MYPY_CODE pulls the trailing [code]. Kept separate so an -# error emitted without a code is still counted (under UNCODED), never dropped. -MYPY_ERROR = re.compile(r"^(?P.+?):\d+: error:") -MYPY_CODE = re.compile(r"\[(?P[a-z][a-z0-9-]*)\]\s*$") - -# Bucket for an error whose rule code we couldn't read (a mypy error with no -# code, or a basedpyright diagnostic with no `rule`). Counted so it's gated. +# Bucket for a basedpyright diagnostic with no `rule`. Counted so it's gated. UNCODED = "" # Ceiling for a rule that shows up at HEAD but isn't in the budget at all -- a @@ -72,20 +63,6 @@ def _to_repo_relative(raw: str) -> str | None: return None -def count_mypy(lines: Iterable[str]) -> dict[str, int]: - """Count in-repo mypy errors per rule code from text output. Errors for - files outside the repo (third-party stubs) are ignored, as before.""" - counts: Counter[str] = Counter() - for raw in lines: - line = raw.rstrip("\n") - match = MYPY_ERROR.match(line) - if match is None or _to_repo_relative(match.group("file")) is None: - continue - code = MYPY_CODE.search(line) - counts[code.group("code") if code else UNCODED] += 1 - return dict(counts) - - def count_basedpyright(payload: str) -> dict[str, int]: """Count in-repo basedpyright errors per rule from `--outputjson`. Warnings and information are ignored; only `severity == "error"` is gated.""" @@ -108,12 +85,6 @@ def count_basedpyright(payload: str) -> dict[str, int]: return dict(counts) -def count_errors(stdin_text: str, tool: str) -> dict[str, int]: - if tool == "basedpyright": - return count_basedpyright(stdin_text) - return count_mypy(stdin_text.splitlines()) - - def evaluate( counts: Mapping[str, int], budget: Mapping[str, Mapping[str, int]] ) -> list[Breach]: @@ -136,13 +107,11 @@ def is_vacuous_run( return not counts and any(spec["baseline"] for spec in budget.values()) -def budget_path(tool: str) -> Path: - return REPO_ROOT / f"{tool}-code-budget.json" +BUDGET_PATH = REPO_ROOT / "basedpyright-code-budget.json" -def cmd_update(tool: str, counts: Mapping[str, int]) -> None: - path = budget_path(tool) - existing = json.loads(path.read_text()) if path.exists() else {} +def cmd_update(counts: Mapping[str, int]) -> None: + existing = json.loads(BUDGET_PATH.read_text()) if BUDGET_PATH.exists() else {} budget = { code: { "baseline": count, @@ -152,18 +121,18 @@ def cmd_update(tool: str, counts: Mapping[str, int]) -> None: } for code, count in sorted(counts.items()) } - path.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") + BUDGET_PATH.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n") print( - f"Re-captured {tool} per-rule budget: {len(budget)} rules, {sum(counts.values())} errors total" + f"Re-captured basedpyright per-rule budget: {len(budget)} rules, {sum(counts.values())} errors total" ) -def cmd_check(tool: str, counts: Mapping[str, int]) -> None: - budget = json.loads(budget_path(tool).read_text()) +def cmd_check(counts: Mapping[str, int]) -> None: + budget = json.loads(BUDGET_PATH.read_text()) if is_vacuous_run(counts, budget): expected = sum(spec["baseline"] for spec in budget.values()) print( - f"FAIL: {tool} produced no errors, but {budget_path(tool).name} expects " + f"FAIL: basedpyright produced no errors, but {BUDGET_PATH.name} expects " f"~{expected}. The type checker almost certainly crashed or emitted " f"nothing; refusing to certify a vacuous run." ) @@ -171,25 +140,24 @@ def cmd_check(tool: str, counts: Mapping[str, int]) -> None: breaches = evaluate(counts, budget) if not breaches: print( - f"OK: every rule is within its {tool} ceiling ({sum(counts.values())} errors total)" + f"OK: every rule is within its basedpyright ceiling ({sum(counts.values())} errors total)" ) return - print(f"FAIL: {tool} errors exceed the per-rule ceiling:") + print("FAIL: basedpyright errors exceed the per-rule ceiling:") for breach in breaches: print(f" {breach.code}: {breach.total} errors over cap {breach.cap}") print( - f"Resolve the new errors, or run 'make lint-{tool}-budget-update' if the ceiling should move." + "Resolve the new errors, or run 'make lint-basedpyright-budget-update' if the ceiling should move." ) raise SystemExit(1) def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--tool", choices=("mypy", "basedpyright"), required=True) parser.add_argument("--update", action="store_true") args = parser.parse_args() - counts = count_errors(sys.stdin.read(), args.tool) - cmd_update(args.tool, counts) if args.update else cmd_check(args.tool, counts) + counts = count_basedpyright(sys.stdin.read()) + cmd_update(counts) if args.update else cmd_check(counts) if __name__ == "__main__": diff --git a/tests/llm_translation/base_llm_unit_tests.py b/tests/llm_translation/base_llm_unit_tests.py index fef1d23d867..a184798b503 100644 --- a/tests/llm_translation/base_llm_unit_tests.py +++ b/tests/llm_translation/base_llm_unit_tests.py @@ -906,7 +906,10 @@ class BaseLLMChatTest(ABC): { "type": "image_url", "image_url": { - "url": "https://www.gstatic.com/webp/gallery/1.webp", + # sha-pinned in-repo logo via jsdelivr; gstatic's + # robots.txt blocks server-side fetchers (e.g. + # Anthropic), which 400s the request. + "url": "https://cdn.jsdelivr.net/gh/BerriAI/litellm@d769e81c90d453240c61fc572cdb27fae06a89d0/ui/litellm-dashboard/public/assets/logos/litellm_logo.jpg", "detail": detail, }, }, diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py index 16b5cbe8589..fe6cb98d1f5 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/openai/test_moderations.py @@ -2,6 +2,7 @@ """ Test OpenAI Moderation Guardrail """ + import os import sys @@ -822,6 +823,108 @@ def test_openai_moderation_process_error_metadata_none_edge_case(): assert "_openai_moderation_response" not in request_data["metadata"] +@pytest.mark.asyncio +async def test_openai_moderation_logs_violation_categories_harmful_content(): + """Flagged content surfaces only the violated category names in + StandardLoggingGuardrailInformation.violation_categories, so OTEL can index + a short ``guardrail_violation_categories`` attribute instead of the full + response blob (LIT-3801).""" + from fastapi import HTTPException + + from litellm.types.utils import GenericGuardrailAPIInputs + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + guardrail = OpenAIModerationGuardrail(guardrail_name="test-openai-moderation") + + mock_response = OpenAIModerationResponse( + id="modr-violations", + model="omni-moderation-latest", + results=[ + OpenAIModerationResult( + flagged=True, + categories={ + "sexual": False, + "hate": False, + "self-harm": True, + "self-harm/intent": True, + "violence": True, + }, + category_scores={ + "sexual": 0.0001, + "hate": 0.0001, + "self-harm": 0.97, + "self-harm/intent": 0.98, + "violence": 0.35, + }, + category_applied_input_types={}, + ) + ], + ) + + with patch.object(guardrail, "async_make_request", return_value=mock_response): + request_data = {"metadata": {}} + with pytest.raises(HTTPException): + await guardrail.apply_guardrail( + inputs=GenericGuardrailAPIInputs( + structured_messages=[{"role": "user", "content": "harmful"}] + ), + request_data=request_data, + input_type="request", + ) + + info = request_data["metadata"]["standard_logging_guardrail_information"][0] + + # Only the flagged categories, never the unflagged ones or the scores + assert info["violation_categories"] == [ + "self-harm", + "self-harm/intent", + "violence", + ] + + +@pytest.mark.asyncio +async def test_openai_moderation_no_violation_categories_safe_content(): + """Safe content carries no violation_categories key, so the short attribute + is absent rather than empty on allowed requests (LIT-3801).""" + from litellm.types.utils import GenericGuardrailAPIInputs + + with patch.dict(os.environ, {"OPENAI_API_KEY": "test-key"}): + guardrail = OpenAIModerationGuardrail(guardrail_name="test-openai-moderation") + + mock_response = OpenAIModerationResponse( + id="modr-safe", + model="omni-moderation-latest", + results=[ + OpenAIModerationResult( + flagged=False, + categories={"hate": False, "violence": False}, + category_scores={"hate": 0.001, "violence": 0.002}, + category_applied_input_types={}, + ) + ], + ) + + with patch.object(guardrail, "async_make_request", return_value=mock_response): + request_data = {"metadata": {}} + await guardrail.apply_guardrail( + inputs=GenericGuardrailAPIInputs( + structured_messages=[{"role": "user", "content": "hi"}] + ), + request_data=request_data, + input_type="request", + ) + + info = request_data["metadata"]["standard_logging_guardrail_information"][0] + assert "violation_categories" not in info + + +def test_openai_moderation_build_tracing_detail_non_dict_responses(): + """Non-dict guardrail responses (the "allow" sentinel, a raw Exception) yield + no tracing detail so logging never crashes when no moderation call ran.""" + assert OpenAIModerationGuardrail._build_tracing_detail("allow") is None + assert OpenAIModerationGuardrail._build_tracing_detail(ValueError("boom")) is None + + @pytest.mark.asyncio async def test_openai_moderation_guardrail_streaming_defaults(): """Defaults match the unified dispatcher: sampled in-stream, every 5th chunk.""" diff --git a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py index 103e05bd3af..cdb09215aa0 100644 --- a/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py +++ b/tests/test_litellm/proxy/openai_files_endpoint/test_files_endpoint.py @@ -2188,3 +2188,329 @@ def test_require_managed_files_accepts_repeated_target_model_names_bracket_form( assert response.status_code == 200, response.text assert response.json()["id"] == "litellm_managed_file_repeated" assert received_target_model_names == ["azure-gpt-3-5-turbo", "gpt-3.5-turbo"] + + +def test_list_files_resolves_wildcard_deployment_credentials( + mocker: MockerFixture, monkeypatch +): + """ + GET /v1/files?target_model_names= must resolve the upstream api_key + from the matching (wildcard) deployment. Regression for the path routing + through llm_router.afile_list(model=...), which reached OpenAI without an + api_key and failed with "api_key client option must be set". + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + wildcard_router = Router( + model_list=[ + { + "model_name": "*", + "litellm_params": { + "model": "openai/*", + "api_key": "wildcard-openai-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.PROXY_ADMIN, + user_id="test-user", + ) + + try: + response = client.get( + "/v1/files?target_model_names=gpt-4o", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "wildcard-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_without_target_model_names_uses_team_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + Plain GET /v1/files (no target_model_names) must resolve the upstream openai + api_key from the team's openai deployment instead of falling through to a + keyless OpenAI client. Regression for "api_key client option must be set". + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + wildcard_router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "team-openai-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=["openai/*"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_restricted_team_does_not_leak_global_openai_credentials( + mocker: MockerFixture, monkeypatch +): + """ + A team whose allowlist only grants anthropic must NOT resolve a global + openai deployment's api_key for plain GET /v1/files. Regression for the + last-resort scan that ignored team access control. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "global-openai-key", + }, + }, + { + "model_name": "claude-opus-4-6", + "litellm_params": { + "model": "anthropic/claude-opus-4-6", + "api_key": "anthropic-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="anthropic-only-team", + team_models=["claude-opus-4-6"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") != "global-openai-key" + + +def test_list_files_prefers_team_byok_over_global_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + When a team has its own BYOK openai deployment (model_info.team_id set), plain + GET /v1/files must use the team's key, not a shared/global openai deployment. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles + + router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "global-openai-key", + }, + }, + { + "model_name": "team-gpt-4o", + "litellm_params": { + "model": "openai/gpt-4o", + "api_key": "team-byok-openai-key", + }, + "model_info": { + "id": "team-byok-deployment-id", + "team_id": "test-team", + "team_public_model_name": "team-gpt-4o", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=["team-gpt-4o"], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-byok-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() + + +def test_list_files_with_all_proxy_models_team_uses_openai_deployment( + mocker: MockerFixture, monkeypatch +): + """ + Teams with all-proxy-models (or empty models) must still resolve openai + credentials for plain GET /v1/files. + """ + import litellm.proxy.proxy_server as ps + from litellm.proxy._types import LitellmUserRoles, SpecialModelNames + + wildcard_router = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "team-openai-key", + }, + }, + { + "model_name": "claude-opus-4-6", + "litellm_params": { + "model": "anthropic/claude-opus-4-6", + "api_key": "anthropic-key", + }, + }, + ] + ) + + proxy_logging_obj = setup_proxy_logging_object(monkeypatch, wildcard_router) + monkeypatch.setattr("litellm.proxy.proxy_server.master_key", None) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None) + monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", wildcard_router) + proxy_logging_obj.update_request_status = mocker.AsyncMock() + proxy_logging_obj.post_call_success_hook = mocker.AsyncMock(return_value=[]) + proxy_logging_obj.post_call_failure_hook = mocker.AsyncMock() + + captured_kwargs: dict = {} + + async def _mock_afile_list(**kwargs): + captured_kwargs.update(kwargs) + return [] + + monkeypatch.setattr(litellm, "afile_list", _mock_afile_list) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + api_key="test-key", + user_role=LitellmUserRoles.INTERNAL_USER, + user_id="test-user", + team_id="test-team", + team_models=[SpecialModelNames.all_proxy_models.value], + ) + + try: + response = client.get( + "/v1/files", + headers={"Authorization": "Bearer test-key"}, + ) + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + assert response.status_code == 200, response.text + assert captured_kwargs.get("api_key") == "team-openai-key" + assert captured_kwargs.get("custom_llm_provider") == "openai" + proxy_logging_obj.post_call_failure_hook.assert_not_called() diff --git a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py index 6a8e0d15d8b..0f87fcda588 100644 --- a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py @@ -15,6 +15,7 @@ import pytest import litellm.proxy.proxy_server as ps from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +from litellm.proxy.common_utils.model_listing_utils import TeamModelNameTranslator from litellm.proxy.proxy_server import ( _get_proxy_model_info, _translate_model_name_for_response, @@ -593,3 +594,664 @@ async def test_model_info_v1_litellm_model_id_team_id_applies_team_filter(monkey team_filter.assert_awaited_once() assert team_filter.await_args.kwargs["team_id"] == "other-team" assert team_filter.await_args.kwargs["all_models"] == [team_row] + + +@pytest.mark.asyncio +async def test_v1_models_translates_team_model_for_access_group_key(monkeypatch): + """Regression (#28382 sibling leak): a virtual key whose model access group + resolves to a team BYOK deployment must list the PUBLIC name in /v1/models, + not the internal routing key model_name_{team_id}_{uuid}. + + The /model/info read-path fix did not cover /v1/models, which builds from + bare model-name strings via access-group expansion. + """ + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + # Default behavior: listing surfaces public names. + monkeypatch.setattr(ps, "general_settings", {}) + + # virtual key granted access via the access group (no team membership) + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key) + + ids = [d["id"] for d in resp["data"]] + assert "tushar-gpt-4.1" in ids + assert "model_name_teamX_uuid9" not in ids + + +@pytest.mark.asyncio +async def test_v1_models_keeps_internal_names_when_public_name_flag_disabled( + monkeypatch, +): + """Compatibility override: /v1/models can still list the internal routing + name for consumers that scripted against those ids. Translation is enabled + by default and disabled via general_settings['use_team_public_model_name']. + """ + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": False}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key) + + ids = [d["id"] for d in resp["data"]] + assert "model_name_teamX_uuid9" in ids # internal id preserved (backward-compat) + assert "tushar-gpt-4.1" not in ids + + +@pytest.mark.asyncio +async def test_v1_models_translates_team_model_with_metadata(monkeypatch): + """include_metadata=true must build metadata for the public model id.""" + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": []}, + } + ] + + +@pytest.mark.asyncio +async def test_v1_models_metadata_fallbacks_use_internal_routing_key(monkeypatch): + """Regression: with include_metadata=true, fallbacks configured for a team + model under its internal routing key must still surface. The metadata lookup + has to run against the internal name, not the translated public name (which + the router's fallback config never keys on) -- otherwise fallbacks silently + drop to [].""" + team_dep = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id1", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + # Fallbacks are keyed on the internal routing name, as the router stores them. + router.fallbacks = [{"model_name_teamX_uuid9": ["gpt-4o-backup"]}] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": ["gpt-4o-backup"]}, + } + ] + + +@pytest.mark.asyncio +async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch): + """Regression: two teams can publish the same team_public_model_name. With + include_metadata=true a caller scoped to teamX must see teamX's fallbacks for + the shared public name, never teamY's. The metadata lookup has to stay within + the caller's accessible models; resolving the public name through a router-wide + reverse map could point it at another team's internal routing key.""" + team_x = { + "model_name": "model_name_teamX_uuid9", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "idX", + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + "access_groups": ["grp-a"], + }, + } + team_y = { + "model_name": "model_name_teamY_uuidZ", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "idY", + "team_id": "teamY", + "team_public_model_name": "tushar-gpt-4.1", # same public name, other team + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_uuid9"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]} + router.get_fully_blocked_model_names.return_value = set() + router.model_list = [team_x, team_y] + router.get_model_list.return_value = [team_x, team_y] + router.fallbacks = [ + {"model_name_teamX_uuid9": ["teamX-backup"]}, + {"model_name_teamY_uuidZ": ["teamY-backup"]}, + ] + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {}) + + key = UserAPIKeyAuth( + user_id="u", api_key="sk-test", models=["grp-a"], team_models=[] + ) + resp = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert resp["data"] == [ + { + "id": "tushar-gpt-4.1", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "metadata": {"fallbacks": ["teamX-backup"]}, + } + ] + + +def test_translate_team_model_names_for_listing_swaps_and_dedupes(): + """Internal team routing keys -> public name; sibling deployments sharing a + public name collapse to one entry (order preserved); globals untouched.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", # sibling: same public name + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + out = TeamModelNameTranslator.translate_listing( + ["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], + router, + {}, + ) + assert out == ["tushar-gpt-4.1", "gpt-4o"] + + +def test_listing_entries_keep_internal_lookup_id_for_team_rows(): + """`listing_entries` returns (public response id, internal lookup id) so the + response shows the public name while metadata lookups keep the routing key. + Sibling deployments collapse to one entry; globals map to themselves.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", # sibling: same public name + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA", "model_name_teamX_uuidB", "gpt-4o"], + router, + {}, + ) + # public id for the client; an internal routing key for the metadata lookup + assert entries[0][0] == "tushar-gpt-4.1" + assert entries[0][1].startswith("model_name_teamX_uuid") + assert entries[1] == ("gpt-4o", "gpt-4o") + assert len(entries) == 2 + + +def test_listing_entries_lookup_id_never_crosses_team_boundary(): + """Regression: when two teams share a team_public_model_name, the lookup id for + the shared public name must stay within the caller's accessible model_names and + never resolve to the other team's internal routing key (which would leak that + team's fallback metadata under include_metadata=true).""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "shared-name", + }, + }, + { + "model_name": "model_name_teamY_uuidB", # different team, same public name + "model_info": { + "team_id": "teamY", + "team_public_model_name": "shared-name", + }, + }, + ] + + # caller can only access teamX's internal key + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("shared-name", "model_name_teamX_uuidA")] + + +def test_listing_entries_global_wins_when_team_alias_collides_with_global(): + """Regression: when an accessible global model shares its name with a team + deployment's `team_public_model_name`, the listing must keep the global + entry rather than overwriting its lookup id with the colliding team's + internal routing key (which would surface the team's metadata under the + global id).""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "gpt-4o", + }, + }, + {"model_name": "gpt-4o", "model_info": {"db_model": False}}, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["gpt-4o", "model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("gpt-4o", "gpt-4o")] + + +def test_listing_and_resolve_agree_on_sibling_internal_key(): + """Regression: when two team deployments share a public name, listing and + retrieve must pick the same internal routing key, otherwise `/v1/models/{id}` + describes a different deployment than what the listing's metadata was built + from.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + { + "model_name": "model_name_teamX_uuidB", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + }, + ] + available = ["model_name_teamX_uuidA", "model_name_teamX_uuidB"] + + [(_, listing_lookup)] = TeamModelNameTranslator.listing_entries( + available, router, {} + ) + resolve_lookup = TeamModelNameTranslator.resolve_public_name( + model_id="tushar-gpt-4.1", + available_models=available, + llm_router=router, + general_settings={}, + ) + + assert listing_lookup == resolve_lookup + + +def test_listing_entries_skips_empty_team_public_model_name(): + """Regression: a misconfigured row with `team_public_model_name: ""` must not + produce a listing entry with an empty `id`; the internal routing key should + pass through unchanged, matching `/v1/model/info`'s falsy-check behavior.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "", + }, + }, + ] + + entries = TeamModelNameTranslator.listing_entries( + ["model_name_teamX_uuidA"], router, {} + ) + + assert entries == [("model_name_teamX_uuidA", "model_name_teamX_uuidA")] + + +def test_listing_entries_passthrough_when_disabled(): + """Legacy flag / no router -> response id equals lookup id (no translation).""" + assert TeamModelNameTranslator.listing_entries(["a", "b"], None, {}) == [ + ("a", "a"), + ("b", "b"), + ] + + +def test_translate_team_model_names_for_listing_leaves_unmapped_names(): + """Names with no team mapping (globals, access-group keys) pass through.""" + router = MagicMock() + router.get_model_list.return_value = [ + {"model_name": "gpt-4o", "model_info": {"db_model": False}} + ] + + assert TeamModelNameTranslator.translate_listing( + ["gpt-4o", "beta-group"], router, {} + ) == ["gpt-4o", "beta-group"] + + +def test_translate_team_model_names_for_listing_none_router(): + """No router -> return the input list unchanged.""" + assert TeamModelNameTranslator.translate_listing(["a", "b"], None, {}) == ["a", "b"] + + +def test_translate_team_model_names_for_listing_respects_legacy_flag(): + """Operators can keep returning the legacy internal routing key.""" + router = MagicMock() + router.get_model_list.return_value = [ + { + "model_name": "model_name_teamX_uuidA", + "model_info": { + "team_id": "teamX", + "team_public_model_name": "tushar-gpt-4.1", + }, + } + ] + + assert TeamModelNameTranslator.translate_listing( + ["model_name_teamX_uuidA"], router, {"use_team_public_model_name": False} + ) == ["model_name_teamX_uuidA"] + + +def _public_named_router(*team_rows: dict) -> MagicMock: + router = MagicMock() + router.get_model_list.return_value = list(team_rows) + return router + + +def test_resolve_public_name_to_internal_routing_key(): + """A public team name resolves back to the internal routing key the router + indexes by, so `GET /v1/models/{public_name}` can find the deployment.""" + router = _public_named_router(_team_row()) + + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + == "model_name_team-abc-123_4a6b8" + ) + + +def test_resolve_public_name_is_access_scoped_across_teams(): + """Two teams can publish the SAME public name. A caller's query must resolve + to the internal key they can actually access, never another team's.""" + # both rows share public name "team-claude-sonnet" + router = _public_named_router(_team_row(), _other_team_row()) + + # caller only has access to their own team's internal key + resolved = TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + assert resolved == "model_name_team-abc-123_4a6b8" + assert resolved != "model_name_team-other_9f2c1" + + +def test_resolve_public_name_unmapped_passes_through(): + """A public name with no accessible internal mapping is returned unchanged so + the caller hits the normal 404/access path; internal names pass through too.""" + router = _public_named_router(_team_row()) + + # not accessible -> unchanged (downstream validate_model_access will 404) + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=[], + llm_router=router, + general_settings={}, + ) + == "team-claude-sonnet" + ) + # already an internal routing key -> unchanged + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="model_name_team-abc-123_4a6b8", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={}, + ) + == "model_name_team-abc-123_4a6b8" + ) + + +def test_resolve_public_name_respects_legacy_flag(): + """With the legacy flag set, no public-name resolution happens.""" + router = _public_named_router(_team_row()) + + assert ( + TeamModelNameTranslator.resolve_public_name( + model_id="team-claude-sonnet", + available_models=["model_name_team-abc-123_4a6b8"], + llm_router=router, + general_settings={"use_team_public_model_name": False}, + ) + == "team-claude-sonnet" + ) + + +@pytest.mark.asyncio +async def test_retrieve_model_by_public_name_returns_200(monkeypatch): + """Regression: `GET /v1/models/{public_name}` must NOT 404. The listing + advertises the public team name, so retrieve must accept the same name, + resolve it to the internal routing key for lookup, and echo the public name + back as the model id.""" + import litellm + import litellm.proxy.utils as proxy_utils + + team_row = _team_row() + router = _public_named_router(team_row) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info(model_id="team-claude-sonnet", user_api_key_dict=key) + + assert resp["id"] == "team-claude-sonnet" + # lookup happened by the internal routing key, not the public name + router.get_deployment_by_model_group_name.assert_called_once_with( + "model_name_team-abc-123_4a6b8" + ) + + +@pytest.mark.asyncio +async def test_retrieve_model_by_internal_name_returns_public_id(monkeypatch): + """Regression: retrieving by the internal routing key must echo the SAME + public id `/v1/models` advertises for that deployment, not the path. Otherwise + a client iterating the listing's id and then retrieving each one would observe + a different id depending on which alias they queried by.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info( + model_id="model_name_team-abc-123_4a6b8", user_api_key_dict=key + ) + + assert resp["id"] == "team-claude-sonnet" + + +@pytest.mark.asyncio +async def test_retrieve_model_by_internal_name_keeps_internal_id_when_flag_disabled( + monkeypatch, +): + """With `use_team_public_model_name=false`, retrieve must keep the internal + routing key as the response id, mirroring `/v1/models`' legacy output.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": False}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=["model_name_team-abc-123_4a6b8"]), + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + resp = await ps.model_info( + model_id="model_name_team-abc-123_4a6b8", user_api_key_dict=key + ) + + assert resp["id"] == "model_name_team-abc-123_4a6b8" + + +@pytest.mark.asyncio +async def test_retrieve_model_by_inaccessible_public_name_404s(monkeypatch): + """A caller without access to a team model still gets 404 when retrieving by + its public name; resolution never crosses the access boundary.""" + import litellm + import litellm.proxy.utils as proxy_utils + + router = _public_named_router(_team_row()) + deployment = MagicMock() + deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + router.get_deployment_by_model_group_name.return_value = deployment + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "general_settings", {}) + monkeypatch.setattr( + proxy_utils, + "get_available_models_for_user", + AsyncMock(return_value=[]), # caller has no access + ) + monkeypatch.setattr( + litellm, "get_llm_provider", lambda model: (model, "openai", None, None) + ) + + key = UserAPIKeyAuth(user_id="u", api_key="sk-test", team_models=[]) + with pytest.raises(ps.HTTPException) as exc_info: + await ps.model_info(model_id="team-claude-sonnet", user_api_key_dict=key) + + assert exc_info.value.status_code == 404 + router.get_deployment_by_model_group_name.assert_not_called() diff --git a/tests/test_litellm/test_budget_ratchet_check.py b/tests/test_litellm/test_budget_ratchet_check.py index 8c4150fd36b..9f19944fdba 100644 --- a/tests/test_litellm/test_budget_ratchet_check.py +++ b/tests/test_litellm/test_budget_ratchet_check.py @@ -48,24 +48,7 @@ def test_dropped_rule_is_a_regression(): def test_new_rule_in_head_is_clean(): - assert ratchet.regressions_for("b.json", {}, {"LIT009": _spec_of(5, 0)}) == [] - - -def test_dropped_file_in_the_any_budget_is_not_a_regression(): - # any-discipline is file-keyed: an absent file means ceiling 0, so cleaning a - # file to zero (which drops its entry on --update) is a tightening, never the - # loosening a dropped rule is for the rule-keyed budgets. - base = {"litellm/x.py": _spec_of(10, 5)} - assert ratchet.regressions_for("any-discipline-budget.json", base, {}) == [] - - -def test_raised_ceiling_in_the_any_budget_is_still_a_regression(): - base = {"litellm/x.py": _spec_of(10, 5)} # ceiling 15 - regs = ratchet.regressions_for( - "any-discipline-budget.json", base, {"litellm/x.py": _spec_of(20, 10)} # ceiling 30 - ) - assert [r.rule for r in regs] == ["litellm/x.py"] - assert "15 -> 30" in regs[0].detail + assert ratchet.regressions_for("b.json", {}, {"new-rule": _spec_of(5, 0)}) == [] def test_deleted_budget_file_is_a_regression(): diff --git a/tests/test_litellm/test_check_any_discipline.py b/tests/test_litellm/test_check_any_discipline.py deleted file mode 100644 index 40385664691..00000000000 --- a/tests/test_litellm/test_check_any_discipline.py +++ /dev/null @@ -1,90 +0,0 @@ -import importlib.util -from pathlib import Path - -_MODULE_PATH = ( - Path(__file__).resolve().parents[2] / "scripts" / "check_any_discipline.py" -) -_spec = importlib.util.spec_from_file_location("check_any_discipline", _MODULE_PATH) -mod = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(mod) - -Violation = mod.Violation - - -def _v(path="litellm/x.py", line=10, code="LIT009"): - return Violation(Path(path), line, 0, code, "Any-typed value") - - -def test_violation_on_a_changed_line_is_in_scope(): - assert mod._in_scope(_v(line=10), {"litellm/x.py": {10, 11}}) is True - - -def test_violation_on_an_unchanged_line_of_a_changed_file_is_out_of_scope(): - assert mod._in_scope(_v(line=99), {"litellm/x.py": {10, 11}}) is False - - -def test_whole_new_file_puts_every_line_in_scope(): - assert mod._in_scope(_v(line=99999), {"litellm/x.py": mod.ALL_LINES}) is True - - -def test_file_absent_from_line_map_is_out_of_scope(): - # Regression: ALL_LINES is a distinct sentinel, so a path missing from the map - # (line_map.get -> None) is NOT mistaken for "whole file in scope". - assert mod._in_scope(_v(path="litellm/other.py"), {"litellm/x.py": {1}}) is False - - -def test_no_line_map_means_no_line_filtering(): - assert mod._in_scope(_v(line=12345), None) is True - - -def test_build_error_is_always_in_scope(): - assert mod._in_scope(_v(code="LIT000", line=1), {"litellm/x.py": {2}}) is True - - -# --- per-file Any budget ------------------------------------------------------ - - -def test_slack_is_50_percent_rounded_up(): - assert mod._slack_for(0) == 0 - assert mod._slack_for(1) == 1 # ceil(0.5): even a 1-Any file gets a little room - assert mod._slack_for(3) == 2 # ceil(1.5) - assert mod._slack_for(20) == 10 - assert mod._slack_for(5145) == 2573 - - -def test_ceiling_is_baseline_plus_slack(): - assert mod._ceiling({"baseline": 20, "slack": 10}) == 30 - assert mod._ceiling({}) == 0 # an absent/empty entry means a zero ceiling - - -def test_lit009_counts_groups_by_file_and_ignores_other_codes(): - violations = [ - _v(path="litellm/a.py", line=1, code="LIT009"), - _v(path="litellm/a.py", line=2, code="LIT009"), - _v(path="litellm/a.py", line=3, code="LIT005"), # suppression hygiene, not an Any - _v(path="litellm/b.py", line=1, code="LIT009"), - _v(path="litellm/c.py", line=0, code="LIT000"), # build error, not an Any - ] - assert mod.lit009_counts(violations) == {"litellm/a.py": 2, "litellm/b.py": 1} - - -def test_save_budget_omits_zero_count_files_and_round_trips(monkeypatch, tmp_path): - monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "any-discipline-budget.json") - mod.save_budget({"litellm/a.py": 20, "litellm/b.py": 0, "litellm/c.py": 1}) - loaded = mod.load_budget() - assert loaded == { - "litellm/a.py": {"baseline": 20, "slack": 10}, - "litellm/c.py": {"baseline": 1, "slack": 1}, - } - assert "litellm/b.py" not in loaded # zero-Any files are never baselined - - -def test_load_budget_missing_file_is_empty(monkeypatch, tmp_path): - monkeypatch.setattr(mod, "BUDGET_PATH", tmp_path / "nope.json") - assert mod.load_budget() == {} - - -def test_update_budget_reports_setup_error_when_git_is_unavailable(): - # all_litellm_py_files returns None when git can't list files; --update must - # surface a clean setup error (exit 2), not crash with a raw traceback. - assert mod.update_budget(list_files=lambda: None) == 2 diff --git a/tests/test_litellm/test_type_check_gate.py b/tests/test_litellm/test_type_check_gate.py index eb01bd3b93e..18374c5db4b 100644 --- a/tests/test_litellm/test_type_check_gate.py +++ b/tests/test_litellm/test_type_check_gate.py @@ -10,22 +10,6 @@ _spec.loader.exec_module(gate) ROOT = gate.REPO_ROOT -def test_mypy_counts_per_code_ignoring_lines_notes_and_summary(): - text = "\n".join( - [ - f"{ROOT}/litellm/utils.py:10: error: missing annotation [no-untyped-def]", - f"{ROOT}/litellm/utils.py:9999: error: missing annotation [no-untyped-def]", - f"{ROOT}/litellm/main.py:5: error: Returning Any [no-any-return]", - f"{ROOT}/litellm/main.py:5: note: see here", - "Found 3 errors in 2 files (checked 100 source files)", - ] - ) - assert gate.count_errors(text, "mypy") == { - "no-untyped-def": 2, - "no-any-return": 1, - } - - def _bpr(file, severity, rule): diag = {"file": str(file), "severity": severity, "message": "msg"} if rule is not None: @@ -46,7 +30,7 @@ def test_basedpyright_counts_per_rule_from_json_not_warnings(): ] } ) - assert gate.count_errors(payload, "basedpyright") == { + assert gate.count_basedpyright(payload) == { "reportUnknownVariableType": 2, "reportArgumentType": 1, } @@ -56,17 +40,10 @@ def test_basedpyright_error_without_a_rule_is_bucketed(): payload = json.dumps( {"generalDiagnostics": [_bpr(f"{ROOT}/litellm/x.py", "error", None)]} ) - assert gate.count_errors(payload, "basedpyright") == {gate.UNCODED: 1} - - -def test_mypy_error_without_a_code_is_bucketed_so_it_is_still_gated(): - text = f"{ROOT}/litellm/x.py:1: error: something broke with no code" - assert gate.count_errors(text, "mypy") == {gate.UNCODED: 1} + assert gate.count_basedpyright(payload) == {gate.UNCODED: 1} def test_paths_outside_repo_are_skipped(): - text = "/tmp/elsewhere.py:1: error: missing annotation [no-untyped-def]" - assert gate.count_errors(text, "mypy") == {} payload = json.dumps( { "generalDiagnostics": [ @@ -74,7 +51,7 @@ def test_paths_outside_repo_are_skipped(): ] } ) - assert gate.count_errors(payload, "basedpyright") == {} + assert gate.count_basedpyright(payload) == {} def test_at_or_under_ceiling_passes(): @@ -124,10 +101,10 @@ def test_malformed_basedpyright_json_exits_loudly_not_as_zero_errors(): import pytest with pytest.raises(SystemExit): - gate.count_errors("startup warning\n{not json", "basedpyright") + gate.count_basedpyright("startup warning\n{not json") def test_empty_basedpyright_payload_counts_zero(): # Empty (not malformed) output parses to zero; the vacuous-run guard, not the # parser, is what rejects an empty run. - assert gate.count_errors("", "basedpyright") == {} + assert gate.count_basedpyright("") == {} diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 100b7523830..13b735ddf7c 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -7827,6 +7827,10 @@ export interface paths { * * Follows OpenAI API specification for individual model retrieval. * https://platform.openai.com/docs/api-reference/models/retrieve + * + * Query parameters mirror `/v1/models` so the same caller context (team + * scoping, health filtering, paused deployments) drives both endpoints; the + * listing's public id must resolve to the same internal deployment here. */ get: operations["model_info_models__model_id__get"]; put?: never; @@ -16663,6 +16667,10 @@ export interface paths { * * Follows OpenAI API specification for individual model retrieval. * https://platform.openai.com/docs/api-reference/models/retrieve + * + * Query parameters mirror `/v1/models` so the same caller context (team + * scoping, health filtering, paused deployments) drives both endpoints; the + * listing's public id must resolve to the same internal deployment here. */ get: operations["model_info_v1_models__model_id__get"]; put?: never; @@ -42956,7 +42964,10 @@ export interface operations { }; model_info_models__model_id__get: { parameters: { - query?: never; + query?: { + team_id?: string | null; + healthy_only?: boolean | null; + }; header?: never; path: { model_id: string; @@ -53834,7 +53845,10 @@ export interface operations { }; model_info_v1_models__model_id__get: { parameters: { - query?: never; + query?: { + team_id?: string | null; + healthy_only?: boolean | null; + }; header?: never; path: { model_id: string; diff --git a/uv.lock b/uv.lock index bc796e6ed07..5339b56df7f 100644 --- a/uv.lock +++ b/uv.lock @@ -9,7 +9,7 @@ resolution-markers = [ ] [options] -exclude-newer = "2026-06-11T06:56:06.940919973Z" +exclude-newer = "2026-06-14T15:53:04.946308996Z" exclude-newer-span = "P3D" [manifest] @@ -3231,65 +3231,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/7e/e7394eeb49a41cc514b3eb49020223666cbf40d86f5721c2f07871e6d84a/legacy_cgi-2.6.4-py3-none-any.whl", hash = "sha256:7e235ce58bf1e25d1fc9b2d299015e4e2cd37305eccafec1e6bac3fc04b878cd", size = 20035, upload-time = "2025-10-27T05:20:04.289Z" }, ] -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f", size = 141706, upload-time = "2026-05-10T18:15:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45", size = 142605, upload-time = "2026-05-10T18:15:18.148Z" }, - { url = "https://files.pythonhosted.org/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c", size = 476555, upload-time = "2026-05-10T18:15:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33", size = 468434, upload-time = "2026-05-10T18:15:20.87Z" }, - { url = "https://files.pythonhosted.org/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884", size = 496918, upload-time = "2026-05-10T18:15:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280", size = 490334, upload-time = "2026-05-10T18:15:24.2Z" }, - { url = "https://files.pythonhosted.org/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c", size = 511287, upload-time = "2026-05-10T18:15:26.226Z" }, - { url = "https://files.pythonhosted.org/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb", size = 517202, upload-time = "2026-05-10T18:15:27.965Z" }, - { url = "https://files.pythonhosted.org/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783", size = 497517, upload-time = "2026-05-10T18:15:29.614Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0", size = 538878, upload-time = "2026-05-10T18:15:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89", size = 100070, upload-time = "2026-05-10T18:15:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4", size = 117918, upload-time = "2026-05-10T18:15:33.678Z" }, - { url = "https://files.pythonhosted.org/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29", size = 141092, upload-time = "2026-05-10T18:15:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9", size = 142035, upload-time = "2026-05-10T18:15:36.242Z" }, - { url = "https://files.pythonhosted.org/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5", size = 475022, upload-time = "2026-05-10T18:15:37.56Z" }, - { url = "https://files.pythonhosted.org/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b", size = 467273, upload-time = "2026-05-10T18:15:39.182Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89", size = 497083, upload-time = "2026-05-10T18:15:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc", size = 489139, upload-time = "2026-05-10T18:15:41.934Z" }, - { url = "https://files.pythonhosted.org/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5", size = 508442, upload-time = "2026-05-10T18:15:43.206Z" }, - { url = "https://files.pythonhosted.org/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7", size = 514230, upload-time = "2026-05-10T18:15:44.761Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d", size = 494231, upload-time = "2026-05-10T18:15:46.308Z" }, - { url = "https://files.pythonhosted.org/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412", size = 537585, upload-time = "2026-05-10T18:15:47.629Z" }, - { url = "https://files.pythonhosted.org/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d", size = 100509, upload-time = "2026-05-10T18:15:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73", size = 118628, upload-time = "2026-05-10T18:15:50.345Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c", size = 103122, upload-time = "2026-05-10T18:15:52.068Z" }, - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, -] - [[package]] name = "litellm" version = "1.89.0" @@ -3441,7 +3382,6 @@ dev = [ { name = "fastapi-offline" }, { name = "flake8" }, { name = "langfuse" }, - { name = "mypy" }, { name = "openapi-core" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -3609,7 +3549,6 @@ dev = [ { name = "fastapi-offline", specifier = "==1.7.6" }, { name = "flake8", specifier = "==7.3.0" }, { name = "langfuse", specifier = "==2.59.7" }, - { name = "mypy", specifier = "==1.19.0" }, { name = "openapi-core", marker = "python_full_version < '3.14'", specifier = "==0.22.0" }, { name = "opentelemetry-api", specifier = "==1.28.0" }, { name = "opentelemetry-exporter-otlp", specifier = "==1.28.0" }, @@ -4224,46 +4163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] -[[package]] -name = "mypy" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, - { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, -] - [[package]] name = "mypy-extensions" version = "1.1.0"