Compare commits
4
Commits
003689868f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5425efee1f | ||
|
|
fd13bc6ea2 | ||
|
|
97ebe69553 | ||
|
|
7f04920327 |
+74
-2
@@ -1658,6 +1658,32 @@ async def broker_create_api_key(email: str, name: str) -> dict:
|
||||
team_id = await litellm_get_or_create_team(email, balance)
|
||||
alias = _api_key_alias(email, name)
|
||||
async with httpx.AsyncClient() as client:
|
||||
# Rotation support: LiteLLM requires unique key aliases, so a re-create
|
||||
# with an existing (email, name) must retire the old key first. Without
|
||||
# this, rotating a key from the dashboard 400s ("alias already exists").
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT hash FROM member_api_keys WHERE email = ? AND name = ?",
|
||||
(email, name),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
if row and row[0]:
|
||||
old_hash = row[0]
|
||||
try:
|
||||
del_res = await client.post(
|
||||
f"{LITELLM_BASE}/key/delete",
|
||||
headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"},
|
||||
json={"keys": [old_hash]},
|
||||
)
|
||||
if del_res.status_code == 200:
|
||||
logger.info("Rotated API key %s for %s (old key retired)", name, email)
|
||||
else:
|
||||
logger.warning(
|
||||
"Old key retire failed for %s/%s: %s %s",
|
||||
email, name, del_res.status_code, del_res.text[:200],
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - rotation is best-effort
|
||||
logger.warning("Old key retire errored for %s/%s: %s", email, name, exc)
|
||||
r = await client.post(
|
||||
f"{LITELLM_BASE}/key/generate",
|
||||
headers={"Authorization": f"Bearer {LITELLM_MASTER_KEY}"},
|
||||
@@ -2445,7 +2471,13 @@ async def admin_overview(request: Request):
|
||||
for email, active, balance, user_id in rows:
|
||||
team = teams.get(f"member:{email}", {})
|
||||
spend = float(team.get("spend") or 0.0)
|
||||
member_all_time = all_time_spend.get(f"member:{email}", 0.0)
|
||||
member_all_time = float(all_time_spend.get(f"member:{email}", 0.0))
|
||||
# Normalize floating-point residue (e.g. -5.55e-17 after a cycle reset)
|
||||
# so the panel/dashboards never render "-0.00".
|
||||
if abs(spend) < 1e-9:
|
||||
spend = 0.0
|
||||
if abs(member_all_time) < 1e-9:
|
||||
member_all_time = 0.0
|
||||
credits = get_credit_balance(email)
|
||||
eff = compute_max_budget(
|
||||
float(balance) if balance is not None else MEMBER_BUDGET,
|
||||
@@ -2682,7 +2714,7 @@ async def broker_get_matrix_mxid(request: Request):
|
||||
return {"matrix_mxid": get_member_matrix_mxid(email), "local_mxid": local_mxid_for_email(email)}
|
||||
|
||||
|
||||
async def local_mxid_for_email(email: str) -> str:
|
||||
def local_mxid_for_email(email: str) -> str:
|
||||
"""Resolve a member's local Matrix ID (@username:inference.coop), or '' if unknown."""
|
||||
import urllib.request
|
||||
try:
|
||||
@@ -3021,6 +3053,46 @@ async def admin_sweep(request: Request):
|
||||
return result
|
||||
|
||||
|
||||
def _verify_sync_token(request: Request) -> None:
|
||||
"""Authenticate the LibreChat key-sync feed via X-Sync-Token.
|
||||
|
||||
Dedicated machine secret (least privilege): it only ever exposes the
|
||||
members' own chat keys, never the admin API. The same secret is set on
|
||||
the LibreChat sandbox app (LIBRECHAT_SYNC_TOKEN in /app/data/env).
|
||||
"""
|
||||
expected = os.environ.get("LIBRECHAT_SYNC_TOKEN", "")
|
||||
if not expected:
|
||||
raise HTTPException(503, "Sync token not configured")
|
||||
provided = request.headers.get("x-sync-token", "")
|
||||
if not secrets.compare_digest(provided, expected):
|
||||
raise HTTPException(401, "Invalid sync token")
|
||||
|
||||
|
||||
@app.get("/admin/librechat/keys")
|
||||
@limiter.limit("30/minute")
|
||||
async def admin_librechat_keys(request: Request):
|
||||
"""Key feed for the LibreChat sandbox's per-member key auto-load.
|
||||
|
||||
Returns every ACTIVE member's portal-provisioned chat key so the sandbox
|
||||
can seed LibreChat's user-key store and members never see an API-key
|
||||
prompt (chat bills to the member's own LiteLLM team/budget). Auth is the
|
||||
dedicated X-Sync-Token header — NOT the admin token.
|
||||
"""
|
||||
_verify_sync_token(request)
|
||||
|
||||
conn = get_db()
|
||||
rows = conn.execute(
|
||||
"SELECT email, key_token FROM members WHERE active = 1 AND key_token != ''"
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return {
|
||||
"members": [
|
||||
{"email": email, "key": key_token}
|
||||
for email, key_token in rows
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def index():
|
||||
return {"service": "Inference Cooperative Member Portal", "version": "0.1.0"}
|
||||
Reference in new issue
Block a user