mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
Merge origin/litellm_internal_staging into litellm_databricks_claude_cache_pricing
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
commit
d03d56ab5a
1021 changed files with 19354 additions and 9276 deletions
|
|
@ -430,7 +430,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv \
|
||||
--cov=./litellm \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise \
|
||||
--cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=20 \
|
||||
|
|
@ -504,7 +504,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv \
|
||||
--cov=./litellm \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise \
|
||||
--cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=20 \
|
||||
|
|
@ -631,7 +631,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-v -x \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 2"
|
||||
|
|
@ -651,126 +651,6 @@ jobs:
|
|||
- auth_ui_unit_tests_coverage.xml
|
||||
- auth_ui_unit_tests_coverage
|
||||
|
||||
proxy_behavior_tests:
|
||||
docker:
|
||||
- *python312_image
|
||||
- image: cimg/postgres:16.0@sha256:b125148bc76e8e8eee5eb3ad6020a3a14110a14e8192f1c645128afebe2e2f84
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: litellm_test
|
||||
working_directory: ~/project
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/litellm_test"
|
||||
steps:
|
||||
- checkout
|
||||
- skip_if_unrelated_changes
|
||||
- setup_google_dns
|
||||
- install_uv
|
||||
- install_rust
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
uv sync --frozen --all-groups --all-extras --python 3.12
|
||||
- wait_for_service:
|
||||
url: tcp://localhost:5432
|
||||
timeout: "60"
|
||||
- run:
|
||||
name: Seed DB schema via prisma db push
|
||||
command: |
|
||||
uv run --no-sync prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss
|
||||
- run:
|
||||
name: Generate Prisma Client
|
||||
command: uv run --no-sync python -m prisma generate
|
||||
- run:
|
||||
name: Run proxy management behavior tests
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
uv run --no-sync python -m pytest tests/proxy_behavior \
|
||||
-v --junitxml=test-results/junit.xml --durations=10
|
||||
no_output_timeout: 15m
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
proxy_security_tests:
|
||||
docker:
|
||||
- *python312_image
|
||||
- image: cimg/postgres:16.0@sha256:b125148bc76e8e8eee5eb3ad6020a3a14110a14e8192f1c645128afebe2e2f84
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: litellm_test
|
||||
working_directory: ~/project
|
||||
environment:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/litellm_test"
|
||||
steps:
|
||||
- checkout
|
||||
- skip_if_unrelated_changes
|
||||
- setup_google_dns
|
||||
- install_uv
|
||||
- install_rust
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
uv sync --frozen --all-groups --all-extras --python 3.12
|
||||
- wait_for_service:
|
||||
url: tcp://localhost:5432
|
||||
timeout: "60"
|
||||
- run:
|
||||
name: Seed DB schema via prisma db push
|
||||
command: |
|
||||
uv run --no-sync prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss
|
||||
- run:
|
||||
name: Generate Prisma Client
|
||||
command: uv run --no-sync python -m prisma generate
|
||||
- run:
|
||||
name: Run proxy security tests
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
uv run --no-sync python -m pytest tests/proxy_security_tests \
|
||||
-v --junitxml=test-results/junit.xml --durations=10
|
||||
no_output_timeout: 15m
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
schema_migration_check:
|
||||
docker:
|
||||
- *python312_image
|
||||
- image: cimg/postgres:16.0@sha256:b125148bc76e8e8eee5eb3ad6020a3a14110a14e8192f1c645128afebe2e2f84
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: litellm_test
|
||||
working_directory: ~/project
|
||||
environment:
|
||||
# An empty database; the test applies every committed migration itself.
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/litellm_test"
|
||||
steps:
|
||||
- checkout
|
||||
- skip_if_unrelated_changes
|
||||
- setup_google_dns
|
||||
- install_uv
|
||||
- install_rust
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
uv sync --frozen --all-groups --all-extras --python 3.12
|
||||
- wait_for_service:
|
||||
url: tcp://localhost:5432
|
||||
timeout: "60"
|
||||
- run:
|
||||
name: Generate Prisma Client
|
||||
command: uv run --no-sync python -m prisma generate
|
||||
- run:
|
||||
name: Check schema.prisma is in sync with committed migrations
|
||||
command: |
|
||||
mkdir -p test-results
|
||||
uv run --no-sync python -m pytest tests/proxy_migration_tests \
|
||||
-v --junitxml=test-results/junit.xml --durations=10
|
||||
no_output_timeout: 15m
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
|
||||
litellm_router_testing: # Runs all tests with the "router" keyword
|
||||
docker:
|
||||
- *python312_image
|
||||
|
|
@ -858,7 +738,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-v -x \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 4"
|
||||
|
|
@ -985,7 +865,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=20 \
|
||||
-n 4 \
|
||||
|
|
@ -1030,7 +910,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5"
|
||||
no_output_timeout: 15m
|
||||
|
|
@ -1074,7 +954,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 2 \
|
||||
|
|
@ -1120,7 +1000,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
--retries 3 --retry-delay 5"
|
||||
|
|
@ -1211,7 +1091,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 4"
|
||||
|
|
@ -1255,7 +1135,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 4"
|
||||
|
|
@ -1274,40 +1154,6 @@ jobs:
|
|||
paths:
|
||||
- search_coverage.xml
|
||||
- search_coverage
|
||||
litellm_mapped_enterprise_tests:
|
||||
docker:
|
||||
- *python312_image
|
||||
working_directory: ~/project
|
||||
resource_class: large
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- skip_if_unrelated_changes
|
||||
- setup_google_dns
|
||||
- install_uv
|
||||
- install_rust
|
||||
- run:
|
||||
name: Install Dependencies
|
||||
command: |
|
||||
uv sync --frozen --all-groups --all-extras --python 3.12
|
||||
- setup_litellm_enterprise_pip
|
||||
- run:
|
||||
name: Run enterprise tests
|
||||
command: |
|
||||
uv run --no-sync python -m prisma generate
|
||||
mkdir -p test-results
|
||||
TEST_FILES=$(circleci tests glob "tests/enterprise/**/test_*.py")
|
||||
echo "$TEST_FILES" | circleci tests run \
|
||||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-v -x \
|
||||
--junitxml=test-results/junit-enterprise.xml \
|
||||
--durations=10 \
|
||||
-n 4"
|
||||
no_output_timeout: 15m
|
||||
# Store test results
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
batches_testing:
|
||||
docker:
|
||||
- *python312_image
|
||||
|
|
@ -1333,7 +1179,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 2"
|
||||
|
|
@ -1377,7 +1223,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 2"
|
||||
|
|
@ -1422,7 +1268,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
-n 4"
|
||||
|
|
@ -1501,7 +1347,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
-n 4 \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 \
|
||||
|
|
@ -1546,7 +1392,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5"
|
||||
no_output_timeout: 15m
|
||||
|
|
@ -1599,7 +1445,7 @@ jobs:
|
|||
--verbose \
|
||||
--command="tr ' ' '\\n' | awk '/\\.py/ {print; next} {sub(/\\.[A-Z][^.]*$/, \"\"); gsub(/\\./, \"/\"); print \$0 \".py\"}' | xargs uv run --no-sync python -m pytest \
|
||||
-vv -x -s \
|
||||
--cov=./litellm --cov-report=xml \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml \
|
||||
--junitxml=test-results/junit.xml \
|
||||
--durations=5 -n 2 \
|
||||
--reruns 2 --reruns-delay 1"
|
||||
|
|
@ -3105,12 +2951,6 @@ workflows:
|
|||
filters: *main_branches
|
||||
- auth_ui_unit_tests:
|
||||
filters: *main_branches
|
||||
- proxy_behavior_tests:
|
||||
filters: *main_branches
|
||||
- proxy_security_tests:
|
||||
filters: *main_branches
|
||||
- schema_migration_check:
|
||||
filters: *main_branches
|
||||
- build_docker_database_image:
|
||||
filters: *main_branches
|
||||
- e2e_ui_testing:
|
||||
|
|
@ -3167,8 +3007,6 @@ workflows:
|
|||
filters: *main_branches
|
||||
- search_testing:
|
||||
filters: *main_branches
|
||||
- litellm_mapped_enterprise_tests:
|
||||
filters: *main_branches
|
||||
- batches_testing:
|
||||
filters: *main_branches
|
||||
- litellm_utils_testing:
|
||||
|
|
@ -3191,7 +3029,6 @@ workflows:
|
|||
- guardrails_testing
|
||||
- ocr_testing
|
||||
- search_testing
|
||||
- litellm_mapped_enterprise_tests
|
||||
- batches_testing
|
||||
- litellm_utils_testing
|
||||
- pass_through_unit_testing
|
||||
|
|
|
|||
36
.github/ci-coverage-allowlist.yml
vendored
36
.github/ci-coverage-allowlist.yml
vendored
|
|
@ -5,24 +5,21 @@ description: >-
|
|||
|
||||
test_paths:
|
||||
- reason: >-
|
||||
The caching suite in tests/local_testing, which runs nowhere. Every job that globs that
|
||||
directory either deselects it (local_testing_part1 and part2 carry `-k "... and not caching
|
||||
and not cache"`) or keeps only another keyword (langfuse, router, assistants), and no job
|
||||
names these files the way redis_caching_unit_tests names test_dual_cache.py. Measured
|
||||
2026-08-20 by collecting the directory under each job's own selector: 118 tests across
|
||||
these eight files are selected by none of them. Listed so the gap is a decision rather
|
||||
than an accident, and so the --slices guard has a baseline to ratchet down from. Revisit
|
||||
when tests/local_testing is ported off CircleCI, where the keyless part of this suite
|
||||
belongs in a real job
|
||||
What is left of the caching suite in tests/local_testing that runs nowhere. Every job that
|
||||
globs that directory either deselects it (local_testing_part1 and part2 carry `-k "... and
|
||||
not caching and not cache"`) or keeps only another keyword (langfuse, router, assistants),
|
||||
and no job names these files the way redis_caching_unit_tests names test_dual_cache.py.
|
||||
The gap was eight files and 118 tests when measured 2026-08-20; the five keyless ones now
|
||||
run in the caching-local shard, leaving these three. Measured 2026-08-21 with no provider
|
||||
credentials and no Redis: test_caching.py needs both (37 of 65 fail without them),
|
||||
test_disk_cache_unit_tests.py needs OPENAI_API_KEY for 2 of its 4, and
|
||||
test_gcs_cache_unit_tests.py needs GCS credentials for all 4. They want the keyless/live
|
||||
split that porting tests/local_testing off CircleCI will force, not a job that is red by
|
||||
construction
|
||||
paths:
|
||||
- tests/local_testing/test_cache_preset_key.py
|
||||
- tests/local_testing/test_caching.py
|
||||
- tests/local_testing/test_caching_handler.py
|
||||
- tests/local_testing/test_disk_cache_unit_tests.py
|
||||
- tests/local_testing/test_gcs_cache_unit_tests.py
|
||||
- tests/local_testing/test_prompt_caching.py
|
||||
- tests/local_testing/test_responses_stream_cache_keys.py
|
||||
- tests/local_testing/test_unit_test_caching.py
|
||||
- reason: >-
|
||||
The end-to-end suite runs against a deployed proxy from its own in-cluster rig rather than
|
||||
from a pull request; it needs a live gateway and provider credentials no PR job holds
|
||||
|
|
@ -92,17 +89,6 @@ test_paths:
|
|||
- tests/integration/sandbox/test_e2b_sandbox.py
|
||||
- tests/integration/test_oci_integration.py
|
||||
- tests/integration/test_oci_proxy_integration.py
|
||||
- reason: >-
|
||||
A unit test for the proxy-extras package that no job invokes, while the package's other
|
||||
tests live under tests/proxy_migration_tests. Measured 2026-08-20: 24 of its 28 tests pass
|
||||
and the 4 in TestMigrationSQLIdempotency fail, because 13 migrations from 2026-03 onward use
|
||||
bare CREATE TABLE, ADD COLUMN, CREATE INDEX and ADD CONSTRAINT rather than the guarded forms
|
||||
this file requires. It also matches those keywords inside SQL comments, so two further
|
||||
migrations are reported that are in fact fine. Wiring it up means deciding what to do about
|
||||
the 13 first, and they cannot simply be edited: Prisma checksums an applied migration, so a
|
||||
changed one breaks migrate deploy for existing installs
|
||||
paths:
|
||||
- tests/litellm-proxy-extras/test_litellm_proxy_extras_utils.py
|
||||
|
||||
dockerfiles:
|
||||
- reason: >-
|
||||
|
|
|
|||
16
.github/scripts/assert_ci_coverage.py
vendored
16
.github/scripts/assert_ci_coverage.py
vendored
|
|
@ -312,8 +312,23 @@ def _matchable_names(relative_path: str) -> frozenset[str]:
|
|||
)
|
||||
|
||||
|
||||
def _workflow_named_tokens() -> frozenset[str]:
|
||||
"""Test tokens a GitHub Actions job names directly.
|
||||
|
||||
A CircleCI `-k` that deselects a file no longer means the file runs nowhere once a
|
||||
workflow names it, so the slice check has to credit those the same way the census does.
|
||||
"""
|
||||
return _invoked_test_tokens(
|
||||
scalar
|
||||
for path in _config_files()
|
||||
if path != CIRCLECI_CONFIG
|
||||
for scalar in _scalars(yaml.safe_load(path.read_text(encoding="utf-8")), path.name)
|
||||
)
|
||||
|
||||
|
||||
def _deselected_everywhere(allowlist: Allowlist) -> tuple[Finding, ...]:
|
||||
slices: Final = _slices()
|
||||
named_by_workflow: Final = _workflow_named_tokens()
|
||||
globbed: Final = tuple(
|
||||
path
|
||||
for path in _test_files()
|
||||
|
|
@ -326,6 +341,7 @@ def _deselected_everywhere(allowlist: Allowlist) -> tuple[Finding, ...]:
|
|||
)
|
||||
for path in globbed
|
||||
if not allowlist.covers_test(path)
|
||||
and not any(_token_covers(token, path) for token in named_by_workflow)
|
||||
and not any(slice_.claims(path, _matchable_names(path)) for slice_ in slices)
|
||||
)
|
||||
|
||||
|
|
|
|||
4
.github/workflows/_test-unit-base.yml
vendored
4
.github/workflows/_test-unit-base.yml
vendored
|
|
@ -149,7 +149,7 @@ jobs:
|
|||
--reruns "${RERUNS}" \
|
||||
--reruns-delay 1 \
|
||||
--durations=20 \
|
||||
--cov=./litellm \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise \
|
||||
--cov-report=xml:coverage.xml \
|
||||
--cov-config=pyproject.toml
|
||||
else
|
||||
|
|
@ -161,7 +161,7 @@ jobs:
|
|||
--reruns-delay 1 \
|
||||
--dist="${DIST}" \
|
||||
--durations=20 \
|
||||
--cov=./litellm \
|
||||
--cov=./litellm --cov=./enterprise/litellm_enterprise \
|
||||
--cov-report=xml:coverage.xml \
|
||||
--cov-config=pyproject.toml
|
||||
fi
|
||||
|
|
|
|||
31
.github/workflows/image-scan.yml
vendored
31
.github/workflows/image-scan.yml
vendored
|
|
@ -23,6 +23,8 @@ on:
|
|||
- tests/proxy_migration_tests/**
|
||||
- uv.lock
|
||||
- ui/litellm-dashboard/package-lock.json
|
||||
- ui/Dockerfile
|
||||
- ui/nginx.conf
|
||||
- .github/workflows/image-scan.yml
|
||||
schedule:
|
||||
- cron: "41 6 * * *"
|
||||
|
|
@ -185,6 +187,35 @@ jobs:
|
|||
python -m pip install "pytest==9.0.3"
|
||||
python -m pytest tests/proxy_migration_tests/test_component_image_serves_offline.py -v
|
||||
|
||||
ui-image:
|
||||
name: ui-image
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name != 'pull_request' ||
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build UI image
|
||||
run: docker build -f ui/Dockerfile -t litellm-ui-scan:${{ github.sha }} .
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Verify the UI serves offline as an arbitrary uid with a read-only root fs
|
||||
env:
|
||||
LITELLM_IMAGE: litellm-ui-scan:${{ github.sha }}
|
||||
run: |
|
||||
python -m pip install "pytest==9.0.3"
|
||||
python -m pytest tests/proxy_migration_tests/test_ui_image_serves_offline.py -v
|
||||
|
||||
backend-image:
|
||||
name: backend-image
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
11
.github/workflows/test-linting.yml
vendored
11
.github/workflows/test-linting.yml
vendored
|
|
@ -67,6 +67,17 @@ jobs:
|
|||
with:
|
||||
version: "0.10.9"
|
||||
|
||||
- name: Cache uv dependencies
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
.venv
|
||||
key: ${{ runner.os }}-uv-lint-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-lint-
|
||||
|
||||
- name: Clean Python cache
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/test-mcp.yml
vendored
2
.github/workflows/test-mcp.yml
vendored
|
|
@ -60,4 +60,4 @@ jobs:
|
|||
- name: Run MCP tests
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
run: |
|
||||
uv run --no-sync pytest tests/mcp_tests -x -vv -n 4 --cov=./litellm --cov-report=xml --durations=5
|
||||
uv run --no-sync pytest tests/mcp_tests -x -vv -n 4 --cov=./litellm --cov=./enterprise/litellm_enterprise --cov-report=xml --durations=5
|
||||
|
|
|
|||
145
.github/workflows/test-postgres.yml
vendored
Normal file
145
.github/workflows/test-postgres.yml
vendored
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
name: "Postgres Tests"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- litellm_internal_staging
|
||||
- litellm_oss_staging
|
||||
- "litellm_**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- litellm_internal_staging
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
postgres:
|
||||
name: ${{ matrix.shard }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: ${{ matrix.job-timeout-minutes }}
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16@sha256:e17e86066e5ef83e0952a9347f5c792b7ece00972e2aa787a6986f471b3dd3d5
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: litellm_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- shard: proxy-behavior
|
||||
test-path: "tests/proxy_behavior"
|
||||
seed: db-push
|
||||
workers: 0
|
||||
timeout-minutes: 25
|
||||
job-timeout-minutes: 50
|
||||
|
||||
- shard: proxy-security
|
||||
test-path: "tests/proxy_security_tests"
|
||||
seed: db-push
|
||||
workers: 0
|
||||
timeout-minutes: 15
|
||||
job-timeout-minutes: 40
|
||||
|
||||
- shard: schema-migration
|
||||
test-path: "tests/proxy_migration_tests"
|
||||
seed: none
|
||||
workers: 0
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 45
|
||||
|
||||
env:
|
||||
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/litellm_test"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
timeout-minutes: 3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect relevant changes
|
||||
id: changes
|
||||
timeout-minutes: 2
|
||||
uses: ./.github/actions/detect-changes
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 3
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up uv
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 3
|
||||
uses: ./.github/actions/setup-uv-with-retries
|
||||
with:
|
||||
version: "0.10.9"
|
||||
|
||||
- name: Cache uv dependencies
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 5
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
~/.cache/uv
|
||||
.venv
|
||||
key: ${{ runner.os }}-uv-postgres-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-uv-postgres-
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 12
|
||||
run: |
|
||||
.github/scripts/uv_sync_with_retries.sh --frozen --all-groups --all-extras
|
||||
|
||||
- name: Cache Prisma binaries
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 3
|
||||
uses: ./.github/actions/cache-prisma-binaries
|
||||
|
||||
- name: Generate Prisma client
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: 5
|
||||
run: |
|
||||
uv run --no-sync prisma generate --schema litellm/proxy/schema.prisma
|
||||
|
||||
- name: Seed database schema
|
||||
if: steps.changes.outputs.decision != 'skip' && matrix.seed != 'none'
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
uv run --no-sync prisma db push --schema litellm/proxy/schema.prisma --accept-data-loss
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changes.outputs.decision != 'skip'
|
||||
timeout-minutes: ${{ matrix.timeout-minutes }}
|
||||
env:
|
||||
TEST_PATH: ${{ matrix.test-path }}
|
||||
WORKERS: ${{ matrix.workers }}
|
||||
run: |
|
||||
if [ "${WORKERS}" = "0" ]; then
|
||||
uv run --no-sync pytest ${TEST_PATH:?} -vv --tb=short --durations=10
|
||||
else
|
||||
uv run --no-sync pytest ${TEST_PATH:?} -vv --tb=short --durations=10 -n "${WORKERS}"
|
||||
fi
|
||||
33
.github/workflows/test-unit.yml
vendored
33
.github/workflows/test-unit.yml
vendored
|
|
@ -168,7 +168,7 @@ jobs:
|
|||
tests/test_litellm/proxy/ui_crud_endpoints
|
||||
tests/test_litellm/proxy/config_resolvers
|
||||
tests/test_litellm/proxy/utils
|
||||
workers: 2
|
||||
workers: 4
|
||||
reruns: 2
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 60
|
||||
|
|
@ -195,11 +195,40 @@ jobs:
|
|||
tests/test_litellm/proxy/types_utils
|
||||
tests/test_litellm/proxy/logging_endpoints
|
||||
tests/test_litellm/proxy/test_*.py
|
||||
workers: 2
|
||||
workers: 4
|
||||
reruns: 2
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 60
|
||||
|
||||
- shard: caching-local
|
||||
artifact-name: caching-local
|
||||
test-path: >-
|
||||
tests/local_testing/test_cache_preset_key.py
|
||||
tests/local_testing/test_caching_handler.py
|
||||
tests/local_testing/test_prompt_caching.py
|
||||
tests/local_testing/test_responses_stream_cache_keys.py
|
||||
tests/local_testing/test_unit_test_caching.py
|
||||
workers: 2
|
||||
reruns: 2
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 55
|
||||
|
||||
- shard: proxy-extras
|
||||
artifact-name: proxy-extras
|
||||
test-path: "tests/litellm-proxy-extras"
|
||||
workers: 2
|
||||
reruns: 2
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 55
|
||||
|
||||
- shard: enterprise-package
|
||||
artifact-name: enterprise-package
|
||||
test-path: "tests/enterprise"
|
||||
workers: 4
|
||||
reruns: 2
|
||||
timeout-minutes: 20
|
||||
job-timeout-minutes: 55
|
||||
|
||||
- shard: responses-caching-types
|
||||
artifact-name: responses-caching-types
|
||||
test-path: >-
|
||||
|
|
|
|||
|
|
@ -966,6 +966,16 @@ class CheckBatchCost:
|
|||
)
|
||||
|
||||
elif response.status in PROVIDER_TERMINAL_BATCH_STATUSES:
|
||||
from litellm.proxy.openai_files_endpoints.common_utils import (
|
||||
_completed_batch_safe_to_retire,
|
||||
)
|
||||
|
||||
if response.status in ("completed", "complete") and not _completed_batch_safe_to_retire(response):
|
||||
verbose_proxy_logger.info(
|
||||
f"CheckBatchCost: batch {batch_id} is completed but its output file id "
|
||||
f"has not appeared yet; leaving job {job.id} for the next poll cycle"
|
||||
)
|
||||
continue
|
||||
await self._finalize_unbilled_terminal_job(job, response)
|
||||
|
||||
# Record polling run metrics (always, even if nothing was processed)
|
||||
|
|
|
|||
|
|
@ -428,9 +428,11 @@ ui:
|
|||
maxUnavailable: ""
|
||||
podAnnotations: {}
|
||||
# Same shape as the gateway blocks of the same name. The nginx runtime
|
||||
# writes its pid, cache, and proxy temp files under the image's root
|
||||
# filesystem, so `securityContext.readOnlyRootFilesystem: true` here needs
|
||||
# emptyDir volumes mounted over those paths.
|
||||
# writes its pid, cache, and proxy temp files under /tmp, so it boots as
|
||||
# any (arbitrary, non-root) uid; `securityContext.readOnlyRootFilesystem:
|
||||
# true` here needs an emptyDir volume mounted over /tmp. Images before
|
||||
# the /tmp move instead need emptyDirs over /var/cache/nginx and /run to
|
||||
# run as a non-root uid at all.
|
||||
podLabels: {}
|
||||
podSecurityContext: {}
|
||||
securityContext: {}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Custom A2A Card Resolver for LiteLLM.
|
|||
Extends the A2A SDK's card resolver to support multiple well-known paths.
|
||||
"""
|
||||
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
|
|
@ -48,6 +49,43 @@ def is_localhost_or_internal_url(url: str | None) -> bool:
|
|||
return any(pattern in url_lower for pattern in LOCALHOST_URL_PATTERNS)
|
||||
|
||||
|
||||
_CANONICAL_PROTOCOL_BINDINGS: Final = MappingProxyType(
|
||||
{
|
||||
"jsonrpc": "JSONRPC",
|
||||
"http+json": "HTTP+JSON",
|
||||
"grpc": "GRPC",
|
||||
}
|
||||
)
|
||||
|
||||
_LEGACY_PROTOCOL_VERSION: Final = "0.3"
|
||||
|
||||
|
||||
def normalize_agent_card_interfaces(agent_card: "AgentCard") -> "AgentCard":
|
||||
"""
|
||||
Canonicalize the supported interfaces of spec-adjacent agent cards.
|
||||
|
||||
Some A2A servers (e.g. LangGraph Platform) serve agent cards with lowercase
|
||||
bindings like "jsonrpc", but a2a-sdk's ClientFactory matches bindings
|
||||
case-sensitively against its uppercase TransportProtocol constants and fails
|
||||
with "no compatible transports found." for spec-adjacent casings.
|
||||
|
||||
The same servers also speak the A2A 0.3 JSON dialect ("kind"-discriminated
|
||||
payloads) while declaring protocolVersion "1.0", which a2a-sdk's strict v1
|
||||
proto parsing rejects. A mis-cased binding fingerprints such a server, so its
|
||||
declared version is downgraded to 0.3 to route the SDK's ClientFactory onto
|
||||
its v0.3 compat transport, which speaks that dialect.
|
||||
"""
|
||||
normalized: Final = type(agent_card)()
|
||||
normalized.CopyFrom(agent_card)
|
||||
for interface in normalized.supported_interfaces:
|
||||
canonical: str | None = _CANONICAL_PROTOCOL_BINDINGS.get(interface.protocol_binding.lower())
|
||||
if canonical is None or canonical == interface.protocol_binding:
|
||||
continue
|
||||
interface.protocol_binding = canonical
|
||||
interface.protocol_version = _LEGACY_PROTOCOL_VERSION
|
||||
return normalized
|
||||
|
||||
|
||||
def get_agent_card_url(agent_card: "AgentCard") -> str | None:
|
||||
"""Return the agent endpoint URL from the resolved SDK card."""
|
||||
url: Final = getattr(agent_card, "url", None)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ except ImportError:
|
|||
from litellm.a2a_protocol.card_resolver import (
|
||||
LiteLLMA2ACardResolver,
|
||||
get_agent_card_url,
|
||||
normalize_agent_card_interfaces,
|
||||
)
|
||||
from litellm.a2a_protocol.exception_mapping_utils import (
|
||||
handle_a2a_localhost_retry,
|
||||
|
|
@ -782,13 +783,17 @@ async def create_a2a_client(
|
|||
if extra_headers:
|
||||
verbose_proxy_logger.debug("A2A client created with extra_headers=%s", list(extra_headers.keys()))
|
||||
|
||||
resolver: Final = A2ACardResolver(httpx_client=httpx_client, base_url=base_url)
|
||||
agent_card: Final = normalize_agent_card_interfaces(
|
||||
await resolver.get_agent_card(http_kwargs={"headers": extra_headers} if extra_headers else None)
|
||||
)
|
||||
|
||||
a2a_client: Final = await create_client( # pyright: ignore[reportOptionalCall]
|
||||
base_url,
|
||||
agent_card,
|
||||
client_config=ClientConfig( # pyright: ignore[reportOptionalCall]
|
||||
httpx_client=httpx_client,
|
||||
streaming=streaming,
|
||||
),
|
||||
resolver_http_kwargs={"headers": extra_headers} if extra_headers else None,
|
||||
)
|
||||
# Stash LiteLLM-owned handles on the client so the localhost-retry path can reuse
|
||||
# the configured httpx client and this agent's headers without excavating
|
||||
|
|
@ -799,9 +804,7 @@ async def create_a2a_client(
|
|||
if extra_headers
|
||||
else None
|
||||
)
|
||||
agent_card: Final = getattr(a2a_client, "_card", None)
|
||||
if agent_card is not None:
|
||||
a2a_client._litellm_agent_card = agent_card
|
||||
a2a_client._litellm_agent_card = agent_card
|
||||
|
||||
verbose_logger.info("A2A client created for %s", base_url)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ from pydantic import BaseModel
|
|||
import litellm
|
||||
from litellm import ModelResponse
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
responses_reasoning_item_from_thinking_blocks,
|
||||
)
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.llms.base_llm.bridges.completion_transformation import (
|
||||
CompletionTransformationBridge,
|
||||
|
|
@ -85,6 +88,22 @@ def _get_reasoning_items(
|
|||
return []
|
||||
|
||||
|
||||
def _reasoning_input_items(msg: "AllMessageValues") -> list[dict[str, object]]: # mutable-ok: API message payload
|
||||
"""Reasoning input items for an assistant message.
|
||||
|
||||
Stored reasoning items win because they carry an id the Responses API minted; thinking
|
||||
blocks are the fallback for turns that arrived over another API surface.
|
||||
"""
|
||||
items: Final = _get_reasoning_items(msg)
|
||||
stored: Final = [_reasoning_item_to_response_input(item) for item in items] # mutable-ok: API message payload
|
||||
if stored:
|
||||
return stored
|
||||
raw_blocks: Final = msg.get("thinking_blocks") or ()
|
||||
blocks: Final = cast("Iterable[ChatCompletionThinkingBlock]", raw_blocks) # cast-ok: untyped client json
|
||||
from_thinking: Final = responses_reasoning_item_from_thinking_blocks(blocks)
|
||||
return [] if from_thinking is None else [dict(from_thinking)] # mutable-ok: API message payload
|
||||
|
||||
|
||||
def _build_reasoning_item(
|
||||
item_id: str,
|
||||
encrypted_content: str | None,
|
||||
|
|
@ -372,8 +391,15 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
)
|
||||
)
|
||||
elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
|
||||
for r_item in _get_reasoning_items(msg):
|
||||
input_items.append(_reasoning_item_to_response_input(r_item))
|
||||
input_items.extend(_reasoning_input_items(msg))
|
||||
if content:
|
||||
input_items.append(
|
||||
{ # mutable-ok: API message payload
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"content": self._convert_content_to_responses_format(content, "assistant"),
|
||||
}
|
||||
)
|
||||
for tool_call in tool_calls:
|
||||
function = tool_call.get("function")
|
||||
custom = tool_call.get("custom")
|
||||
|
|
@ -400,15 +426,16 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
raise ValueError(f"tool call not supported: {tool_call}")
|
||||
elif content is not None:
|
||||
if role == "assistant":
|
||||
for r_item in _get_reasoning_items(msg):
|
||||
input_items.append(_reasoning_item_to_response_input(r_item))
|
||||
input_items.extend(_reasoning_input_items(msg))
|
||||
input_items.append(
|
||||
{
|
||||
{ # mutable-ok: API message payload
|
||||
"type": "message",
|
||||
"role": role,
|
||||
"content": self._convert_content_to_responses_format(content, cast(str, role)),
|
||||
}
|
||||
)
|
||||
elif role == "assistant":
|
||||
input_items.extend(_reasoning_input_items(msg))
|
||||
|
||||
return input_items, instructions
|
||||
|
||||
|
|
|
|||
|
|
@ -1542,6 +1542,8 @@ SPEND_LOG_WRITE_BATCH_MAX_ROWS: Final = max(1, int(os.getenv("SPEND_LOG_WRITE_BA
|
|||
SPEND_LOG_QUEUE_SIZE_THRESHOLD: Final = int(os.getenv("SPEND_LOG_QUEUE_SIZE_THRESHOLD", 100))
|
||||
SPEND_LOG_QUEUE_MAX_BYTES: Final = max(1, int(os.getenv("SPEND_LOG_QUEUE_MAX_BYTES", "64000000")))
|
||||
SPEND_LOG_QUEUE_POLL_INTERVAL: Final = float(os.getenv("SPEND_LOG_QUEUE_POLL_INTERVAL", 2.0))
|
||||
RESPONSES_SESSION_LOOKUP_MAX_ATTEMPTS: Final = max(1, int(os.getenv("RESPONSES_SESSION_LOOKUP_MAX_ATTEMPTS", "3")))
|
||||
RESPONSES_SESSION_LOOKUP_RETRY_INTERVAL: Final = float(os.getenv("RESPONSES_SESSION_LOOKUP_RETRY_INTERVAL", "0.2"))
|
||||
SPEND_COUNTER_RESEED_LOCKS_MAX_SIZE: Final = int(os.getenv("SPEND_COUNTER_RESEED_LOCKS_MAX_SIZE", 10000))
|
||||
DEFAULT_CRON_JOB_LOCK_TTL_SECONDS: Final = int(os.getenv("DEFAULT_CRON_JOB_LOCK_TTL_SECONDS", 60)) # 1 minute
|
||||
PROXY_BUDGET_RESCHEDULER_MIN_TIME: Final = int(os.getenv("PROXY_BUDGET_RESCHEDULER_MIN_TIME", 597))
|
||||
|
|
@ -1561,6 +1563,19 @@ STALE_OBJECT_CLEANUP_BATCH_SIZE: Final = max(1, int(os.getenv("STALE_OBJECT_CLEA
|
|||
# installations with large numbers of stale managed objects).
|
||||
_batch_polling_env: Final = os.getenv("PROXY_BATCH_POLLING_ENABLED", "true").lower()
|
||||
PROXY_BATCH_POLLING_ENABLED: Final = _batch_polling_env == "true"
|
||||
BACKGROUND_INTERACTION_COST_POLL_INITIAL_INTERVAL_SECONDS: Final = float(
|
||||
os.getenv("BACKGROUND_INTERACTION_COST_POLL_INITIAL_INTERVAL_SECONDS", "5")
|
||||
)
|
||||
BACKGROUND_INTERACTION_COST_POLL_MAX_INTERVAL_SECONDS: Final = float(
|
||||
os.getenv("BACKGROUND_INTERACTION_COST_POLL_MAX_INTERVAL_SECONDS", "60")
|
||||
)
|
||||
BACKGROUND_INTERACTION_COST_POLL_TIMEOUT_SECONDS: Final = float(
|
||||
os.getenv("BACKGROUND_INTERACTION_COST_POLL_TIMEOUT_SECONDS", "3600")
|
||||
)
|
||||
_background_interaction_cost_polling_env: Final = os.getenv(
|
||||
"BACKGROUND_INTERACTION_COST_POLLING_ENABLED", "true"
|
||||
).lower()
|
||||
BACKGROUND_INTERACTION_COST_POLLING_ENABLED: Final = _background_interaction_cost_polling_env == "true"
|
||||
PROXY_BUDGET_RESCHEDULER_MAX_TIME: Final = int(os.getenv("PROXY_BUDGET_RESCHEDULER_MAX_TIME", 605))
|
||||
PROXY_BATCH_WRITE_AT: Final = int(os.getenv("PROXY_BATCH_WRITE_AT", 10)) # in seconds, increased from 10
|
||||
PROXY_CONFIG_RELOAD_INTERVAL_SECONDS: Final = get_env_int("PROXY_CONFIG_RELOAD_INTERVAL_SECONDS", 30)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
|
|||
StandardBuiltInToolCostTracking,
|
||||
)
|
||||
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
|
||||
InteractionsUsageObjectTransformation,
|
||||
TranscriptionUsageObjectTransformation,
|
||||
)
|
||||
from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
||||
|
|
@ -912,6 +913,8 @@ def _get_usage_object(
|
|||
usage_obj,
|
||||
)
|
||||
)
|
||||
elif isinstance(usage_obj, dict) and InteractionsUsageObjectTransformation.is_interactions_usage_object(usage_obj):
|
||||
return InteractionsUsageObjectTransformation.transform_interactions_usage_object(usage_obj)
|
||||
elif isinstance(usage_obj, dict):
|
||||
return Usage(**usage_obj)
|
||||
elif isinstance(usage_obj, BaseModel):
|
||||
|
|
@ -1288,6 +1291,10 @@ def completion_cost(
|
|||
)
|
||||
if tr_usage is not None:
|
||||
_usage = tr_usage.model_dump()
|
||||
elif InteractionsUsageObjectTransformation.is_interactions_usage_object(_usage):
|
||||
_usage = InteractionsUsageObjectTransformation.transform_interactions_usage_object(
|
||||
_usage
|
||||
).model_dump()
|
||||
else:
|
||||
_usage = _usage
|
||||
|
||||
|
|
|
|||
|
|
@ -422,24 +422,32 @@ def image_generation(
|
|||
aimg_generation=aimg_generation,
|
||||
)
|
||||
elif custom_llm_provider == "azure_ai":
|
||||
from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
|
||||
from litellm.llms.azure_ai.common_utils import (
|
||||
AzureFoundryModelInfo,
|
||||
get_azure_ai_auth_headers,
|
||||
)
|
||||
|
||||
api_base = AzureFoundryModelInfo.get_api_base(api_base)
|
||||
api_key = AzureFoundryModelInfo.get_api_key(api_key)
|
||||
if extra_headers is not None:
|
||||
optional_params["extra_headers"] = extra_headers
|
||||
|
||||
default_headers = {
|
||||
caller_header_names = frozenset(name.lower() for name in headers)
|
||||
caller_set_auth = "api-key" in caller_header_names or "authorization" in caller_header_names
|
||||
auth_headers = (
|
||||
headers
|
||||
if caller_set_auth
|
||||
else get_azure_ai_auth_headers(
|
||||
api_key=api_key,
|
||||
litellm_params=litellm_params_dict,
|
||||
api_key_header="api-key",
|
||||
)
|
||||
)
|
||||
request_headers: Final = {
|
||||
"Content-Type": "application/json",
|
||||
**auth_headers,
|
||||
**headers,
|
||||
}
|
||||
# Only add api-key header if api_key is not None
|
||||
# Azure AD authentication will use Authorization header instead
|
||||
if api_key is not None:
|
||||
default_headers["api-key"] = api_key
|
||||
|
||||
for k, v in default_headers.items():
|
||||
if k not in headers:
|
||||
headers[k] = v
|
||||
|
||||
model_response = azure_chat_completions.image_generation(
|
||||
model=model,
|
||||
|
|
@ -455,7 +463,7 @@ def image_generation(
|
|||
api_version=api_version,
|
||||
aimg_generation=aimg_generation,
|
||||
client=client,
|
||||
headers=headers,
|
||||
headers=request_headers,
|
||||
litellm_params=litellm_params_dict,
|
||||
)
|
||||
elif (
|
||||
|
|
|
|||
|
|
@ -294,12 +294,18 @@
|
|||
"id": "newrelic",
|
||||
"displayName": "New Relic",
|
||||
"logo": "newrelic.png",
|
||||
"supports_key_team_logging": false,
|
||||
"supports_key_team_logging": true,
|
||||
"dynamic_params": {
|
||||
"NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED": {
|
||||
"newrelic_api_key": {
|
||||
"type": "password",
|
||||
"ui_name": "New Relic Ingest License Key",
|
||||
"description": "Per-team ingest (license) key. Team traces export to this key's New Relic account over OTLP.",
|
||||
"required": false
|
||||
},
|
||||
"newrelic_region": {
|
||||
"type": "text",
|
||||
"ui_name": "Record AI Content (default: true)",
|
||||
"description": "Whether to record AI message content. Set to false to disable.",
|
||||
"ui_name": "New Relic Region (us or eu)",
|
||||
"description": "Data center region for this team's account. Defaults to us.",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class SpanEmitter:
|
|||
links=list(links) if links else None,
|
||||
)
|
||||
|
||||
def mark_emitted(self, dedup_key: str | None, role: SpanRole) -> None:
|
||||
"""Register a span emitted outside :meth:`emit` (the boundary-opened
|
||||
LLM-call span closed via :meth:`finish_span`) so a later :meth:`emit`
|
||||
for the same ``(dedup_key, role)`` deduplicates against it."""
|
||||
self._seen(dedup_key, role)
|
||||
|
||||
def _seen(self, dedup_key: str | None, role: SpanRole) -> bool:
|
||||
"""Return True once a ``(dedup_key, role)`` pair has been emitted.
|
||||
|
||||
|
|
|
|||
|
|
@ -484,10 +484,15 @@ class OpenTelemetryV2(CustomLogger):
|
|||
# ``pop`` is the dedup: this method runs from both the success and failure
|
||||
# paths, and whichever fires first removes the carrier and closes the span.
|
||||
carrier: Final = self._open_llm_calls.pop(call_id, None) if call_id else None
|
||||
if carrier is None:
|
||||
# A missing carrier does not always mean nothing happened: a team/key-scoped
|
||||
# logger is a success/failure callback only, so ``pre_call`` never reaches it
|
||||
# and no carrier exists. The payload plus the request-level provider-handoff
|
||||
# stamp (``upstream_started``) is the affirmative signal of a real call; a
|
||||
# gate rejection carries ``is_no_upstream_call`` and gets no span.
|
||||
if carrier is None and (call.is_no_upstream_call or not call.upstream_started or call.payload is None):
|
||||
return None
|
||||
try:
|
||||
return self._finish_carrier(carrier, call, end_time)
|
||||
return self._finish_carrier(carrier, call, start_time, end_time)
|
||||
finally:
|
||||
# After the span has ended, so a release-triggered provider shutdown
|
||||
# force-flushes it out rather than racing its enqueue.
|
||||
|
|
@ -497,8 +502,11 @@ class OpenTelemetryV2(CustomLogger):
|
|||
"""Remember an in-flight LLM call, evicting the oldest if over budget.
|
||||
|
||||
A call that opens but never closes (a stream that only fires stream
|
||||
events) would linger otherwise; the evicted span is simply dropped
|
||||
(never exported).
|
||||
events) would linger otherwise. Eviction only drops the boundary carrier,
|
||||
not the call: if that call later closes as a real completed call, it still
|
||||
emits through the deferred branch in ``_close_llm_call`` (the same path a
|
||||
team/key-scoped logger uses, since it never opens a carrier), deduplicated
|
||||
by call id. Only a call that is evicted and never closes goes unexported.
|
||||
"""
|
||||
self._open_llm_calls[call_id] = carrier
|
||||
if len(self._open_llm_calls) > _OPEN_CALLS_MAX:
|
||||
|
|
@ -512,15 +520,20 @@ class OpenTelemetryV2(CustomLogger):
|
|||
|
||||
def _finish_carrier(
|
||||
self,
|
||||
carrier: _LLMCallSpan,
|
||||
carrier: "_LLMCallSpan | None",
|
||||
call: LLMCallEvent,
|
||||
start_time: datetime | float | None,
|
||||
end_time: datetime | float | None,
|
||||
) -> Span | None:
|
||||
payload: Final = call.payload
|
||||
call_id: Final = call.call_id
|
||||
if payload is None:
|
||||
if carrier.span is not None:
|
||||
if carrier is not None and carrier.span is not None:
|
||||
# Opened at the boundary but the payload never materialized — end
|
||||
# it (named provisionally) so it isn't leaked as an open span.
|
||||
# it (named provisionally) so it isn't leaked as an open span, and
|
||||
# register the dedup marker so a later payload-carrying close for
|
||||
# the same call id cannot re-emit through the deferred branch.
|
||||
self._emitter.mark_emitted(call_id, SpanRole.LLM_CALL)
|
||||
carrier.span.end(end_time=to_ns(end_time))
|
||||
return None
|
||||
data: Final = LLMCallSpanData.from_standard_logging_payload(
|
||||
|
|
@ -529,10 +542,13 @@ class OpenTelemetryV2(CustomLogger):
|
|||
time_to_first_chunk_seconds=call.time_to_first_chunk_seconds,
|
||||
)
|
||||
end_time_ns: Final = to_ns(end_time)
|
||||
if carrier.span is not None:
|
||||
if carrier is not None and carrier.span is not None:
|
||||
# Born at the boundary: stamp attributes from the typed payload, set
|
||||
# status, and end it. Its parent (the server span) was captured at
|
||||
# creation from real ambient context.
|
||||
# creation from real ambient context. Register the dedup marker so a
|
||||
# second close for the same call id (success then failure on one
|
||||
# logging object) cannot re-emit through the deferred branch.
|
||||
self._emitter.mark_emitted(call_id, SpanRole.LLM_CALL)
|
||||
self._emitter.finish_span(SpanRole.LLM_CALL, carrier.span, data, end_time_ns=end_time_ns)
|
||||
return carrier.span
|
||||
# Deferred: ``pre_call`` saw no recordable parent, so create the span now.
|
||||
|
|
@ -549,7 +565,7 @@ class OpenTelemetryV2(CustomLogger):
|
|||
SpanRole.LLM_CALL,
|
||||
data,
|
||||
parent_context=(set_span_in_context(INVALID_SPAN, parent_ctx) if route.detached else parent_ctx),
|
||||
start_time_ns=carrier.start_time_ns,
|
||||
start_time_ns=(carrier.start_time_ns if carrier is not None else to_ns(start_time)),
|
||||
end_time_ns=end_time_ns,
|
||||
tracer=route.tracer,
|
||||
links=_request_trace_links(parent_ctx) if route.detached else None,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class ExporterOwner(str, Enum):
|
|||
WEAVE_OTEL = "weave_otel"
|
||||
LEVO = "levo"
|
||||
AGENTOPS = "agentops"
|
||||
NEWRELIC = "newrelic"
|
||||
|
||||
|
||||
class _OTelV2Flag(BaseSettings):
|
||||
|
|
@ -97,6 +98,15 @@ class ExporterSpec(BaseModel):
|
|||
"auto (Simple for console/in_memory, Batch otherwise)."
|
||||
),
|
||||
)
|
||||
requires_headers: bool = Field(
|
||||
default=False,
|
||||
description=(
|
||||
"Skip this exporter when no headers are resolved. For destinations "
|
||||
"that reject unauthenticated exports (e.g. New Relic), a spec kept "
|
||||
"only as the per-request credential-stamping target would otherwise "
|
||||
"export keyless traffic and produce a 4xx for every span batch."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class OpenTelemetryV2Config(BaseSettings):
|
||||
|
|
|
|||
|
|
@ -203,6 +203,11 @@ class LLMCallEvent:
|
|||
# True for synthetic proxy-gate logs (auth / rate-limit rejections): they fire
|
||||
# the ``pre_call`` hook but never made an upstream call, so they get no span.
|
||||
is_no_upstream_call: bool
|
||||
# True once the request handed off to a provider (``pre_call`` stamped
|
||||
# ``api_call_start_time``). The affirmative signal that an LLM call was
|
||||
# actually attempted — router pre-call rejections, SDK failures before the
|
||||
# provider handoff, and standalone guardrail runs all lack it.
|
||||
upstream_started: bool
|
||||
# A best-effort ``"{operation} {model}"`` name known at ``pre_call`` time. The
|
||||
# span is renamed from the typed payload at close (``finish_span``); this only
|
||||
# needs to be reasonable for a span that never gets closed (a leak).
|
||||
|
|
@ -221,6 +226,7 @@ class LLMCallEvent:
|
|||
dynamic_params=kwargs.get("standard_callback_dynamic_params"),
|
||||
auth_metadata=auth_metadata(payload, kwargs),
|
||||
is_no_upstream_call=bool(kwargs.get(LITELLM_LOGGING_NO_UPSTREAM_LLM_CALL)),
|
||||
upstream_started=kwargs.get("api_call_start_time") is not None,
|
||||
provisional_span_name=f"{operation.value} {model}".strip(),
|
||||
time_to_first_chunk_seconds=time_to_first_chunk_seconds(kwargs),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -436,6 +436,8 @@ def build_tracer_provider(
|
|||
# ``config._normalize`` guarantees at least one spec (it folds the top-level
|
||||
# ``exporter``/``endpoint``/``headers`` fields in when ``exporters`` is empty).
|
||||
for spec in config.exporters:
|
||||
if spec.requires_headers and not spec.headers:
|
||||
continue
|
||||
exp = _exporter_from_spec(spec)
|
||||
provider.add_span_processor(
|
||||
_processor_for(
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from litellm.integrations.otel.plumbing.providers import (
|
|||
get_tracer,
|
||||
)
|
||||
from litellm.integrations.otel.presets import (
|
||||
dynamic_otlp_endpoint,
|
||||
dynamic_otlp_headers,
|
||||
project_routing_headers,
|
||||
)
|
||||
|
|
@ -129,7 +130,9 @@ class TenantTracerCache:
|
|||
# thread-pool workers concurrently with the event loop, so cache
|
||||
# updates, span counts, and retirement must be atomic.
|
||||
self._lock: Final = threading.Lock()
|
||||
self._providers: OrderedDict[tuple[_HeaderItems, _HeaderItems], TracerProvider] = OrderedDict()
|
||||
self._providers: OrderedDict[tuple[_HeaderItems, _HeaderItems, str | None], TracerProvider] = (
|
||||
OrderedDict() # mutable-ok: bounded LRU; eviction needs in-place ordered mutation
|
||||
)
|
||||
self._open_span_counts: dict[TracerProvider, int] = {} # mutable-ok: live refcount state
|
||||
# Oldest-first so an overflow of draining providers sheds the stalest.
|
||||
self._retired: OrderedDict[TracerProvider, None] = OrderedDict() # mutable-ok: draining evicted providers
|
||||
|
|
@ -182,12 +185,16 @@ class TenantTracerCache:
|
|||
project_headers: Final = self._project_headers(auth_metadata)
|
||||
if not credential_headers and not project_headers:
|
||||
return TenantRoute(tracer=default, detached=False)
|
||||
# A fixed per-integration region endpoint (New Relic us/eu), never a
|
||||
# caller-supplied host; ``None`` keeps the preset's own endpoint.
|
||||
endpoint: Final = dynamic_otlp_endpoint(self._callback_name, dynamic_params)
|
||||
cache_key: Final = (
|
||||
tuple(sorted(credential_headers.items())),
|
||||
tuple(sorted(project_headers.items())),
|
||||
endpoint,
|
||||
)
|
||||
with self._lock:
|
||||
provider: Final = self._cached_provider_locked(cache_key, credential_headers, project_headers)
|
||||
provider: Final = self._cached_provider_locked(cache_key, credential_headers, project_headers, endpoint)
|
||||
self._open_span_counts[provider] = self._open_span_counts.get(provider, 0) + 1
|
||||
evicted: Final = self._evicted_on_overflow_locked()
|
||||
if evicted is not None:
|
||||
|
|
@ -200,15 +207,16 @@ class TenantTracerCache:
|
|||
|
||||
def _cached_provider_locked(
|
||||
self,
|
||||
cache_key: tuple[_HeaderItems, _HeaderItems],
|
||||
cache_key: tuple[_HeaderItems, _HeaderItems, str | None],
|
||||
credential_headers: Mapping[str, str],
|
||||
project_headers: Mapping[str, str],
|
||||
endpoint: str | None,
|
||||
) -> TracerProvider:
|
||||
cached: Final = self._providers.get(cache_key)
|
||||
if cached is not None:
|
||||
self._providers.move_to_end(cache_key)
|
||||
return cached
|
||||
built: Final = build_tracer_provider(self._routed_config(credential_headers, project_headers))
|
||||
built: Final = build_tracer_provider(self._routed_config(credential_headers, project_headers, endpoint))
|
||||
self._providers[cache_key] = built
|
||||
return built
|
||||
|
||||
|
|
@ -257,6 +265,7 @@ class TenantTracerCache:
|
|||
self,
|
||||
credential_headers: Mapping[str, str],
|
||||
project_headers: Mapping[str, str],
|
||||
endpoint: str | None = None,
|
||||
) -> OpenTelemetryV2Config:
|
||||
"""Clone the config, rewriting headers on the callback's own exporter.
|
||||
|
||||
|
|
@ -272,7 +281,8 @@ class TenantTracerCache:
|
|||
``Authorization``), which must survive routing to a project.
|
||||
"""
|
||||
exporters: Final = [
|
||||
self._routed_exporter(spec, credential_headers, project_headers) for spec in self._config.exporters
|
||||
self._routed_exporter(spec, credential_headers, project_headers, endpoint)
|
||||
for spec in self._config.exporters
|
||||
]
|
||||
return self._config.model_copy(update={"exporters": exporters})
|
||||
|
||||
|
|
@ -281,6 +291,7 @@ class TenantTracerCache:
|
|||
spec: ExporterSpec,
|
||||
credential_headers: Mapping[str, str],
|
||||
project_headers: Mapping[str, str],
|
||||
endpoint: str | None = None,
|
||||
) -> ExporterSpec:
|
||||
kind: Final = spec.kind.lower()
|
||||
if spec.owner != self._callback_name or kind in _NON_OTLP_KINDS:
|
||||
|
|
@ -291,4 +302,10 @@ class TenantTracerCache:
|
|||
if project_headers and kind not in _GRPC_KINDS
|
||||
else base
|
||||
)
|
||||
return spec if routed == spec.headers else spec.model_copy(update={"headers": routed})
|
||||
update: Final = { # mutable-ok: model_copy(update=...) requires a plain dict
|
||||
field: value
|
||||
for field, value in (("headers", routed), ("endpoint", endpoint))
|
||||
if (field == "headers" and routed != spec.headers)
|
||||
or (field == "endpoint" and endpoint is not None and endpoint != spec.endpoint)
|
||||
}
|
||||
return spec if not update else spec.model_copy(update=update)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@ from litellm.integrations.otel.presets.langfuse import (
|
|||
)
|
||||
from litellm.integrations.otel.presets.langtrace import langtrace_preset
|
||||
from litellm.integrations.otel.presets.levo import levo_preset
|
||||
from litellm.integrations.otel.presets.newrelic import (
|
||||
newrelic_dynamic_endpoint,
|
||||
newrelic_dynamic_headers,
|
||||
newrelic_preset,
|
||||
)
|
||||
from litellm.integrations.otel.presets.phoenix import (
|
||||
phoenix_preset,
|
||||
phoenix_project_headers,
|
||||
|
|
@ -30,25 +35,45 @@ from litellm.types.utils import StandardCallbackDynamicParams
|
|||
|
||||
#: Callback name → preset. The ``Preset`` annotation makes mypy verify every
|
||||
#: registered value matches the preset interface.
|
||||
PRESET_BY_CALLBACK: Final[dict[str, Preset]] = {
|
||||
"agentops": agentops_preset,
|
||||
"arize": arize_preset,
|
||||
"arize_phoenix": phoenix_preset,
|
||||
"langfuse_otel": langfuse_preset,
|
||||
"langtrace": langtrace_preset,
|
||||
"levo": levo_preset,
|
||||
"weave_otel": weave_preset,
|
||||
}
|
||||
PRESET_BY_CALLBACK: Final[Mapping[str, Preset]] = MappingProxyType(
|
||||
{
|
||||
"agentops": agentops_preset,
|
||||
"arize": arize_preset,
|
||||
"arize_phoenix": phoenix_preset,
|
||||
"langfuse_otel": langfuse_preset,
|
||||
"langtrace": langtrace_preset,
|
||||
"levo": levo_preset,
|
||||
"newrelic": newrelic_preset,
|
||||
"weave_otel": weave_preset,
|
||||
}
|
||||
)
|
||||
|
||||
#: Callback name → per-request OTLP header builder (team/key multi-tenant
|
||||
#: routing). Only integrations that support dynamic credentials appear here —
|
||||
#: Arize-Phoenix/Langtrace/Levo/AgentOps don't, so they use the logger's
|
||||
#: default tracer.
|
||||
DYNAMIC_HEADERS_BY_CALLBACK: Final[dict[str, Callable[[StandardCallbackDynamicParams], dict[str, str]]]] = {
|
||||
"arize": arize_dynamic_headers,
|
||||
"langfuse_otel": langfuse_dynamic_headers,
|
||||
"weave_otel": weave_dynamic_headers,
|
||||
}
|
||||
DYNAMIC_HEADERS_BY_CALLBACK: Final[Mapping[str, Callable[[StandardCallbackDynamicParams], dict[str, str]]]] = (
|
||||
MappingProxyType(
|
||||
{
|
||||
"arize": arize_dynamic_headers,
|
||||
"langfuse_otel": langfuse_dynamic_headers,
|
||||
"newrelic": newrelic_dynamic_headers,
|
||||
"weave_otel": weave_dynamic_headers,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
#: Callback name → per-request OTLP endpoint resolver. Only integrations whose
|
||||
#: destination host varies per tenant (from a fixed region table, never a
|
||||
#: caller-supplied URL) appear here; for everyone else the preset's endpoint is
|
||||
#: authoritative.
|
||||
DYNAMIC_ENDPOINT_BY_CALLBACK: Final[Mapping[str, Callable[[StandardCallbackDynamicParams], str | None]]] = (
|
||||
MappingProxyType(
|
||||
{
|
||||
"newrelic": newrelic_dynamic_endpoint,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
#: Callback name → per-request *routing* header builder, sourced from the key/team
|
||||
|
|
@ -98,17 +123,34 @@ def project_routing_headers(
|
|||
return builder(auth_metadata)
|
||||
|
||||
|
||||
def dynamic_otlp_endpoint(
|
||||
callback_name: str | None,
|
||||
dynamic_params: StandardCallbackDynamicParams | None,
|
||||
) -> str | None:
|
||||
"""Per-request OTLP endpoint for ``callback_name``, or ``None`` if N/A.
|
||||
|
||||
``None`` means "keep the preset's own endpoint".
|
||||
"""
|
||||
resolver: Final = DYNAMIC_ENDPOINT_BY_CALLBACK.get(callback_name or "")
|
||||
if resolver is None or not dynamic_params:
|
||||
return None
|
||||
return resolver(dynamic_params)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DYNAMIC_ENDPOINT_BY_CALLBACK",
|
||||
"DYNAMIC_HEADERS_BY_CALLBACK",
|
||||
"PRESET_BY_CALLBACK",
|
||||
"PROJECT_HEADERS_BY_CALLBACK",
|
||||
"Preset",
|
||||
"agentops_preset",
|
||||
"arize_preset",
|
||||
"dynamic_otlp_endpoint",
|
||||
"dynamic_otlp_headers",
|
||||
"langfuse_preset",
|
||||
"langtrace_preset",
|
||||
"levo_preset",
|
||||
"newrelic_preset",
|
||||
"phoenix_preset",
|
||||
"project_routing_headers",
|
||||
"weave_preset",
|
||||
|
|
|
|||
104
litellm/integrations/otel/presets/newrelic.py
Normal file
104
litellm/integrations/otel/presets/newrelic.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""New Relic preset — OTLP/HTTP exporter to New Relic + GenAI vocabulary."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from types import MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.integrations.otel.model.config import (
|
||||
ExporterOwner,
|
||||
ExporterSpec,
|
||||
OpenTelemetryV2Config,
|
||||
)
|
||||
from litellm.integrations.otel.presets.utils import ensure_mappers
|
||||
from litellm.types.utils import StandardCallbackDynamicParams
|
||||
|
||||
#: Region -> OTLP base endpoint. A fixed table by design: team config picks a
|
||||
#: region enum rather than a free-form endpoint, so callback vars can never
|
||||
#: redirect telemetry to an arbitrary host.
|
||||
NEWRELIC_OTLP_ENDPOINT_BY_REGION: Final[Mapping[str, str]] = MappingProxyType(
|
||||
{
|
||||
"us": "https://otlp.nr-data.net",
|
||||
"eu": "https://otlp.eu01.nr-data.net",
|
||||
}
|
||||
)
|
||||
|
||||
_DEFAULT_REGION: Final = "us"
|
||||
|
||||
|
||||
class _NewRelicSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(case_sensitive=False, extra="ignore")
|
||||
|
||||
# The same env vars the agent-based integration documents; the key is the
|
||||
# operator-level fallback for traffic without team credentials, the region
|
||||
# picks that fallback's data center, and the record-content flag keeps its
|
||||
# documented meaning when the OTel path replaces the agent.
|
||||
license_key: str | None = Field(default=None, validation_alias="NEW_RELIC_LICENSE_KEY")
|
||||
region: str | None = Field(default=None, validation_alias="NEW_RELIC_REGION")
|
||||
record_content: bool | None = Field(default=None, validation_alias="NEW_RELIC_AI_MONITORING_RECORD_CONTENT_ENABLED")
|
||||
|
||||
|
||||
def newrelic_preset(
|
||||
*,
|
||||
config_overrides: OpenTelemetryV2Config | None = None,
|
||||
) -> OpenTelemetryV2Config:
|
||||
settings: Final = _NewRelicSettings()
|
||||
base: Final = config_overrides or OpenTelemetryV2Config()
|
||||
endpoint: Final = NEWRELIC_OTLP_ENDPOINT_BY_REGION.get(
|
||||
(settings.region or _DEFAULT_REGION).lower(), NEWRELIC_OTLP_ENDPOINT_BY_REGION[_DEFAULT_REGION]
|
||||
)
|
||||
return base.model_copy(
|
||||
update={
|
||||
"exporters": [
|
||||
*base.exporters,
|
||||
ExporterSpec(
|
||||
kind="otlp_http",
|
||||
endpoint=endpoint,
|
||||
headers=(f"api-key={settings.license_key}" if settings.license_key else None),
|
||||
owner=ExporterOwner.NEWRELIC,
|
||||
requires_headers=True,
|
||||
),
|
||||
],
|
||||
# New Relic ingests the OTLP GenAI semantic conventions natively.
|
||||
"mapper_names": ensure_mappers(base.mapper_names, "genai"),
|
||||
**(
|
||||
{"capture_message_content": ("span_only" if settings.record_content else "no_content")}
|
||||
if settings.record_content is not None
|
||||
else {}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def newrelic_dynamic_headers(params: StandardCallbackDynamicParams) -> dict[str, str]:
|
||||
"""Per-request New Relic OTLP headers from team/key dynamic params."""
|
||||
api_key: Final = params.get("newrelic_api_key")
|
||||
return {header: value for header, value in (("api-key", api_key),) if value}
|
||||
|
||||
|
||||
def newrelic_dynamic_endpoint(params: StandardCallbackDynamicParams) -> str:
|
||||
"""Per-request OTLP endpoint for the team's ``newrelic_region``.
|
||||
|
||||
Always the team's own region endpoint, defaulting to US when the team left
|
||||
the region unset. It never falls through to the preset's endpoint, which
|
||||
follows the operator's ``NEW_RELIC_REGION`` env; a team that saved only its
|
||||
ingest key must not inherit the operator's region and have its US-account
|
||||
spans rejected by an EU-configured default (or vice versa). An unknown
|
||||
region likewise resolves to the documented US default rather than a guess.
|
||||
"""
|
||||
region: Final = params.get("newrelic_region")
|
||||
default_endpoint: Final = NEWRELIC_OTLP_ENDPOINT_BY_REGION[_DEFAULT_REGION]
|
||||
if not region:
|
||||
return default_endpoint
|
||||
endpoint: Final = NEWRELIC_OTLP_ENDPOINT_BY_REGION.get(region.lower())
|
||||
if endpoint is None:
|
||||
verbose_logger.warning(
|
||||
"New Relic: unknown newrelic_region %r; supported regions: %s. Using the default (US) endpoint.",
|
||||
region,
|
||||
", ".join(sorted(NEWRELIC_OTLP_ENDPOINT_BY_REGION)),
|
||||
)
|
||||
return default_endpoint
|
||||
return endpoint
|
||||
|
|
@ -215,7 +215,9 @@ class PrometheusLogger(CustomLogger):
|
|||
# request latency metrics
|
||||
self.litellm_request_total_latency_metric = self._histogram_factory(
|
||||
"litellm_request_total_latency_metric",
|
||||
"Total latency (seconds) for a request to LiteLLM",
|
||||
"End-to-end latency (seconds) for a request to LiteLLM Proxy Server, from the moment "
|
||||
"the request reached the proxy through the end of processing -- includes "
|
||||
"authentication, pre-call hooks, the LLM API call, and post-call processing",
|
||||
labelnames=self.get_labels_for_metric("litellm_request_total_latency_metric"),
|
||||
buckets=self.latency_buckets,
|
||||
)
|
||||
|
|
@ -458,7 +460,8 @@ class PrometheusLogger(CustomLogger):
|
|||
# Request queue time metric
|
||||
self.litellm_request_queue_time_metric = self._histogram_factory(
|
||||
"litellm_request_queue_time_seconds",
|
||||
"Time spent in request queue before processing starts (seconds)",
|
||||
"Time (seconds) from request arrival at the proxy to the start of pre-call "
|
||||
"processing -- includes authentication and any ASGI-level queueing",
|
||||
labelnames=self.get_labels_for_metric("litellm_request_queue_time_seconds"),
|
||||
buckets=self.latency_buckets,
|
||||
)
|
||||
|
|
@ -2078,27 +2081,37 @@ class PrometheusLogger(CustomLogger):
|
|||
_labels,
|
||||
)
|
||||
|
||||
# total request latency
|
||||
# request queue time (time from arrival to processing start) -- read first so
|
||||
# it can be folded into the total-latency metric below. start_time/end_time
|
||||
# only span from after auth completes, so without this the "total" latency
|
||||
# metric silently excludes auth and pre-call hook time.
|
||||
_litellm_params: Final = kwargs.get("litellm_params", {}) or {}
|
||||
queue_time_seconds: Final = (_litellm_params.get("metadata") or {}).get("queue_time_seconds")
|
||||
|
||||
# total request latency: true end-to-end, from request arrival (queue_time_seconds,
|
||||
# when available) through the end of processing.
|
||||
total_time_seconds: Final = self._safe_duration_seconds(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
)
|
||||
if total_time_seconds is not None:
|
||||
_observed_total_time_seconds: Final = (
|
||||
total_time_seconds + queue_time_seconds
|
||||
if queue_time_seconds is not None and queue_time_seconds >= 0
|
||||
else total_time_seconds
|
||||
)
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(metric_name="litellm_request_total_latency_metric"),
|
||||
enum_values=enum_values,
|
||||
label_context=label_context,
|
||||
)
|
||||
self.litellm_request_total_latency_metric.labels(**_labels).observe(total_time_seconds)
|
||||
self.litellm_request_total_latency_metric.labels(**_labels).observe(_observed_total_time_seconds)
|
||||
self._track_end_user_metric_series(
|
||||
self.litellm_request_total_latency_metric,
|
||||
"litellm_request_total_latency_metric",
|
||||
_labels,
|
||||
)
|
||||
|
||||
# request queue time (time from arrival to processing start)
|
||||
_litellm_params: Final = kwargs.get("litellm_params", {}) or {}
|
||||
queue_time_seconds: Final = (_litellm_params.get("metadata") or {}).get("queue_time_seconds")
|
||||
if queue_time_seconds is not None and queue_time_seconds >= 0:
|
||||
_labels = prometheus_label_factory(
|
||||
supported_enum_labels=self.get_labels_for_metric(metric_name="litellm_request_queue_time_seconds"),
|
||||
|
|
|
|||
313
litellm/interactions/background_cost_polling.py
Normal file
313
litellm/interactions/background_cost_polling.py
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
"""
|
||||
Cost tracking for background interactions.
|
||||
|
||||
A create request with ``background=true`` returns ``in_progress`` with no
|
||||
usage block, and GET polls are deliberately never billed (billing them would
|
||||
double-charge every poll; the GET response also does not echo ``background``,
|
||||
so a poll cannot be told apart from a re-fetch of an already-billed
|
||||
interaction). The create call is therefore the only place that can own
|
||||
billing: it schedules a poll task that fetches the interaction until it
|
||||
reaches a terminal status and logs the final usage as a single success event
|
||||
attributed to the original request.
|
||||
|
||||
``requires_action`` is terminal for the interaction it names. The API has no
|
||||
operation that resumes one: a caller answers a tool request by creating a new
|
||||
interaction whose ``previous_interaction_id`` points at it, and that new
|
||||
interaction bills itself. The paused interaction keeps the tokens it already
|
||||
spent producing the tool request, so it is billed and settled where it stops
|
||||
rather than polled until the timeout, which would both lose that usage and
|
||||
hold its budget reservation open for the whole timeout window.
|
||||
|
||||
Deleting an interaction makes every subsequent poll fail, which would let a
|
||||
caller retrieve the completed output themselves and then delete it before the
|
||||
poll task settles, leaving the work unbilled and the budget reservation
|
||||
refunded at the poll timeout. ``adelete`` therefore settles any pending poll
|
||||
for the interaction before dispatching the delete: it fetches the current
|
||||
state with the create's credentials, bills it if it is terminal with usage,
|
||||
and releases the reservation otherwise. A settlement gate on the create's
|
||||
logging object makes the poll task and the delete path mutually exclusive, so
|
||||
the interaction is billed exactly once no matter who settles first.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable, Iterator, Mapping
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Final, TypeAlias
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.constants import (
|
||||
BACKGROUND_INTERACTION_COST_POLL_INITIAL_INTERVAL_SECONDS,
|
||||
BACKGROUND_INTERACTION_COST_POLL_MAX_INTERVAL_SECONDS,
|
||||
BACKGROUND_INTERACTION_COST_POLL_TIMEOUT_SECONDS,
|
||||
BACKGROUND_INTERACTION_COST_POLLING_ENABLED,
|
||||
)
|
||||
from litellm.litellm_core_utils.core_helpers import get_litellm_metadata_from_kwargs
|
||||
from litellm.types.interactions import InteractionsAPIResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
|
||||
_TERMINAL_STATUSES: Final = frozenset(
|
||||
{"completed", "failed", "cancelled", "incomplete", "budget_exceeded", "requires_action"}
|
||||
)
|
||||
|
||||
_POLLABLE_STATUSES: Final = frozenset({"in_progress", "queued"})
|
||||
|
||||
_STATUSES_THAT_PRODUCED_OUTPUT: Final = frozenset({"completed", "requires_action"})
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class BackgroundInteractionPollContext:
|
||||
interaction_id: str
|
||||
custom_llm_provider: str
|
||||
logging_obj: "LiteLLMLoggingObj"
|
||||
api_key: str | None = None
|
||||
api_base: str | None = None
|
||||
initial_interval_seconds: float = BACKGROUND_INTERACTION_COST_POLL_INITIAL_INTERVAL_SECONDS
|
||||
max_interval_seconds: float = BACKGROUND_INTERACTION_COST_POLL_MAX_INTERVAL_SECONDS
|
||||
timeout_seconds: float = BACKGROUND_INTERACTION_COST_POLL_TIMEOUT_SECONDS
|
||||
|
||||
|
||||
FetchInteraction: TypeAlias = Callable[[BackgroundInteractionPollContext], Awaitable[InteractionsAPIResponse]]
|
||||
|
||||
|
||||
async def _fetch_interaction(context: BackgroundInteractionPollContext) -> InteractionsAPIResponse:
|
||||
from litellm.interactions import aget
|
||||
|
||||
return await aget(
|
||||
interaction_id=context.interaction_id,
|
||||
custom_llm_provider=context.custom_llm_provider,
|
||||
api_key=context.api_key,
|
||||
api_base=context.api_base,
|
||||
**{
|
||||
"no-log": True
|
||||
}, # mutable-ok: "no-log" is not a valid identifier, so it can only be passed through a mapping
|
||||
)
|
||||
|
||||
|
||||
def _poll_intervals(initial: float, maximum: float, timeout: float) -> Iterator[float]:
|
||||
elapsed = 0.0
|
||||
interval = initial
|
||||
while interval > 0 and elapsed + interval <= timeout:
|
||||
yield interval
|
||||
elapsed += interval
|
||||
interval = min(interval * 2, maximum)
|
||||
|
||||
|
||||
_SETTLED_KEY = "background_interaction_settled"
|
||||
|
||||
|
||||
def _is_settled(logging_obj: "LiteLLMLoggingObj") -> bool:
|
||||
return logging_obj.model_call_details.get(_SETTLED_KEY) is True
|
||||
|
||||
|
||||
def _claim_settlement(logging_obj: "LiteLLMLoggingObj") -> bool:
|
||||
"""
|
||||
Exactly-once gate between the poll task and the delete-time settlement:
|
||||
both run on the same event loop and neither awaits between reading and
|
||||
setting the flag, so whichever claims first owns billing or release.
|
||||
"""
|
||||
if _is_settled(logging_obj):
|
||||
return False
|
||||
logging_obj.model_call_details[_SETTLED_KEY] = True # rebind-ok: both settlers must see the same settlement flag
|
||||
return True
|
||||
|
||||
|
||||
async def poll_and_log_background_interaction_cost(
|
||||
context: BackgroundInteractionPollContext,
|
||||
fetch_interaction: FetchInteraction = _fetch_interaction,
|
||||
) -> None:
|
||||
last_seen_status: str | None = None
|
||||
for interval in _poll_intervals(
|
||||
initial=context.initial_interval_seconds,
|
||||
maximum=context.max_interval_seconds,
|
||||
timeout=context.timeout_seconds,
|
||||
):
|
||||
await asyncio.sleep(interval)
|
||||
if _is_settled(context.logging_obj):
|
||||
return
|
||||
try:
|
||||
response = await fetch_interaction(context)
|
||||
except Exception as e: # noqa: BLE001 # any fetch error must not kill the billing poll loop
|
||||
verbose_logger.debug(
|
||||
"Background interaction cost poll for %s failed, will retry: %s",
|
||||
context.interaction_id,
|
||||
e,
|
||||
)
|
||||
continue
|
||||
last_seen_status = response.status
|
||||
if response.status not in _TERMINAL_STATUSES:
|
||||
continue
|
||||
if not _claim_settlement(context.logging_obj):
|
||||
return
|
||||
if response.usage is not None:
|
||||
await _bill_settled_interaction(logging_obj=context.logging_obj, response=response)
|
||||
else:
|
||||
await _release_open_budget_reservation(logging_obj=context.logging_obj)
|
||||
return
|
||||
if not _claim_settlement(context.logging_obj):
|
||||
return
|
||||
if last_seen_status is not None and last_seen_status not in _POLLABLE_STATUSES:
|
||||
verbose_logger.error(
|
||||
"Gave up cost polling for background interaction %s after %ss: its last status %r is in neither "
|
||||
"the pollable nor the terminal set, so this proxy never learned how to settle it and its usage "
|
||||
"will not be tracked",
|
||||
context.interaction_id,
|
||||
context.timeout_seconds,
|
||||
last_seen_status,
|
||||
)
|
||||
else:
|
||||
verbose_logger.warning(
|
||||
"Gave up cost polling for background interaction %s after %ss; its usage will not be tracked",
|
||||
context.interaction_id,
|
||||
context.timeout_seconds,
|
||||
)
|
||||
await _release_open_budget_reservation(logging_obj=context.logging_obj)
|
||||
|
||||
|
||||
async def _release_open_budget_reservation(logging_obj: "LiteLLMLoggingObj") -> None:
|
||||
"""
|
||||
The proxy keeps the pre-call budget reservation open for an in-progress
|
||||
background interaction so concurrent creates cannot stack past the budget.
|
||||
The completion success event reconciles it to the actual cost; when the
|
||||
interaction terminates without billable usage (or polling gives up, or it
|
||||
is deleted before settling), no such event fires, so whoever claims the
|
||||
settlement must release the reservation here or the spend counters stay
|
||||
pinned at the estimated cost.
|
||||
"""
|
||||
metadata = get_litellm_metadata_from_kwargs(kwargs=logging_obj.model_call_details)
|
||||
budget_reservation = metadata.get("user_api_key_budget_reservation")
|
||||
if not isinstance(budget_reservation, dict):
|
||||
return
|
||||
|
||||
from litellm.proxy.spend_tracking.budget_reservation import release_budget_reservation
|
||||
|
||||
try:
|
||||
await release_budget_reservation(budget_reservation=budget_reservation)
|
||||
except Exception: # noqa: BLE001 # a failed release must not crash the poll task; counters expire via TTL
|
||||
verbose_logger.exception("Failed to release budget reservation for an unbilled background interaction")
|
||||
|
||||
|
||||
async def _bill_settled_interaction(logging_obj: "LiteLLMLoggingObj", response: InteractionsAPIResponse) -> None:
|
||||
"""
|
||||
Claiming the settlement makes the claimer solely responsible for the
|
||||
reservation, and no one retries a claim that is already set. A billing
|
||||
failure here must therefore release the reservation on its way out, or it
|
||||
stays pinned at the estimated cost until the whole poll times out.
|
||||
"""
|
||||
try:
|
||||
await logging_obj.async_log_background_interaction_completion(result=response)
|
||||
except Exception:
|
||||
await _release_open_budget_reservation(logging_obj=logging_obj)
|
||||
raise
|
||||
|
||||
|
||||
def is_pollable_background_interaction(response: InteractionsAPIResponse) -> bool:
|
||||
"""
|
||||
The single gate deciding whether a create's response gets a poll task.
|
||||
The proxy's success callback defers releasing the budget reservation for
|
||||
exactly these responses, on the promise that a poll task will settle them,
|
||||
so a response one site accepts and the other refuses strands its
|
||||
reservation on the spend counters with nothing left to reconcile it.
|
||||
|
||||
``queued`` belongs here alongside ``in_progress``. It is the API's
|
||||
not-started-yet state, so it reaches a terminal status the same way and
|
||||
needs polling for the same reason: nothing else in the proxy ever bills a
|
||||
create that came back without usage, so a status missing from both this
|
||||
set and ``_TERMINAL_STATUSES`` is billed nowhere and alerts nobody.
|
||||
"""
|
||||
return response.status in _POLLABLE_STATUSES and bool(response.id)
|
||||
|
||||
|
||||
def missing_usage_is_expected(response: InteractionsAPIResponse) -> bool:
|
||||
"""
|
||||
Whether a response arriving with no usage block is a normal outcome rather
|
||||
than lost billing data. An interaction that is still running, or that
|
||||
stopped at ``failed``, ``cancelled``, ``incomplete`` or ``budget_exceeded``,
|
||||
has nothing to charge for and should not raise a cost-tracking alarm.
|
||||
|
||||
``completed`` and ``requires_action`` both mean the model produced output,
|
||||
so a usage block is always expected with them. If one arrives without it
|
||||
the charge for real work has been lost, which is precisely what the
|
||||
proxy's cost-tracking alert exists to surface.
|
||||
"""
|
||||
return response.status not in _STATUSES_THAT_PRODUCED_OUTPUT
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _ActiveBackgroundPoll:
|
||||
task: "asyncio.Task[None]"
|
||||
context: BackgroundInteractionPollContext
|
||||
|
||||
|
||||
_ACTIVE_POLLS: dict[str, _ActiveBackgroundPoll] = {} # mutable-ok: asyncio needs strong refs to running poll tasks
|
||||
|
||||
|
||||
def _discard_poll(interaction_id: str, task: "asyncio.Task[None]") -> None:
|
||||
entry = _ACTIVE_POLLS.get(interaction_id)
|
||||
if entry is not None and entry.task is task:
|
||||
del _ACTIVE_POLLS[interaction_id]
|
||||
|
||||
|
||||
def maybe_schedule_background_interaction_cost_polling(
|
||||
response: object,
|
||||
create_kwargs: Mapping[str, object],
|
||||
custom_llm_provider: str,
|
||||
) -> "asyncio.Task[None] | None":
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging
|
||||
|
||||
if not BACKGROUND_INTERACTION_COST_POLLING_ENABLED:
|
||||
return None
|
||||
if not isinstance(response, InteractionsAPIResponse):
|
||||
return None
|
||||
if not is_pollable_background_interaction(response):
|
||||
return None
|
||||
logging_obj = create_kwargs.get("litellm_logging_obj")
|
||||
if not isinstance(logging_obj, Logging):
|
||||
return None
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return None
|
||||
api_key = create_kwargs.get("api_key")
|
||||
api_base = create_kwargs.get("api_base")
|
||||
context = BackgroundInteractionPollContext(
|
||||
interaction_id=response.id,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
logging_obj=logging_obj,
|
||||
api_key=api_key if isinstance(api_key, str) else None,
|
||||
api_base=api_base if isinstance(api_base, str) else None,
|
||||
)
|
||||
task = asyncio.create_task(poll_and_log_background_interaction_cost(context))
|
||||
_ACTIVE_POLLS[context.interaction_id] = _ActiveBackgroundPoll(task=task, context=context)
|
||||
task.add_done_callback(
|
||||
lambda finished, interaction_id=context.interaction_id: _discard_poll(interaction_id, finished)
|
||||
)
|
||||
return task
|
||||
|
||||
|
||||
async def maybe_settle_background_interaction_before_delete(
|
||||
interaction_id: str,
|
||||
fetch_interaction: FetchInteraction = _fetch_interaction,
|
||||
) -> None:
|
||||
entry = _ACTIVE_POLLS.get(interaction_id)
|
||||
if entry is None:
|
||||
return
|
||||
context = entry.context
|
||||
try:
|
||||
response = await fetch_interaction(context)
|
||||
except Exception as e: # noqa: BLE001 # unfetchable pre-delete state settles by releasing the reservation
|
||||
verbose_logger.debug(
|
||||
"Could not fetch background interaction %s before delete, releasing its reservation: %s",
|
||||
interaction_id,
|
||||
e,
|
||||
)
|
||||
if _claim_settlement(context.logging_obj):
|
||||
await _release_open_budget_reservation(logging_obj=context.logging_obj)
|
||||
return
|
||||
if not _claim_settlement(context.logging_obj):
|
||||
return
|
||||
if response.status in _TERMINAL_STATUSES and response.usage is not None:
|
||||
await _bill_settled_interaction(logging_obj=context.logging_obj, response=response)
|
||||
return
|
||||
await _release_open_budget_reservation(logging_obj=context.logging_obj)
|
||||
|
|
@ -40,6 +40,10 @@ from typing import Any, Final
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.interactions.background_cost_polling import (
|
||||
maybe_schedule_background_interaction_cost_polling,
|
||||
maybe_settle_background_interaction_before_delete,
|
||||
)
|
||||
from litellm.interactions.http_handler import interactions_http_handler
|
||||
from litellm.interactions.utils import (
|
||||
InteractionsAPIRequestUtils,
|
||||
|
|
@ -171,6 +175,12 @@ async def acreate(
|
|||
else:
|
||||
response = init_response
|
||||
|
||||
maybe_schedule_background_interaction_cost_polling(
|
||||
response=response,
|
||||
create_kwargs=kwargs,
|
||||
custom_llm_provider=custom_llm_provider,
|
||||
)
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
raise litellm.exception_type(
|
||||
|
|
@ -462,6 +472,8 @@ async def adelete(
|
|||
loop: Final = asyncio.get_event_loop()
|
||||
kwargs["adelete_interaction"] = True
|
||||
|
||||
await maybe_settle_background_interaction_before_delete(interaction_id=interaction_id)
|
||||
|
||||
func: Final = partial(
|
||||
delete,
|
||||
interaction_id=interaction_id,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ def validate_no_callback_env_reference(param: str, value: object, *, source: str
|
|||
|
||||
|
||||
# Hardcoded list of supported callback params to avoid runtime inspection issues with TypedDict
|
||||
_supported_callback_params: Final = [
|
||||
_supported_callback_params: Final[tuple[str, ...]] = (
|
||||
"langfuse_public_key",
|
||||
"langfuse_secret",
|
||||
"langfuse_secret_key",
|
||||
|
|
@ -72,8 +72,10 @@ _supported_callback_params: Final = [
|
|||
"dd_site",
|
||||
"dd_agent_host",
|
||||
"dd_agent_port",
|
||||
"newrelic_api_key",
|
||||
"newrelic_region",
|
||||
"turn_off_message_logging",
|
||||
]
|
||||
)
|
||||
|
||||
_request_blocked_callback_params: Final = frozenset(
|
||||
{
|
||||
|
|
@ -83,6 +85,20 @@ _request_blocked_callback_params: Final = frozenset(
|
|||
"dd_site",
|
||||
"dd_agent_host",
|
||||
"dd_agent_port",
|
||||
"newrelic_api_key",
|
||||
"newrelic_region",
|
||||
}
|
||||
)
|
||||
|
||||
# Request-blocked params that must still reach ``standard_callback_dynamic_params``
|
||||
# when the proxy itself stamped them from admin-configured team/key callback
|
||||
# settings (the trusted-vars channel). The OTel per-tenant tracer routing reads
|
||||
# ``standard_callback_dynamic_params``, so without this overlay a blocked param
|
||||
# could never drive routing at all.
|
||||
_trusted_overlay_callback_params: Final = frozenset(
|
||||
{
|
||||
"newrelic_api_key",
|
||||
"newrelic_region",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -121,7 +137,9 @@ def initialize_standard_callback_dynamic_params(
|
|||
if param in kwargs:
|
||||
_param_value = kwargs.get(param)
|
||||
validate_no_callback_env_reference(param, _param_value, source="request body")
|
||||
standard_callback_dynamic_params[param] = _param_value
|
||||
standard_callback_dynamic_params[param] = ( # pyright: ignore[reportGeneralTypeIssues] # several supported params predate their StandardCallbackDynamicParams fields
|
||||
_param_value
|
||||
)
|
||||
|
||||
for slot_label, metadata in iter_client_callback_metadata_dicts(kwargs):
|
||||
for param in _supported_callback_params:
|
||||
|
|
@ -130,6 +148,12 @@ def initialize_standard_callback_dynamic_params(
|
|||
if param not in standard_callback_dynamic_params and param in metadata:
|
||||
_param_value = metadata.get(param)
|
||||
validate_no_callback_env_reference(param, _param_value, source=slot_label)
|
||||
standard_callback_dynamic_params[param] = _param_value
|
||||
standard_callback_dynamic_params[param] = ( # pyright: ignore[reportGeneralTypeIssues] # several supported params predate their StandardCallbackDynamicParams fields
|
||||
_param_value
|
||||
)
|
||||
|
||||
for param, trusted_value in get_trusted_callback_params(kwargs):
|
||||
if param in _trusted_overlay_callback_params:
|
||||
standard_callback_dynamic_params[param] = trusted_value
|
||||
|
||||
return standard_callback_dynamic_params
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ from litellm.litellm_core_utils.llm_cost_calc.guardrail_cost import (
|
|||
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
|
||||
StandardBuiltInToolCostTracking,
|
||||
)
|
||||
from litellm.litellm_core_utils.llm_cost_calc.usage_object_transformation import (
|
||||
InteractionsUsageObjectTransformation,
|
||||
)
|
||||
from litellm.litellm_core_utils.logging_utils import truncate_base64_in_messages
|
||||
from litellm.litellm_core_utils.model_param_helper import ModelParamHelper
|
||||
from litellm.litellm_core_utils.redact_messages import (
|
||||
|
|
@ -83,6 +86,10 @@ from litellm.llms.base_llm.search.transformation import SearchResponse
|
|||
from litellm.responses.utils import ResponseAPILoggingUtils
|
||||
from litellm.types.agents import LiteLLMSendMessageResponse
|
||||
from litellm.types.containers.main import ContainerObject
|
||||
from litellm.types.interactions import (
|
||||
InteractionsAPIResponse,
|
||||
InteractionsAPIStreamingResponse,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
AllMessageValues,
|
||||
Batch,
|
||||
|
|
@ -2145,6 +2152,11 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
or isinstance(logging_result, OpenAIModerationResponse)
|
||||
or isinstance(logging_result, OCRResponse) # OCR
|
||||
or isinstance(logging_result, SearchResponse) # Search API
|
||||
or (
|
||||
isinstance(logging_result, InteractionsAPIResponse)
|
||||
and logging_result.usage is not None
|
||||
and self._is_interactions_create_call_type()
|
||||
)
|
||||
or isinstance(logging_result, dict)
|
||||
and logging_result.get("object") == "vector_store.search_results.page"
|
||||
or isinstance(logging_result, dict)
|
||||
|
|
@ -2157,6 +2169,87 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
return True
|
||||
return False
|
||||
|
||||
def _is_interactions_create_call_type(self) -> bool:
|
||||
"""
|
||||
Only interaction creation is billable. GET polls, deletes, and cancels
|
||||
also return an ``InteractionsAPIResponse`` (with usage once completed),
|
||||
so recognizing those would write spend on every poll of a background
|
||||
interaction. The proxy sets ``call_type`` from its route_type
|
||||
(``create_interaction``/``acreate_interaction``); the SDK sets it from
|
||||
the decorated function name (``create``/``acreate``).
|
||||
|
||||
Recognition additionally requires a usage block (checked at the call
|
||||
site): a ``background=true`` create returns ``in_progress`` without
|
||||
usage, and billing it would write a $0 spend log under the interaction
|
||||
id that collides with the row the background poll task writes once the
|
||||
interaction completes (see
|
||||
``litellm.interactions.background_cost_polling``).
|
||||
"""
|
||||
return self.call_type in (
|
||||
CallTypes.create_interaction.value,
|
||||
CallTypes.acreate_interaction.value,
|
||||
"create",
|
||||
"acreate",
|
||||
)
|
||||
|
||||
async def async_log_background_interaction_completion(
|
||||
self,
|
||||
result: InteractionsAPIResponse,
|
||||
) -> None:
|
||||
"""
|
||||
Log the terminal result of a background interaction as a fresh success
|
||||
event. The create request already ran success logging for its
|
||||
``in_progress`` response (no usage, so no cost was tracked); clearing
|
||||
the dedup flags lets the completed result flow through cost calculation
|
||||
and spend tracking exactly once, spanning create to completion.
|
||||
|
||||
The poll fetched this body through its own client call, which priced it
|
||||
against a throwaway logging object holding none of this request's
|
||||
deployment context: no ``model_info``, no router ``model_id``, no
|
||||
deployment ``litellm_params``. Keeping that price would bill a
|
||||
custom-priced deployment at the wrong rate, and it would also satisfy
|
||||
the "already calculated" shortcut and skip repricing here, leaving the
|
||||
cost breakdown at the zeros the usage-less create stamped and writing
|
||||
those zeros to the spend log. Dropping it makes this event price the
|
||||
settled body itself, against the deployment that served the create.
|
||||
|
||||
The same throwaway call stamped the deployment identity that travels
|
||||
with the price, so ``model_id`` and ``litellm_model_name`` go with it.
|
||||
Left in place they overwrite the create's real deployment with the
|
||||
poll's empty one in the payload every logging integration reads.
|
||||
"""
|
||||
settled_hidden_params: Final = getattr(result, "_hidden_params", None)
|
||||
if isinstance(settled_hidden_params, dict):
|
||||
for poll_scoped_key in ("response_cost", "model_id", "litellm_model_name"):
|
||||
settled_hidden_params.pop(poll_scoped_key, None)
|
||||
self._reset_success_emission_dedupe()
|
||||
await self.async_success_handler(result=result)
|
||||
|
||||
def _reset_success_emission_dedupe(self) -> None:
|
||||
"""
|
||||
Success callbacks dedupe per request, because the sync and async
|
||||
handlers both fire on some paths and would otherwise report one call
|
||||
twice. A settled background interaction is a genuinely second success
|
||||
event on the same request, so every such marker has to be cleared or
|
||||
the completion, the only event that carries usage and cost, is
|
||||
discarded as a duplicate of the in-progress create.
|
||||
"""
|
||||
self.model_call_details.pop("has_logged_async_success", None)
|
||||
litellm_params = self.model_call_details.get("litellm_params")
|
||||
if not isinstance(litellm_params, dict):
|
||||
return
|
||||
metadata = litellm_params.get("metadata")
|
||||
if not isinstance(metadata, dict):
|
||||
return
|
||||
otel_internal = metadata.get("_otel_internal")
|
||||
if not isinstance(otel_internal, dict):
|
||||
return
|
||||
spans_logged = otel_internal.get("spans_logged")
|
||||
if not isinstance(spans_logged, dict):
|
||||
return
|
||||
for scope in [key for key in spans_logged if isinstance(key, tuple) and key[-1:] == ("success",)]:
|
||||
del spans_logged[scope]
|
||||
|
||||
def _flush_passthrough_collected_chunks_helper(
|
||||
self,
|
||||
raw_bytes: list[bytes],
|
||||
|
|
@ -2282,7 +2375,9 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
is_sync_request: Final = self._is_sync_litellm_request(litellm_params)
|
||||
try:
|
||||
## BUILD COMPLETE STREAMED RESPONSE
|
||||
complete_streaming_response: ModelResponse | TextCompletionResponse | ResponsesAPIResponse | None = None
|
||||
complete_streaming_response: (
|
||||
ModelResponse | TextCompletionResponse | ResponsesAPIResponse | InteractionsAPIResponse | None
|
||||
) = None
|
||||
if "complete_streaming_response" in self.model_call_details:
|
||||
return # break out of this.
|
||||
complete_streaming_response = self._get_assembled_streaming_response(
|
||||
|
|
@ -2768,14 +2863,14 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
## BUILD COMPLETE STREAMED RESPONSE
|
||||
if "async_complete_streaming_response" in self.model_call_details:
|
||||
return # break out of this.
|
||||
complete_streaming_response: Final[ModelResponse | TextCompletionResponse | ResponsesAPIResponse | None] = (
|
||||
self._get_assembled_streaming_response(
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_async=True,
|
||||
streaming_chunks=self.streaming_chunks,
|
||||
)
|
||||
complete_streaming_response: Final[
|
||||
ModelResponse | TextCompletionResponse | ResponsesAPIResponse | InteractionsAPIResponse | None
|
||||
] = self._get_assembled_streaming_response(
|
||||
result=result,
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
is_async=True,
|
||||
streaming_chunks=self.streaming_chunks,
|
||||
)
|
||||
|
||||
if complete_streaming_response is not None:
|
||||
|
|
@ -3558,7 +3653,7 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
end_time: datetime.datetime,
|
||||
is_async: bool,
|
||||
streaming_chunks: list[object],
|
||||
) -> ModelResponse | TextCompletionResponse | ResponsesAPIResponse | None:
|
||||
) -> ModelResponse | TextCompletionResponse | ResponsesAPIResponse | InteractionsAPIResponse | None:
|
||||
if self.stream is not True:
|
||||
return None
|
||||
if isinstance(result, ModelResponse) or isinstance(result, TextCompletionResponse):
|
||||
|
|
@ -3583,9 +3678,40 @@ class Logging(LiteLLMLoggingBaseClass):
|
|||
),
|
||||
)
|
||||
return result.response
|
||||
elif isinstance(result, InteractionsAPIStreamingResponse):
|
||||
return self._assemble_completed_interaction_response(result)
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _assemble_completed_interaction_response(
|
||||
result: InteractionsAPIStreamingResponse,
|
||||
) -> InteractionsAPIResponse | None:
|
||||
"""
|
||||
The Interactions API streaming iterator hands the terminal event to the
|
||||
success handlers: the new schema (Api-Revision: 2026-05-20) emits
|
||||
``interaction.completed`` carrying the full interaction object, the
|
||||
legacy schema (2026-05-07) emits a chunk with ``status="completed"``
|
||||
and usage on the chunk itself. Build the equivalent non-streaming
|
||||
response so cost calculation and spend tracking see one shape.
|
||||
"""
|
||||
if result.event_type == "interaction.completed" and result.interaction is not None:
|
||||
return InteractionsAPIResponse(**result.interaction)
|
||||
if result.status == "completed":
|
||||
return InteractionsAPIResponse(
|
||||
**result.model_dump(
|
||||
exclude={ # mutable-ok: pydantic types exclude as set[str], which a frozenset does not satisfy
|
||||
"event_type",
|
||||
"delta",
|
||||
"index",
|
||||
"step",
|
||||
"interaction_id",
|
||||
"interaction",
|
||||
}
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
def _handle_anthropic_messages_response_logging(self, result: Any) -> ModelResponse:
|
||||
"""
|
||||
Handles logging for Anthropic messages responses.
|
||||
|
|
@ -4503,6 +4629,9 @@ def _init_custom_logger_compatible_class(
|
|||
_in_memory_loggers.append(gitlab_logger)
|
||||
return gitlab_logger
|
||||
elif logging_integration == "newrelic":
|
||||
_v2 = _maybe_construct_otel_v2("newrelic", _in_memory_loggers)
|
||||
if _v2 is not None:
|
||||
return _v2
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, NewRelicLogger):
|
||||
return callback
|
||||
|
|
@ -4789,7 +4918,11 @@ def get_custom_logger_compatible_class(
|
|||
if isinstance(callback, SMTPEmailLogger):
|
||||
return callback
|
||||
elif logging_integration == "newrelic":
|
||||
from litellm.integrations.otel.logger import OpenTelemetryV2
|
||||
|
||||
for callback in _in_memory_loggers:
|
||||
if isinstance(callback, OpenTelemetryV2) and callback.callback_name == "newrelic":
|
||||
return callback
|
||||
if isinstance(callback, NewRelicLogger):
|
||||
return callback
|
||||
return None
|
||||
|
|
@ -5085,6 +5218,8 @@ class StandardLoggingPayloadSetup:
|
|||
elif isinstance(usage, dict):
|
||||
if ResponseAPILoggingUtils._is_response_api_usage(usage):
|
||||
return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(usage)
|
||||
if InteractionsUsageObjectTransformation.is_interactions_usage_object(usage):
|
||||
return InteractionsUsageObjectTransformation.transform_interactions_usage_object(usage)
|
||||
return Usage(**usage)
|
||||
|
||||
raise ValueError(f"usage is required, got={usage} of type {type(usage)}")
|
||||
|
|
@ -5111,6 +5246,8 @@ class StandardLoggingPayloadSetup:
|
|||
if isinstance(_raw, dict):
|
||||
if ResponseAPILoggingUtils._is_response_api_usage(_raw):
|
||||
return ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(_raw).model_dump()
|
||||
if InteractionsUsageObjectTransformation.is_interactions_usage_object(_raw):
|
||||
return InteractionsUsageObjectTransformation.transform_interactions_usage_object(_raw).model_dump()
|
||||
return _raw
|
||||
if isinstance(_raw, Usage):
|
||||
return _raw.model_dump()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
from collections.abc import Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from litellm.types.utils import (
|
||||
CompletionTokensDetailsWrapper,
|
||||
PromptTokensDetailsWrapper,
|
||||
TranscriptionUsageDurationObject,
|
||||
TranscriptionUsageTokensObject,
|
||||
|
|
@ -34,3 +37,127 @@ class TranscriptionUsageObjectTransformation:
|
|||
),
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
_INTERACTIONS_MODALITY_FIELDS: Mapping[str, str] = MappingProxyType(
|
||||
{
|
||||
"text": "text_tokens",
|
||||
"audio": "audio_tokens",
|
||||
"image": "image_tokens",
|
||||
"video": "video_tokens",
|
||||
"document": "text_tokens",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _modality_field(entry: Mapping[str, Any]) -> str | None:
|
||||
return _INTERACTIONS_MODALITY_FIELDS.get(str(entry.get("modality", "")).lower())
|
||||
|
||||
|
||||
def _token_count(value: object) -> int:
|
||||
return value if isinstance(value, int) else 0
|
||||
|
||||
|
||||
def _modality_token_sums(entries: Sequence[Mapping[str, Any]]) -> Mapping[str, int]:
|
||||
fields = frozenset(field for entry in entries if (field := _modality_field(entry)) is not None)
|
||||
return MappingProxyType(
|
||||
{
|
||||
field: sum(_token_count(entry.get("tokens")) for entry in entries if _modality_field(entry) == field)
|
||||
for field in fields
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _google_search_query_count(usage_object: Mapping[str, Any]) -> int:
|
||||
return sum(
|
||||
_token_count(entry.get("count"))
|
||||
for entry in tuple(usage_object.get("grounding_tool_count") or ())
|
||||
if isinstance(entry, Mapping) and entry.get("type") == "google_search" # pyright: ignore[reportUnnecessaryIsInstance] # provider JSON, not the empty tuple inferred from `or ()`
|
||||
)
|
||||
|
||||
|
||||
def _subtract_cached_from_input(
|
||||
input_sums: Mapping[str, int],
|
||||
cached_sums: Mapping[str, int],
|
||||
total_cached_tokens: int,
|
||||
) -> Mapping[str, int]:
|
||||
if cached_sums:
|
||||
return MappingProxyType(
|
||||
{field: max(0, tokens - cached_sums.get(field, 0)) for field, tokens in input_sums.items()}
|
||||
)
|
||||
if total_cached_tokens and "text_tokens" in input_sums:
|
||||
return MappingProxyType(
|
||||
{
|
||||
**input_sums,
|
||||
"text_tokens": max(0, input_sums["text_tokens"] - total_cached_tokens),
|
||||
}
|
||||
)
|
||||
return input_sums
|
||||
|
||||
|
||||
class InteractionsUsageObjectTransformation:
|
||||
"""
|
||||
Maps the Google Interactions API usage block (total_input_tokens,
|
||||
output_tokens_by_modality, ...) into LiteLLM's chat-format ``Usage`` so the
|
||||
generic cost calculator and spend tracking can bill it.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_interactions_usage_object(usage_object: object) -> bool:
|
||||
if not isinstance(usage_object, dict):
|
||||
return False
|
||||
if "prompt_tokens" in usage_object or "input_tokens" in usage_object:
|
||||
return False
|
||||
return "total_input_tokens" in usage_object or "total_output_tokens" in usage_object
|
||||
|
||||
@staticmethod
|
||||
def transform_interactions_usage_object(usage_object: Mapping[str, Any]) -> Usage:
|
||||
input_entries = tuple(usage_object.get("input_tokens_by_modality") or ()) + tuple(
|
||||
usage_object.get("tool_use_tokens_by_modality") or ()
|
||||
)
|
||||
cached_sums = _modality_token_sums(tuple(usage_object.get("cached_tokens_by_modality") or ()))
|
||||
output_sums = _modality_token_sums(tuple(usage_object.get("output_tokens_by_modality") or ()))
|
||||
|
||||
total_cached_tokens = _token_count(usage_object.get("total_cached_tokens"))
|
||||
input_sums = _subtract_cached_from_input(
|
||||
input_sums=_modality_token_sums(input_entries),
|
||||
cached_sums=cached_sums,
|
||||
total_cached_tokens=total_cached_tokens,
|
||||
)
|
||||
|
||||
reasoning_tokens = _token_count(usage_object.get("total_reasoning_tokens")) or _token_count(
|
||||
usage_object.get("total_thought_tokens")
|
||||
)
|
||||
prompt_tokens = _token_count(usage_object.get("total_input_tokens")) + _token_count(
|
||||
usage_object.get("total_tool_use_tokens")
|
||||
)
|
||||
completion_tokens = _token_count(usage_object.get("total_output_tokens")) + reasoning_tokens
|
||||
total_tokens = _token_count(usage_object.get("total_tokens")) or (prompt_tokens + completion_tokens)
|
||||
|
||||
web_search_requests = _google_search_query_count(usage_object)
|
||||
prompt_tokens_details = (
|
||||
PromptTokensDetailsWrapper(
|
||||
cached_tokens=total_cached_tokens or None,
|
||||
web_search_requests=web_search_requests or None,
|
||||
**input_sums,
|
||||
)
|
||||
if input_sums or total_cached_tokens or web_search_requests
|
||||
else None
|
||||
)
|
||||
completion_tokens_details = (
|
||||
CompletionTokensDetailsWrapper(
|
||||
reasoning_tokens=reasoning_tokens or None,
|
||||
**output_sums,
|
||||
)
|
||||
if output_sums or reasoning_tokens
|
||||
else None
|
||||
)
|
||||
|
||||
return Usage(
|
||||
prompt_tokens=prompt_tokens,
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens,
|
||||
prompt_tokens_details=prompt_tokens_details,
|
||||
completion_tokens_details=completion_tokens_details,
|
||||
cache_read_input_tokens=total_cached_tokens or None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -28,8 +28,12 @@ from litellm.types.llms.openai import (
|
|||
ChatCompletionAssistantMessage,
|
||||
ChatCompletionFileObject,
|
||||
ChatCompletionImageObject,
|
||||
ChatCompletionReasoningItem,
|
||||
ChatCompletionReasoningSummaryTextBlock,
|
||||
ChatCompletionRedactedThinkingBlock,
|
||||
ChatCompletionResponseMessage,
|
||||
ChatCompletionTextObject,
|
||||
ChatCompletionThinkingBlock,
|
||||
ChatCompletionToolParam,
|
||||
ChatCompletionUserMessage,
|
||||
)
|
||||
|
|
@ -1549,6 +1553,44 @@ def _extract_reasoning_content(message: dict) -> tuple[str | None, str | None]:
|
|||
return None, message_content
|
||||
|
||||
|
||||
def _readable_thinking_text(
|
||||
block: ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock,
|
||||
) -> str:
|
||||
"""The text a chat model can read back, empty for redacted blocks and malformed ones."""
|
||||
if block.get("type") != "thinking":
|
||||
return ""
|
||||
thinking: Final = cast(ChatCompletionThinkingBlock, block).get("thinking") # cast-ok: narrowed by the type tag
|
||||
return str(thinking or "")
|
||||
|
||||
|
||||
def reasoning_content_from_thinking_blocks(
|
||||
thinking_blocks: Iterable[ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock],
|
||||
) -> str:
|
||||
"""Flatten Anthropic thinking blocks into the `reasoning_content` string chat models expect.
|
||||
|
||||
Redacted blocks carry no readable text, so they contribute nothing.
|
||||
"""
|
||||
return "\n".join(text for block in thinking_blocks if (text := _readable_thinking_text(block)))
|
||||
|
||||
|
||||
def responses_reasoning_item_from_thinking_blocks(
|
||||
thinking_blocks: Iterable[ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock],
|
||||
) -> ChatCompletionReasoningItem | None:
|
||||
"""Build a Responses API `reasoning` input item from Anthropic thinking blocks.
|
||||
|
||||
The item carries no `id`: the Responses API rejects an empty one and 404s on any id it
|
||||
did not mint itself, while an item without an id is always accepted.
|
||||
"""
|
||||
summary: Final[list[ChatCompletionReasoningSummaryTextBlock]] = [ # mutable-ok: API message payload
|
||||
ChatCompletionReasoningSummaryTextBlock(type="summary_text", text=text)
|
||||
for block in thinking_blocks
|
||||
if (text := _readable_thinking_text(block))
|
||||
]
|
||||
if not summary:
|
||||
return None
|
||||
return ChatCompletionReasoningItem(type="reasoning", summary=summary)
|
||||
|
||||
|
||||
def _parse_content_for_reasoning(
|
||||
message_text: str | None,
|
||||
) -> tuple[str | None, str | None]:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import json
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Mapping, Sequence
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Final, NoReturn, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -121,6 +122,32 @@ else:
|
|||
# response side.
|
||||
_ANTHROPIC_TOOL_NAME_INVALID_CHARS: Final = re.compile(r"[^a-zA-Z0-9_-]")
|
||||
_ANTHROPIC_TOOL_NAME_MAX_LEN: Final = 128
|
||||
|
||||
_ENUM_TYPE_CHECKS: Final[Mapping[str, Callable[[Any], bool]]] = MappingProxyType(
|
||||
{
|
||||
"null": lambda v: v is None,
|
||||
"boolean": lambda v: isinstance(v, bool),
|
||||
"integer": lambda v: isinstance(v, int) and not isinstance(v, bool),
|
||||
"number": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),
|
||||
"string": lambda v: isinstance(v, str),
|
||||
"array": lambda v: isinstance(v, list),
|
||||
"object": lambda v: isinstance(v, dict),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _enum_conflicts_with_declared_type(schema: Mapping[str, Any]) -> bool:
|
||||
"""Whether ``schema``'s ``enum`` cannot match its declared ``type``."""
|
||||
enum_values: Final = schema.get("enum")
|
||||
declared_type: Final = schema.get("type")
|
||||
if not isinstance(enum_values, list) or declared_type is None:
|
||||
return False
|
||||
if isinstance(declared_type, list):
|
||||
return True
|
||||
check: Final = _ENUM_TYPE_CHECKS.get(declared_type)
|
||||
return check is not None and not all(check(value) for value in enum_values)
|
||||
|
||||
|
||||
# Single, internal-only key on ``litellm_params`` used to thread the per-
|
||||
# request reverse map (sanitized -> original) from request build to response
|
||||
# parsing. ``litellm_params`` is never serialized to a provider; ``optional_
|
||||
|
|
@ -565,9 +592,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
|||
else:
|
||||
result["description"] = constraint_note
|
||||
|
||||
drops_conflicting_type: Final = _enum_conflicts_with_declared_type(schema)
|
||||
|
||||
for key, value in schema.items():
|
||||
if key in unsupported_fields:
|
||||
continue
|
||||
if key == "type" and drops_conflicting_type:
|
||||
continue
|
||||
if key == "description" and "description" in result:
|
||||
# Already handled above
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ from openai.types.chat.chat_completion_chunk import Choice as OpenAIStreamingCho
|
|||
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
parse_tool_call_arguments,
|
||||
reasoning_content_from_thinking_blocks,
|
||||
with_prompt_cache_breakpoint,
|
||||
)
|
||||
from litellm.litellm_core_utils.prompt_templates.factory import (
|
||||
|
|
@ -592,6 +593,9 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
assistant_message["tool_calls"] = tool_calls
|
||||
if len(thinking_blocks) > 0:
|
||||
assistant_message["thinking_blocks"] = thinking_blocks
|
||||
reasoning_content = reasoning_content_from_thinking_blocks(thinking_blocks)
|
||||
if reasoning_content:
|
||||
assistant_message["reasoning_content"] = reasoning_content
|
||||
new_messages.append(assistant_message)
|
||||
|
||||
return new_messages
|
||||
|
|
|
|||
|
|
@ -152,7 +152,10 @@ class AnthropicResponsesStreamWrapper:
|
|||
if block_idx < 0:
|
||||
if not delta:
|
||||
return
|
||||
block_idx = self._open_block(item_id, {"type": "thinking", "thinking": ""})
|
||||
block_idx = self._open_block(
|
||||
item_id,
|
||||
{"type": "thinking", "thinking": "", "signature": ""}, # mutable-ok: API message payload
|
||||
)
|
||||
self._chunk_queue.append(
|
||||
{
|
||||
"type": "content_block_delta",
|
||||
|
|
|
|||
|
|
@ -6,12 +6,14 @@ path used for OpenAI and Azure models.
|
|||
"""
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from collections.abc import Iterable, Mapping
|
||||
from itertools import groupby
|
||||
from typing import Any, Final, cast
|
||||
|
||||
from litellm.litellm_core_utils.prompt_templates.common_utils import (
|
||||
TOOL_RESULT_IMAGE_BOUNDARY,
|
||||
TOOL_RESULT_IMAGE_PLACEHOLDER,
|
||||
responses_reasoning_item_from_thinking_blocks,
|
||||
with_prompt_cache_breakpoint,
|
||||
)
|
||||
from litellm.litellm_core_utils.reasoning_effort_utils import (
|
||||
|
|
@ -36,7 +38,11 @@ from litellm.types.llms.anthropic_messages.anthropic_response import (
|
|||
AnthropicMessagesResponse,
|
||||
AnthropicUsage,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse
|
||||
from litellm.types.llms.openai import (
|
||||
ChatCompletionThinkingBlock,
|
||||
ResponseAPIUsage,
|
||||
ResponsesAPIResponse,
|
||||
)
|
||||
|
||||
|
||||
class LiteLLMAnthropicToResponsesAPIAdapter:
|
||||
|
|
@ -100,6 +106,58 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
if isinstance(block, dict) and block.get("type") == "text" and (text := block.get("text")) # pyright: ignore[reportUnnecessaryIsInstance] # untrusted client payload
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _summary_part_text(part: object) -> str:
|
||||
if isinstance(part, Mapping):
|
||||
mapping: Final = cast(Mapping[str, Any], part) # cast-ok: summary parts are untyped provider json
|
||||
return str(mapping.get("text") or "")
|
||||
return str(getattr(part, "text", None) or "")
|
||||
|
||||
@classmethod
|
||||
def _thinking_blocks_from_reasoning_item(
|
||||
cls,
|
||||
summary: Iterable[object],
|
||||
) -> tuple[dict[str, Any], ...]: # mutable-ok: API message payload
|
||||
"""Anthropic thinking blocks for one Responses reasoning item.
|
||||
|
||||
The signature stays empty: only Anthropic can sign a thinking block, and a stand-in
|
||||
value would be replayed as a real one and rejected by every backend that verifies it.
|
||||
"""
|
||||
return tuple(
|
||||
AnthropicResponseContentBlockThinking(
|
||||
type="thinking",
|
||||
thinking=text,
|
||||
signature=None,
|
||||
).model_dump()
|
||||
for part in summary
|
||||
if (text := cls._summary_part_text(part))
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _assistant_block_group_key(indexed_block: tuple[int, Mapping[str, Any]]) -> str:
|
||||
"""Group a run of consecutive thinking blocks together; keep every other block alone."""
|
||||
index, block = indexed_block
|
||||
return "thinking" if block.get("type") == "thinking" else f"block:{index}"
|
||||
|
||||
@classmethod
|
||||
def _assistant_group_to_input_item(
|
||||
cls, group: tuple[Mapping[str, Any], ...]
|
||||
) -> dict[str, Any] | None: # mutable-ok: API message payload
|
||||
first: Final = group[0]
|
||||
btype: Final = first.get("type")
|
||||
if btype == "thinking":
|
||||
blocks: Final = cast(tuple[ChatCompletionThinkingBlock, ...], group) # cast-ok: untrusted client payload
|
||||
reasoning_item: Final = responses_reasoning_item_from_thinking_blocks(blocks)
|
||||
return None if reasoning_item is None else dict(reasoning_item) # mutable-ok: API message payload
|
||||
if btype == "tool_use":
|
||||
return { # mutable-ok: API message payload
|
||||
"type": "function_call",
|
||||
"call_id": first.get("id", ""),
|
||||
"name": first.get("name", ""),
|
||||
"arguments": json.dumps(first.get("input", {})), # mutable-ok: API message payload
|
||||
}
|
||||
return None
|
||||
|
||||
def translate_messages_to_responses_input(
|
||||
self,
|
||||
messages: list[AllAnthropicPassThroughMessageValues],
|
||||
|
|
@ -113,6 +171,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
user image -> message(role=user, input_image)
|
||||
user tool_result -> function_call_output
|
||||
assistant text -> message(role=assistant, output_text)
|
||||
assistant thinking -> reasoning
|
||||
assistant tool_use -> function_call
|
||||
"""
|
||||
input_items: Final[list[dict[str, Any]]] = []
|
||||
|
|
@ -233,27 +292,17 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
}
|
||||
)
|
||||
elif isinstance(content, list):
|
||||
asst_parts: list[dict[str, Any]] = []
|
||||
for block in content:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
btype = block.get("type")
|
||||
if btype == "text":
|
||||
asst_parts.append({"type": "output_text", "text": block.get("text", "")})
|
||||
elif btype == "tool_use":
|
||||
# tool_use becomes a top-level function_call item
|
||||
input_items.append(
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": block.get("id", ""),
|
||||
"name": block.get("name", ""),
|
||||
"arguments": json.dumps(block.get("input", {})),
|
||||
}
|
||||
)
|
||||
elif btype == "thinking":
|
||||
thinking_text = block.get("thinking", "")
|
||||
if thinking_text:
|
||||
asst_parts.append({"type": "output_text", "text": thinking_text})
|
||||
blocks = tuple(block for block in content if isinstance(block, dict))
|
||||
input_items.extend(
|
||||
item
|
||||
for _, group in groupby(enumerate(blocks), key=self._assistant_block_group_key)
|
||||
if (item := self._assistant_group_to_input_item(tuple(block for _, block in group))) is not None
|
||||
)
|
||||
asst_parts: list[dict[str, Any]] = [ # mutable-ok: API message payload
|
||||
{"type": "output_text", "text": block.get("text", "")} # mutable-ok: API message payload
|
||||
for block in blocks
|
||||
if block.get("type") == "text"
|
||||
]
|
||||
if asst_parts:
|
||||
input_items.append(
|
||||
{
|
||||
|
|
@ -514,16 +563,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
|
||||
for item in response.output:
|
||||
if isinstance(item, ResponseReasoningItem):
|
||||
for summary in item.summary:
|
||||
text = getattr(summary, "text", "")
|
||||
if text:
|
||||
content.append(
|
||||
AnthropicResponseContentBlockThinking(
|
||||
type="thinking",
|
||||
thinking=text,
|
||||
signature=None,
|
||||
).model_dump()
|
||||
)
|
||||
content.extend(self._thinking_blocks_from_reasoning_item(item.summary))
|
||||
|
||||
elif isinstance(item, ResponseOutputMessage):
|
||||
for part in item.content:
|
||||
|
|
@ -555,6 +595,12 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
|
|||
content.append(
|
||||
AnthropicResponseContentBlockText(type="text", text=part.get("text", "")).model_dump()
|
||||
)
|
||||
elif item_type == "reasoning":
|
||||
content.extend(
|
||||
self._thinking_blocks_from_reasoning_item(
|
||||
cast(Iterable[object], item.get("summary") or ()), # cast-ok: untyped provider json
|
||||
)
|
||||
)
|
||||
elif item_type == "function_call":
|
||||
try:
|
||||
input_data = json.loads(item.get("arguments", "{}"))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import hashlib
|
|||
import json
|
||||
import os
|
||||
from collections.abc import Callable, Mapping
|
||||
from functools import lru_cache
|
||||
from typing import Any, Final, Literal, NamedTuple, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -75,6 +76,24 @@ def process_azure_headers(headers: httpx.Headers | dict) -> dict:
|
|||
return {**llm_response_headers, **openai_headers}
|
||||
|
||||
|
||||
@lru_cache(maxsize=128)
|
||||
def _cached_entra_id_token_provider(
|
||||
tenant_id: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
scope: str,
|
||||
) -> Callable[[], str]:
|
||||
"""Build (once per credential set) a bearer token provider backed by a `ClientSecretCredential`.
|
||||
|
||||
The credential caches the access token internally and only talks to Entra ID when it is close
|
||||
to expiry, so reusing the provider keeps one AAD round trip per token lifetime instead of one
|
||||
per request.
|
||||
"""
|
||||
from azure.identity import ClientSecretCredential, get_bearer_token_provider
|
||||
|
||||
return get_bearer_token_provider(ClientSecretCredential(tenant_id, client_id, client_secret), scope)
|
||||
|
||||
|
||||
def get_azure_ad_token_from_entra_id(
|
||||
tenant_id: str,
|
||||
client_id: str,
|
||||
|
|
@ -93,8 +112,6 @@ def get_azure_ad_token_from_entra_id(
|
|||
Returns:
|
||||
callable that returns a bearer token.
|
||||
"""
|
||||
from azure.identity import ClientSecretCredential, get_bearer_token_provider
|
||||
|
||||
verbose_logger.debug("Getting Azure AD Token from Entra ID")
|
||||
|
||||
if tenant_id.startswith("os.environ/"):
|
||||
|
|
@ -120,9 +137,13 @@ def get_azure_ad_token_from_entra_id(
|
|||
)
|
||||
if _tenant_id is None or _client_id is None or _client_secret is None:
|
||||
raise ValueError("tenant_id, client_id, and client_secret must be provided")
|
||||
credential: Final = ClientSecretCredential(_tenant_id, _client_id, _client_secret)
|
||||
|
||||
token_provider: Final = get_bearer_token_provider(credential, scope)
|
||||
token_provider: Final = _cached_entra_id_token_provider(
|
||||
tenant_id=_tenant_id,
|
||||
client_id=_client_id,
|
||||
client_secret=_client_secret,
|
||||
scope=scope,
|
||||
)
|
||||
|
||||
verbose_logger.debug("token_provider %s", token_provider)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,12 @@ class AzureFoundryErrorStrings(str, enum.Enum):
|
|||
SET_EXTRA_PARAMETERS_TO_PASS_THROUGH = "Set extra-parameters to 'pass-through'"
|
||||
|
||||
|
||||
NON_OPENAI_SPEC_MESSAGE_FIELDS: Final = ("thinking_blocks", "provider_specific_fields", "cache_control")
|
||||
NON_OPENAI_SPEC_MESSAGE_FIELDS: Final = (
|
||||
"thinking_blocks",
|
||||
"reasoning_content",
|
||||
"provider_specific_fields",
|
||||
"cache_control",
|
||||
)
|
||||
|
||||
|
||||
class AzureAIStudioConfig(OpenAIConfig):
|
||||
|
|
@ -173,7 +178,8 @@ class AzureAIStudioConfig(OpenAIConfig):
|
|||
"""
|
||||
- Azure AI Studio doesn't support content as a list. This handles:
|
||||
1. Strips message fields that are not part of the OpenAI chat-completions
|
||||
schema (thinking_blocks, provider_specific_fields, cache_control).
|
||||
schema (thinking_blocks, reasoning_content, provider_specific_fields,
|
||||
cache_control).
|
||||
Azure AI Foundry backends set additionalProperties=false and reject
|
||||
these with "Extra inputs are not permitted", which breaks multi-turn
|
||||
Anthropic-format clients that echo thinking blocks back as history.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,54 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Final, Literal
|
||||
|
||||
import litellm
|
||||
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.llms.openai import AllMessageValues
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
AzureAIApiKeyHeader = Literal["Authorization", "api-key", "Api-Key", "Ocp-Apim-Subscription-Key"]
|
||||
|
||||
|
||||
def get_azure_ai_entra_token(litellm_params: Mapping[str, object] | None = None) -> str | None:
|
||||
"""
|
||||
Resolve an Entra ID / OAuth access token for an Azure AI Foundry deployment.
|
||||
|
||||
Accepts the same credential set as the `azure` provider: service principal
|
||||
(`tenant_id` / `client_id` / `client_secret`), a pre-fetched `azure_ad_token`, an OIDC
|
||||
federated token, username/password, or `DefaultAzureCredential` / managed identity.
|
||||
"""
|
||||
from litellm.llms.azure.common_utils import get_azure_ad_token
|
||||
|
||||
params = GenericLiteLLMParams.model_validate(litellm_params) if litellm_params else GenericLiteLLMParams()
|
||||
|
||||
return get_azure_ad_token(params)
|
||||
|
||||
|
||||
def get_azure_ai_auth_headers(
|
||||
api_key: str | None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
api_key_header: AzureAIApiKeyHeader = "Authorization",
|
||||
api_key_env_var: str = "AZURE_AI_API_KEY",
|
||||
) -> Mapping[str, str]:
|
||||
"""
|
||||
Build the auth headers for an Azure AI Foundry route.
|
||||
|
||||
Prefers the API key when one is configured, and otherwise falls back to Entra ID / OAuth,
|
||||
sending the access token as a bearer token.
|
||||
"""
|
||||
if api_key:
|
||||
return {api_key_header: f"Bearer {api_key}" if api_key_header == "Authorization" else api_key}
|
||||
|
||||
azure_ad_token = get_azure_ai_entra_token(litellm_params=litellm_params)
|
||||
if azure_ad_token:
|
||||
return {"Authorization": f"Bearer {azure_ad_token}"}
|
||||
|
||||
raise ValueError(
|
||||
f"Missing Azure AI credentials - set an API key (`api_key` or {api_key_env_var}), or Entra ID / OAuth "
|
||||
"credentials (`tenant_id` + `client_id` + `client_secret`, `azure_ad_token`, an OIDC token, or a managed "
|
||||
"identity with `litellm.enable_azure_ad_token_refresh = True`)"
|
||||
)
|
||||
|
||||
|
||||
class AzureFoundryModelInfo(BaseLLMModelInfo):
|
||||
|
|
@ -43,7 +88,7 @@ class AzureFoundryModelInfo(BaseLLMModelInfo):
|
|||
|
||||
@staticmethod
|
||||
def get_api_key(api_key: str | None = None) -> str | None:
|
||||
return api_key or litellm.api_key or litellm.openai_key or get_secret_str("AZURE_AI_API_KEY")
|
||||
return api_key or litellm.api_key or get_secret_str("AZURE_AI_API_KEY")
|
||||
|
||||
@property
|
||||
def api_version(self, api_version: str | None = None) -> str | None:
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ from typing import Any, Final
|
|||
from httpx._types import RequestFiles
|
||||
|
||||
import litellm
|
||||
from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
|
||||
from litellm.llms.azure_ai.common_utils import (
|
||||
AzureFoundryModelInfo,
|
||||
get_azure_ai_auth_headers,
|
||||
)
|
||||
from litellm.llms.azure_ai.image_generation.flux_transformation import (
|
||||
AzureFoundryFluxImageGenerationConfig,
|
||||
)
|
||||
|
|
@ -71,16 +74,13 @@ class AzureFoundryFlux2ImageEditConfig(OpenAIImageEditConfig):
|
|||
"""
|
||||
Validate Azure AI Foundry environment and set up authentication
|
||||
"""
|
||||
api_key = AzureFoundryModelInfo.get_api_key(api_key)
|
||||
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
f"Azure AI API key is required for model {model}. Set AZURE_AI_API_KEY environment variable or pass api_key parameter."
|
||||
)
|
||||
|
||||
headers.update(
|
||||
{
|
||||
"Api-Key": api_key,
|
||||
**get_azure_ai_auth_headers(
|
||||
api_key=AzureFoundryModelInfo.get_api_key(api_key),
|
||||
litellm_params=litellm_params,
|
||||
api_key_header="Api-Key",
|
||||
),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ from typing import TYPE_CHECKING, Any, Final, cast
|
|||
import httpx
|
||||
from httpx._types import RequestFiles
|
||||
|
||||
from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
|
||||
from litellm.llms.azure_ai.common_utils import (
|
||||
AzureFoundryModelInfo,
|
||||
get_azure_ai_auth_headers,
|
||||
)
|
||||
from litellm.llms.azure_ai.image_generation.mai_transformation import (
|
||||
AzureFoundryMAIImageGenerationConfig,
|
||||
)
|
||||
|
|
@ -91,15 +94,13 @@ class AzureFoundryMAIImageEditConfig(OpenAIImageEditConfig):
|
|||
litellm_params: dict | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> dict:
|
||||
api_key = AzureFoundryModelInfo.get_api_key(api_key)
|
||||
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
f"Azure AI API key is required for model {model}. "
|
||||
"Set AZURE_AI_API_KEY environment variable or pass api_key parameter."
|
||||
headers.update(
|
||||
get_azure_ai_auth_headers(
|
||||
api_key=AzureFoundryModelInfo.get_api_key(api_key),
|
||||
litellm_params=litellm_params,
|
||||
api_key_header="api-key",
|
||||
)
|
||||
|
||||
headers.update({"api-key": api_key})
|
||||
)
|
||||
return headers
|
||||
|
||||
def get_complete_url(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ from typing import Final
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.llms.azure_ai.common_utils import AzureFoundryModelInfo
|
||||
from litellm.llms.azure_ai.common_utils import (
|
||||
AzureFoundryModelInfo,
|
||||
get_azure_ai_auth_headers,
|
||||
)
|
||||
from litellm.llms.openai.image_edit.transformation import OpenAIImageEditConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.utils import _add_path_to_api_base
|
||||
|
|
@ -30,19 +33,14 @@ class AzureFoundryFluxImageEditConfig(OpenAIImageEditConfig):
|
|||
) -> dict:
|
||||
"""
|
||||
Validate Azure AI Foundry environment and set up authentication
|
||||
Uses Api-Key header format
|
||||
Uses the Api-Key header format, or an Entra ID / OAuth bearer token when no key is set
|
||||
"""
|
||||
api_key = AzureFoundryModelInfo.get_api_key(api_key)
|
||||
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
f"Azure AI API key is required for model {model}. Set AZURE_AI_API_KEY environment variable or pass api_key parameter."
|
||||
)
|
||||
|
||||
headers.update(
|
||||
{
|
||||
"Api-Key": api_key, # Azure AI Foundry uses Api-Key header format
|
||||
}
|
||||
get_azure_ai_auth_headers(
|
||||
api_key=AzureFoundryModelInfo.get_api_key(api_key),
|
||||
litellm_params=litellm_params,
|
||||
api_key_header="Api-Key",
|
||||
)
|
||||
)
|
||||
return headers
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from litellm.constants import (
|
|||
)
|
||||
from litellm.exceptions import UnsupportedParamsError
|
||||
from litellm.litellm_core_utils.url_utils import SSRFError, assert_same_origin, encode_url_path_segment
|
||||
from litellm.llms.azure_ai.common_utils import get_azure_ai_auth_headers
|
||||
from litellm.llms.base_llm.ocr.transformation import (
|
||||
OCR_REQUEST_FORMAT_PARAM,
|
||||
BaseOCRConfig,
|
||||
|
|
@ -236,17 +237,13 @@ class AzureDocumentIntelligenceOCRConfig(BaseOCRConfig):
|
|||
"""
|
||||
Validate environment and return headers for Azure Document Intelligence.
|
||||
|
||||
Authentication uses Ocp-Apim-Subscription-Key header.
|
||||
Authentication uses the Ocp-Apim-Subscription-Key header, or an Entra ID / OAuth bearer
|
||||
token when no subscription key is set.
|
||||
"""
|
||||
# Get API key from environment if not provided
|
||||
if api_key is None:
|
||||
api_key = get_secret_str(AZURE_DOCUMENT_INTELLIGENCE_API_KEY_ENV_VAR)
|
||||
|
||||
if api_key is None:
|
||||
raise ValueError(
|
||||
"Missing Azure Document Intelligence API Key - Set AZURE_DOCUMENT_INTELLIGENCE_API_KEY environment variable or pass api_key parameter"
|
||||
)
|
||||
|
||||
# Validate API base/endpoint is provided
|
||||
if api_base is None:
|
||||
api_base = get_secret_str("AZURE_DOCUMENT_INTELLIGENCE_ENDPOINT")
|
||||
|
|
@ -257,7 +254,12 @@ class AzureDocumentIntelligenceOCRConfig(BaseOCRConfig):
|
|||
)
|
||||
|
||||
headers = {
|
||||
"Ocp-Apim-Subscription-Key": api_key,
|
||||
**get_azure_ai_auth_headers(
|
||||
api_key=api_key,
|
||||
litellm_params=litellm_params,
|
||||
api_key_header="Ocp-Apim-Subscription-Key",
|
||||
api_key_env_var=AZURE_DOCUMENT_INTELLIGENCE_API_KEY_ENV_VAR,
|
||||
),
|
||||
"Content-Type": "application/json",
|
||||
**headers,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from litellm.litellm_core_utils.prompt_templates.image_handling import (
|
|||
async_convert_url_to_base64,
|
||||
convert_url_to_base64,
|
||||
)
|
||||
from litellm.llms.azure_ai.common_utils import get_azure_ai_auth_headers
|
||||
from litellm.llms.base_llm.ocr.transformation import DocumentType, OCRRequestData
|
||||
from litellm.llms.mistral.ocr.transformation import MistralOCRConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
|
|
@ -47,17 +48,12 @@ class AzureAIOCRConfig(MistralOCRConfig):
|
|||
"""
|
||||
Validate environment and return headers for Azure AI OCR.
|
||||
|
||||
Azure AI uses Bearer token authentication with AZURE_AI_API_KEY.
|
||||
Authenticates with AZURE_AI_API_KEY, or with an Entra ID / OAuth token when no key is set.
|
||||
"""
|
||||
# Get API key from environment if not provided
|
||||
if api_key is None:
|
||||
api_key = get_secret_str(AZURE_AI_OCR_API_KEY_ENV_VAR)
|
||||
|
||||
if api_key is None:
|
||||
raise ValueError(
|
||||
"Missing Azure AI API Key - A call is being made to Azure AI but no key is set either in the environment variables or via params"
|
||||
)
|
||||
|
||||
# Validate API base is provided
|
||||
if api_base is None:
|
||||
api_base = get_secret_str("AZURE_AI_API_BASE")
|
||||
|
|
@ -68,7 +64,7 @@ class AzureAIOCRConfig(MistralOCRConfig):
|
|||
)
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
**get_azure_ai_auth_headers(api_key=api_key, litellm_params=litellm_params),
|
||||
"Content-Type": "application/json",
|
||||
**headers,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
Translate between Cohere's `/rerank` format and Azure AI's `/rerank` format.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.azure_ai.common_utils import get_azure_ai_auth_headers
|
||||
from litellm.llms.cohere.rerank.transformation import CohereRerankConfig
|
||||
from litellm.secret_managers.main import get_secret_str
|
||||
from litellm.types.utils import RerankResponse
|
||||
|
|
@ -64,15 +66,13 @@ class AzureAIRerankConfig(CohereRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("AZURE_AI_API_KEY") or litellm.azure_key
|
||||
|
||||
if api_key is None:
|
||||
raise ValueError("Azure AI API key is required. Please set 'AZURE_AI_API_KEY' or 'litellm.azure_key'")
|
||||
|
||||
default_headers: Final = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
**get_azure_ai_auth_headers(api_key=api_key, litellm_params=litellm_params),
|
||||
"accept": "application/json",
|
||||
"content-type": "application/json",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -24,6 +25,7 @@ class BaseRerankConfig(ABC):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -567,6 +567,10 @@ class AWSEventStreamDecoder:
|
|||
if thinking_blocks:
|
||||
self._thinking_ran = True
|
||||
|
||||
carries_message_content: Final = any(
|
||||
key in chunk_data for key in ("start", "delta", "contentBlockIndex", "stopReason", "trace")
|
||||
)
|
||||
|
||||
model_response_provider_specific_fields: Final = {}
|
||||
if "trace" in chunk_data:
|
||||
trace: Final = chunk_data.get("trace")
|
||||
|
|
@ -577,8 +581,8 @@ class AWSEventStreamDecoder:
|
|||
finish_reason=finish_reason,
|
||||
index=0, # Always 0 - Bedrock never returns multiple choices
|
||||
delta=Delta(
|
||||
content=text,
|
||||
role="assistant",
|
||||
content=text if carries_message_content else None,
|
||||
role="assistant" if carries_message_content else None,
|
||||
tool_calls=[tool_use] if tool_use else None,
|
||||
provider_specific_fields=(provider_specific_fields if provider_specific_fields else None),
|
||||
thinking_blocks=thinking_blocks,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -81,6 +82,7 @@ class CohereRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("COHERE_API_KEY") or get_secret_str("CO_API_KEY") or litellm.cohere_key
|
||||
|
|
|
|||
|
|
@ -1108,6 +1108,7 @@ class BaseLLMHTTPHandler:
|
|||
headers=headers or {},
|
||||
model=model,
|
||||
optional_params=optional_rerank_params,
|
||||
litellm_params=litellm_params,
|
||||
)
|
||||
|
||||
api_base = provider_config.get_complete_url(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ as supported only for gte-rerank-v2 / qwen3-vl-rerank.
|
|||
Docs - https://help.aliyun.com/zh/model-studio/text-rerank-api
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -85,6 +86,7 @@ class DashScopeRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("DASHSCOPE_API_KEY")
|
||||
|
|
|
|||
|
|
@ -22,9 +22,7 @@ _LEGACY_ENDPOINT_NAMES: Final = MappingProxyType(
|
|||
)
|
||||
|
||||
|
||||
def _base_model(model: str) -> str:
|
||||
"""The registry key for ``model``, mapping the endpoint names that predate the
|
||||
``databricks-`` prefixed keys onto their current entries."""
|
||||
def _registry_key(model: str) -> str:
|
||||
name: Final = model.removeprefix("databricks/")
|
||||
return next(
|
||||
(key for prefix, key in _LEGACY_ENDPOINT_NAMES.items() if name.startswith(prefix)),
|
||||
|
|
@ -44,7 +42,7 @@ def cost_per_token(model: str, usage: Usage) -> tuple[float, float]:
|
|||
Tuple[float, float] - prompt_cost_in_usd, completion_cost_in_usd
|
||||
"""
|
||||
return generic_cost_per_token(
|
||||
model=_base_model(model),
|
||||
model=_registry_key(model),
|
||||
usage=usage,
|
||||
custom_llm_provider="databricks",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Translate between Cohere's `/rerank` format and Deepinfra's `/rerank` format.
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -67,6 +68,7 @@ class DeepinfraRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("DEEPINFRA_API_KEY")
|
||||
|
|
|
|||
|
|
@ -504,6 +504,7 @@ class FireworksAIConfig(FireworksAIMixin, OpenAIGPTConfig):
|
|||
m = cast(dict, message)
|
||||
m.pop("provider_specific_fields", None)
|
||||
m.pop("thinking_blocks", None)
|
||||
m.pop("reasoning_content", None)
|
||||
|
||||
return messages
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Fireworks AI Rerank API transformation
|
|||
Reference: https://docs.fireworks.ai/inference-api-reference/rerank
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -102,6 +103,7 @@ class FireworksAIRerankConfig(FireworksAIMixin, BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
api_key = self._get_api_key(api_key)
|
||||
if api_key is None:
|
||||
|
|
|
|||
|
|
@ -164,12 +164,13 @@ class HostedVLLMChatConfig(OpenAIGPTConfig):
|
|||
"""
|
||||
Support translating:
|
||||
- video files from file_id or file_data to video_url
|
||||
- thinking_blocks on assistant messages are removed, and content lists
|
||||
are converted to strings for vLLM compatibility
|
||||
- thinking_blocks and reasoning_content on assistant messages are removed,
|
||||
and content lists are converted to strings for vLLM compatibility
|
||||
"""
|
||||
for message in messages:
|
||||
if message["role"] == "assistant":
|
||||
message.pop("thinking_blocks", None)
|
||||
message.pop("reasoning_content", None)
|
||||
existing_content = message.get("content")
|
||||
if isinstance(existing_content, list):
|
||||
text_parts = []
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
Transformation logic for Hosted VLLM rerank
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -107,6 +108,7 @@ class HostedVLLMRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("HOSTED_VLLM_API_KEY") or "fake-api-key"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -123,6 +124,7 @@ class HuggingFaceRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
api_base: str | None = None,
|
||||
) -> dict:
|
||||
# Get API credentials
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Transformation logic from Cohere's /v1/rerank format to Infinity's `/v1/rerank`
|
|||
Why separate file? Make it easy to see how transformation works
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -46,6 +47,7 @@ class InfinityRerankConfig(CohereRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("INFINITY_API_KEY") or get_secret_str("INFINITY_API_KEY") or litellm.infinity_key
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Why separate file? Make it easy to see how transformation works
|
|||
Docs - https://jina.ai/reranker
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
from httpx import URL, Response
|
||||
|
|
@ -139,6 +140,7 @@ class JinaAIRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
raise ValueError("api_key is required. Set via `api_key` parameter or `JINA_API_KEY` environment variable.")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, Final, Literal
|
||||
|
||||
import httpx
|
||||
|
|
@ -152,6 +153,7 @@ class NvidiaNimRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Validate that the Nvidia NIM API key is present.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Translates from Cohere's `/v1/rerank` input format to Vertex AI Discovery Engine
|
|||
Why separate file? Make it easy to see how transformation works
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -74,14 +75,15 @@ class VertexAIRerankConfig(BaseRerankConfig, VertexBase):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Validate and set up authentication for Vertex AI Discovery Engine API
|
||||
"""
|
||||
# Get credentials and project info from optional_params (which contains vertex_credentials, etc.)
|
||||
litellm_params: Final = optional_params.copy() if optional_params else {}
|
||||
vertex_credentials: Final = self.safe_get_vertex_ai_credentials(litellm_params)
|
||||
vertex_project: Final = self.safe_get_vertex_ai_project(litellm_params)
|
||||
vertex_params: Final = optional_params.copy() if optional_params else {}
|
||||
vertex_credentials: Final = self.safe_get_vertex_ai_credentials(vertex_params)
|
||||
vertex_project: Final = self.safe_get_vertex_ai_project(vertex_params)
|
||||
|
||||
# Get access token using the base class method
|
||||
access_token, project_id = self._ensure_access_token(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ Transformation logic for Voyage AI's /v1/rerank endpoint.
|
|||
Docs - https://docs.voyageai.com/docs/reranker
|
||||
"""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final
|
||||
|
||||
import httpx
|
||||
|
|
@ -137,6 +138,7 @@ class VoyageRerankConfig(BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
if api_key is None:
|
||||
api_key = get_secret_str("VOYAGE_API_KEY") or get_secret_str("VOYAGE_AI_API_KEY")
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ Docs - https://cloud.ibm.com/apidocs/watsonx-ai#text-rerank
|
|||
"""
|
||||
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from typing import Any, Final, cast
|
||||
|
||||
import httpx
|
||||
|
|
@ -60,6 +61,7 @@ class IBMWatsonXRerankConfig(IBMWatsonXMixin, BaseRerankConfig):
|
|||
model: str,
|
||||
api_key: str | None = None,
|
||||
optional_params: dict | None = None,
|
||||
litellm_params: Mapping[str, object] | None = None,
|
||||
) -> dict:
|
||||
optional_params = optional_params or {}
|
||||
|
||||
|
|
|
|||
|
|
@ -6828,6 +6828,8 @@ def embedding(
|
|||
aembedding=aembedding,
|
||||
)
|
||||
elif custom_llm_provider == "azure_ai":
|
||||
from litellm.llms.azure_ai.common_utils import get_azure_ai_entra_token
|
||||
|
||||
api_base = (
|
||||
api_base # for deepinfra/perplexity/anyscale/groq/friendliai we check in get_llm_provider and pass in the api base from there
|
||||
or litellm.api_base
|
||||
|
|
@ -6837,8 +6839,8 @@ def embedding(
|
|||
api_key = (
|
||||
api_key
|
||||
or litellm.api_key # for deepinfra/perplexity/anyscale/friendliai we check in get_llm_provider and pass in the api key from there
|
||||
or litellm.openai_key
|
||||
or get_secret_str("AZURE_AI_API_KEY")
|
||||
or get_azure_ai_entra_token(litellm_params=litellm_params_dict)
|
||||
)
|
||||
|
||||
## EMBEDDING CALL
|
||||
|
|
|
|||
|
|
@ -4901,11 +4901,11 @@
|
|||
"supports_prompt_caching": false,
|
||||
"supports_reasoning": false,
|
||||
"supports_response_schema": false,
|
||||
"supports_audio_input": true,
|
||||
"supports_audio_output": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": false
|
||||
"supports_vision": false,
|
||||
"supports_audio_input": true,
|
||||
"supports_audio_output": true
|
||||
},
|
||||
"azure/gpt-audio-mini-2025-10-06": {
|
||||
"deprecation_date": "2027-04-06",
|
||||
|
|
@ -5126,7 +5126,6 @@
|
|||
"cache_creation_input_audio_token_cost": 3e-07,
|
||||
"cache_read_input_token_cost": 6e-08,
|
||||
"input_cost_per_audio_token": 1e-05,
|
||||
"input_cost_per_image_token": 8e-07,
|
||||
"input_cost_per_token": 6e-07,
|
||||
"litellm_provider": "azure",
|
||||
"max_input_tokens": 32000,
|
||||
|
|
@ -5152,7 +5151,8 @@
|
|||
"supports_function_calling": true,
|
||||
"supports_parallel_function_calling": true,
|
||||
"supports_system_messages": true,
|
||||
"supports_tool_choice": true
|
||||
"supports_tool_choice": true,
|
||||
"input_cost_per_image_token": 8e-07
|
||||
},
|
||||
"azure/gpt-realtime-mini-2025-10-06": {
|
||||
"cache_creation_input_audio_token_cost": 3e-07,
|
||||
|
|
@ -14588,6 +14588,8 @@
|
|||
]
|
||||
},
|
||||
"databricks/databricks-bge-large-en": {
|
||||
"cache_creation_input_token_cost": 1.0003e-07,
|
||||
"cache_read_input_token_cost": 1.0003e-07,
|
||||
"input_cost_per_token": 1.0003e-07,
|
||||
"input_dbu_cost_per_token": 1.429e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14603,8 +14605,8 @@
|
|||
"source": "https://www.databricks.com/product/pricing/foundation-model-serving"
|
||||
},
|
||||
"databricks/databricks-claude-3-7-sonnet": {
|
||||
"cache_creation_input_token_cost": 3.7499875e-06,
|
||||
"cache_read_input_token_cost": 2.9999900000000006e-07,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.9999900000000002e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14625,8 +14627,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-claude-fable-5": {
|
||||
"cache_creation_input_token_cost": 1.2500075e-05,
|
||||
"cache_read_input_token_cost": 1.0000060000000001e-06,
|
||||
"cache_creation_input_token_cost": 1.250004e-05,
|
||||
"cache_read_input_token_cost": 1.00002e-06,
|
||||
"input_cost_per_token": 1.000006e-05,
|
||||
"input_dbu_cost_per_token": 0.000142858,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14634,26 +14636,27 @@
|
|||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"metadata": {
|
||||
"notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation."
|
||||
"notes": "Costs per token are the published Global DBU rates times $0.070 per DBU. The '*_dbu_cost_per_token' fields are provided for reference; cost calculation reads the dollar '*_cost_per_token' fields."
|
||||
},
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 5.0000020000000004e-05,
|
||||
"output_cost_per_token": 5.000002e-05,
|
||||
"output_dbu_cost_per_token": 0.000714286,
|
||||
"prompt_cache_min_tokens": 512,
|
||||
"source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving",
|
||||
"supports_adaptive_thinking": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_sampling_params": false,
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_vision": false,
|
||||
"thinking_always_on": true
|
||||
},
|
||||
"databricks/databricks-claude-haiku-4-5": {
|
||||
"cache_creation_input_token_cost": 1.2500250000000002e-06,
|
||||
"cache_read_input_token_cost": 1.0000200000000002e-07,
|
||||
"cache_creation_input_token_cost": 1.24999e-06,
|
||||
"cache_read_input_token_cost": 1.0003e-07,
|
||||
"input_cost_per_token": 1.00002e-06,
|
||||
"input_dbu_cost_per_token": 1.4286e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14675,8 +14678,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4": {
|
||||
"cache_creation_input_token_cost": 1.8750025e-05,
|
||||
"cache_read_input_token_cost": 1.500002e-06,
|
||||
"cache_creation_input_token_cost": 1.874999e-05,
|
||||
"cache_read_input_token_cost": 1.50003e-06,
|
||||
"input_cost_per_token": 1.5000020000000002e-05,
|
||||
"input_dbu_cost_per_token": 0.000214286,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14697,8 +14700,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4-1": {
|
||||
"cache_creation_input_token_cost": 1.8750025e-05,
|
||||
"cache_read_input_token_cost": 1.500002e-06,
|
||||
"cache_creation_input_token_cost": 1.874999e-05,
|
||||
"cache_read_input_token_cost": 1.50003e-06,
|
||||
"input_cost_per_token": 1.5000020000000002e-05,
|
||||
"input_dbu_cost_per_token": 0.000214286,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14720,8 +14723,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4-5": {
|
||||
"cache_creation_input_token_cost": 6.2500375e-06,
|
||||
"cache_read_input_token_cost": 5.000030000000001e-07,
|
||||
"cache_creation_input_token_cost": 6.25002e-06,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14744,8 +14747,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4-6": {
|
||||
"cache_creation_input_token_cost": 6.2500375e-06,
|
||||
"cache_read_input_token_cost": 5.000030000000001e-07,
|
||||
"cache_creation_input_token_cost": 6.25002e-06,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14766,8 +14769,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4-7": {
|
||||
"cache_creation_input_token_cost": 6.2500375e-06,
|
||||
"cache_read_input_token_cost": 5.000030000000001e-07,
|
||||
"cache_creation_input_token_cost": 6.25002e-06,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14775,10 +14778,10 @@
|
|||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"metadata": {
|
||||
"notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation."
|
||||
"notes": "Costs per token are the published Global DBU rates times $0.070 per DBU. The '*_dbu_cost_per_token' fields are provided for reference; cost calculation reads the dollar '*_cost_per_token' fields."
|
||||
},
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.5000010000000002e-05,
|
||||
"output_cost_per_token": 2.500001e-05,
|
||||
"output_dbu_cost_per_token": 0.000357143,
|
||||
"prompt_cache_min_tokens": 2048,
|
||||
"source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving",
|
||||
|
|
@ -14792,8 +14795,8 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-4-8": {
|
||||
"cache_creation_input_token_cost": 6.2500375e-06,
|
||||
"cache_read_input_token_cost": 5.000030000000001e-07,
|
||||
"cache_creation_input_token_cost": 6.25002e-06,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14801,16 +14804,17 @@
|
|||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"metadata": {
|
||||
"notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation."
|
||||
"notes": "Costs per token are the published Global DBU rates times $0.070 per DBU. The '*_dbu_cost_per_token' fields are provided for reference; cost calculation reads the dollar '*_cost_per_token' fields."
|
||||
},
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.5000010000000002e-05,
|
||||
"output_cost_per_token": 2.500001e-05,
|
||||
"output_dbu_cost_per_token": 0.000357143,
|
||||
"prompt_cache_min_tokens": 1024,
|
||||
"source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving",
|
||||
"supports_adaptive_thinking": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_sampling_params": false,
|
||||
|
|
@ -14818,8 +14822,8 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"databricks/databricks-claude-opus-5": {
|
||||
"cache_creation_input_token_cost": 6.2500375e-06,
|
||||
"cache_read_input_token_cost": 5.000030000000001e-07,
|
||||
"cache_creation_input_token_cost": 6.25002e-06,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14827,16 +14831,17 @@
|
|||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"metadata": {
|
||||
"notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation."
|
||||
"notes": "Costs per token are the published Global DBU rates times $0.070 per DBU. The '*_dbu_cost_per_token' fields are provided for reference; cost calculation reads the dollar '*_cost_per_token' fields."
|
||||
},
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 2.5000010000000002e-05,
|
||||
"output_cost_per_token": 2.500001e-05,
|
||||
"output_dbu_cost_per_token": 0.000357143,
|
||||
"prompt_cache_min_tokens": 512,
|
||||
"source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving",
|
||||
"supports_adaptive_thinking": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_sampling_params": false,
|
||||
|
|
@ -14844,8 +14849,8 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"databricks/databricks-claude-sonnet-4": {
|
||||
"cache_creation_input_token_cost": 3.7499875e-06,
|
||||
"cache_read_input_token_cost": 2.9999900000000006e-07,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.9999900000000002e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14867,8 +14872,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-claude-sonnet-4-1": {
|
||||
"cache_creation_input_token_cost": 3.7499875e-06,
|
||||
"cache_read_input_token_cost": 2.9999900000000006e-07,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.9999900000000002e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14889,8 +14894,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-claude-sonnet-4-5": {
|
||||
"cache_creation_input_token_cost": 3.7499875e-06,
|
||||
"cache_read_input_token_cost": 2.9999900000000006e-07,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.9999900000000002e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14912,8 +14917,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-claude-sonnet-4-6": {
|
||||
"cache_creation_input_token_cost": 3.7499875e-06,
|
||||
"cache_read_input_token_cost": 2.9999900000000006e-07,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.9999900000000002e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -14934,25 +14939,26 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-claude-sonnet-5": {
|
||||
"cache_creation_input_token_cost": 2.4999625e-06,
|
||||
"cache_read_input_token_cost": 1.9999700000000004e-07,
|
||||
"input_cost_per_token": 1.9999700000000004e-06,
|
||||
"input_dbu_cost_per_token": 2.8571e-05,
|
||||
"cache_creation_input_token_cost": 3.74997e-06,
|
||||
"cache_read_input_token_cost": 3.0002e-07,
|
||||
"input_cost_per_token": 2.99999e-06,
|
||||
"input_dbu_cost_per_token": 4.2857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
"max_input_tokens": 1000000,
|
||||
"max_output_tokens": 128000,
|
||||
"max_tokens": 128000,
|
||||
"metadata": {
|
||||
"notes": "Input/output cost per token is dbu cost * $0.070. Number provided for reference, '*_dbu_cost_per_token' used in actual calculation. Claude Sonnet 5 DBU rates are Anthropic's introductory launch pricing, in effect through 2026-08-31, after which the standard Sonnet 5 rates (equal to Sonnet 4.5 / 4.6) take effect."
|
||||
"notes": "Costs per token are the published Global DBU rates times $0.070 per DBU. The '*_dbu_cost_per_token' fields are provided for reference; cost calculation reads the dollar '*_cost_per_token' fields. Introductory launch rates of 28.571 input / 142.857 output / 35.714 cache write / 2.857 cache read DBU run through 2026-08-31; the standard rates are listed here because entries carry no expiry date."
|
||||
},
|
||||
"mode": "chat",
|
||||
"output_cost_per_token": 9.999990000000002e-06,
|
||||
"output_dbu_cost_per_token": 0.000142857,
|
||||
"output_cost_per_token": 1.500002e-05,
|
||||
"output_dbu_cost_per_token": 0.000214286,
|
||||
"prompt_cache_min_tokens": 1024,
|
||||
"source": "https://www.databricks.com/product/pricing/proprietary-foundation-model-serving",
|
||||
"supports_adaptive_thinking": true,
|
||||
"supports_assistant_prefill": false,
|
||||
"supports_function_calling": true,
|
||||
"supports_mid_conversation_system": true,
|
||||
"supports_prompt_caching": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_sampling_params": false,
|
||||
|
|
@ -14960,7 +14966,7 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"databricks/databricks-gemini-2-5-flash": {
|
||||
"cache_creation_input_token_cost": 3.0001999999999996e-07,
|
||||
"cache_creation_input_token_cost": 3.0002e-07,
|
||||
"cache_read_input_token_cost": 3.0002e-08,
|
||||
"input_cost_per_token": 3.0001999999999996e-07,
|
||||
"input_dbu_cost_per_token": 4.285999999999999e-06,
|
||||
|
|
@ -15003,7 +15009,7 @@
|
|||
},
|
||||
"databricks/databricks-gemini-3-1-flash-lite": {
|
||||
"cache_creation_input_token_cost": 3.1248e-07,
|
||||
"cache_read_input_token_cost": 3.1248e-08,
|
||||
"cache_read_input_token_cost": 3.122e-08,
|
||||
"input_cost_per_token": 3.1248e-07,
|
||||
"input_dbu_cost_per_token": 4.464e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15023,7 +15029,7 @@
|
|||
},
|
||||
"databricks/databricks-gemini-3-1-pro": {
|
||||
"cache_creation_input_token_cost": 2.49998e-06,
|
||||
"cache_read_input_token_cost": 2.49998e-07,
|
||||
"cache_read_input_token_cost": 2.4997e-07,
|
||||
"input_cost_per_token": 2.49998e-06,
|
||||
"input_dbu_cost_per_token": 3.5714e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15042,8 +15048,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-gemini-3-flash": {
|
||||
"cache_creation_input_token_cost": 6.250300000000001e-07,
|
||||
"cache_read_input_token_cost": 6.250300000000001e-08,
|
||||
"cache_creation_input_token_cost": 6.2503e-07,
|
||||
"cache_read_input_token_cost": 6.251e-08,
|
||||
"input_cost_per_token": 6.2503e-07,
|
||||
"input_dbu_cost_per_token": 8.929e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15063,7 +15069,7 @@
|
|||
},
|
||||
"databricks/databricks-gemini-3-pro": {
|
||||
"cache_creation_input_token_cost": 2.49998e-06,
|
||||
"cache_read_input_token_cost": 2.49998e-07,
|
||||
"cache_read_input_token_cost": 2.4997e-07,
|
||||
"input_cost_per_token": 2.49998e-06,
|
||||
"input_dbu_cost_per_token": 3.5714e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15082,6 +15088,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-gemma-3-12b": {
|
||||
"cache_creation_input_token_cost": 1.5001e-07,
|
||||
"cache_read_input_token_cost": 1.5001e-07,
|
||||
"input_cost_per_token": 1.5000999999999998e-07,
|
||||
"input_dbu_cost_per_token": 2.1429999999999996e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15099,7 +15107,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5": {
|
||||
"cache_creation_input_token_cost": 1.24999e-06,
|
||||
"cache_read_input_token_cost": 1.24999e-07,
|
||||
"cache_read_input_token_cost": 1.2502e-07,
|
||||
"input_cost_per_token": 1.24999e-06,
|
||||
"input_dbu_cost_per_token": 1.7857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15118,7 +15126,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-1": {
|
||||
"cache_creation_input_token_cost": 1.24999e-06,
|
||||
"cache_read_input_token_cost": 1.24999e-07,
|
||||
"cache_read_input_token_cost": 1.2502e-07,
|
||||
"input_cost_per_token": 1.24999e-06,
|
||||
"input_dbu_cost_per_token": 1.7857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15137,7 +15145,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-1-codex-max": {
|
||||
"cache_creation_input_token_cost": 1.24999e-06,
|
||||
"cache_read_input_token_cost": 1.24999e-07,
|
||||
"cache_read_input_token_cost": 1.2502e-07,
|
||||
"input_cost_per_token": 1.24999e-06,
|
||||
"input_dbu_cost_per_token": 1.7857e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15154,8 +15162,8 @@
|
|||
"supports_prompt_caching": true
|
||||
},
|
||||
"databricks/databricks-gpt-5-1-codex-mini": {
|
||||
"cache_creation_input_token_cost": 2.4997000000000006e-07,
|
||||
"cache_read_input_token_cost": 2.4997000000000005e-08,
|
||||
"cache_creation_input_token_cost": 2.4997e-07,
|
||||
"cache_read_input_token_cost": 2.499e-08,
|
||||
"input_cost_per_token": 2.4997e-07,
|
||||
"input_dbu_cost_per_token": 3.571e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15172,8 +15180,8 @@
|
|||
"supports_prompt_caching": true
|
||||
},
|
||||
"databricks/databricks-gpt-5-2": {
|
||||
"cache_creation_input_token_cost": 1.7500000000000002e-06,
|
||||
"cache_read_input_token_cost": 1.7500000000000002e-07,
|
||||
"cache_creation_input_token_cost": 1.75e-06,
|
||||
"cache_read_input_token_cost": 1.75e-07,
|
||||
"input_cost_per_token": 1.75e-06,
|
||||
"input_dbu_cost_per_token": 2.5e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15190,8 +15198,8 @@
|
|||
"supports_prompt_caching": true
|
||||
},
|
||||
"databricks/databricks-gpt-5-2-codex": {
|
||||
"cache_creation_input_token_cost": 1.7500000000000002e-06,
|
||||
"cache_read_input_token_cost": 1.7500000000000002e-07,
|
||||
"cache_creation_input_token_cost": 1.75e-06,
|
||||
"cache_read_input_token_cost": 1.75e-07,
|
||||
"input_cost_per_token": 1.75e-06,
|
||||
"input_dbu_cost_per_token": 2.5e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15208,8 +15216,8 @@
|
|||
"supports_prompt_caching": true
|
||||
},
|
||||
"databricks/databricks-gpt-5-3-codex": {
|
||||
"cache_creation_input_token_cost": 1.7500000000000002e-06,
|
||||
"cache_read_input_token_cost": 1.7500000000000002e-07,
|
||||
"cache_creation_input_token_cost": 1.75e-06,
|
||||
"cache_read_input_token_cost": 1.75e-07,
|
||||
"input_cost_per_token": 1.75e-06,
|
||||
"input_dbu_cost_per_token": 2.5e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15227,7 +15235,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-4": {
|
||||
"cache_creation_input_token_cost": 2.49998e-06,
|
||||
"cache_read_input_token_cost": 2.49998e-07,
|
||||
"cache_read_input_token_cost": 2.4997e-07,
|
||||
"input_cost_per_token": 2.49998e-06,
|
||||
"input_dbu_cost_per_token": 3.5714e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15245,7 +15253,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-4-mini": {
|
||||
"cache_creation_input_token_cost": 7.4998e-07,
|
||||
"cache_read_input_token_cost": 7.499800000000001e-08,
|
||||
"cache_read_input_token_cost": 7.497e-08,
|
||||
"input_cost_per_token": 7.4998e-07,
|
||||
"input_dbu_cost_per_token": 1.0714e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15263,7 +15271,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-4-nano": {
|
||||
"cache_creation_input_token_cost": 1.9999e-07,
|
||||
"cache_read_input_token_cost": 1.9999000000000003e-08,
|
||||
"cache_read_input_token_cost": 2.002e-08,
|
||||
"input_cost_per_token": 1.9999e-07,
|
||||
"input_dbu_cost_per_token": 2.857e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15280,8 +15288,8 @@
|
|||
"supports_prompt_caching": true
|
||||
},
|
||||
"databricks/databricks-gpt-5-mini": {
|
||||
"cache_creation_input_token_cost": 2.4997000000000006e-07,
|
||||
"cache_read_input_token_cost": 2.4997000000000005e-08,
|
||||
"cache_creation_input_token_cost": 2.4997e-07,
|
||||
"cache_read_input_token_cost": 2.499e-08,
|
||||
"input_cost_per_token": 2.4997000000000006e-07,
|
||||
"input_dbu_cost_per_token": 3.571e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15300,7 +15308,7 @@
|
|||
},
|
||||
"databricks/databricks-gpt-5-nano": {
|
||||
"cache_creation_input_token_cost": 4.998e-08,
|
||||
"cache_read_input_token_cost": 4.998000000000001e-09,
|
||||
"cache_read_input_token_cost": 4.97e-09,
|
||||
"input_cost_per_token": 4.998e-08,
|
||||
"input_dbu_cost_per_token": 7.14e-07,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15318,6 +15326,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-gpt-oss-120b": {
|
||||
"cache_creation_input_token_cost": 1.5001e-07,
|
||||
"cache_read_input_token_cost": 1.5001e-07,
|
||||
"input_cost_per_token": 1.5000999999999998e-07,
|
||||
"input_dbu_cost_per_token": 2.1429999999999996e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15334,6 +15344,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-gpt-oss-20b": {
|
||||
"cache_creation_input_token_cost": 7e-08,
|
||||
"cache_read_input_token_cost": 7e-08,
|
||||
"input_cost_per_token": 7e-08,
|
||||
"input_dbu_cost_per_token": 1e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15350,6 +15362,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-gte-large-en": {
|
||||
"cache_creation_input_token_cost": 1.2999e-07,
|
||||
"cache_read_input_token_cost": 1.2999e-07,
|
||||
"input_cost_per_token": 1.2999000000000001e-07,
|
||||
"input_dbu_cost_per_token": 1.857e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15365,6 +15379,8 @@
|
|||
"source": "https://www.databricks.com/product/pricing/foundation-model-serving"
|
||||
},
|
||||
"databricks/databricks-llama-2-70b-chat": {
|
||||
"cache_creation_input_token_cost": 5.0001e-07,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.0001e-07,
|
||||
"input_dbu_cost_per_token": 7.143e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15381,6 +15397,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-llama-4-maverick": {
|
||||
"cache_creation_input_token_cost": 5.0001e-07,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.0001e-07,
|
||||
"input_dbu_cost_per_token": 7.143e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15398,6 +15416,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-meta-llama-3-1-405b-instruct": {
|
||||
"cache_creation_input_token_cost": 5.00003e-06,
|
||||
"cache_read_input_token_cost": 5.00003e-06,
|
||||
"input_cost_per_token": 5.00003e-06,
|
||||
"input_dbu_cost_per_token": 7.1429e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15414,6 +15434,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-meta-llama-3-1-8b-instruct": {
|
||||
"cache_creation_input_token_cost": 1.5001e-07,
|
||||
"cache_read_input_token_cost": 1.5001e-07,
|
||||
"input_cost_per_token": 1.5000999999999998e-07,
|
||||
"input_dbu_cost_per_token": 2.1429999999999996e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15430,6 +15452,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-meta-llama-3-3-70b-instruct": {
|
||||
"cache_creation_input_token_cost": 5.0001e-07,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.0001e-07,
|
||||
"input_dbu_cost_per_token": 7.143e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15447,6 +15471,8 @@
|
|||
"supports_response_schema": true
|
||||
},
|
||||
"databricks/databricks-meta-llama-3-70b-instruct": {
|
||||
"cache_creation_input_token_cost": 1.00002e-06,
|
||||
"cache_read_input_token_cost": 1.00002e-06,
|
||||
"input_cost_per_token": 1.00002e-06,
|
||||
"input_dbu_cost_per_token": 1.4286e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15463,6 +15489,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-mixtral-8x7b-instruct": {
|
||||
"cache_creation_input_token_cost": 5.0001e-07,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.0001e-07,
|
||||
"input_dbu_cost_per_token": 7.143e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15479,6 +15507,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-mpt-30b-instruct": {
|
||||
"cache_creation_input_token_cost": 1.00002e-06,
|
||||
"cache_read_input_token_cost": 1.00002e-06,
|
||||
"input_cost_per_token": 1.00002e-06,
|
||||
"input_dbu_cost_per_token": 1.4286e-05,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -15495,6 +15525,8 @@
|
|||
"supports_tool_choice": true
|
||||
},
|
||||
"databricks/databricks-mpt-7b-instruct": {
|
||||
"cache_creation_input_token_cost": 5.0001e-07,
|
||||
"cache_read_input_token_cost": 5.0001e-07,
|
||||
"input_cost_per_token": 5.0001e-07,
|
||||
"input_dbu_cost_per_token": 7.143e-06,
|
||||
"litellm_provider": "databricks",
|
||||
|
|
@ -31641,7 +31673,6 @@
|
|||
"supports_vision": false
|
||||
},
|
||||
"multimodalembedding": {
|
||||
"deprecation_date": "2027-04-01",
|
||||
"input_cost_per_character": 2e-07,
|
||||
"input_cost_per_image": 0.0001,
|
||||
"input_cost_per_token": 8e-07,
|
||||
|
|
@ -31662,7 +31693,8 @@
|
|||
"text",
|
||||
"image",
|
||||
"video"
|
||||
]
|
||||
],
|
||||
"deprecation_date": "2027-04-01"
|
||||
},
|
||||
"multimodalembedding@001": {
|
||||
"deprecation_date": "2027-04-01",
|
||||
|
|
@ -42449,7 +42481,6 @@
|
|||
"source": "https://wandb.ai/site/pricing/tokens/"
|
||||
},
|
||||
"wandb/Qwen/Qwen3-Coder-480B-A35B-Instruct": {
|
||||
"deprecation_date": "2026-08-25",
|
||||
"max_tokens": 262000,
|
||||
"max_input_tokens": 262000,
|
||||
"max_output_tokens": 262000,
|
||||
|
|
@ -42457,17 +42488,18 @@
|
|||
"output_cost_per_token": 1.5e-06,
|
||||
"litellm_provider": "wandb",
|
||||
"mode": "chat",
|
||||
"deprecation_date": "2026-08-25",
|
||||
"source": "https://wandb.ai/site/pricing/tokens/"
|
||||
},
|
||||
"wandb/moonshotai/Kimi-K2-Instruct": {
|
||||
"deprecation_date": "2026-03-04",
|
||||
"max_tokens": 128000,
|
||||
"max_input_tokens": 128000,
|
||||
"max_output_tokens": 128000,
|
||||
"input_cost_per_token": 6e-07,
|
||||
"output_cost_per_token": 2.5e-06,
|
||||
"litellm_provider": "wandb",
|
||||
"mode": "chat"
|
||||
"mode": "chat",
|
||||
"deprecation_date": "2026-03-04"
|
||||
},
|
||||
"wandb/moonshotai/Kimi-K2.5": {
|
||||
"max_tokens": 262144,
|
||||
|
|
@ -42485,7 +42517,6 @@
|
|||
"supports_vision": true
|
||||
},
|
||||
"wandb/MiniMaxAI/MiniMax-M2.5": {
|
||||
"deprecation_date": "2026-08-25",
|
||||
"max_tokens": 197000,
|
||||
"max_input_tokens": 197000,
|
||||
"max_output_tokens": 197000,
|
||||
|
|
@ -42496,7 +42527,8 @@
|
|||
"source": "https://wandb.ai/site/pricing/tokens/",
|
||||
"supports_function_calling": true,
|
||||
"supports_reasoning": true,
|
||||
"supports_response_schema": true
|
||||
"supports_response_schema": true,
|
||||
"deprecation_date": "2026-08-25"
|
||||
},
|
||||
"wandb/meta-llama/Llama-3.1-8B-Instruct": {
|
||||
"max_tokens": 128000,
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,9 +1,9 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[347257,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"ClientPageRoot"]
|
||||
3:I[871135,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js","/litellm-asset-prefix/_next/static/chunks/21b4hw_igldhz.js","/litellm-asset-prefix/_next/static/chunks/2_hxghav3pe9j.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","/litellm-asset-prefix/_next/static/chunks/1gwzs-8xkvx8f.js","/litellm-asset-prefix/_next/static/chunks/1oob52g5gib5j.js","/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","/litellm-asset-prefix/_next/static/chunks/1yok3x3_3gr1p.js","/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","/litellm-asset-prefix/_next/static/chunks/2mxcro_n8i1ef.js","/litellm-asset-prefix/_next/static/chunks/3xciut9pzmr7-.js","/litellm-asset-prefix/_next/static/chunks/1jkcw8ug0uobj.js","/litellm-asset-prefix/_next/static/chunks/3k3r6waxmnsvu.js","/litellm-asset-prefix/_next/static/chunks/3l0glczkblv8_.js","/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","/litellm-asset-prefix/_next/static/chunks/2ca0bgyj3-r_j.js","/litellm-asset-prefix/_next/static/chunks/29nmr1sywlx25.js","/litellm-asset-prefix/_next/static/chunks/0gh1eppc9ekzh.js","/litellm-asset-prefix/_next/static/chunks/3hk5c4q5k-j7x.js","/litellm-asset-prefix/_next/static/chunks/2mhbxmykyh83f.js","/litellm-asset-prefix/_next/static/chunks/1xk5l9lxa0dv-.js","/litellm-asset-prefix/_next/static/chunks/26e7zpdybuhtq.js","/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","/litellm-asset-prefix/_next/static/chunks/0m-cn894wctv5.js","/litellm-asset-prefix/_next/static/chunks/3cw_k7_vr9pcu.js","/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","/litellm-asset-prefix/_next/static/chunks/2udc_95331vyv.js","/litellm-asset-prefix/_next/static/chunks/3mkd81u36rwju.js","/litellm-asset-prefix/_next/static/chunks/0kh9ov64og3-k.js","/litellm-asset-prefix/_next/static/chunks/3580ki1m5g-sx.js","/litellm-asset-prefix/_next/static/chunks/2xuwoxcnxuv39.js","/litellm-asset-prefix/_next/static/chunks/3bwziv83xzehe.js"],"default"]
|
||||
6:I[897367,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"OutletBoundary"]
|
||||
2:I[347257,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"ClientPageRoot"]
|
||||
3:I[871135,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/37t2cfzl_b58p.js","/litellm-asset-prefix/_next/static/chunks/1zf358k334atp.js","/litellm-asset-prefix/_next/static/chunks/3kil-7y33kpm9.js","/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","/litellm-asset-prefix/_next/static/chunks/1natmx9lu3mus.js","/litellm-asset-prefix/_next/static/chunks/1dmg55q8kht9j.js","/litellm-asset-prefix/_next/static/chunks/1lrl_8p0h2sbm.js","/litellm-asset-prefix/_next/static/chunks/1e-4-g6x6zyse.js","/litellm-asset-prefix/_next/static/chunks/0gygfcpmiijl8.js","/litellm-asset-prefix/_next/static/chunks/2cu4j3g1tldv4.js","/litellm-asset-prefix/_next/static/chunks/2kmqjpt047tjo.js","/litellm-asset-prefix/_next/static/chunks/1phty1k2nx8fx.js","/litellm-asset-prefix/_next/static/chunks/0u3cfuz-tf0wj.js","/litellm-asset-prefix/_next/static/chunks/39s4-rh6l9sa1.js","/litellm-asset-prefix/_next/static/chunks/0dylouuq8ak8p.js","/litellm-asset-prefix/_next/static/chunks/3a3jpg95umjho.js","/litellm-asset-prefix/_next/static/chunks/28hnu_qv5e_c_.js","/litellm-asset-prefix/_next/static/chunks/0qf1_0kt4uuxa.js","/litellm-asset-prefix/_next/static/chunks/0ab_ntohf1wik.js","/litellm-asset-prefix/_next/static/chunks/3kpec-qy1uzod.js"],"default"]
|
||||
6:I[897367,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"OutletBoundary"]
|
||||
7:"$Sreact.suspense"
|
||||
0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/3l0glczkblv8_.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/1cea03gg5a_c7.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2ca0bgyj3-r_j.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/29nmr1sywlx25.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/0gh1eppc9ekzh.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/3hk5c4q5k-j7x.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/2mhbxmykyh83f.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1xk5l9lxa0dv-.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/26e7zpdybuhtq.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/2c90xukbd3il6.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/0m-cn894wctv5.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/3cw_k7_vr9pcu.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/12wsfsljxg4xv.js","async":true}],["$","script","script-13",{"src":"/litellm-asset-prefix/_next/static/chunks/2udc_95331vyv.js","async":true}],["$","script","script-14",{"src":"/litellm-asset-prefix/_next/static/chunks/3mkd81u36rwju.js","async":true}],["$","script","script-15",{"src":"/litellm-asset-prefix/_next/static/chunks/0kh9ov64og3-k.js","async":true}],["$","script","script-16",{"src":"/litellm-asset-prefix/_next/static/chunks/3580ki1m5g-sx.js","async":true}],["$","script","script-17",{"src":"/litellm-asset-prefix/_next/static/chunks/2xuwoxcnxuv39.js","async":true}],["$","script","script-18",{"src":"/litellm-asset-prefix/_next/static/chunks/3bwziv83xzehe.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"TeJ852IBdcKgsOMzGKY73"}
|
||||
0:{"rsc":["$","$1","c",{"children":[["$","$L2",null,{"Component":"$3","serverProvidedParams":{"searchParams":{},"params":{},"promises":["$@4","$@5"]}}],[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/1e-4-g6x6zyse.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/0gygfcpmiijl8.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/2cu4j3g1tldv4.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2kmqjpt047tjo.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/1phty1k2nx8fx.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/0u3cfuz-tf0wj.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/39s4-rh6l9sa1.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/0dylouuq8ak8p.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/3a3jpg95umjho.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/28hnu_qv5e_c_.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/0qf1_0kt4uuxa.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/0ab_ntohf1wik.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/3kpec-qy1uzod.js","async":true}]],["$","$L6",null,{"children":["$","$7",null,{"name":"Next.MetadataOutlet","children":"$@8"}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0cE25rDXvGu3tj4HWOGKy"}
|
||||
4:{}
|
||||
5:"$0:rsc:props:children:0:props:serverProvidedParams:params"
|
||||
8:null
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[92825,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"ClientSegmentRoot"]
|
||||
3:I[216370,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js","/litellm-asset-prefix/_next/static/chunks/21b4hw_igldhz.js","/litellm-asset-prefix/_next/static/chunks/2_hxghav3pe9j.js","/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","/litellm-asset-prefix/_next/static/chunks/1gwzs-8xkvx8f.js","/litellm-asset-prefix/_next/static/chunks/1oob52g5gib5j.js","/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","/litellm-asset-prefix/_next/static/chunks/1yok3x3_3gr1p.js","/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","/litellm-asset-prefix/_next/static/chunks/2mxcro_n8i1ef.js","/litellm-asset-prefix/_next/static/chunks/3xciut9pzmr7-.js","/litellm-asset-prefix/_next/static/chunks/1jkcw8ug0uobj.js","/litellm-asset-prefix/_next/static/chunks/3k3r6waxmnsvu.js"],"default"]
|
||||
4:I[339756,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
5:I[837457,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
0:{"rsc":["$","$1","c",{"children":[[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/21b4hw_igldhz.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/2_hxghav3pe9j.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/0g_w4tf2inv3i.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/1gwzs-8xkvx8f.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/1oob52g5gib5j.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1yok3x3_3gr1p.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","async":true}],["$","script","script-9",{"src":"/litellm-asset-prefix/_next/static/chunks/2mxcro_n8i1ef.js","async":true}],["$","script","script-10",{"src":"/litellm-asset-prefix/_next/static/chunks/3xciut9pzmr7-.js","async":true}],["$","script","script-11",{"src":"/litellm-asset-prefix/_next/static/chunks/1jkcw8ug0uobj.js","async":true}],["$","script","script-12",{"src":"/litellm-asset-prefix/_next/static/chunks/3k3r6waxmnsvu.js","async":true}]],["$","$L2",null,{"Component":"$3","slots":{"children":["$","$L4",null,{"parallelRouterKey":"children","template":["$","$L5",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]},"serverProvidedParams":{"params":{},"promises":["$@6"]}}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"TeJ852IBdcKgsOMzGKY73"}
|
||||
2:I[92825,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"ClientSegmentRoot"]
|
||||
3:I[216370,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/37t2cfzl_b58p.js","/litellm-asset-prefix/_next/static/chunks/1zf358k334atp.js","/litellm-asset-prefix/_next/static/chunks/3kil-7y33kpm9.js","/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","/litellm-asset-prefix/_next/static/chunks/1natmx9lu3mus.js","/litellm-asset-prefix/_next/static/chunks/1dmg55q8kht9j.js","/litellm-asset-prefix/_next/static/chunks/1lrl_8p0h2sbm.js"],"default"]
|
||||
4:I[339756,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"default"]
|
||||
5:I[837457,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"default"]
|
||||
0:{"rsc":["$","$1","c",{"children":[[["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/37t2cfzl_b58p.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/1zf358k334atp.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/3kil-7y33kpm9.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/2i0218zvrsasm.js","async":true}],["$","script","script-4",{"src":"/litellm-asset-prefix/_next/static/chunks/3ytz29phknzsy.js","async":true}],["$","script","script-5",{"src":"/litellm-asset-prefix/_next/static/chunks/0aoel7yrv88fp.js","async":true}],["$","script","script-6",{"src":"/litellm-asset-prefix/_next/static/chunks/1natmx9lu3mus.js","async":true}],["$","script","script-7",{"src":"/litellm-asset-prefix/_next/static/chunks/1dmg55q8kht9j.js","async":true}],["$","script","script-8",{"src":"/litellm-asset-prefix/_next/static/chunks/1lrl_8p0h2sbm.js","async":true}]],["$","$L2",null,{"Component":"$3","slots":{"children":["$","$L4",null,{"parallelRouterKey":"children","template":["$","$L5",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]},"serverProvidedParams":{"params":{},"promises":["$@6"]}}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0cE25rDXvGu3tj4HWOGKy"}
|
||||
6:"$0:rsc:props:children:1:props:serverProvidedParams:params"
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,6 +1,6 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[897367,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"ViewportBoundary"]
|
||||
3:I[897367,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"MetadataBoundary"]
|
||||
2:I[897367,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"ViewportBoundary"]
|
||||
3:I[897367,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"MetadataBoundary"]
|
||||
4:"$Sreact.suspense"
|
||||
5:I[27201,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"IconMark"]
|
||||
0:{"rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"LiteLLM Dashboard"}],["$","meta","1",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.3arlap5n8tyzg.ico","sizes":"48x48","type":"image/x-icon"}],["$","link","3",{"rel":"icon","href":"/get_favicon"}],["$","$L5","4",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"TeJ852IBdcKgsOMzGKY73"}
|
||||
5:I[27201,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"IconMark"]
|
||||
0:{"rsc":["$","$1","h",{"children":[null,["$","$L2",null,{"children":[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]}],["$","div",null,{"hidden":true,"children":["$","$L3",null,{"children":["$","$4",null,{"name":"Next.Metadata","children":[["$","title","0",{"children":"LiteLLM Dashboard"}],["$","meta","1",{"name":"description","content":"LiteLLM Proxy Admin UI"}],["$","link","2",{"rel":"icon","href":"/favicon.ico?favicon.3arlap5n8tyzg.ico","sizes":"48x48","type":"image/x-icon"}],["$","link","3",{"rel":"icon","href":"/get_favicon"}],["$","$L5","4",{}]]}]}]}],["$","meta",null,{"name":"next-size-adjust","content":""}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0cE25rDXvGu3tj4HWOGKy"}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
1:"$Sreact.fragment"
|
||||
2:I[12985,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"NuqsAdapter"]
|
||||
3:I[867271,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
4:I[71195,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
5:I[557951,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"AuthProvider"]
|
||||
6:I[339756,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
7:I[837457,["/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js"],"default"]
|
||||
2:I[363178,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"ThemeProvider"]
|
||||
3:I[12985,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"NuqsAdapter"]
|
||||
4:I[867271,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"default"]
|
||||
5:I[557951,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"AuthProvider"]
|
||||
6:I[339756,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"default"]
|
||||
7:I[837457,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"default"]
|
||||
8:I[713354,["/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js"],"Toaster"]
|
||||
:HL["/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","style"]
|
||||
:HL["/litellm-asset-prefix/_next/static/chunks/0cefehsj9nby1.css","style"]
|
||||
0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/0cefehsj9nby1.css","precedence":"next"}],["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/19frz_r2jewoi.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/3_zdkdwptdu3w.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","async":true}],["$","script","script-3",{"src":"/litellm-asset-prefix/_next/static/chunks/1dg0y22lcfxz2.js","async":true}]],["$","html",null,{"lang":"en","children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"children":["$","$L3",null,{"children":["$","$L4",null,{"children":["$","$L5",null,{"children":["$","$L6",null,{"parallelRouterKey":"children","template":["$","$L7",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}]}]}]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"TeJ852IBdcKgsOMzGKY73"}
|
||||
:HL["/litellm-asset-prefix/_next/static/chunks/2hicgq-mjp8vy.css","style"]
|
||||
0:{"rsc":["$","$1","c",{"children":[[["$","link","0",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","precedence":"next"}],["$","link","1",{"rel":"stylesheet","href":"/litellm-asset-prefix/_next/static/chunks/2hicgq-mjp8vy.css","precedence":"next"}],["$","script","script-0",{"src":"/litellm-asset-prefix/_next/static/chunks/3_06chgeyldml.js","async":true}],["$","script","script-1",{"src":"/litellm-asset-prefix/_next/static/chunks/26h-ny89yaww0.js","async":true}],["$","script","script-2",{"src":"/litellm-asset-prefix/_next/static/chunks/1ntn7efqc-iiw.js","async":true}]],["$","html",null,{"lang":"en","suppressHydrationWarning":true,"children":["$","body",null,{"className":"inter_5972bc34-module__OU16Qa__className","children":["$","$L2",null,{"attribute":"class","defaultTheme":"light","enableSystem":true,"disableTransitionOnChange":true,"children":["$","$L3",null,{"children":["$","$L4",null,{"children":[["$","$L5",null,{"children":["$","$L6",null,{"parallelRouterKey":"children","template":["$","$L7",null,{}],"notFound":[[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":404}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],[]]}]}],["$","$L8",null,{}]]}]}]}]}]}]]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"0cE25rDXvGu3tj4HWOGKy"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
:HL["/litellm-asset-prefix/_next/static/chunks/1kid9zr1--h6y.css","style"]
|
||||
:HL["/litellm-asset-prefix/_next/static/chunks/0cefehsj9nby1.css","style"]
|
||||
:HL["/litellm-asset-prefix/_next/static/chunks/2hicgq-mjp8vy.css","style"]
|
||||
:HL["/litellm-asset-prefix/_next/static/media/83afe278b6a6bb3c-s.p.2bn3s6zvc0dyp.woff2","font",{"crossOrigin":"","type":"font/woff2"}]
|
||||
0:{"tree":{"name":"","param":null,"prefetchHints":16,"slots":{"children":{"name":"(dashboard)","param":null,"prefetchHints":0,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":0,"slots":null}}}}},"staleTime":300,"buildId":"TeJ852IBdcKgsOMzGKY73"}
|
||||
0:{"tree":{"name":"","param":null,"prefetchHints":16,"slots":{"children":{"name":"(dashboard)","param":null,"prefetchHints":0,"slots":{"children":{"name":"__PAGE__","param":null,"prefetchHints":0,"slots":null}}}}},"staleTime":300,"buildId":"0cE25rDXvGu3tj4HWOGKy"}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1,35 +0,0 @@
|
|||
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,515288,e=>{"use strict";var r=e.i(843476),t=e.i(271645),a=e.i(115504);let s=t.forwardRef(({className:e,size:t="default",...s},d)=>(0,r.jsx)("div",{ref:d,"data-slot":"card","data-size":t,className:(0,a.cn)("group/card flex flex-col gap-(--card-spacing) rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",e),...s}));s.displayName="Card";let d=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-header",className:(0,a.cn)("group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",e),...t}));d.displayName="CardHeader";let i=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-title",className:(0,a.cn)("text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",e),...t}));i.displayName="CardTitle";let o=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-description",className:(0,a.cn)("text-sm text-muted-foreground",e),...t}));o.displayName="CardDescription";let n=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-action",className:(0,a.cn)("col-start-2 row-span-2 row-start-1 self-start justify-self-end",e),...t}));n.displayName="CardAction";let l=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-content",className:(0,a.cn)("px-(--card-spacing)",e),...t}));l.displayName="CardContent";let c=t.forwardRef(({className:e,...t},s)=>(0,r.jsx)("div",{ref:s,"data-slot":"card-footer",className:(0,a.cn)("flex items-center rounded-b-xl px-(--card-spacing) [.border-t]:pt-(--card-spacing)",e),...t}));c.displayName="CardFooter",e.s(["Card",0,s,"CardAction",0,n,"CardContent",0,l,"CardDescription",0,o,"CardFooter",0,c,"CardHeader",0,d,"CardTitle",0,i])},972520,e=>{"use strict";let r=(0,e.i(475254).default)("arrow-right",[["path",{d:"M5 12h14",key:"1ays0h"}],["path",{d:"m12 5 7 7-7 7",key:"xquz4c"}]]);e.s(["ArrowRight",0,r],972520)},411929,e=>{"use strict";var r=e.i(843476),t=e.i(271645),a=e.i(972520),s=e.i(174886),d=e.i(519455),i=e.i(515288),o=e.i(624687),n=e.i(571303),l=e.i(602869),c=e.i(727749);let u=({accessToken:e})=>{let[u,m]=(0,t.useState)(`{
|
||||
"model": "openai/gpt-4o",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Explain quantum computing in simple terms"
|
||||
}
|
||||
],
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 500,
|
||||
"stream": true
|
||||
}`),[p,f]=(0,t.useState)(""),[x,h]=(0,t.useState)(!1),g=async()=>{h(!0);try{let s;try{s=JSON.parse(u)}catch(e){c.default.fromBackend("Invalid JSON in request body"),h(!1);return}let d={call_type:"completion",request_body:s};if(!e){c.default.fromBackend("No access token found"),h(!1);return}let i=await (0,l.transformRequestCall)(e,d);if(i.raw_request_api_base&&i.raw_request_body){var r,t,a;let e,s,d=(r=i.raw_request_api_base,t=i.raw_request_body,a=i.raw_request_headers||{},e=JSON.stringify(t,null,2).split("\n").map(e=>` ${e}`).join("\n"),s=Object.entries(a).map(([e,r])=>`-H '${e}: ${r}'`).join(" \\\n "),`curl -X POST \\
|
||||
${r} \\
|
||||
${s?`${s} \\
|
||||
`:""}-H 'Content-Type: application/json' \\
|
||||
-d '{
|
||||
${e}
|
||||
}'`);f(d),c.default.success("Request transformed successfully")}else{let e="string"==typeof i?i:JSON.stringify(i);f(e),c.default.info("Transformed request received in unexpected format")}}catch(e){console.error("Error transforming request:",e),c.default.fromBackend("Failed to transform request")}finally{h(!1)}};return(0,r.jsxs)("div",{className:"p-2",children:[(0,r.jsx)("h1",{className:"text-lg font-medium text-foreground",children:"Playground"}),(0,r.jsx)("p",{className:"text-sm text-muted-foreground",children:"See how LiteLLM transforms your request for the specified provider."}),(0,r.jsxs)("div",{className:"mt-4 grid grid-cols-1 gap-4 lg:grid-cols-2",children:[(0,r.jsxs)(i.Card,{children:[(0,r.jsxs)(i.CardHeader,{children:[(0,r.jsx)(i.CardTitle,{className:"text-2xl font-bold",children:"Original Request"}),(0,r.jsx)(i.CardDescription,{children:"The request you would send to LiteLLM /chat/completions endpoint."})]}),(0,r.jsx)(i.CardContent,{children:(0,r.jsx)(o.Textarea,{className:"h-72 resize-none p-4 font-mono text-sm field-sizing-fixed",value:u,onChange:e=>m(e.target.value),onKeyDown:e=>{(e.metaKey||e.ctrlKey)&&"Enter"===e.key&&(e.preventDefault(),g())},placeholder:"Press Cmd/Ctrl + Enter to transform"})}),(0,r.jsx)(i.CardFooter,{className:"justify-end",children:(0,r.jsxs)(d.Button,{onClick:g,disabled:x,children:[(0,r.jsx)("span",{children:"Transform"}),x?(0,r.jsx)(n.UiLoadingSpinner,{className:"size-4"}):(0,r.jsx)(a.ArrowRight,{})]})})]}),(0,r.jsxs)(i.Card,{children:[(0,r.jsxs)(i.CardHeader,{children:[(0,r.jsx)(i.CardTitle,{className:"text-2xl font-bold",children:"Transformed Request"}),(0,r.jsx)(i.CardDescription,{children:"How LiteLLM transforms your request for the specified provider."}),(0,r.jsx)("p",{className:"mt-2 text-xs text-muted-foreground",children:"Note: Sensitive headers are not shown."})]}),(0,r.jsx)(i.CardContent,{children:(0,r.jsxs)("div",{className:"relative rounded-md bg-muted",children:[(0,r.jsx)("pre",{className:"h-72 overflow-auto p-4 font-mono text-sm",children:p||`curl -X POST \\
|
||||
https://api.openai.com/v1/chat/completions \\
|
||||
-H 'Authorization: Bearer sk-xxx' \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a helpful assistant."
|
||||
}
|
||||
],
|
||||
"temperature": 0.7
|
||||
}'`}),(0,r.jsx)(d.Button,{variant:"ghost",size:"icon-sm","aria-label":"Copy to clipboard",className:"absolute top-2 right-2",onClick:()=>{navigator.clipboard.writeText(p||""),c.default.success("Copied to clipboard")},children:(0,r.jsx)(s.Copy,{})})]})})]})]}),(0,r.jsx)("div",{className:"mt-4 text-right",children:(0,r.jsxs)("p",{className:"text-sm text-muted-foreground",children:["Found an error? File an issue"," ",(0,r.jsx)("a",{className:"underline underline-offset-4",href:"https://github.com/BerriAI/litellm/issues",target:"_blank",rel:"noopener noreferrer",children:"here"}),"."]})})]})};var m=e.i(135214);e.s(["default",0,function(){let{accessToken:e}=(0,m.default)();return(0,r.jsx)(u,{accessToken:e})}],411929)}]);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue