cacodex commited on
Commit
2c073e0
·
verified ·
1 Parent(s): c0aaca1

Upload 13 files

Browse files
.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=24
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", "24"))
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(UTC)
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 dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
13
- month: "2-digit",
14
- day: "2-digit",
15
- hour: "2-digit",
16
- minute: "2-digit",
17
- hour12: false,
18
- });
19
-
20
- function formatDateTime(value) {
21
- if (!value) return "--";
22
- const date = new Date(value);
23
- if (Number.isNaN(date.getTime())) return "--";
24
- return dateTimeFormatter.format(date);
25
- }
26
-
27
- function createSummaryCard(label, value, detail = "") {
28
- const card = document.createElement("article");
29
- card.className = "summary-card";
30
- card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
31
- return card;
32
- }
33
-
34
- function renderSummary(data) {
35
- catalogSummary.innerHTML = "";
36
- catalogSummary.appendChild(createSummaryCard("官方模型总数", data.total_models ?? 0, "来自 NVIDIA 官方 /v1/models"));
37
- catalogSummary.appendChild(createSummaryCard("提供商数量", data.providers?.length ?? 0, "按模型 ID 中的提供商前缀自动归类"));
38
- }
39
-
40
- function renderFilterBar() {
41
- providerFilterBar.innerHTML = "";
42
- const options = [{ provider: "all", count: catalogState.providers.reduce((sum, item) => sum + (item.count || 0), 0), label: "全部" }].concat(
43
- catalogState.providers.map((group) => ({ provider: group.provider, count: group.count, label: group.provider }))
44
- );
45
-
46
- options.forEach((option) => {
47
- const button = document.createElement("button");
48
- button.className = `provider-filter-btn ${catalogState.activeProvider === option.provider ? "active" : ""}`;
49
- button.type = "button";
50
- button.textContent = `${option.label} (${option.count})`;
51
- button.addEventListener("click", () => {
52
- catalogState.activeProvider = option.provider;
53
- renderFilterBar();
54
- renderProviders();
55
- });
56
- providerFilterBar.appendChild(button);
57
- });
58
- }
59
-
60
- function renderProviders() {
61
- providerGrid.innerHTML = "";
62
- const filtered = catalogState.activeProvider === "all"
63
- ? catalogState.providers
64
- : catalogState.providers.filter((group) => group.provider === catalogState.activeProvider);
65
-
66
- if (filtered.length === 0) {
67
- catalogEmpty.textContent = "当前筛选条件下没有可展示的模型。";
68
- return;
69
- }
70
- catalogEmpty.textContent = "";
71
-
72
- filtered.forEach((group) => {
73
- const card = document.createElement("article");
74
- card.className = "provider-card provider-card-fixed";
75
- card.innerHTML = `
76
- <div class="provider-card-head">
77
- <div>
78
- <h3>${group.provider}</h3>
79
- <p>${group.count} 个模型</p>
80
- </div>
81
- </div>
82
- `;
83
-
84
- const list = document.createElement("div");
85
- list.className = "provider-model-list";
86
- (group.models || []).forEach((model) => {
87
- const chip = document.createElement("div");
88
- chip.className = "provider-model-chip";
89
- chip.title = model.id;
90
- chip.textContent = model.id;
91
- list.appendChild(chip);
92
- });
93
-
94
- card.appendChild(list);
95
- providerGrid.appendChild(card);
96
- });
97
- }
98
-
99
- async function loadCatalog() {
100
- const response = await fetch("/api/catalog", { headers: { Accept: "application/json" } });
101
- if (!response.ok) {
102
- throw new Error("模型列表加载失败");
103
- }
104
- const payload = await response.json();
105
- catalogUpdated.textContent = formatDateTime(payload.synced_at || payload.generated_at);
106
- catalogState.providers = payload.providers || [];
107
- renderSummary(payload);
108
- renderFilterBar();
109
- renderProviders();
110
- }
111
-
112
- window.addEventListener("DOMContentLoaded", async () => {
113
- try {
114
- await loadCatalog();
115
- } catch (error) {
116
- catalogEmpty.textContent = error.message;
117
- providerGrid.innerHTML = "";
118
- providerFilterBar.innerHTML = "";
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;