Upload 13 files
Browse files- .env.example +2 -2
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/main.py +4 -2
- static/models.js +122 -119
- static/public.js +3 -0
.env.example
CHANGED
|
@@ -1,9 +1,9 @@
|
|
| 1 |
NVIDIA_API_BASE=https://integrate.api.nvidia.com/v1
|
| 2 |
MODEL_LIST=z-ai/glm5,z-ai/glm4.7,minimaxai/minimax-m2.5,minimaxai/minimax-m2.7,moonshotai/kimi-k2.5,deepseek-ai/deepseek-v3.2,google/gemma-4-31b-it,qwen/qwen3.5-397b-a17b
|
|
|
|
| 3 |
MODEL_SYNC_INTERVAL_MINUTES=30
|
| 4 |
-
PUBLIC_HISTORY_BUCKETS=
|
| 5 |
REQUEST_TIMEOUT_SECONDS=90
|
| 6 |
MAX_UPSTREAM_CONNECTIONS=512
|
| 7 |
MAX_KEEPALIVE_CONNECTIONS=128
|
| 8 |
DATABASE_PATH=./data.sqlite3
|
| 9 |
-
|
|
|
|
| 1 |
NVIDIA_API_BASE=https://integrate.api.nvidia.com/v1
|
| 2 |
MODEL_LIST=z-ai/glm5,z-ai/glm4.7,minimaxai/minimax-m2.5,minimaxai/minimax-m2.7,moonshotai/kimi-k2.5,deepseek-ai/deepseek-v3.2,google/gemma-4-31b-it,qwen/qwen3.5-397b-a17b
|
| 3 |
+
APP_TIMEZONE=Asia/Shanghai
|
| 4 |
MODEL_SYNC_INTERVAL_MINUTES=30
|
| 5 |
+
PUBLIC_HISTORY_BUCKETS=22
|
| 6 |
REQUEST_TIMEOUT_SECONDS=90
|
| 7 |
MAX_UPSTREAM_CONNECTIONS=512
|
| 8 |
MAX_KEEPALIVE_CONNECTIONS=128
|
| 9 |
DATABASE_PATH=./data.sqlite3
|
|
|
app/__pycache__/main.cpython-313.pyc
CHANGED
|
Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ
|
|
|
app/main.py
CHANGED
|
@@ -11,6 +11,7 @@ from contextlib import asynccontextmanager
|
|
| 11 |
from datetime import UTC, datetime, timedelta
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Any
|
|
|
|
| 14 |
|
| 15 |
import httpx
|
| 16 |
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
|
|
@@ -30,10 +31,11 @@ REQUEST_TIMEOUT_SECONDS = float(os.getenv("REQUEST_TIMEOUT_SECONDS", "90"))
|
|
| 30 |
MAX_UPSTREAM_CONNECTIONS = int(os.getenv("MAX_UPSTREAM_CONNECTIONS", "512"))
|
| 31 |
MAX_KEEPALIVE_CONNECTIONS = int(os.getenv("MAX_KEEPALIVE_CONNECTIONS", "128"))
|
| 32 |
MODEL_SYNC_INTERVAL_MINUTES = int(os.getenv("MODEL_SYNC_INTERVAL_MINUTES", "30"))
|
| 33 |
-
PUBLIC_HISTORY_BUCKETS = int(os.getenv("PUBLIC_HISTORY_BUCKETS", "
|
| 34 |
BUCKET_MINUTES = 10
|
| 35 |
DEFAULT_MONITORED_MODELS = "z-ai/glm5,z-ai/glm4.7,minimaxai/minimax-m2.5,minimaxai/minimax-m2.7,moonshotai/kimi-k2.5,deepseek-ai/deepseek-v3.2,google/gemma-4-31b-it,qwen/qwen3.5-397b-a17b"
|
| 36 |
MODEL_LIST = [item.strip() for item in os.getenv("MODEL_LIST", DEFAULT_MONITORED_MODELS).split(",") if item.strip()]
|
|
|
|
| 37 |
|
| 38 |
http_client: httpx.AsyncClient | None = None
|
| 39 |
model_cache: list[dict[str, Any]] = []
|
|
@@ -43,7 +45,7 @@ model_sync_task: asyncio.Task[None] | None = None
|
|
| 43 |
|
| 44 |
|
| 45 |
def utcnow() -> datetime:
|
| 46 |
-
return datetime.now(
|
| 47 |
|
| 48 |
|
| 49 |
def utcnow_iso() -> str:
|
|
|
|
| 11 |
from datetime import UTC, datetime, timedelta
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Any
|
| 14 |
+
from zoneinfo import ZoneInfo
|
| 15 |
|
| 16 |
import httpx
|
| 17 |
from fastapi import Depends, FastAPI, Header, HTTPException, Request, status
|
|
|
|
| 31 |
MAX_UPSTREAM_CONNECTIONS = int(os.getenv("MAX_UPSTREAM_CONNECTIONS", "512"))
|
| 32 |
MAX_KEEPALIVE_CONNECTIONS = int(os.getenv("MAX_KEEPALIVE_CONNECTIONS", "128"))
|
| 33 |
MODEL_SYNC_INTERVAL_MINUTES = int(os.getenv("MODEL_SYNC_INTERVAL_MINUTES", "30"))
|
| 34 |
+
PUBLIC_HISTORY_BUCKETS = int(os.getenv("PUBLIC_HISTORY_BUCKETS", "22"))
|
| 35 |
BUCKET_MINUTES = 10
|
| 36 |
DEFAULT_MONITORED_MODELS = "z-ai/glm5,z-ai/glm4.7,minimaxai/minimax-m2.5,minimaxai/minimax-m2.7,moonshotai/kimi-k2.5,deepseek-ai/deepseek-v3.2,google/gemma-4-31b-it,qwen/qwen3.5-397b-a17b"
|
| 37 |
MODEL_LIST = [item.strip() for item in os.getenv("MODEL_LIST", DEFAULT_MONITORED_MODELS).split(",") if item.strip()]
|
| 38 |
+
APP_TIMEZONE = ZoneInfo(os.getenv("APP_TIMEZONE", "Asia/Shanghai"))
|
| 39 |
|
| 40 |
http_client: httpx.AsyncClient | None = None
|
| 41 |
model_cache: list[dict[str, Any]] = []
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def utcnow() -> datetime:
|
| 48 |
+
return datetime.now(APP_TIMEZONE)
|
| 49 |
|
| 50 |
|
| 51 |
def utcnow_iso() -> str:
|
static/models.js
CHANGED
|
@@ -1,120 +1,123 @@
|
|
| 1 |
-
const catalogSummary = document.getElementById("catalog-summary");
|
| 2 |
-
const providerFilterBar = document.getElementById("provider-filter-bar");
|
| 3 |
-
const providerGrid = document.getElementById("provider-grid");
|
| 4 |
-
const catalogUpdated = document.getElementById("catalog-updated");
|
| 5 |
-
const catalogEmpty = document.getElementById("catalog-empty");
|
| 6 |
-
|
| 7 |
-
let catalogState = {
|
| 8 |
-
providers: [],
|
| 9 |
-
activeProvider: "all",
|
| 10 |
-
};
|
| 11 |
-
|
| 12 |
-
const
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
button
|
| 51 |
-
button.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
<
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
chip
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
|
|
|
| 120 |
});
|
|
|
|
| 1 |
+
const catalogSummary = document.getElementById("catalog-summary");
|
| 2 |
+
const providerFilterBar = document.getElementById("provider-filter-bar");
|
| 3 |
+
const providerGrid = document.getElementById("provider-grid");
|
| 4 |
+
const catalogUpdated = document.getElementById("catalog-updated");
|
| 5 |
+
const catalogEmpty = document.getElementById("catalog-empty");
|
| 6 |
+
|
| 7 |
+
let catalogState = {
|
| 8 |
+
providers: [],
|
| 9 |
+
activeProvider: "all",
|
| 10 |
+
};
|
| 11 |
+
|
| 12 |
+
const DISPLAY_TIMEZONE = "Asia/Shanghai";
|
| 13 |
+
|
| 14 |
+
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
|
| 15 |
+
month: "2-digit",
|
| 16 |
+
day: "2-digit",
|
| 17 |
+
hour: "2-digit",
|
| 18 |
+
minute: "2-digit",
|
| 19 |
+
hour12: false,
|
| 20 |
+
timeZone: DISPLAY_TIMEZONE,
|
| 21 |
+
});
|
| 22 |
+
|
| 23 |
+
function formatDateTime(value) {
|
| 24 |
+
if (!value) return "--";
|
| 25 |
+
const date = new Date(value);
|
| 26 |
+
if (Number.isNaN(date.getTime())) return "--";
|
| 27 |
+
return dateTimeFormatter.format(date);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
function createSummaryCard(label, value, detail = "") {
|
| 31 |
+
const card = document.createElement("article");
|
| 32 |
+
card.className = "summary-card";
|
| 33 |
+
card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
|
| 34 |
+
return card;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function renderSummary(data) {
|
| 38 |
+
catalogSummary.innerHTML = "";
|
| 39 |
+
catalogSummary.appendChild(createSummaryCard("官方模型总数", data.total_models ?? 0, "来自 NVIDIA 官方 /v1/models"));
|
| 40 |
+
catalogSummary.appendChild(createSummaryCard("提供商数量", data.providers?.length ?? 0, "按模型 ID 中的提供商前缀自动归类"));
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function renderFilterBar() {
|
| 44 |
+
providerFilterBar.innerHTML = "";
|
| 45 |
+
const options = [{ provider: "all", count: catalogState.providers.reduce((sum, item) => sum + (item.count || 0), 0), label: "全部" }].concat(
|
| 46 |
+
catalogState.providers.map((group) => ({ provider: group.provider, count: group.count, label: group.provider }))
|
| 47 |
+
);
|
| 48 |
+
|
| 49 |
+
options.forEach((option) => {
|
| 50 |
+
const button = document.createElement("button");
|
| 51 |
+
button.className = `provider-filter-btn ${catalogState.activeProvider === option.provider ? "active" : ""}`;
|
| 52 |
+
button.type = "button";
|
| 53 |
+
button.textContent = `${option.label} (${option.count})`;
|
| 54 |
+
button.addEventListener("click", () => {
|
| 55 |
+
catalogState.activeProvider = option.provider;
|
| 56 |
+
renderFilterBar();
|
| 57 |
+
renderProviders();
|
| 58 |
+
});
|
| 59 |
+
providerFilterBar.appendChild(button);
|
| 60 |
+
});
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
function renderProviders() {
|
| 64 |
+
providerGrid.innerHTML = "";
|
| 65 |
+
const filtered = catalogState.activeProvider === "all"
|
| 66 |
+
? catalogState.providers
|
| 67 |
+
: catalogState.providers.filter((group) => group.provider === catalogState.activeProvider);
|
| 68 |
+
|
| 69 |
+
if (filtered.length === 0) {
|
| 70 |
+
catalogEmpty.textContent = "当前筛选条件下没有可展示的模型。";
|
| 71 |
+
return;
|
| 72 |
+
}
|
| 73 |
+
catalogEmpty.textContent = "";
|
| 74 |
+
|
| 75 |
+
filtered.forEach((group) => {
|
| 76 |
+
const card = document.createElement("article");
|
| 77 |
+
card.className = "provider-card provider-card-fixed";
|
| 78 |
+
card.innerHTML = `
|
| 79 |
+
<div class="provider-card-head">
|
| 80 |
+
<div>
|
| 81 |
+
<h3>${group.provider}</h3>
|
| 82 |
+
<p>${group.count} 个模型</p>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
`;
|
| 86 |
+
|
| 87 |
+
const list = document.createElement("div");
|
| 88 |
+
list.className = "provider-model-list";
|
| 89 |
+
(group.models || []).forEach((model) => {
|
| 90 |
+
const chip = document.createElement("div");
|
| 91 |
+
chip.className = "provider-model-chip";
|
| 92 |
+
chip.title = model.id;
|
| 93 |
+
chip.textContent = model.id;
|
| 94 |
+
list.appendChild(chip);
|
| 95 |
+
});
|
| 96 |
+
|
| 97 |
+
card.appendChild(list);
|
| 98 |
+
providerGrid.appendChild(card);
|
| 99 |
+
});
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
async function loadCatalog() {
|
| 103 |
+
const response = await fetch("/api/catalog", { headers: { Accept: "application/json" } });
|
| 104 |
+
if (!response.ok) {
|
| 105 |
+
throw new Error("模型列表加载失败");
|
| 106 |
+
}
|
| 107 |
+
const payload = await response.json();
|
| 108 |
+
catalogUpdated.textContent = formatDateTime(payload.synced_at || payload.generated_at);
|
| 109 |
+
catalogState.providers = payload.providers || [];
|
| 110 |
+
renderSummary(payload);
|
| 111 |
+
renderFilterBar();
|
| 112 |
+
renderProviders();
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
window.addEventListener("DOMContentLoaded", async () => {
|
| 116 |
+
try {
|
| 117 |
+
await loadCatalog();
|
| 118 |
+
} catch (error) {
|
| 119 |
+
catalogEmpty.textContent = error.message;
|
| 120 |
+
providerGrid.innerHTML = "";
|
| 121 |
+
providerFilterBar.innerHTML = "";
|
| 122 |
+
}
|
| 123 |
});
|
static/public.js
CHANGED
|
@@ -5,10 +5,13 @@ const healthGrid = document.getElementById("health-grid");
|
|
| 5 |
const dashboardEmpty = document.getElementById("dashboard-empty");
|
| 6 |
const refreshDashboardBtn = document.getElementById("refresh-dashboard");
|
| 7 |
|
|
|
|
|
|
|
| 8 |
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
|
| 9 |
hour: "2-digit",
|
| 10 |
minute: "2-digit",
|
| 11 |
hour12: false,
|
|
|
|
| 12 |
});
|
| 13 |
|
| 14 |
const SUMMARY_WIDTH = 360;
|
|
|
|
| 5 |
const dashboardEmpty = document.getElementById("dashboard-empty");
|
| 6 |
const refreshDashboardBtn = document.getElementById("refresh-dashboard");
|
| 7 |
|
| 8 |
+
const DISPLAY_TIMEZONE = "Asia/Shanghai";
|
| 9 |
+
|
| 10 |
const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
|
| 11 |
hour: "2-digit",
|
| 12 |
minute: "2-digit",
|
| 13 |
hour12: false,
|
| 14 |
+
timeZone: DISPLAY_TIMEZONE,
|
| 15 |
});
|
| 16 |
|
| 17 |
const SUMMARY_WIDTH = 360;
|