Repository navigation
feat(memory): add automatic gateway memory with admin policies #40844
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
14d03e6
feat(memory): add policy-controlled gateway memory and forwarding pilot
moe-berri d5dea2b
fix(memory): use writable Rust toolchain directories on Render
moe-berri 5c0fd2d
fix(memory): include database runtime in pilot build
moe-berri 142088d
fix(memory): isolate request accounting and support split deployments
moe-berri fdde260
fix(memory): invalidate worker hints and bound pilot validation
moe-berri c955df5
fix(memory): share activation hints through coordination cache
moe-berri 658c362
fix(memory): preserve client network context for internal calls
moe-berri ecf23d7
fix(memory): enforce an atomic namespace storage limit
moe-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # Memory gateway pilot on Render | ||
|
|
||
| Run this branch as an isolated forwarding gateway. Colleagues keep their existing | ||
| upstream LiteLLM key and model name and change only their gateway base URL. They | ||
| need no plugin or client-side memory tools. Choosing the pilot URL opts them into | ||
| the pilot; returning to the original URL stops using and collecting pilot memory. | ||
|
|
||
| Every preparation and answer call uses that caller's upstream key. The upstream | ||
| gateway continues to enforce its model permissions, budgets, rate limits, and | ||
| guardrails. The pilot checks the key against the upstream model catalog, then | ||
| registers its hash as a local virtual key so LiteLLM's normal authentication and | ||
| memory authorization still apply. No upstream key or provider credential is | ||
| configured on Render. The administrator credential belongs only to this pilot. | ||
|
|
||
| The forwarding pilot isolates memories by virtual key. Upstream management APIs | ||
| may deny ordinary keys access to user/team/org details, so the pilot does not | ||
| infer those identities from client metadata. Install the feature directly in an | ||
| organization's gateway to use its existing user/team/project/org policies. | ||
| Never connect this pilot to an older gateway's production database. | ||
|
|
||
| ## Create the service | ||
|
|
||
| 1. Create a separate Render Postgres 16 database in the same region as the web | ||
| service. Restrict public database access; use its internal connection URL. | ||
| 2. Create a Python web service from this repository and the Memory V2 branch. | ||
| A Standard service and the smallest paid Postgres plan are sufficient starting | ||
| points for a small pilot. They incur Render hosting charges. | ||
| 3. Set the build command to `bash deploy/memory-pilot/build.sh`, the start command | ||
| to `bash deploy/memory-pilot/start.sh`, and the health path to | ||
| `/health/readiness`. The build includes the dashboard from this branch. | ||
| 4. Set these environment variables in Render: | ||
|
|
||
| | Variable | Value | | ||
| | --- | --- | | ||
| | `DATABASE_URL` | The new database's internal connection URL | | ||
| | `UPSTREAM_LITELLM_BASE_URL` | Your original gateway URL, without `/v1` | | ||
| | `LITELLM_MASTER_KEY` | A new random `sk-` administrator key | | ||
| | `LITELLM_SALT_KEY` | A separate random encryption secret; preserve it across deploys | | ||
| | `PYTHON_VERSION` | `3.12.14` | | ||
| | `NODE_VERSION` | `24.19.0` | | ||
| | `NEXT_TELEMETRY_DISABLED` | `1` | | ||
| | `PORT` | `4000` | | ||
|
|
||
| 5. After deployment, open `/ui/memory`, sign in as `admin` using the pilot's master | ||
| key, and save a policy for **Whole gateway**, **Enabled automatically**, | ||
| **Private to each virtual key**. This policy persists across restarts. Memory | ||
| stays disabled until an administrator enables it. | ||
|
|
||
| Equivalent activation through the API, with secrets supplied in shell variables: | ||
|
|
||
| ```bash | ||
| curl --fail-with-body "$PILOT_URL/v2/memory/policies" \ | ||
| -H "Authorization: Bearer $PILOT_ADMIN_KEY" \ | ||
| -H 'Content-Type: application/json' \ | ||
| -X PUT \ | ||
| -d '{"target_type":"gateway","target_id":"*","activation":"automatic","scope":"key"}' | ||
| ``` | ||
|
|
||
| Administrators can instead require opt-in, disable a particular registered key, | ||
| or disable the whole gateway. Under an opt-in policy, callers set their preference | ||
| with `PUT /v2/memory/preference` and `{"enabled":true}` using their own key. | ||
|
|
||
| ## Try it | ||
|
|
||
| Set an OpenAI-compatible client's base URL to `https://YOUR-SERVICE.onrender.com/v1`. | ||
| For Claude Code, set `ANTHROPIC_BASE_URL` to `https://YOUR-SERVICE.onrender.com`. | ||
| Retain the same gateway key and model setting. | ||
|
|
||
| In one conversation, say “Remember that my demo project is Cobalt Heron and its | ||
| staging port is 8347.” In a **new conversation**, ask “What is my demo project and | ||
| its staging port?” Check actual saved entries with `GET /v2/memory/entries` using | ||
| the same key. An unrelated key must not see them. Administrators can inspect, | ||
| correct, or delete entries in Memory; callers can use the self-service API. | ||
|
|
||
| ## Behavior and limits | ||
|
|
||
| - Supported surfaces: Chat Completions, Responses, and Anthropic Messages, | ||
| including their native streaming responses and client tool continuation. | ||
| - The selected model must support function calling. Memory preparation adds up | ||
| to three billed model calls before the visible answer, with a 60-second bound. | ||
| It uses the original conversation, so long coding sessions can add substantial | ||
| prompt-token usage and latency. Existing upstream quotas apply to these calls. | ||
| - Preparation stores durable facts supported by the conversation, then searches | ||
| and reads relevant entries. Search is bounded keyword matching in Postgres. | ||
| There is no vector database, extraction model, scheduler, or nightly process. | ||
| - On gateway/backend deployments without shared Redis, first-time activation | ||
| can take up to 30 seconds to reach another process. Policy revocation is | ||
| checked against the primary database before memory operations. | ||
| - Each memory scope can hold up to 1,000 entries. Creation checks this limit | ||
| atomically; correction and deletion remain available when the scope is full. | ||
| - Stored references are untrusted data. They cannot grant API permissions or | ||
| change the namespace derived from authentication. Current user corrections | ||
| take precedence. Replacements require the current revision. | ||
| - Memory/model preparation errors fail the request rather than silently claiming | ||
| successful memory. Administrators can disable memory to restore ordinary calls. | ||
| - Switching away or disabling memory stops automatic use; it does not delete | ||
| existing entries. Delete memories explicitly through Memory or the API. | ||
| - Shared upstream keys share a pilot namespace. Give each person a distinct key | ||
| when their memories must be private from each other. | ||
| - Other API surfaces are outside this forwarding pilot. Use the original gateway | ||
| for embeddings, images, realtime, batches, and administration. | ||
|
moe-berri marked this conversation as resolved.
Resolved
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| # Render's preinstalled Rust toolchain directory is read-only. Maturin needs | ||
| # writable homes to install the repository's pinned toolchain during uv sync. | ||
| export RUSTUP_HOME="$PWD/.memory-pilot-rustup" | ||
| export CARGO_HOME="$PWD/.memory-pilot-cargo" | ||
| export PRISMA_BINARY_CACHE_DIR="$PWD/.memory-pilot-prisma" | ||
| python -m pip install uv==0.11.7 | ||
| export UV_PROJECT_ENVIRONMENT="$PWD/.memory-pilot-venv" | ||
| uv sync --frozen --extra proxy --extra extra_proxy --no-default-groups | ||
| export PATH="$UV_PROJECT_ENVIRONMENT/bin:$PATH" | ||
| prisma generate --schema schema.prisma | ||
| ( | ||
| cd ui/litellm-dashboard | ||
| npm ci | ||
| npm run build | ||
| ) | ||
| rm -rf litellm/proxy/_experimental/out | ||
| cp -R ui/litellm-dashboard/out litellm/proxy/_experimental/out |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| from pilot import forward_credential as forward_credential |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| """An isolated office pilot that preserves upstream gateway credentials.""" | ||
|
|
||
| import asyncio | ||
| import hashlib | ||
| import os | ||
| import secrets | ||
| from contextvars import ContextVar | ||
| from typing import Final | ||
|
|
||
| import httpx | ||
| from starlette.exceptions import HTTPException | ||
| from starlette.requests import Request | ||
| from starlette.responses import JSONResponse | ||
| from starlette.types import ASGIApp, Receive, Scope, Send | ||
|
|
||
| from litellm.caching.caching import DualCache | ||
| from litellm.integrations.custom_logger import CustomLogger | ||
| from litellm.llms.custom_httpx.http_handler import get_async_httpx_client | ||
| from litellm.proxy._types import UI_TEAM_ID, UserAPIKeyAuth | ||
| from litellm.proxy.auth.auth_checks import ExperimentalUIJWTToken | ||
| from litellm.proxy.auth.user_api_key_auth import _get_bearer_token_or_received_api_key | ||
| from litellm.repositories.verification_token_repository import VerificationTokenRepository | ||
| from litellm.types.llms.custom_http import httpxSpecialProvider | ||
| from litellm.types.utils import CallTypesLiteral | ||
|
|
||
| _UPSTREAM: Final = os.environ["UPSTREAM_LITELLM_BASE_URL"].rstrip("/") | ||
| _CREDENTIAL: Final[ContextVar[str | None]] = ContextVar("memory_pilot_credential", default=None) | ||
| _INFERENCE: Final = frozenset( | ||
| ("/chat/completions", "/v1/chat/completions", "/responses", "/v1/responses", "/v1/messages") | ||
| ) | ||
| _SELF_SERVICE: Final = frozenset(("/v2/memory/status", "/v2/memory/preference", "/v2/memory/entries")) | ||
|
|
||
|
|
||
| class ForwardCredential(CustomLogger): | ||
| async def async_pre_call_hook( | ||
| self, user_api_key_dict: UserAPIKeyAuth, cache: DualCache, data: dict[str, object], call_type: CallTypesLiteral | ||
| ) -> dict[str, object]: | ||
| credential: Final = _CREDENTIAL.get() | ||
| if credential is None: | ||
| raise HTTPException(status_code=403, detail="Use your upstream gateway key for model calls") | ||
| return {**data, "api_key": credential, "api_base": _UPSTREAM} | ||
|
|
||
|
|
||
| forward_credential: Final = ForwardCredential() | ||
|
|
||
|
|
||
| class PilotGateway: | ||
| def __init__(self, app: ASGIApp) -> None: | ||
| self.app = app | ||
| self.validation_slots = asyncio.Semaphore(16) | ||
| self.upstream = get_async_httpx_client( | ||
| httpxSpecialProvider.PassThroughEndpoint, | ||
| params={"timeout": 20, "client_alias": "memory-pilot-upstream"}, | ||
| ) | ||
|
|
||
| async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: | ||
| if scope["type"] == "lifespan": | ||
| try: | ||
| await self.app(scope, receive, send) | ||
| finally: | ||
| await self.upstream.close() | ||
| return | ||
| if scope["type"] != "http": | ||
| await self.app(scope, receive, send) | ||
| return | ||
| request: Final = Request(scope, receive) | ||
| credential: Final = _get_bearer_token_or_received_api_key( | ||
| request.headers.get("x-litellm-api-key") | ||
| or request.headers.get("authorization") | ||
| or request.headers.get("x-api-key") | ||
| or "" | ||
| ) | ||
| from litellm.proxy.proxy_server import master_key, prisma_client | ||
|
|
||
| if not credential or master_key and secrets.compare_digest(credential, master_key): | ||
| await self.app(scope, receive, send) | ||
| return | ||
| if prisma_client is None: | ||
| await JSONResponse({"error": "Pilot database unavailable"}, status_code=503)(scope, receive, send) | ||
| return | ||
| digest: Final = hashlib.sha256(credential.encode()).hexdigest() | ||
| tokens: Final = VerificationTokenRepository(prisma_client) | ||
| local_key: Final = await tokens.find_by_id(digest) | ||
| if local_key and local_key.team_id == UI_TEAM_ID: | ||
| await self.app(scope, receive, send) | ||
| return | ||
| if ( | ||
| not credential.startswith("sk-") | ||
| and ExperimentalUIJWTToken.get_key_object_from_ui_hash_key(credential) is not None | ||
| ): | ||
| await self.app(scope, receive, send) | ||
| return | ||
| path: Final = request.url.path.rstrip("/") | ||
| if path not in _INFERENCE | _SELF_SERVICE | {"/models", "/v1/models"} and not path.startswith( | ||
| "/v2/memory/entries/" | ||
| ): | ||
| await JSONResponse( | ||
| {"error": "Upstream keys can only use inference and their own memories"}, status_code=403 | ||
| )(scope, receive, send) | ||
| return | ||
| try: | ||
| await asyncio.wait_for(self.validation_slots.acquire(), timeout=0.05) | ||
| except TimeoutError: | ||
| await JSONResponse( | ||
| {"error": "Pilot credential validation is busy; retry shortly"}, | ||
| status_code=503, | ||
| headers={"Retry-After": "1"}, | ||
| )(scope, receive, send) | ||
| return | ||
| try: | ||
| models: Final = await self.upstream.get( | ||
|
veria-ai[bot] marked this conversation as resolved.
Resolved
|
||
| _UPSTREAM + "/v1/models", headers={"Authorization": "Bearer " + credential} | ||
| ) | ||
| except httpx.HTTPError: | ||
| await JSONResponse({"error": "Upstream gateway unavailable"}, status_code=503)(scope, receive, send) | ||
| return | ||
| finally: | ||
| self.validation_slots.release() | ||
| if models.is_error: | ||
| await JSONResponse({"error": "Upstream gateway rejected this key"}, status_code=models.status_code)( | ||
| scope, receive, send | ||
| ) | ||
| return | ||
| if path in ("/models", "/v1/models"): | ||
|
moe-berri marked this conversation as resolved.
Resolved
|
||
| await JSONResponse(models.json())(scope, receive, send) | ||
| return | ||
| await tokens.table.upsert( | ||
| where={"token": digest}, | ||
| data={ | ||
| "create": {"token": digest, "models": [], "key_alias": "Memory pilot " + digest[:8]}, | ||
| "update": {}, | ||
| }, | ||
| ) | ||
| token: Final = _CREDENTIAL.set(credential) | ||
| try: | ||
| await self.app(scope, receive, send) | ||
| finally: | ||
| _CREDENTIAL.reset(token) | ||
|
moe-berri marked this conversation as resolved.
Resolved
|
||
|
|
||
|
|
||
| def create_app() -> PilotGateway: | ||
| from litellm.proxy.proxy_server import app | ||
|
|
||
| return PilotGateway(app) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| model_list: | ||
| - model_name: "*" | ||
| litellm_params: | ||
| model: litellm_proxy/* | ||
| api_base: os.environ/UPSTREAM_LITELLM_BASE_URL | ||
| model_info: | ||
| supports_function_calling: true | ||
| litellm_settings: | ||
| callbacks: | ||
| - hooks.forward_credential | ||
| drop_params: true | ||
| turn_off_message_logging: true | ||
| general_settings: | ||
| master_key: os.environ/LITELLM_MASTER_KEY | ||
| store_model_in_db: true |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| export PATH="$PWD/.memory-pilot-venv/bin:$PATH" | ||
| export PRISMA_BINARY_CACHE_DIR="$PWD/.memory-pilot-prisma" | ||
| export PRISMA_CLI_PATH="$PRISMA_BINARY_CACHE_DIR/node_modules/.bin/prisma" | ||
| prisma migrate deploy --schema litellm-proxy-extras/litellm_proxy_extras/schema.prisma | ||
| export WORKER_CONFIG="$PWD/deploy/memory-pilot/proxy_config.yaml" | ||
| export PYTHONPATH="$PWD/deploy/memory-pilot${PYTHONPATH:+:$PYTHONPATH}" | ||
| exec uvicorn pilot:create_app --factory --host 0.0.0.0 --port "${PORT:-4000}" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
...oxy-extras/litellm_proxy_extras/migrations/20260912000000_gateway_memory_v2/migration.sql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| ALTER TABLE "LiteLLM_MemoryTable" ADD COLUMN IF NOT EXISTS "namespace" TEXT; | ||
|
|
||
| CREATE INDEX IF NOT EXISTS "LiteLLM_MemoryTable_namespace_updated_at_idx" | ||
| ON "LiteLLM_MemoryTable"("namespace", "updated_at"); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS "LiteLLM_MemoryPolicy" ( | ||
| "policy_id" TEXT NOT NULL, | ||
| "target_type" TEXT NOT NULL, | ||
| "target_id" TEXT NOT NULL, | ||
| "activation" TEXT NOT NULL, | ||
| "scope" TEXT NOT NULL, | ||
| "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| "updated_by" TEXT NOT NULL, | ||
| CONSTRAINT "LiteLLM_MemoryPolicy_pkey" PRIMARY KEY ("policy_id") | ||
| ); | ||
|
|
||
| CREATE UNIQUE INDEX IF NOT EXISTS "LiteLLM_MemoryPolicy_target_type_target_id_key" | ||
| ON "LiteLLM_MemoryPolicy"("target_type", "target_id"); | ||
|
|
||
| CREATE TABLE IF NOT EXISTS "LiteLLM_MemoryPreference" ( | ||
| "subject" TEXT NOT NULL, | ||
| "enabled" BOOLEAN NOT NULL DEFAULT false, | ||
| "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| CONSTRAINT "LiteLLM_MemoryPreference_pkey" PRIMARY KEY ("subject") | ||
| ); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.