Bring Your Own LLM: per-user LLM config with system default fallback
This commit is contained in:
@@ -224,13 +224,89 @@ def verify_magic_link(token: str, response: Response, db: Session = Depends(get_
|
||||
@app.get("/api/me")
|
||||
def me(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||
llm = u.llm_config or {}
|
||||
return {
|
||||
"id": u.id, "username": u.username, "email": u.email or "",
|
||||
"display_name": u.display_name, "role": u.role,
|
||||
"has_llm": bool(llm.get("api_key")),
|
||||
"llm_provider": llm.get("provider_type", ""),
|
||||
"llm_model": llm.get("default_model", ""),
|
||||
"created_at": u.created_at.isoformat() if u.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
class UserLLMConfig(BaseModel):
|
||||
provider_type: str = "" # anthropic, openai, litellm, ollama
|
||||
api_url: str = ""
|
||||
api_key: str = ""
|
||||
default_model: str = ""
|
||||
|
||||
|
||||
@app.get("/api/me/llm")
|
||||
def get_my_llm(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
"""Get current user's LLM config."""
|
||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||
llm = u.llm_config or {}
|
||||
return {
|
||||
"provider_type": llm.get("provider_type", ""),
|
||||
"api_url": llm.get("api_url", ""),
|
||||
"api_key": "***" if llm.get("api_key") else "",
|
||||
"default_model": llm.get("default_model", ""),
|
||||
"configured": bool(llm.get("api_key")),
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/me/llm")
|
||||
def update_my_llm(data: UserLLMConfig, user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
"""Update current user's LLM config (bring your own LLM)."""
|
||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||
current = u.llm_config or {}
|
||||
update = data.model_dump()
|
||||
# Only update api_key if a real value was provided (not "***")
|
||||
if update.get("api_key") == "***" or not update.get("api_key"):
|
||||
update["api_key"] = current.get("api_key", "")
|
||||
u.llm_config = update
|
||||
db.commit()
|
||||
return {"status": "updated"}
|
||||
|
||||
|
||||
@app.delete("/api/me/llm")
|
||||
def delete_my_llm(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||
"""Remove user's LLM config (fall back to system default)."""
|
||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||
u.llm_config = {}
|
||||
db.commit()
|
||||
return {"status": "removed"}
|
||||
|
||||
|
||||
@app.get("/api/users/{user_id}/llm")
|
||||
def get_user_llm(user_id: int, db: Session = Depends(get_db)):
|
||||
"""Internal: resolve LLM config for a user. Returns user's own config if set, otherwise system default."""
|
||||
u = db.query(User).filter(User.id == user_id).first()
|
||||
if not u:
|
||||
raise HTTPException(status_code=404)
|
||||
user_llm = u.llm_config or {}
|
||||
if user_llm.get("api_key"):
|
||||
return {
|
||||
"source": "user",
|
||||
"provider_type": user_llm.get("provider_type", ""),
|
||||
"api_url": user_llm.get("api_url", ""),
|
||||
"api_key": user_llm["api_key"],
|
||||
"default_model": user_llm.get("default_model", ""),
|
||||
}
|
||||
# Fall back to system default
|
||||
default = db.query(LLMProvider).filter(LLMProvider.is_default == True).first()
|
||||
if default:
|
||||
return {
|
||||
"source": "system",
|
||||
"provider_type": default.provider_type,
|
||||
"api_url": default.api_url,
|
||||
"api_key": default.api_key,
|
||||
"default_model": default.default_model,
|
||||
}
|
||||
return {"source": "none", "provider_type": "", "api_url": "", "api_key": "", "default_model": ""}
|
||||
|
||||
|
||||
# --- Health ---
|
||||
|
||||
@app.get("/api/health")
|
||||
|
||||
@@ -13,6 +13,7 @@ class User(Base):
|
||||
password_hash = Column(String, nullable=False)
|
||||
display_name = Column(String, default="")
|
||||
role = Column(String, default="user") # admin or user
|
||||
llm_config = Column(JSON, default=dict) # user's own LLM provider config
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
|
||||
|
||||
instances = relationship("AgentInstance", back_populates="user")
|
||||
|
||||
@@ -106,6 +106,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;b
|
||||
<div class="dot"></div>
|
||||
<span id="agent-count">0 agents</span>
|
||||
<span class="user-name" id="user-display"></span>
|
||||
<button class="small-btn" onclick="showLLMSettings()">LLM</button>
|
||||
<button class="small-btn" id="admin-btn" style="display:none" onclick="location.href='/admin'">Admin</button>
|
||||
<button class="small-btn" onclick="logout()">Logout</button>
|
||||
</div>
|
||||
@@ -340,6 +341,71 @@ async function enableAgent(catalogId,name){
|
||||
if(res.ok){closeModal();refresh()}
|
||||
}
|
||||
|
||||
// --- LLM Settings ---
|
||||
async function showLLMSettings(){
|
||||
const res=await fetch(API+'/api/me/llm');
|
||||
if(res.status===401){location.href='/login';return}
|
||||
const llm=await res.json();
|
||||
document.getElementById('modal-content').innerHTML=`
|
||||
<button class="close-btn" onclick="closeModal()">×</button>
|
||||
<h2>LLM Provider</h2>
|
||||
<p style="color:var(--text-dim);margin-bottom:1.5rem">Bring your own LLM API key, or leave blank to use the system default.</p>
|
||||
<div class="config-section">
|
||||
<h3>Your LLM Configuration</h3>
|
||||
<div class="config-grid">
|
||||
<div class="config-field">
|
||||
<label>Provider</label>
|
||||
<select id="llm-provider">
|
||||
<option value="" ${!llm.provider_type?'selected':''}>None (use system default)</option>
|
||||
<option value="anthropic" ${llm.provider_type==='anthropic'?'selected':''}>Anthropic (Claude)</option>
|
||||
<option value="openai" ${llm.provider_type==='openai'?'selected':''}>OpenAI</option>
|
||||
<option value="litellm" ${llm.provider_type==='litellm'?'selected':''}>LiteLLM</option>
|
||||
<option value="ollama" ${llm.provider_type==='ollama'?'selected':''}>Ollama</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<label>Model</label>
|
||||
<input type="text" id="llm-model" value="${llm.default_model||''}" placeholder="claude-sonnet-4-5-20250514">
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<label>API URL</label>
|
||||
<input type="text" id="llm-url" value="${llm.api_url||''}" placeholder="https://api.anthropic.com (optional)">
|
||||
</div>
|
||||
<div class="config-field">
|
||||
<label>API Key</label>
|
||||
<input type="password" id="llm-key" value="${llm.api_key||''}" placeholder="sk-...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-actions">
|
||||
<button class="btn-save" onclick="saveLLM()">Save</button>
|
||||
<button class="btn-secondary" onclick="removeLLM()">Remove (use system default)</button>
|
||||
<span class="save-msg" id="llm-save-msg"></span>
|
||||
</div>
|
||||
<p style="color:var(--text-dim);font-size:.8rem;margin-top:1rem">
|
||||
${llm.configured?'Status: <span style="color:var(--green)">Configured</span> ('+llm.provider_type+')':'Status: Using system default'}
|
||||
</p>
|
||||
</div>`;
|
||||
document.getElementById('modal-overlay').classList.add('open');
|
||||
}
|
||||
|
||||
async function saveLLM(){
|
||||
const body={
|
||||
provider_type:document.getElementById('llm-provider').value,
|
||||
api_url:document.getElementById('llm-url').value,
|
||||
api_key:document.getElementById('llm-key').value,
|
||||
default_model:document.getElementById('llm-model').value,
|
||||
};
|
||||
const res=await fetch(API+'/api/me/llm',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
||||
const msg=document.getElementById('llm-save-msg');
|
||||
if(res.ok){msg.textContent='Saved';msg.style.color='var(--green)';setTimeout(()=>msg.textContent='',2000)}
|
||||
else{msg.textContent='Error';msg.style.color='var(--red)'}
|
||||
}
|
||||
|
||||
async function removeLLM(){
|
||||
await fetch(API+'/api/me/llm',{method:'DELETE'});
|
||||
showLLMSettings();
|
||||
}
|
||||
|
||||
// --- Bridge ---
|
||||
async function loadBridge(){
|
||||
try{
|
||||
|
||||
Reference in New Issue
Block a user