mirror of
https://github.com/Routstr/routstr-core.git
synced 2026-10-05 20:28:23 +00:00
test
This commit is contained in:
+115
-1
@@ -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()">×</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; }
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user