Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .circleci/scripts/unit_selection.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ legacy_paths() {
echo tests/unit/proxy/test_update_spend.py
echo tests/unit/skills/test_skills_db.py ;;
proxy-db-endpoints-and-responses)
echo tests/unit/proxy/engine
echo tests/unit/proxy/lens
echo tests/unit/proxy/auth/test_models_fallback_endpoint.py
echo tests/unit/proxy/common_utils/test_check_batch_cost.py
echo tests/unit/proxy/common_utils/test_check_responses_cost.py
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/lens-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ on:
branches: [main, litellm_oss_branch, "litellm_**"]
paths:
- deploy/lens/**
- litellm/proxy/engine/**
- litellm/proxy/lens/**
- .github/workflows/lens-worker.yml
push:
branches: [main, litellm_agent_engine]
branches: [main]
paths:
- deploy/lens/**
- litellm/proxy/engine/**
- litellm/proxy/lens/**
- .github/workflows/lens-worker.yml
workflow_dispatch:

Expand Down Expand Up @@ -41,8 +41,8 @@ jobs:
--security-opt no-new-privileges --entrypoint python \
lens-worker:${{ github.sha }} -c '
import os
import engine.worker
from engine.trace_store import trace_store
import lens.worker
from lens.trace_store import trace_store
assert os.getuid() == 65532
with trace_store() as store:
assert store.count() == 0
Expand Down
4 changes: 2 additions & 2 deletions backend/routes/allowlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
# Spend / analytics
"/spend/",
"/analytics/",
"/engine/",
"/lens/",
"/v1/traces",
"/global/",
"/user_agent",
Expand Down Expand Up @@ -146,7 +146,7 @@
{
"/",
"/routes",
"/engine",
"/lens",
"/openapi.json",
"/docs",
"/docs/oauth2-redirect",
Expand Down
4 changes: 2 additions & 2 deletions deploy/lens/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FROM python:3.12-slim
WORKDIR /app
RUN pip install --no-cache-dir httpx==0.28.1 pydantic==2.11.7
COPY litellm/proxy/engine/__init__.py litellm/proxy/engine/models.py litellm/proxy/engine/trace_store.py litellm/proxy/engine/analysis.py litellm/proxy/engine/worker.py /app/engine/
COPY litellm/proxy/lens/__init__.py litellm/proxy/lens/models.py litellm/proxy/lens/trace_store.py litellm/proxy/lens/analysis.py litellm/proxy/lens/worker.py /app/lens/
USER 65532:65532
CMD ["python", "-m", "engine.worker"]
CMD ["python", "-m", "lens.worker"]
10 changes: 5 additions & 5 deletions deploy/lens/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
**
!litellm/
!litellm/proxy/
!litellm/proxy/engine/
!litellm/proxy/engine/__init__.py
!litellm/proxy/engine/models.py
!litellm/proxy/engine/analysis.py
!litellm/proxy/engine/worker.py
!litellm/proxy/lens/
!litellm/proxy/lens/__init__.py
!litellm/proxy/lens/models.py
!litellm/proxy/lens/analysis.py
!litellm/proxy/lens/worker.py
18 changes: 13 additions & 5 deletions deploy/lens/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ V1 requires ClickHouse for both sources. It does not reconstruct sessions from u
The UI and API use the same scan lifecycle. Authenticate with a proxy administrator credential for writes, or a proxy-admin viewer credential for reads. Worker credentials are only for worker operations

```bash
curl "$LITELLM_URL/engine" -H "Authorization: Bearer $LITELLM_API_KEY" \
curl "$LITELLM_URL/lens" -H "Authorization: Bearer $LITELLM_API_KEY" \
-H 'Content-Type: application/json' -d '{
"name": "Research quality", "model": "your-model-alias",
"context": "Answer the requested question using cited, retrieved evidence.",
Expand All @@ -83,14 +83,14 @@ curl "$LITELLM_URL/engine" -H "Authorization: Bearer $LITELLM_API_KEY" \
"enabled": true, "interval_minutes": 1440, "monthly_budget": 50
}'

curl "$LITELLM_URL/engine/$LENS_ID/runs" -X POST \
curl "$LITELLM_URL/lens/$LENS_ID/runs" -X POST \
-H "Authorization: Bearer $LITELLM_API_KEY" -H 'Content-Type: application/json' -d '{}'

curl "$LITELLM_URL/engine/$LENS_ID/runs?offset=0" -H "Authorization: Bearer $LITELLM_API_KEY"
curl "$LITELLM_URL/engine/$LENS_ID/runs/$BATCH_ID" -H "Authorization: Bearer $LITELLM_API_KEY"
curl "$LITELLM_URL/lens/$LENS_ID/runs?offset=0" -H "Authorization: Bearer $LITELLM_API_KEY"
curl "$LITELLM_URL/lens/$LENS_ID/runs/$BATCH_ID" -H "Authorization: Bearer $LITELLM_API_KEY"
```

Creation queues the first batch. Posting to `/engine/{id}/runs` queues another, or returns the existing active batch. The run response contains its ID under `jobs[0].id`. Poll the batch URL for status, findings and assessments. List responses omit large result payloads; request a batch to retrieve them. Supply an optional complete `settings` object on the runs POST for a one-off override; the saved lens stays unchanged. Selection accepts `team_id`, exact `filters`, and opaque `execution_ids` returned by `/engine/preview/sample`. Preview accepts `offset` and `as_of` to keep the time window fixed while paging. Feedback uses `PATCH /engine/{id}/findings/{finding_id}` with `status` and `reason`
Creation queues the first batch. Posting to `/lens/{id}/runs` queues another, or returns the existing active batch. The run response contains its ID under `jobs[0].id`. Poll the batch URL for status, findings and assessments. List responses omit large result payloads; request a batch to retrieve them. Supply an optional complete `settings` object on the runs POST for a one-off override; the saved lens stays unchanged. Selection accepts `team_id`, exact `filters`, and opaque `execution_ids` returned by `/lens/preview/sample`. Preview accepts `offset` and `as_of` to keep the time window fixed while paging. Feedback uses `PATCH /lens/{id}/findings/{finding_id}` with `status` and `reason`

## Quality evaluation

Expand All @@ -107,3 +107,11 @@ Set `LITELLM_API_KEY` privately. This makes paid model calls. Inspect missed and
The worker uses temporary disk space for trace content while reviewing it, and removes those files after each review. The Docker command supplies a writable temporary mount while keeping the application filesystem read-only

To check that accepted behavior stays accepted without hiding new problems, run the evaluator with `--dataset tests/proxy_behavior/lens/feedback_cases.json`. Reports include elapsed time, model call count, reported cost when the proxy provides it, missed checks, unexpected checks, and inconclusive candidates

## Upgrading from the original Lens API

The Lens API now uses `/lens` instead of `/engine`, list responses use `lenses`, and worker claims use `lens_id`. Upgrade the proxy and recreate every worker with the image shown by the upgraded dashboard before starting new scans. Update API clients to the new paths and response fields. Old worker images cannot poll the renamed API

Stop workers and let active scans finish before upgrading. Deploy proxy instances together: older proxies cannot use the renamed database tables. The schema migration renames the three Lens tables and the run-history identifier column in place, preserving saved investigations, findings, history, worker credentials, and billing assignments. Existing migration files retain their original names and checksums

Upgrades using `--use_prisma_db_push` stop before schema changes if any legacy Lens table exists, preventing Prisma from dropping saved data. Apply `litellm-proxy-extras/litellm_proxy_extras/migrations/20261001100000_rename_lens/migration.sql` to the configured database schema before retrying. Deployments already using migration history can instead start without `--use_prisma_db_push` to apply the shipped migration normally. Fresh databases and databases already using the renamed tables can continue using database push
2 changes: 1 addition & 1 deletion deploy/lens/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
lens-worker:
image: ${LENS_WORKER_IMAGE:-ghcr.io/berriai/litellm-lens-worker@sha256:c41e932eaf3e4efbcaf8cc5027c7e93021e5b2823f21cb8785cd107e37b91c9a}
image: ${LENS_WORKER_IMAGE:-ghcr.io/berriai/litellm-lens-worker@sha256:a8e8731d954916594eea462969946b9292fb771681ff515a9fd296b53f856c77}
environment:
LITELLM_URL: ${LITELLM_URL:?Set the URL reachable from this container}
LENS_WORKER_TOKEN: ${LENS_WORKER_TOKEN:?Create a worker credential in the Lens UI}
Expand Down
Binary file removed deploy/lens/screenshots/after.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/before.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/finding.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/progress.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/setup.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/trace.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/worker-billing-after.png
Binary file not shown.
Binary file removed deploy/lens/screenshots/worker-billing-before.png
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
DO $$
BEGIN
ALTER TABLE IF EXISTS "LiteLLM_Engine" RENAME TO "LiteLLM_Lens";
ALTER TABLE IF EXISTS "LiteLLM_EngineRun" RENAME TO "LiteLLM_LensRun";
ALTER TABLE IF EXISTS "LiteLLM_EngineWorker" RENAME TO "LiteLLM_LensWorker";
IF EXISTS (
SELECT 1 FROM pg_attribute
WHERE attrelid = to_regclass('"LiteLLM_LensRun"')
AND attname = 'engine_id' AND NOT attisdropped
) THEN
ALTER TABLE "LiteLLM_LensRun" RENAME COLUMN "engine_id" TO "lens_id";
END IF;
Comment thread
cursor[bot] marked this conversation as resolved.
ALTER INDEX IF EXISTS "LiteLLM_Engine_pkey" RENAME TO "LiteLLM_Lens_pkey";
ALTER INDEX IF EXISTS "LiteLLM_EngineRun_pkey" RENAME TO "LiteLLM_LensRun_pkey";
ALTER INDEX IF EXISTS "LiteLLM_EngineWorker_pkey" RENAME TO "LiteLLM_LensWorker_pkey";
ALTER INDEX IF EXISTS "LiteLLM_EngineWorker_token_hash_key" RENAME TO "LiteLLM_LensWorker_token_hash_key";
ALTER INDEX IF EXISTS "LiteLLM_EngineRun_engine_id_created_at_idx" RENAME TO "LiteLLM_LensRun_lens_id_created_at_idx";
END $$;
10 changes: 5 additions & 5 deletions litellm-proxy-extras/litellm_proxy_extras/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1895,22 +1895,22 @@ model LiteLLM_WorkflowMessage {
@@index([run_id])
}

model LiteLLM_Engine {
model LiteLLM_Lens {
id String @id
version Int @default(0)
data Json
}

model LiteLLM_EngineRun {
model LiteLLM_LensRun {
id String @id
engine_id String
lens_id String
created_at DateTime
data Json

@@index([engine_id, created_at])
@@index([lens_id, created_at])
}

model LiteLLM_EngineWorker {
model LiteLLM_LensWorker {
id String @id
token_hash String @unique
data Json
Expand Down
32 changes: 32 additions & 0 deletions litellm-proxy-extras/litellm_proxy_extras/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,36 @@ def _mark_migrations_applied(migrations_dir: str) -> None:
f"Failed to resolve migration {migration_name}: {e.stderr}"
)

@staticmethod
def raise_if_lens_rename_pending() -> None:
database_url: Final = os.environ.get("DATABASE_URL")
if not database_url:
return
try:
import psycopg
except ImportError as exc:
raise RuntimeError("Install psycopg to verify Lens data safety before prisma db push.") from exc
Comment thread
greptile-apps[bot] marked this conversation as resolved.
try:
with psycopg.connect(
ProxyExtrasDBManager._strip_prisma_query_params(database_url), connect_timeout=10, autocommit=True
) as connection:
legacy: Final = connection.execute(
"SELECT 1 FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace "
"WHERE n.nspname=%s AND c.relname IN ('LiteLLM_Engine', 'LiteLLM_EngineRun', 'LiteLLM_EngineWorker') "
"LIMIT 1",
(ProxyExtrasDBManager._prisma_schema_param(database_url) or "public",),
).fetchone()
except psycopg.Error as exc:
raise RuntimeError(
"Cannot verify Lens data safety; refusing prisma db push. Check database connectivity and psycopg installation."
) from exc
if legacy is not None:
raise RuntimeError(
"Legacy Lens tables exist. prisma db push would drop saved Lens data. "
"Apply the shipped 20261001100000_rename_lens migration to this database schema before retrying. "
"Deployments using migration history can upgrade without --use_prisma_db_push instead."
)

@staticmethod
def spend_logs_is_partitioned() -> bool:
"""True when the connected database's LiteLLM_SpendLogs is a
Expand Down Expand Up @@ -895,6 +925,7 @@ def _run_database_v2(
migrations_dir = ProxyExtrasDBManager._get_prisma_dir()

if not use_migrate:
ProxyExtrasDBManager.raise_if_lens_rename_pending()
if ProxyExtrasDBManager.spend_logs_is_partitioned():
raise RuntimeError(PARTITIONED_SPEND_LOGS_PUSH_ERROR)
original_dir = os.getcwd()
Expand Down Expand Up @@ -1398,6 +1429,7 @@ def _run_migrations(use_migrate: bool, use_v2_resolver: bool) -> bool:
if ProxyExtrasDBManager.spend_logs_is_partitioned():
raise RuntimeError(PARTITIONED_SPEND_LOGS_PUSH_ERROR)
# Use prisma db push with increased timeout
ProxyExtrasDBManager.raise_if_lens_rename_pending()
prisma_toolchain.run_prisma(
[_get_prisma_command(), "db", "push", "--accept-data-loss"],
timeout=prisma_command_timeout(),
Expand Down
4 changes: 4 additions & 0 deletions litellm-proxy-extras/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ version = "0.4.103"
description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package."
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"psycopg>=3.2,<4.0",
"psycopg-binary>=3.2,<4.0",
]
license = "MIT"
license-files = ["LICENSE"]
authors = [
Expand Down
42 changes: 21 additions & 21 deletions litellm/proxy/_lazy_openapi_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -34666,14 +34666,15 @@
]
}
},
"/engine/workers/register": {
"/guardrails/register": {
"post": {
"operationId": "register_worker_engine_workers_register_post",
"description": "Register a guardrail for onboarding (team submission).\n\nAccepts a guardrail config in the\n[Generic Guardrail API](https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api) format.\nThe submission is stored with status `pending_review` until an admin approves it.",
"operationId": "register_guardrail_guardrails_register_post_2",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkerName"
"$ref": "#/components/schemas/RegisterGuardrailRequest"
}
}
},
Expand All @@ -34684,7 +34685,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/WorkerCreated"
"$ref": "#/components/schemas/RegisterGuardrailResponse"
}
}
},
Expand All @@ -34706,21 +34707,21 @@
"APIKeyHeader": []
}
],
"summary": "Register Worker",
"summary": "Register Guardrail",
"tags": [
"mcp_discoverable"
]
}
},
"/guardrails/register": {
"/introspect": {
"post": {
"description": "Register a guardrail for onboarding (team submission).\n\nAccepts a guardrail config in the\n[Generic Guardrail API](https://docs.litellm.ai/docs/adding_provider/generic_guardrail_api) format.\nThe submission is stored with status `pending_review` until an admin approves it.",
"operationId": "register_guardrail_guardrails_register_post_2",
"description": "RFC 7662 introspection for gateway-issued session tokens (``llm_session_`` /\n``llm_srefresh_``), so an external gateway can validate them without the signing\nsecret. The caller authenticates with a LiteLLM virtual key (section 2.1, enforced by\nthe route dependency); any token the gateway cannot vouch for answers\n``{\"active\": false}`` with no further detail.",
"operationId": "introspect_endpoint_introspect_post",
"requestBody": {
"content": {
"application/json": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/RegisterGuardrailRequest"
"$ref": "#/components/schemas/Body_introspect_endpoint_introspect_post"
}
}
},
Expand All @@ -34730,9 +34731,7 @@
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RegisterGuardrailResponse"
}
"schema": {}
}
},
"description": "Successful Response"
Expand All @@ -34753,21 +34752,20 @@
"APIKeyHeader": []
}
],
"summary": "Register Guardrail",
"summary": "Introspect Endpoint",
"tags": [
"mcp_discoverable"
]
}
},
"/introspect": {
"/lens/workers/register": {
"post": {
"description": "RFC 7662 introspection for gateway-issued session tokens (``llm_session_`` /\n``llm_srefresh_``), so an external gateway can validate them without the signing\nsecret. The caller authenticates with a LiteLLM virtual key (section 2.1, enforced by\nthe route dependency); any token the gateway cannot vouch for answers\n``{\"active\": false}`` with no further detail.",
"operationId": "introspect_endpoint_introspect_post",
"operationId": "register_worker_lens_workers_register_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_introspect_endpoint_introspect_post"
"$ref": "#/components/schemas/WorkerName"
}
}
},
Expand All @@ -34777,7 +34775,9 @@
"200": {
"content": {
"application/json": {
"schema": {}
"schema": {
"$ref": "#/components/schemas/WorkerCreated"
}
}
},
"description": "Successful Response"
Expand All @@ -34798,7 +34798,7 @@
"APIKeyHeader": []
}
],
"summary": "Introspect Endpoint",
"summary": "Register Worker",
"tags": [
"mcp_discoverable"
]
Expand Down
20 changes: 10 additions & 10 deletions litellm/proxy/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,16 +520,16 @@ class LiteLLMRoutes(enum.Enum):
"/v1/rag/ingest",
"/rag/query",
"/v1/rag/query",
"/engine",
"/engine/{engine_id}",
"/engine/{engine_id}/runs",
"/engine/{engine_id}/runs/{job_id}",
"/engine/{engine_id}/executions/{execution_id}",
"/engine/{engine_id}/cancel",
"/engine/{engine_id}/findings/{finding_id}",
"/engine/preview/sample",
"/engine/workers/register",
"/engine/workers/{worker_id}",
"/lens",
"/lens/{lens_id}",
"/lens/{lens_id}/runs",
"/lens/{lens_id}/runs/{job_id}",
"/lens/{lens_id}/executions/{execution_id}",
"/lens/{lens_id}/cancel",
"/lens/{lens_id}/findings/{finding_id}",
"/lens/preview/sample",
"/lens/workers/register",
"/lens/workers/{worker_id}",
"/v1/traces",
"/v1/traces/{trace_id}",
"/v1/traces/{trace_id}/spans/{span_id}",
Expand Down
3 changes: 3 additions & 0 deletions litellm/proxy/db/prisma_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@ def setup_database(use_migrate: bool = False, use_v2_resolver: bool = False) ->
verbose_proxy_logger.error("\x1b[1;31mLiteLLM: Failed to import proxy extras. Got %s\x1b[0m", e)
return False

from litellm_proxy_extras.utils import ProxyExtrasDBManager

ProxyExtrasDBManager.raise_if_lens_rename_pending()
PrismaManager._raise_if_partitioned_spend_logs()
run_prisma(
[
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading
Loading