Socrates_docker / db_api_keys.py
AlessandroAmodioNGI's picture
Claude Sonnet 5
Add user-supplied API keys with automatic cross-provider model substitution
2a11aad
Raw History Blame Contribute Delete
3.37 kB
"""
db_api_keys.py β€” user-supplied Anthropic/OpenAI API keys (user_api_keys table).
Ciphertext only ever leaves this module decrypted via
get_active_decrypted_api_keys() / get_decrypted_api_key(), for runtime
injection into an LLM call (see llm_client.py). Never log a decrypted value β€”
see the hard rule in crypto_util.py.
"""
from db_client import supabase
from crypto_util import encrypt_secret, decrypt_secret
PROVIDERS = {"anthropic", "openai"}
def get_api_key_status(user_id: str) -> dict:
"""{'anthropic': {'has_key': bool, 'active': bool, 'last4': str|None}, 'openai': {...}}.
No decryption β€” safe for GET /profile/load."""
status = {p: {"has_key": False, "active": False, "last4": None} for p in PROVIDERS}
try:
resp = supabase.table("user_api_keys").select("provider,active,key_last4").eq("user_id", user_id).execute()
for row in resp.data or []:
provider = row.get("provider")
if provider in status:
status[provider] = {
"has_key": True,
"active": bool(row.get("active")),
"last4": row.get("key_last4"),
}
except Exception as e:
print(f"[get_api_key_status] failed for {user_id}: {e}")
return status
def get_decrypted_api_key(user_id: str, provider: str) -> str | None:
"""Decrypted key for runtime use only, or None if no active key is stored."""
resp = (
supabase.table("user_api_keys")
.select("key_ciphertext")
.eq("user_id", user_id)
.eq("provider", provider)
.eq("active", True)
.execute()
)
if not resp.data:
return None
return decrypt_secret(resp.data[0]["key_ciphertext"])
def get_active_decrypted_api_keys(user_id: str) -> dict:
"""{'anthropic': 'sk-...', 'openai': 'sk-...'} β€” only providers with an active
stored key. Used once per request to populate llm_client's ContextVar."""
resp = supabase.table("user_api_keys").select("provider,key_ciphertext").eq("user_id", user_id).eq("active", True).execute()
return {row["provider"]: decrypt_secret(row["key_ciphertext"]) for row in (resp.data or [])}
def set_api_key(user_id: str, provider: str, raw_key: str) -> dict:
"""Encrypt and store raw_key, marking it active. Caller must have already
validated raw_key with a live provider call β€” this function does not validate."""
ciphertext = encrypt_secret(raw_key)
last4 = raw_key[-4:] if len(raw_key) >= 4 else raw_key
supabase.table("user_api_keys").upsert({
"user_id": user_id,
"provider": provider,
"key_ciphertext": ciphertext,
"key_last4": last4,
"active": True,
}).execute()
return {"has_key": True, "active": True, "last4": last4}
def set_active(user_id: str, provider: str, active: bool) -> dict:
"""Flip the use-my-key/use-default toggle without touching the stored ciphertext."""
supabase.table("user_api_keys").update({"active": active}).eq("user_id", user_id).eq("provider", provider).execute()
status = get_api_key_status(user_id)
return status.get(provider, {"has_key": False, "active": False, "last4": None})
def delete_api_key(user_id: str, provider: str) -> None:
supabase.table("user_api_keys").delete().eq("user_id", user_id).eq("provider", provider).execute()