Removes DEFAULT_MODEL in favour of a force_model setting configurable via the admin UI. When set, every proxy request's model field is overridden, preventing uncoordinated model switches during lab sessions. Updates schemas, admin API, all three proxy endpoints, frontend, init_db, and docs (README, DOCKERHUB, KURZANLEITUNG).
173 lines
6.3 KiB
Python
173 lines
6.3 KiB
Python
import os
|
|
import secrets
|
|
import httpx
|
|
from pathlib import Path
|
|
from fastapi import FastAPI, Depends, HTTPException, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.staticfiles import StaticFiles
|
|
from sqlalchemy.orm import Session
|
|
from database import get_db
|
|
import crud, schemas
|
|
from models import APIKey as APIKeyModel, Usage as UsageModel
|
|
|
|
app = FastAPI(title="Ollama Proxy Admin API")
|
|
|
|
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173").split(",")
|
|
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
|
|
if not ADMIN_PASSWORD:
|
|
raise RuntimeError("ADMIN_PASSWORD environment variable must be set")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=ALLOWED_ORIGINS,
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
allow_headers=["Authorization", "Content-Type"],
|
|
)
|
|
|
|
def require_admin_auth(request: Request):
|
|
auth = request.headers.get("Authorization", "")
|
|
token = auth.removeprefix("Bearer ").strip()
|
|
if not secrets.compare_digest(token, ADMIN_PASSWORD):
|
|
raise HTTPException(status_code=401, detail="Invalid admin password")
|
|
|
|
@app.get("/api/api-keys", response_model=list[schemas.APIKeyWithUsage])
|
|
async def read_api_keys(
|
|
skip: int = 0, limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
keys = db.query(APIKeyModel).offset(skip).limit(limit).all()
|
|
result = []
|
|
for key in keys:
|
|
item = schemas.APIKeyWithUsage.model_validate(key)
|
|
usage = db.query(UsageModel).filter(UsageModel.api_key_id == key.id).first()
|
|
if usage:
|
|
item.tokens_used_today = usage.tokens_used_today or 0
|
|
item.tokens_used_month = usage.tokens_used_month or 0
|
|
item.requests_today = usage.requests_today or 0
|
|
item.requests_month = usage.requests_month or 0
|
|
item.daily_reset_at = usage.daily_reset_at
|
|
item.monthly_reset_at = usage.monthly_reset_at
|
|
result.append(item)
|
|
return result
|
|
|
|
@app.post("/api/api-keys", response_model=schemas.APIKeyCreated)
|
|
async def create_api_key(
|
|
api_key: schemas.APIKeyCreate,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
db_key, raw_key = crud.create_api_key(
|
|
db,
|
|
name=api_key.name,
|
|
expires_at=api_key.expires_at,
|
|
daily_tokens=api_key.daily_tokens,
|
|
monthly_tokens=api_key.monthly_tokens,
|
|
daily_requests=api_key.daily_requests,
|
|
monthly_requests=api_key.monthly_requests,
|
|
)
|
|
result = schemas.APIKeyCreated.model_validate(db_key)
|
|
result.plaintext_key = raw_key
|
|
return result
|
|
|
|
@app.patch("/api/api-keys/{api_key_id}/quota", response_model=schemas.APIKey)
|
|
async def update_quota(
|
|
api_key_id: int,
|
|
quota: schemas.QuotaUpdate,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
db_key = db.query(APIKeyModel).filter(APIKeyModel.id == api_key_id).first()
|
|
if not db_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
for field, value in quota.model_dump(exclude_unset=True).items():
|
|
setattr(db_key, field, value)
|
|
db.commit()
|
|
db.refresh(db_key)
|
|
return db_key
|
|
|
|
@app.put("/api/api-keys/{api_key_id}/deactivate")
|
|
async def deactivate_api_key(
|
|
api_key_id: int,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
db_key = db.query(APIKeyModel).filter(APIKeyModel.id == api_key_id).first()
|
|
if not db_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
db_key.is_active = False
|
|
db.commit()
|
|
return {"message": "API key deactivated"}
|
|
|
|
@app.put("/api/api-keys/{api_key_id}/activate")
|
|
async def activate_api_key(
|
|
api_key_id: int,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
db_key = db.query(APIKeyModel).filter(APIKeyModel.id == api_key_id).first()
|
|
if not db_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
db_key.is_active = True
|
|
db.commit()
|
|
return {"message": "API key activated"}
|
|
|
|
@app.delete("/api/api-keys/{api_key_id}")
|
|
async def delete_api_key(
|
|
api_key_id: int,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
db_key = db.query(APIKeyModel).filter(APIKeyModel.id == api_key_id).first()
|
|
if not db_key:
|
|
raise HTTPException(status_code=404, detail="API key not found")
|
|
db.delete(db_key)
|
|
db.commit()
|
|
return {"message": "API key deleted"}
|
|
|
|
@app.get("/api/proxy-info")
|
|
async def get_proxy_info(_ = Depends(require_admin_auth)):
|
|
host = os.getenv("PROXY_HOST", "0.0.0.0")
|
|
port = os.getenv("PROXY_PORT", "8000")
|
|
display_host = "localhost" if host in ("0.0.0.0", "::") else host
|
|
return {"endpoint": f"http://{display_host}:{port}"}
|
|
|
|
@app.get("/api/settings", response_model=schemas.Settings)
|
|
async def read_settings(db: Session = Depends(get_db), _ = Depends(require_admin_auth)):
|
|
return schemas.Settings(
|
|
ollama_url=crud.get_setting(db, "ollama_url", "http://localhost:11434"),
|
|
force_model=crud.get_setting(db, "force_model") or None,
|
|
)
|
|
|
|
@app.put("/api/settings", response_model=schemas.Settings)
|
|
async def update_settings(
|
|
settings: schemas.Settings,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
ollama_url = settings.ollama_url.rstrip('/').removesuffix('/v1')
|
|
crud.set_setting(db, "ollama_url", ollama_url)
|
|
crud.set_setting(db, "force_model", settings.force_model or "")
|
|
return schemas.Settings(ollama_url=ollama_url, force_model=settings.force_model or None)
|
|
|
|
@app.get("/api/ollama-models")
|
|
async def get_ollama_models(
|
|
url: str = None,
|
|
db: Session = Depends(get_db),
|
|
_ = Depends(require_admin_auth),
|
|
):
|
|
ollama_url = url or crud.get_setting(db, "ollama_url", "http://localhost:11434")
|
|
try:
|
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
response = await client.get(f"{ollama_url}/api/tags")
|
|
models = [m["name"] for m in response.json().get("models", [])]
|
|
return {"models": models, "reachable": True}
|
|
except Exception:
|
|
return {"models": [], "reachable": False}
|
|
|
|
# Statisches Frontend ausliefern (nur im Produktivbetrieb, wenn dist/ existiert)
|
|
_dist = Path(__file__).parent.parent / "frontend" / "dist"
|
|
if _dist.exists():
|
|
app.mount("/", StaticFiles(directory=_dist, html=True), name="frontend")
|