Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
331e3f2
Surface per-member total spend in Teams > Members tab
ryan-crabbe-berri Apr 22, 2026
5e5a94a
Surface budget_reset_at on team info and members tab
ryan-crabbe-berri Apr 23, 2026
a23edd7
Restore Current Cycle Spend column on team members tab
ryan-crabbe-berri Apr 23, 2026
befddec
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
ryan-crabbe-berri Apr 23, 2026
ac453c9
fix(team): surface budget_reset_at on /team/info and cloned member bu…
ryan-crabbe-berri Apr 23, 2026
6be8313
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
ryan-crabbe-berri Apr 23, 2026
e9e86ed
Merge pull request #26298 from BerriAI/litellm_internal_staging
yuneng-berri Apr 23, 2026
c41567e
fix(budget_reset): use raw SQL for IS NOT NULL filter on Json? columns
yuneng-berri Apr 23, 2026
e37d1b0
[Fix] Deflake spend tracking tests
yuneng-berri Apr 23, 2026
8adb3a6
Apply suggestion from @greptile-apps[bot]
yuneng-berri Apr 23, 2026
4af2b67
[Fix] Drop orphan teardown step from Greptile merge
yuneng-berri Apr 23, 2026
c4ea0e9
fix: drain logging worker in test_router_caching_ttl to remove flake
yuneng-berri Apr 23, 2026
c14a73f
fix: make LoggingWorker.flush() wait for in-flight callbacks
yuneng-berri Apr 23, 2026
4a2deae
[Fix] Infra: grant contents:write to create-release-branch caller job
yuneng-berri Apr 23, 2026
9bdd447
Merge pull request #26355 from BerriAI/litellm_fixFlakyTpmRoutingTest
yuneng-berri Apr 23, 2026
1f6e018
Show absolute date in Budget Reset column
ryan-crabbe-berri Apr 23, 2026
b6d0f6b
fix(vertex_ai): use aiplatform.{geo}.rep.googleapis.com for multi-reg…
milan-berri Apr 23, 2026
bb94144
Merge pull request #26359 from BerriAI/litellm_fixCreateReleasePerms
shin-berri Apr 23, 2026
7c69262
Merge pull request #26349 from BerriAI/litellm_deflakeSpendTests
shin-berri Apr 23, 2026
2001d91
fix(mcp): share temporary MCP OAuth sessions across instances via Red…
milan-berri Apr 23, 2026
87e120d
Merge pull request #26346 from BerriAI/litellm_reset_budget_is_not_null
yuneng-berri Apr 23, 2026
ea626d9
Merge remote-tracking branch 'origin/litellm_internal_staging' into l…
ryan-crabbe-berri Apr 23, 2026
29e30d9
bump: version 1.83.12 β†’ 1.83.13
yuneng-berri Apr 23, 2026
ffaeff5
add uv
yuneng-berri Apr 24, 2026
fbaedc3
revert TeamInfo budget reset display changes
ryan-crabbe-berri Apr 24, 2026
6b6b8c7
restore budget_reset_at on TeamMembership type
ryan-crabbe-berri Apr 24, 2026
08cc1e6
Merge pull request #26207 from BerriAI/litellm_team_member_total_spen…
yuneng-berri Apr 24, 2026
1f6ce45
Merge pull request #26370 from BerriAI/litellm_version_bump
yuneng-berri Apr 24, 2026
fed1a14
chore(deps): bump vulnerable dependencies
stuxf Apr 23, 2026
1cc935e
chore(deps): bump Wolfi base digest to latest
stuxf Apr 24, 2026
5ba6bc0
chore(deps): bump uv to 0.11.7 + drop dead npm sed
stuxf Apr 24, 2026
4d74a30
chore(deps): fix brace-expansion pin and revert risky dev bumps
stuxf Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,19 @@ commands:
- wait_for_service:
url: tcp://localhost:5432
timeout: "60"
start_redis:
description: "Start a redis container on port 6379 and wait until it accepts connections. Use this to isolate a job from the shared remote Redis so concurrent CI pipelines don't contend for pod locks or buffer keys."
steps:
- run:
name: Start Redis
command: |
docker run -d \
--name redis-cache \
-p 6379:6379 \
redis:7-alpine@sha256:7aec734b2bb298a1d769fd8729f13b8514a41bf90fcdd1f38ec52267fbaa8ee6
- wait_for_service:
url: tcp://localhost:6379
timeout: "60"
setup_litellm_enterprise_pip:
steps:
- run:
Expand Down Expand Up @@ -1591,6 +1604,7 @@ jobs:
command: |
uv sync --frozen --all-groups --all-extras --python 3.12
- start_postgres
- start_redis
- attach_workspace:
at: ~/project
- run:
Expand All @@ -1600,15 +1614,18 @@ jobs:
docker images | grep litellm-docker-database
- run:
name: Run Docker container
# intentionally give bad redis credentials here
# the OTEL test - should get this as a trace
# Point the proxy at the job-local Redis (start_redis) instead of the
# shared remote Redis. The Redis transaction buffer uses a single
# global pod-lock key (cronjob_lock:db_spend_update_job) and a single
# global buffer list (litellm_spend_update_buffer); sharing those
# across concurrent CI pipelines causes spend flushes to stall or
# land in the wrong DB, which is what makes this test flaky.
command: |
docker run -d \
-p 4000:4000 \
-e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/circle_test \
-e REDIS_HOST=$REDIS_HOST \
-e REDIS_PASSWORD=$REDIS_PASSWORD \
-e REDIS_PORT=$REDIS_PORT \
-e REDIS_HOST=host.docker.internal \
-e REDIS_PORT=6379 \
-e LITELLM_MASTER_KEY="sk-1234" \
-e OPENAI_API_KEY=$OPENAI_API_KEY \
-e LITELLM_LICENSE=$LITELLM_LICENSE \
Expand Down Expand Up @@ -1638,12 +1655,14 @@ jobs:
command: |
uv run --no-sync python -m pytest -vv tests/spend_tracking_tests -x --junitxml=test-results/junit.xml --durations=5
no_output_timeout: 15m
# Clean up first container
- run:
name: Stop and remove first container
when: always
command: |
docker stop my-app
docker rm my-app
docker stop redis-cache
docker rm redis-cache

proxy_multi_instance_tests:
machine:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/create-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ jobs:
create-branch:
name: Create Release Branch
needs: release
permissions:
contents: write
uses: ./.github/workflows/create-release-branch.yml
with:
tag: ${{ inputs.tag }}
Expand Down
29 changes: 9 additions & 20 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Base image for building
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f

# Runtime image
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8

FROM $UV_IMAGE AS uvbin

Expand Down Expand Up @@ -69,25 +69,14 @@ FROM $LITELLM_RUNTIME_IMAGE AS runtime
USER root

RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile supervisor && \
npm install -g npm@11.12.1 tar@7.5.11 glob@11.1.0 @isaacs/brace-expansion@5.0.1 minimatch@10.2.4 diff@8.0.3 && \
npm install -g npm@11.12.1 tar@7.5.11 glob@13.0.6 @isaacs/brace-expansion@5.0.1 brace-expansion@5.0.5 minimatch@10.2.4 diff@8.0.3 picomatch@4.0.4 && \
GLOBAL="$(npm root -g)" && \
find "$GLOBAL/npm" -type d -name "tar" -path "*/node_modules/tar" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/tar" "$d"; \
for pkg in tar glob @isaacs/brace-expansion brace-expansion minimatch diff picomatch; do \
name="${pkg##*/}"; \
find "$GLOBAL/npm" -type d -name "$name" -path "*/node_modules/$pkg" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/$pkg" "$d"; \
done; \
done && \
find "$GLOBAL/npm" -type d -name "glob" -path "*/node_modules/glob" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/glob" "$d"; \
done && \
find "$GLOBAL/npm" -type d -name "brace-expansion" -path "*/node_modules/@isaacs/brace-expansion" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/@isaacs/brace-expansion" "$d"; \
done && \
find "$GLOBAL/npm" -type d -name "minimatch" -path "*/node_modules/minimatch" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/minimatch" "$d"; \
done && \
find "$GLOBAL/npm" -type d -name "diff" -path "*/node_modules/diff" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/diff" "$d"; \
done && \
find /usr/local/lib /usr/lib -path "*/node_modules/npm/package.json" -exec \
sed -i 's/"tar": "\^7\.5\.[0-9]*"/"tar": "^7.5.10"/g; s/"minimatch": "\^10\.[0-9.]*"/"minimatch": "^10.2.4"/g' {} + 2>/dev/null && \
npm cache clean --force && \
{ apk del --no-cache npm 2>/dev/null || true; }

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ARG LITELLM_BUILD_IMAGE=python:3.11-alpine@sha256:f07e2ace46f560f09a6eeec7b4913b

# Runtime image
ARG LITELLM_RUNTIME_IMAGE=python:3.11-alpine@sha256:f07e2ace46f560f09a6eeec7b4913b80ee99546e749ef82342a419a326620856
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8

FROM $UV_IMAGE AS uvbin

Expand Down
8 changes: 3 additions & 5 deletions docker/Dockerfile.database
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Base image for building
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f

# Runtime image
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8

FROM $UV_IMAGE AS uvbin

Expand Down Expand Up @@ -84,8 +84,6 @@ RUN apk add --no-cache bash openssl tzdata nodejs npm python3 libsndfile supervi
find "$GLOBAL/npm" -type d -name "diff" -path "*/node_modules/diff" | while read d; do \
rm -rf "$d" && cp -rL "$GLOBAL/diff" "$d"; \
done && \
find /usr/local/lib /usr/lib -path "*/node_modules/npm/package.json" -exec \
sed -i 's/"tar": "\^7\.5\.[0-9]*"/"tar": "^7.5.10"/g; s/"minimatch": "\^10\.[0-9.]*"/"minimatch": "^10.2.4"/g' {} + 2>/dev/null && \
npm cache clean --force && \
{ apk del --no-cache npm 2>/dev/null || true; }

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile.dev
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ ARG LITELLM_BUILD_IMAGE=python:3.13-slim@sha256:739e7213785e88c0f702dcdc12c0973a

# Runtime image
ARG LITELLM_RUNTIME_IMAGE=python:3.13-slim@sha256:739e7213785e88c0f702dcdc12c0973afcbd606dbf021a589cab77d6b00b579d
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8

FROM $UV_IMAGE AS uvbin

Expand Down
2 changes: 1 addition & 1 deletion docker/Dockerfile.health_check
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8
FROM $UV_IMAGE AS uvbin

FROM python:3.13-slim@sha256:739e7213785e88c0f702dcdc12c0973afcbd606dbf021a589cab77d6b00b579d
Expand Down
6 changes: 3 additions & 3 deletions docker/Dockerfile.non_root
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Base images
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:a5a619c1793039dcf92f02178f37c94bb3d6001403716da59d6092dfe8d9b502
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:f26d42a15d09d9a643b231df929fa3cf609bedc58a728eb445be89a9d8d1da9f
ARG PROXY_EXTRAS_SOURCE=published
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.10.9@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:733b4042187702f832f7fdecb3aff14a61b288c4ca37af188bb5715c1caebaf8

FROM $UV_IMAGE AS uvbin

Expand Down
2 changes: 1 addition & 1 deletion enterprise/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ requires = ["uv_build==0.10.7"]
build-backend = "uv_build"

[tool.uv]
required-version = "==0.10.9"
required-version = ">=0.10.9"

[tool.uv.build-backend]
module-root = ""
Expand Down
2 changes: 1 addition & 1 deletion litellm-proxy-extras/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ requires = ["uv_build==0.10.7"]
build-backend = "uv_build"

[tool.uv]
required-version = "==0.10.9"
required-version = ">=0.10.9"

[tool.uv.build-backend]
module-root = ""
Expand Down
12 changes: 9 additions & 3 deletions litellm/litellm_core_utils/logging_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,11 +370,17 @@ async def stop(self) -> None:
self._running_tasks.clear()

async def flush(self) -> None:
"""Flush the logging queue."""
"""Flush the logging queue.

Waits until every enqueued task has completed. ``queue.join()`` blocks
on the queue's unfinished-task counter (decremented by ``task_done()``),
so it correctly handles items that have been dequeued but whose
callback hasn't finished yet β€” ``queue.empty()`` would return True in
that window and cause us to skip the wait.
"""
if self._queue is None:
return
while not self._queue.empty():
await self._queue.join()
await self._queue.join()

async def clear_queue(self):
"""
Expand Down
6 changes: 6 additions & 0 deletions litellm/llms/vertex_ai/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,19 @@ def get_vertex_base_url(
) -> str:
"""
Get the base URL for Vertex AI API calls.

- ``global`` uses the global control plane host.
- Multi-region geographies (e.g. ``us``, ``eu``) use ``aiplatform.{geo}.rep.googleapis.com``.
- Regional locations (e.g. ``us-central1``) use ``{region}-aiplatform.googleapis.com``.
"""
if vertex_location == "global":
return "https://aiplatform.googleapis.com"
if vertex_location is None:
raise ValueError("vertex_location is required")
if not re.match(r"^[a-z][a-z0-9-]*$", vertex_location):
raise ValueError("Invalid vertex_location format")
if "-" not in vertex_location:
return f"https://aiplatform.{vertex_location}.rep.googleapis.com"
return f"https://{vertex_location}-aiplatform.googleapis.com"


Expand Down
2 changes: 1 addition & 1 deletion litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3907,7 +3907,7 @@ class OrganizationMemberUpdateResponse(MemberUpdateResponse):


class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable):
team_member_budget_table: Optional[LiteLLM_BudgetTable] = None
team_member_budget_table: Optional[LiteLLM_BudgetTableFull] = None
# Resources inherited from access groups (separate from direct assignments)
access_group_models: Optional[List[str]] = None
access_group_mcp_server_ids: Optional[List[str]] = None
Expand Down
34 changes: 20 additions & 14 deletions litellm/proxy/common_utils/reset_budget_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,28 +632,35 @@ async def reset_budget_windows(self) -> None:

now = datetime.utcnow()

# Note on raw SQL: prisma-client-python does not support null-filtering
# on `Json?` columns (no DbNull/JsonNull sentinel β€” see
# RobertCraigie/prisma-client-py#714). We use `query_raw` with
# `IS NOT NULL` so we don't materialize every key/team row on each
# tick of the reset job. Writes still go through the ORM.

# --- Keys ---
try:
all_keys = await self.prisma_client.db.litellm_verificationtoken.find_many(
where={"budget_limits": {"not": None}} # type: ignore[arg-type]
key_rows = await self.prisma_client.db.query_raw(
'SELECT token, budget_limits FROM "LiteLLM_VerificationToken" '
"WHERE budget_limits IS NOT NULL"
)
for key in all_keys:
raw = key.budget_limits # type: ignore[attr-defined]
for row in key_rows:
raw = row["budget_limits"]
if not raw:
continue
windows: list = raw if isinstance(raw, list) else json.loads(raw)
changed = False
for window in windows:
counter_key = (
f"spend:key:{key.token}:window:{window['budget_duration']}"
f"spend:key:{row['token']}:window:{window['budget_duration']}"
)
if await ResetBudgetJob._reset_expired_window(
window, counter_key, spend_counter_cache, now
):
changed = True
if changed:
await self.prisma_client.db.litellm_verificationtoken.update(
where={"token": key.token},
where={"token": row["token"]},
data={"budget_limits": json.dumps(windows)}, # type: ignore[arg-type]
)
except Exception as e:
Expand All @@ -663,26 +670,25 @@ async def reset_budget_windows(self) -> None:

# --- Teams ---
try:
all_teams = await self.prisma_client.db.litellm_teamtable.find_many(
where={"budget_limits": {"not": None}} # type: ignore[arg-type]
team_rows = await self.prisma_client.db.query_raw(
'SELECT team_id, budget_limits FROM "LiteLLM_TeamTable" '
"WHERE budget_limits IS NOT NULL"
)
for team in all_teams:
raw = team.budget_limits # type: ignore[attr-defined]
for row in team_rows:
raw = row["budget_limits"]
if not raw:
continue
windows = raw if isinstance(raw, list) else json.loads(raw)
changed = False
for window in windows:
counter_key = (
f"spend:team:{team.team_id}:window:{window['budget_duration']}"
)
counter_key = f"spend:team:{row['team_id']}:window:{window['budget_duration']}"
if await ResetBudgetJob._reset_expired_window(
window, counter_key, spend_counter_cache, now
):
changed = True
if changed:
await self.prisma_client.db.litellm_teamtable.update(
where={"team_id": team.team_id},
where={"team_id": row["team_id"]},
data={"budget_limits": json.dumps(windows)}, # type: ignore[arg-type]
)
except Exception as e:
Expand Down
Loading
Loading