feat(agent): v4.11.0 - clés API par utilisateur, chargement dynamique modèles, commandes slash
This commit is contained in:
+111
-4
@@ -16,7 +16,11 @@ from app.config import settings
|
||||
from app.auth.session import get_current_user
|
||||
from app.services.agent_engine import AgentEngine, undo_action
|
||||
from app.services.llm_client import LLMClient, PROVIDERS
|
||||
from app.services.llm_config import get_llm_config, set_llm_config, provider_info
|
||||
from app.services.llm_config import (
|
||||
get_llm_config, set_llm_config, provider_info,
|
||||
get_user_llm_key, list_user_llm_keys, upsert_user_llm_key,
|
||||
delete_user_llm_key, fetch_provider_models,
|
||||
)
|
||||
from app.services.tool_registry import ToolRegistry
|
||||
from app.services.permission_manager import PermissionManager
|
||||
|
||||
@@ -227,6 +231,29 @@ async def delete_conversation(request: Request, conversation_id: int):
|
||||
return {"id": conversation_id, "status": "deleted"}
|
||||
|
||||
|
||||
@router.patch("/conversations/{conversation_id}")
|
||||
async def patch_conversation(request: Request, conversation_id: int):
|
||||
"""Update a conversation's title / provider / model (slash-command support)."""
|
||||
user_id = await _current_user_id(request)
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
with get_conn() as conn:
|
||||
conv = conn.execute(
|
||||
"SELECT id FROM agent_conversations WHERE id=? AND user_id=?",
|
||||
(conversation_id, user_id),
|
||||
).fetchone()
|
||||
if not conv:
|
||||
raise HTTPException(status_code=404, detail="Conversation introuvable")
|
||||
sets, params = ["updated_at=CURRENT_TIMESTAMP"], []
|
||||
for col in ("title", "provider", "model"):
|
||||
if body.get(col) is not None:
|
||||
sets.append(f"{col}=?")
|
||||
params.append(str(body[col]))
|
||||
params.append(conversation_id)
|
||||
conn.execute(f"UPDATE agent_conversations SET {', '.join(sets)} WHERE id=?", params)
|
||||
conn.commit()
|
||||
return {"id": conversation_id, "status": "updated"}
|
||||
|
||||
|
||||
# ── Run (SSE) ──
|
||||
|
||||
|
||||
@@ -257,7 +284,12 @@ async def run_conversation(request: Request, conversation_id: int):
|
||||
|
||||
engine = AgentEngine(user_id, workspace_id=ws)
|
||||
if eff_provider:
|
||||
engine.llm = LLMClient(provider=eff_provider)
|
||||
user_key = get_user_llm_key(user_id, eff_provider)
|
||||
if user_key and user_key.get("api_key"):
|
||||
engine.llm = LLMClient(provider=eff_provider, api_key=user_key["api_key"],
|
||||
api_base=user_key.get("api_base") or None)
|
||||
else:
|
||||
engine.llm = LLMClient(provider=eff_provider)
|
||||
|
||||
async def event_stream():
|
||||
async for ev in engine.run(
|
||||
@@ -396,8 +428,19 @@ async def list_tools(request: Request):
|
||||
@router.get("/providers")
|
||||
async def list_providers(request: Request):
|
||||
"""Expose the LLM providers list + current config to the UI."""
|
||||
user_id = await _current_user_id(request)
|
||||
llm = LLMClient()
|
||||
cfg = get_llm_config()
|
||||
keys = list_user_llm_keys(user_id)
|
||||
keys_map = {k["provider"]: k for k in keys}
|
||||
provs = provider_info()
|
||||
for p in provs:
|
||||
k = keys_map.get(p["id"])
|
||||
if k and k["models"]:
|
||||
for m in k["models"]:
|
||||
if m not in p["models"]:
|
||||
p["models"].append(m)
|
||||
p["has_key"] = bool(k and k["has_key"])
|
||||
return {
|
||||
"provider": llm.provider,
|
||||
"model": llm.default_model,
|
||||
@@ -405,13 +448,77 @@ async def list_providers(request: Request):
|
||||
"max_iterations": settings.agent_max_iterations,
|
||||
"api_base": cfg["api_base"],
|
||||
"has_api_key": bool(cfg["api_key"]),
|
||||
"providers": provider_info(),
|
||||
"providers": provs,
|
||||
"keys": keys,
|
||||
}
|
||||
|
||||
|
||||
# ── Per-user provider API keys (non-admin: any authenticated user) ──
|
||||
|
||||
|
||||
@router.get("/keys")
|
||||
async def list_llm_keys(request: Request):
|
||||
"""The user's saved provider keys + API keys (masked)."""
|
||||
user_id = await _current_user_id(request)
|
||||
return {"keys": list_user_llm_keys(user_id)}
|
||||
|
||||
|
||||
@router.put("/keys/{llm_provider}")
|
||||
async def save_llm_key(request: Request, llm_provider: str):
|
||||
"""Upsert a provider key for the current user (masked in responses)."""
|
||||
user_id = await _current_user_id(request)
|
||||
provider = llm_provider.lower()
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
raw = upsert_user_llm_key(
|
||||
user_id,
|
||||
provider,
|
||||
api_key=(body.get("api_key") or "").strip(),
|
||||
api_base=(body.get("api_base") or "").strip(),
|
||||
default_model=(body.get("default_model") or "").strip(),
|
||||
models=body.get("models"),
|
||||
)
|
||||
stored = bool(raw.get("api_key"))
|
||||
return {"status": "saved", "provider": provider, "has_key": stored}
|
||||
|
||||
|
||||
@router.delete("/keys/{llm_provider}")
|
||||
async def delete_llm_key(request: Request, llm_provider: str):
|
||||
"""Remove a saved provider key for the current user."""
|
||||
user_id = await _current_user_id(request)
|
||||
provider = llm_provider.lower()
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
delete_user_llm_key(user_id, provider)
|
||||
return {"status": "deleted", "provider": provider}
|
||||
|
||||
|
||||
@router.post("/keys/{llm_provider}/models")
|
||||
async def fetch_llm_models(request: Request, llm_provider: str):
|
||||
"""Fetch the live model list from a provider. Falls back to the user's
|
||||
stored key when no key is supplied in the body."""
|
||||
user_id = await _current_user_id(request)
|
||||
provider = llm_provider.lower()
|
||||
if provider not in PROVIDERS:
|
||||
raise HTTPException(status_code=400, detail=f"Provider inconnu: {provider}")
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
api_key = (body.get("api_key") or "").strip()
|
||||
api_base = (body.get("api_base") or "").strip()
|
||||
if not api_key:
|
||||
stored = get_user_llm_key(user_id, provider)
|
||||
if stored and stored.get("api_key"):
|
||||
api_key = stored["api_key"]
|
||||
api_base = api_base or (stored.get("api_base") or "").strip()
|
||||
try:
|
||||
models = await fetch_provider_models(provider, api_key=api_key, api_base=api_base)
|
||||
return {"ok": True, "provider": provider, "models": models}
|
||||
except Exception as exc: # noqa: BLE001 — surface connectivity errors
|
||||
return {"ok": False, "provider": provider, "error": str(exc)}
|
||||
|
||||
|
||||
@router.patch("/providers")
|
||||
async def update_provider_config(request: Request):
|
||||
"""Admin: persist the global LLM provider/model/key/base (runtime, DB-backed)."""
|
||||
await _current_admin(request)
|
||||
body = await request.json() if request.headers.get("content-type") else {}
|
||||
provider = (body.get("provider") or "").strip().lower()
|
||||
|
||||
Reference in New Issue
Block a user