gateway: let member keys reach the Metis pass-through route
LiteLLM gates auth=true pass-through routes behind allowed_passthrough_routes, a LiteLLM Enterprise field: on this community-edition proxy the management API refuses to set it, so no member key can ever be granted access, and auth:false would instead expose the route (and our GreenPT key) to anonymous callers. Add patch_passthrough_key_access.py (house style: build-time, non-fatal, idempotent, compile-checked) so routes the operator names in general_settings.coop_passthrough_open_to_keys are callable by ANY authenticated key. Authentication itself is unchanged: auth=true still installs Depends(user_api_key_auth), so unauthenticated requests are still rejected. Validated locally against LiteLLM 1.103.2: a plain generated key gets 200 with real Metis answers; an unauthenticated POST gets 401.
This commit is contained in:
1 parent
e4e70962ec
commit
557999a16f
3 files changed
+127
No files matched your search
+10
@@ -66,6 +66,16 @@ RUN python3 /app/code/patch_strip_provider_headers.py
|
|||||||
COPY patch_expose_cost_headers.py /app/code/patch_expose_cost_headers.py
|
COPY patch_expose_cost_headers.py /app/code/patch_expose_cost_headers.py
|
||||||
RUN python3 /app/code/patch_expose_cost_headers.py
|
RUN python3 /app/code/patch_expose_cost_headers.py
|
||||||
|
|
||||||
|
# Member access to config-declared pass-through routes: LiteLLM gates auth=true
|
||||||
|
# pass-through endpoints behind `allowed_passthrough_routes`, an Enterprise-only
|
||||||
|
# field that cannot be set on this community-edition proxy. This patch lets any
|
||||||
|
# authenticated key call routes the operator lists in
|
||||||
|
# general_settings.coop_passthrough_open_to_keys (unauthenticated access is
|
||||||
|
# unaffected — still rejected). Non-fatal; check the "[coop-patch] applied" line
|
||||||
|
# in the build log on every LiteLLM upgrade.
|
||||||
|
COPY patch_passthrough_key_access.py /app/code/patch_passthrough_key_access.py
|
||||||
|
RUN python3 /app/code/patch_passthrough_key_access.py
|
||||||
|
|
||||||
# Audio keep-alive: scheduled pings keep the Tinfoil TEE audio enclave
|
# Audio keep-alive: scheduled pings keep the Tinfoil TEE audio enclave
|
||||||
# warm so voice mode skips the ~24s cold-start (see audio_keepalive.py header).
|
# warm so voice mode skips the ~24s cold-start (see audio_keepalive.py header).
|
||||||
COPY audio_keepalive.py /app/code/audio_keepalive.py
|
COPY audio_keepalive.py /app/code/audio_keepalive.py
|
||||||
|
|||||||
+10
@@ -251,6 +251,16 @@ general_settings:
|
|||||||
# per-token billing (reading usage.input_tokens from the Metis response) would
|
# per-token billing (reading usage.input_tokens from the Metis response) would
|
||||||
# need a patch in the house style of the existing patch_*.py files; until then
|
# need a patch in the house style of the existing patch_*.py files; until then
|
||||||
# treat the spend figure for this route as an approximation, not an invoice.
|
# treat the spend figure for this route as an approximation, not an invoice.
|
||||||
|
# Routes here (matched by prefix) may be called by ANY authenticated member
|
||||||
|
# key. LiteLLM's own gate for auth=true pass-through routes
|
||||||
|
# (`allowed_passthrough_routes`) is an Enterprise-only field, so on this
|
||||||
|
# community-edition proxy it can never be granted to member keys — the only
|
||||||
|
# other option, auth:false, would expose the route publicly and forward the
|
||||||
|
# co-op's GreenPT key to anonymous callers. This list is consumed at build
|
||||||
|
# time by patch_passthrough_key_access.py; authentication is unchanged (the
|
||||||
|
# endpoint still requires a valid key).
|
||||||
|
coop_passthrough_open_to_keys:
|
||||||
|
- "/v1/systemone"
|
||||||
pass_through_endpoints:
|
pass_through_endpoints:
|
||||||
- path: "/v1/systemone"
|
- path: "/v1/systemone"
|
||||||
target: "https://api.greenpt.ai/v1/systemone"
|
target: "https://api.greenpt.ai/v1/systemone"
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Open explicitly-listed pass-through routes to any authenticated key.
|
||||||
|
|
||||||
|
WHY. Config-declared pass-through endpoints registered with ``auth: true`` are
|
||||||
|
gated at request time by ``allowed_passthrough_routes`` on the key or team
|
||||||
|
(``RouteChecks.check_passthrough_route_access``). That field is a **LiteLLM
|
||||||
|
Enterprise** feature: on a community-edition proxy the management API refuses to
|
||||||
|
set it ("This feature is only available for LiteLLM Enterprise users"), so a
|
||||||
|
member key can never be granted access by any supported route. The only other
|
||||||
|
lever is ``auth: false``, which forwards **unauthenticated** requests — a public
|
||||||
|
relay of the co-op's GreenPT key. Neither is acceptable.
|
||||||
|
|
||||||
|
This patch lets the operator name such routes in the proxy config:
|
||||||
|
|
||||||
|
general_settings:
|
||||||
|
coop_passthrough_open_to_keys: ["/v1/systemone"]
|
||||||
|
|
||||||
|
Any route listed there is allowed for any key that has already authenticated.
|
||||||
|
Authentication itself is untouched: ``auth: true`` still installs
|
||||||
|
``Depends(user_api_key_auth)``, so an unauthenticated request is rejected earlier
|
||||||
|
and never reaches this function. The patch only relaxes the *second* gate
|
||||||
|
(per-key allow-list), and only for routes the operator listed by hand.
|
||||||
|
|
||||||
|
House style: run at BUILD time (the runtime rootfs is read-only under Cloudron);
|
||||||
|
non-fatal — if upstream renames or restructures this function, WARN and exit 0;
|
||||||
|
idempotent — detect the marker on a rebuild and skip; and compile the patched
|
||||||
|
source in memory before writing, so a patch that cannot import never ships.
|
||||||
|
|
||||||
|
Check the ``[coop-patch] applied`` line in the build log on every LiteLLM bump.
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MARKER = "coop_passthrough_open_to_keys"
|
||||||
|
|
||||||
|
# Unique multi-line anchor: the def + docstring + the first statement of the body.
|
||||||
|
OLD = (
|
||||||
|
" def check_passthrough_route_access(route: str, user_api_key_dict: UserAPIKeyAuth) -> bool:\n"
|
||||||
|
" \"\"\"\n"
|
||||||
|
" Check if route is a passthrough route.\n"
|
||||||
|
" Supports both exact match and prefix match.\n"
|
||||||
|
" \"\"\"\n"
|
||||||
|
" metadata: Final = user_api_key_dict.metadata\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
NEW = (
|
||||||
|
" def check_passthrough_route_access(route: str, user_api_key_dict: UserAPIKeyAuth) -> bool:\n"
|
||||||
|
" \"\"\"\n"
|
||||||
|
" Check if route is a passthrough route.\n"
|
||||||
|
" Supports both exact match and prefix match.\n"
|
||||||
|
" \"\"\"\n"
|
||||||
|
" # [coop-patch] Routes the operator lists in\n"
|
||||||
|
" # general_settings.coop_passthrough_open_to_keys are callable by ANY\n"
|
||||||
|
" # authenticated key. LiteLLM's own gate (allowed_passthrough_routes)\n"
|
||||||
|
" # is Enterprise-only, so it can never be granted on this deployment.\n"
|
||||||
|
" # Authentication is unaffected: auth=True on the endpoint still\n"
|
||||||
|
" # requires a valid key before this function runs.\n"
|
||||||
|
" try:\n"
|
||||||
|
" from litellm.proxy.proxy_server import general_settings as _coop_general_settings\n"
|
||||||
|
"\n"
|
||||||
|
" _coop_open_to_keys = (_coop_general_settings or {}).get(\"coop_passthrough_open_to_keys\") or []\n"
|
||||||
|
" for _coop_route in _coop_open_to_keys:\n"
|
||||||
|
" if RouteChecks._route_matches_allowed_route(route=route, allowed_route=_coop_route):\n"
|
||||||
|
" return True\n"
|
||||||
|
" except Exception:\n"
|
||||||
|
" pass\n"
|
||||||
|
"\n"
|
||||||
|
" metadata: Final = user_api_key_dict.metadata\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
# ROOT override (house style) lets the same script run against a pulled copy
|
||||||
|
# locally: COOP_PATCH_ROOT=<dir containing .venv/lib>.
|
||||||
|
root = Path(os.environ.get("COOP_PATCH_ROOT", "/app"))
|
||||||
|
candidates = sorted(
|
||||||
|
(root / ".venv/lib").glob("python3*/site-packages/litellm/proxy/auth/route_checks.py")
|
||||||
|
)
|
||||||
|
if not candidates:
|
||||||
|
print("WARN passthrough-key-access: route_checks.py not found under /app/.venv/lib — skipping")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
target = candidates[-1]
|
||||||
|
src = target.read_text()
|
||||||
|
if MARKER in src:
|
||||||
|
print("passthrough-key-access: already applied")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
found = src.count(OLD)
|
||||||
|
if found != 1:
|
||||||
|
print(f"WARN passthrough-key-access: expected exactly 1 anchor, found {found} — skipping")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
patched = src.replace(OLD, NEW)
|
||||||
|
try:
|
||||||
|
compile(patched, str(target), "exec")
|
||||||
|
except SyntaxError as e:
|
||||||
|
print(f"WARN passthrough-key-access: patched source does not compile ({e}) — skipping")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
target.write_text(patched)
|
||||||
|
print(f"[coop-patch] applied: passthrough-key-access -> {target}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in new issue
Block a user