diff --git a/Dockerfile b/Dockerfile index 9e02f6d..f73641e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 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 # warm so voice mode skips the ~24s cold-start (see audio_keepalive.py header). COPY audio_keepalive.py /app/code/audio_keepalive.py diff --git a/config.yaml b/config.yaml index fd2c4f2..4ae3f15 100644 --- a/config.yaml +++ b/config.yaml @@ -251,6 +251,16 @@ general_settings: # 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 # 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: - path: "/v1/systemone" target: "https://api.greenpt.ai/v1/systemone" diff --git a/patch_passthrough_key_access.py b/patch_passthrough_key_access.py new file mode 100644 index 0000000..fa25f75 --- /dev/null +++ b/patch_passthrough_key_access.py @@ -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=. + 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())