mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
* test(integration): streamed Bedrock Messages usage cost equals the recorded spend (Pylon #6667) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): echoed cost-map model info is not persisted as deployment overrides (Pylon #6844) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): reset sweep runs on one pod per tick while replicas share the lease (Pylon #6521) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): bedrock post-call guardrail scans streamed Anthropic Messages tool use without 500 (Pylon #6503) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): realtime cached audio tokens bill at the audio cache-read rate (Pylon #6704) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): legacy GET /spend/logs returns at most the 10000 most recent rows and flags truncation (Pylon #6752) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): itemize Responses API cache write tokens as cache creation cost (Pylon #6454) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): migration entrypoint deploys pending migrations before proxy startup (Pylon #6649) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): opted-in team keys stop at the owner's personal budget (Pylon #6641) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): guardrail information stays in the spend log when the caller sends metadata on /v1/messages (Pylon #6614) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): key model allowlist is enforced on Bedrock passthrough routes (Pylon #6419) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): JWT mapped key backfills a null user email from token claims (Pylon #6266) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): scheduled budget reset recovers from a transient DB transport failure (Pylon #6582) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): team key lists models granted through a team access group (Pylon #6044) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): JWT subject without team claim lands in the configured default team (Pylon #5895) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): end-user spend lands for a key without user_id when the auth cache is Redis (Pylon #6021) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): stale-low redis counter still blocks team member over budget (Pylon #5824) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): prompt-carrying spend rows are written in byte-bounded statements (Pylon #6083) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): logs UI session_total_spend sums every round of a multi-round session (Pylon #5928) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): config.yaml guardrails are served by the guardrail usage detail and overview (Pylon #5813) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): plain chat request skips the object permission lookup (Pylon #5965) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): register july accounting regression contracts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): isolate cost map override clear on owned proxy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): make reset lease claim and db relay refusal deterministic Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): poll pg_stat settle, bound unbanned relay refusals, clear reset lease on teardown Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): bound relay refusals so the budget sweep can reconnect Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: kerry <kerry@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
99 lines
4.3 KiB
Python
99 lines
4.3 KiB
Python
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import uuid
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Final
|
|
from urllib.parse import urlsplit, urlunsplit
|
|
|
|
import psycopg
|
|
import pytest
|
|
from integration._support.client import Gateway
|
|
from integration._support.process import owned_proxy
|
|
from psycopg import sql
|
|
from psycopg.rows import dict_row
|
|
|
|
REPO_ROOT: Final = Path(__file__).resolve().parents[3]
|
|
PRISMA_DIR: Final = REPO_ROOT / "litellm-proxy-extras" / "litellm_proxy_extras"
|
|
MISSING_MIGRATION: Final = "20260626120000_add_mcp_tool_search_enabled"
|
|
SHIPPED_MIGRATIONS: Final = tuple(sorted(path.name for path in (PRISMA_DIR / "migrations").iterdir() if path.is_dir()))
|
|
|
|
|
|
@contextmanager
|
|
def fresh_database() -> Iterator[str]:
|
|
name: Final = f"integration_upgrade_{uuid.uuid4().hex}"
|
|
admin_url: Final = os.environ["DATABASE_URL"]
|
|
parsed: Final = urlsplit(admin_url)
|
|
with psycopg.connect(admin_url, autocommit=True) as admin:
|
|
admin.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(name)))
|
|
try:
|
|
yield urlunsplit(parsed._replace(path=f"/{name}"))
|
|
finally:
|
|
admin.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(name)))
|
|
|
|
|
|
def deploy_older_schema(database_url: str, directory: Path) -> None:
|
|
older: Final = directory / "older-release"
|
|
(older / "migrations").mkdir(parents=True)
|
|
shutil.copy(PRISMA_DIR / "schema.prisma", older / "schema.prisma")
|
|
shutil.copy(PRISMA_DIR / "migrations" / "migration_lock.toml", older / "migrations" / "migration_lock.toml")
|
|
for name in (name for name in SHIPPED_MIGRATIONS if name < MISSING_MIGRATION):
|
|
shutil.copytree(PRISMA_DIR / "migrations" / name, older / "migrations" / name)
|
|
subprocess.run(
|
|
[sys.executable, "-I", "-m", "prisma", "migrate", "deploy", "--schema", str(older / "schema.prisma")],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
env={**os.environ, "DATABASE_URL": database_url},
|
|
)
|
|
|
|
|
|
def applied_migrations(database_url: str) -> tuple[str, ...]:
|
|
with psycopg.connect(database_url, row_factory=dict_row) as connection:
|
|
rows: Final = connection.execute(
|
|
'SELECT migration_name FROM "_prisma_migrations" '
|
|
"WHERE finished_at IS NOT NULL AND rolled_back_at IS NULL ORDER BY migration_name"
|
|
).fetchall()
|
|
return tuple(str(row["migration_name"]) for row in rows)
|
|
|
|
|
|
def object_permission_columns(database_url: str) -> tuple[str, ...]:
|
|
with psycopg.connect(database_url, row_factory=dict_row) as connection:
|
|
rows: Final = connection.execute(
|
|
"SELECT column_name FROM information_schema.columns "
|
|
"WHERE table_name = 'LiteLLM_ObjectPermissionTable' AND column_name = 'mcp_tool_search_enabled'"
|
|
).fetchall()
|
|
return tuple(str(row["column_name"]) for row in rows)
|
|
|
|
|
|
@pytest.mark.covers("other.database.migrations.entrypoint_deploys_pending_migrations_before_startup")
|
|
def test_migration_entrypoint_upgrades_an_older_schema_so_the_proxy_serves_mcp_tools(
|
|
gateway: Gateway, tmp_path: Path
|
|
) -> None:
|
|
with fresh_database() as database_url:
|
|
deploy_older_schema(database_url, tmp_path)
|
|
assert object_permission_columns(database_url) == ()
|
|
assert applied_migrations(database_url) == tuple(
|
|
name for name in SHIPPED_MIGRATIONS if name < MISSING_MIGRATION
|
|
)
|
|
entrypoint: Final = subprocess.run(
|
|
[sys.executable, "-I", "-m", "litellm.proxy.prisma_migration"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
cwd=REPO_ROOT,
|
|
env={**os.environ, "DATABASE_URL": database_url},
|
|
)
|
|
assert entrypoint.returncode == 0, entrypoint.stdout + entrypoint.stderr
|
|
assert object_permission_columns(database_url) == ("mcp_tool_search_enabled",), entrypoint.stdout
|
|
assert applied_migrations(database_url) == SHIPPED_MIGRATIONS, entrypoint.stdout
|
|
with owned_proxy(
|
|
gateway, tmp_path, {"DATABASE_URL": database_url, "DISABLE_SCHEMA_UPDATE": "true"}
|
|
) as upgraded:
|
|
tools: Final = upgraded.request("GET", "/mcp-rest/tools/list")
|
|
assert tools.status_code == 200, tools.text
|
|
assert tools.json()["tools"] == [], tools.text
|