This commit is contained in:
9qeklajc
2025-10-15 20:23:00 +02:00
parent ecbe3158c0
commit 4327f542a0
4 changed files with 202 additions and 32 deletions
+115 -1
View File
@@ -8,6 +8,7 @@ from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlmodel import select
from ..payment.models import sync_models_with_api
from ..wallet import (
fetch_all_balances,
get_proofs_per_mint_and_unit,
@@ -417,6 +418,64 @@ async def dashboard(request: Request) -> str:
window.location.href = `/admin/logs/${requestId}`;
}
function openSyncModelsModal() {
const modal = document.getElementById('sync-models-modal');
modal.style.display = 'block';
}
function closeSyncModelsModal() {
const modal = document.getElementById('sync-models-modal');
modal.style.display = 'none';
}
async function syncModels() {
const deleteRemoved = document.getElementById('delete-removed-models').checked;
const button = document.getElementById('sync-models-btn');
const resultDiv = document.getElementById('sync-models-result');
button.disabled = true;
button.textContent = 'Syncing...';
resultDiv.style.display = 'none';
try {
const response = await fetch('/admin/api/sync_models', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
delete_removed: deleteRemoved
})
});
if (response.ok) {
const data = await response.json();
resultDiv.innerHTML = `<strong>✅ Success!</strong><br>${data.message}`;
resultDiv.style.display = 'block';
resultDiv.style.backgroundColor = '#d4edda';
resultDiv.style.borderColor = '#c3e6cb';
resultDiv.style.color = '#155724';
} else {
const errorData = await response.json();
resultDiv.innerHTML = `<strong>❌ Error:</strong><br>${errorData.detail || 'Unknown error'}`;
resultDiv.style.display = 'block';
resultDiv.style.backgroundColor = '#f8d7da';
resultDiv.style.borderColor = '#f5c6cb';
resultDiv.style.color = '#721c24';
}
} catch (error) {
resultDiv.innerHTML = `<strong>❌ Error:</strong><br>${error.message}`;
resultDiv.style.display = 'block';
resultDiv.style.backgroundColor = '#f8d7da';
resultDiv.style.borderColor = '#f5c6cb';
resultDiv.style.color = '#721c24';
} finally {
button.disabled = false;
button.textContent = 'Sync Models';
}
}
async function openSettingsModal() {
const modal = document.getElementById('settings-modal');
const textarea = document.getElementById('settings-json');
@@ -498,12 +557,15 @@ async def dashboard(request: Request) -> str:
const withdrawModal = document.getElementById('withdraw-modal');
const investigateModal = document.getElementById('investigate-modal');
const settingsModal = document.getElementById('settings-modal');
const syncModelsModal = document.getElementById('sync-models-modal');
if (event.target == withdrawModal) {
closeWithdrawModal();
} else if (event.target == investigateModal) {
closeInvestigateModal();
} else if (event.target == settingsModal) {
closeSettingsModal();
} else if (event.target == syncModelsModal) {
closeSyncModelsModal();
}
}
</script>
@@ -532,6 +594,29 @@ async def dashboard(request: Request) -> str:
<button onclick="openSettingsModal()">
⚙️ Settings
</button>
<button onclick="openSyncModelsModal()">
🔄 Sync Models
</button>
<div id="sync-models-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeSyncModelsModal()">&times;</span>
<h3>Sync Models from API</h3>
<p>This will fetch the latest models from OpenRouter and update the database.</p>
<div style="margin: 15px 0;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox" id="delete-removed-models" style="width: auto; margin: 0;">
<span>Delete models that are no longer available</span>
</label>
<p style="font-size: 0.85rem; color: #718096; margin-top: 5px; margin-left: 30px;">
⚠️ Warning: This will permanently remove models that are no longer in the API.
</p>
</div>
<div id="sync-models-result" style="display: none; padding: 12px; border-radius: 6px; margin: 15px 0; border: 1px solid;"></div>
<button id="sync-models-btn" onclick="syncModels()">🔄 Sync Models</button>
<button onclick="closeSyncModelsModal()" style="background-color: #718096;">Cancel</button>
</div>
</div>
<div id="withdraw-modal" class="modal">
<div class="modal-content">
@@ -721,7 +806,6 @@ async def view_logs(request: Request, request_id: str) -> str:
async def withdraw(
request: Request, withdraw_request: WithdrawRequest
) -> dict[str, str]:
# Get wallet and check balance
from .settings import settings as global_settings
wallet = await get_wallet(
@@ -750,6 +834,36 @@ async def withdraw(
return {"token": token}
class SyncModelsRequest(BaseModel):
delete_removed: bool = False
@admin_router.post("/api/sync_models", dependencies=[Depends(require_admin_api)])
async def sync_models(request: Request, sync_request: SyncModelsRequest) -> dict:
try:
src = settings.source or None
source_filter = src if src and src.strip() else None
except Exception:
source_filter = None
logger.info(
"Manual models sync triggered",
extra={"delete_removed": sync_request.delete_removed},
)
counts = await sync_models_with_api(
source_filter=source_filter, delete_removed=sync_request.delete_removed
)
return {
"success": True,
"inserted": counts["inserted"],
"updated": counts["updated"],
"deleted": counts["deleted"],
"message": f"Synced: {counts['inserted']} inserted, {counts['updated']} updated, {counts['deleted']} deleted",
}
DASHBOARD_CSS: str = """
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; color: #2c3e50; line-height: 1.6; padding: 2rem; }
+1 -2
View File
@@ -67,8 +67,7 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
await ensure_models_bootstrapped()
pricing_task = asyncio.create_task(update_sats_pricing())
if global_settings.models_refresh_interval_seconds > 0:
models_refresh_task = asyncio.create_task(refresh_models_periodically())
models_refresh_task = asyncio.create_task(refresh_models_periodically())
payout_task = asyncio.create_task(periodic_payout())
nip91_task = asyncio.create_task(announce_provider())
providers_task = asyncio.create_task(providers_cache_refresher())
+2 -1
View File
@@ -64,10 +64,11 @@ class Settings(BaseSettings):
default=120, env="PRICING_REFRESH_INTERVAL_SECONDS"
)
models_refresh_interval_seconds: int = Field(
default=0, env="MODELS_REFRESH_INTERVAL_SECONDS"
default=30, env="MODELS_REFRESH_INTERVAL_SECONDS"
)
enable_pricing_refresh: bool = Field(default=True, env="ENABLE_PRICING_REFRESH")
enable_models_refresh: bool = Field(default=True, env="ENABLE_MODELS_REFRESH")
delete_removed_models: bool = Field(default=False, env="DELETE_REMOVED_MODELS")
refund_cache_ttl_seconds: int = Field(default=3600, env="REFUND_CACHE_TTL_SECONDS")
# Logging
+84 -28
View File
@@ -379,11 +379,76 @@ async def update_sats_pricing() -> None:
break
async def sync_models_with_api(
source_filter: str | None = None, delete_removed: bool = False
) -> dict[str, int]:
"""Fetch models from OpenRouter and sync with database.
Args:
source_filter: Optional source filter (e.g., 'anthropic')
delete_removed: If True, delete models that no longer exist in API
Returns:
Dict with counts: inserted, updated, deleted
"""
models = fetch_openrouter_models(source_filter=source_filter)
if not models:
return {"inserted": 0, "updated": 0, "deleted": 0}
async with create_session() as s:
result = await s.exec(select(ModelRow)) # type: ignore
existing_rows = {row.id: row for row in result.all()}
fetched_ids = set()
inserted = 0
updated = 0
for m in models:
try:
model = Model(**m) # type: ignore
except Exception:
continue
fetched_ids.add(model.id)
payload = _model_to_row_payload(model)
if model.id not in existing_rows:
try:
s.add(ModelRow(**payload)) # type: ignore
inserted += 1
except Exception:
pass
else:
existing_row = existing_rows[model.id]
changed = False
for key, value in payload.items():
if getattr(existing_row, key) != value:
setattr(existing_row, key, value)
changed = True
if changed:
s.add(existing_row)
updated += 1
deleted = 0
if delete_removed:
for existing_id in existing_rows:
if existing_id not in fetched_ids:
row_to_delete = existing_rows[existing_id]
await s.delete(row_to_delete)
deleted += 1
if inserted or updated or deleted:
await s.commit()
return {"inserted": inserted, "updated": updated, "deleted": deleted}
async def refresh_models_periodically() -> None:
"""Background task: periodically fetch OpenRouter models and insert new ones.
"""Background task: periodically fetch OpenRouter models and sync with database.
- Respects optional SOURCE filter from settings
- Does not overwrite existing rows
- Updates existing models with new information
- Optionally deletes models no longer in API (if settings.delete_removed_models)
- Sleeps according to settings.models_refresh_interval_seconds; disabled when 0
"""
interval = getattr(settings, "models_refresh_interval_seconds", 0)
@@ -403,33 +468,24 @@ async def refresh_models_periodically() -> None:
except Exception:
source_filter = None
models = fetch_openrouter_models(source_filter=source_filter)
if not models:
await asyncio.sleep(interval)
continue
try:
delete_removed = getattr(settings, "delete_removed_models", False)
except Exception:
delete_removed = False
async with create_session() as s:
result = await s.exec(select(ModelRow.id)) # type: ignore
existing_ids = {
row[0] if isinstance(row, tuple) else row for row in result.all()
}
inserted = 0
for m in models:
try:
model = Model(**m) # type: ignore
except Exception:
continue
if model.id in existing_ids:
continue
payload = _model_to_row_payload(model)
try:
s.add(ModelRow(**payload)) # type: ignore
except Exception:
pass
inserted += 1
if inserted:
await s.commit()
logger.info(f"Inserted {inserted} new models from OpenRouter")
counts = await sync_models_with_api(
source_filter=source_filter, delete_removed=delete_removed
)
if counts["inserted"] or counts["updated"] or counts["deleted"]:
logger.info(
"Models synced",
extra={
"inserted": counts["inserted"],
"updated": counts["updated"],
"deleted": counts["deleted"],
},
)
except asyncio.CancelledError:
break
except Exception as e: