From 42f4b0cc38fac580c80c703ed3cca286f721eea8 Mon Sep 17 00:00:00 2001 From: ThreeFish Date: Tue, 26 May 2026 17:27:02 +0800 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20=E6=96=B0=E5=A2=9E=20Model?= =?UTF-8?q?=20Calling=20=E5=AE=9E=E6=97=B6=E7=8A=B6=E6=80=81=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=EF=BC=8C=E5=8F=AF=E8=A7=86=E5=8C=96=E6=AF=8F=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=B9=B6=E5=8F=91=E4=B8=8E=E6=8E=92=E9=98=9F=E6=B7=B1?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ๐Ÿค– Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist) Co-Authored-By: Aurelius Huang --- src/coding/proxy/server/dashboard.py | 163 ++++++++++++++++++++++++ src/coding/proxy/vendors/concurrency.py | 7 +- src/coding/proxy/vendors/zhipu.py | 9 ++ 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/coding/proxy/server/dashboard.py b/src/coding/proxy/server/dashboard.py index 54533e6..c81b72c 100644 --- a/src/coding/proxy/server/dashboard.py +++ b/src/coding/proxy/server/dashboard.py @@ -557,6 +557,89 @@ def _build_favicon() -> bytes: .tab-btn:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: 2px; } .tab-pane { display: none; } .tab-pane.active { display: block; } + + /* โ”€โ”€ Model Calling ๅฎžๆ—ถ็Šถๆ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ */ + .model-calling-card { + margin-bottom: 5px; + } + .mc-empty { + text-align: center; + color: var(--text-muted); + padding: 16px 0; + font-size: 13px; + } + .mc-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 8px; + } + .mc-model-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 12px; + background: var(--bg-secondary); + border-radius: var(--radius-sm); + border: 1px solid var(--border-subtle); + } + .mc-model-name { + font-family: 'JetBrains Mono', monospace; + font-size: 12px; + color: var(--text-primary); + min-width: 140px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .mc-bar-wrap { + flex: 1; + min-width: 60px; + height: 6px; + background: rgba(255,255,255,.06); + border-radius: 3px; + overflow: hidden; + } + .mc-bar-fill { + height: 100%; + border-radius: 3px; + transition: width .3s ease, background .3s ease; + } + .mc-bar-fill.mc-low { background: var(--accent-green); } + .mc-bar-fill.mc-mid { background: var(--accent-yellow); } + .mc-bar-fill.mc-high { background: var(--accent-red); } + .mc-stats { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-family: 'JetBrains Mono', monospace; + color: var(--text-muted); + white-space: nowrap; + } + .mc-badge { + display: inline-flex; + align-items: center; + padding: 1px 6px; + border-radius: 4px; + font-size: 10px; + font-weight: 600; + font-family: 'JetBrains Mono', monospace; + } + .mc-badge-pending { + background: rgba(251,146,60,.15); + color: #fb923c; + } + .mc-badge-active { + background: rgba(74,222,128,.12); + color: #4ade80; + } + .mc-vendor-tag { + font-size: 10px; + color: var(--text-muted); + background: rgba(255,255,255,.06); + padding: 1px 6px; + border-radius: 3px; + } @@ -626,6 +709,14 @@ def _build_favicon() -> bytes: + +
+
๐Ÿ“ก Model Calling ๅฎžๆ—ถ็Šถๆ€
+
+
ๅŠ ่ฝฝไธญโ€ฆ
+
+
+
@@ -1134,6 +1225,74 @@ def _build_favicon() -> bytes: }).join(''); } +// โ”€โ”€ Model Calling ๅฎžๆ—ถ็Šถๆ€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function updateModelCalling(status) { + var wrap = document.getElementById('model-calling-wrap'); + if (!wrap) return; + var tiers = status.tiers || []; + + // ๆ”ถ้›†ๆ‰€ๆœ‰ๅธฆ concurrency ่ฏŠๆ–ญ็š„ๆจกๅž‹ + var models = []; + for (var i = 0; i < tiers.length; i++) { + var tier = tiers[i]; + var diag = tier.diagnostics || {}; + var conc = diag.concurrency; + if (!conc) continue; + var names = Object.keys(conc); + for (var j = 0; j < names.length; j++) { + var model = names[j]; + var d = conc[model]; + models.push({ + vendor: tier.name, + model: model, + limit: d.limit || 0, + in_use: d.in_use || 0, + available: d.available || 0, + pending: d.pending || 0, + }); + } + } + + if (!models.length) { + wrap.innerHTML = '
ๆ— ๆดป่ทƒๆจกๅž‹่ฐƒ็”จ
'; + return; + } + + var html = '
'; + for (var k = 0; k < models.length; k++) { + var m = models[k]; + var pct = m.limit > 0 ? Math.round((m.in_use / m.limit) * 100) : 0; + var barClass = pct <= 50 ? 'mc-low' : (pct <= 80 ? 'mc-mid' : 'mc-high'); + + html += '
' + + '' + escapeHtml(m.vendor + '/' + m.model) + '' + + '
' + + '
' + + '' + m.in_use + '/' + m.limit + '' + + (m.pending > 0 ? 'โณ ' + m.pending + '' : '') + + '
' + + '
'; + } + html += '
'; + wrap.innerHTML = html; +} + +// Model Calling ็‹ฌ็ซ‹็Ÿญ้—ด้š”่ฝฎ่ฏข +var _mcTimer = null; +function startModelCallingPoll() { + stopModelCallingPoll(); + function tick() { + fetchJSON('/api/status').then(function(status) { + updateModelCalling(status); + }).catch(function() {}); + } + tick(); + _mcTimer = setInterval(tick, 5000); +} +function stopModelCallingPoll() { + if (_mcTimer) { clearInterval(_mcTimer); _mcTimer = null; } +} + // โ”€โ”€ ๆŒ‰ tiers ้กบๅบๆŽ’ๅบ vendor ๅˆ—่กจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function sortByTierOrder(vendors, tierOrder) { if (!tierOrder || !tierOrder.length) return vendors.sort(); @@ -1713,6 +1872,7 @@ def _build_favicon() -> bytes: updateKPI(summary); updateVendorStatus(status); + updateModelCalling(status); updateChartTitles(days); const rows = timeline.rows || []; @@ -1788,6 +1948,8 @@ def _build_favicon() -> bytes: currentTab = name; applyTabState(name); syncTabUrl(name); + // Model Calling ่ฝฎ่ฏข้š้กต็ญพๅˆ‡ๆขๅฏๅœ + if (name === 'overview') { startModelCallingPoll(); } else { stopModelCallingPoll(); } refresh(); } @@ -1807,6 +1969,7 @@ def _build_favicon() -> bytes: }).catch(function(){}); refresh(); // ไป…ๅŠ ่ฝฝๅˆๅง‹้กต็ญพ็š„ๆ•ฐๆฎ setInterval(refresh, 600000); // ๆฏ 10 ๅˆ†้’Ÿๅˆทๆ–ฐๅฝ“ๅ‰้กต็ญพ + if (initial === 'overview') startModelCallingPoll(); })(); diff --git a/src/coding/proxy/vendors/concurrency.py b/src/coding/proxy/vendors/concurrency.py index b4f4df7..148bb53 100644 --- a/src/coding/proxy/vendors/concurrency.py +++ b/src/coding/proxy/vendors/concurrency.py @@ -67,10 +67,15 @@ def get_diagnostics(self) -> dict[str, dict[str, int]]: limit = self._config.get_limit(model) # asyncio.Semaphore ๅ†…้ƒจ _value ่กจ็คบๅ‰ฉไฝ™ๅฏ็”จๆงฝไฝ available = sem._value # noqa: SLF001 โ€” ๅ…ฌๅผ€ API ๆœชๆšด้œฒ + in_use = max(limit - available, 0) + # _waiters ไธบๆญฃๅœจๆŽ’้˜Ÿ็ญ‰ๅพ…็š„ๅ็จ‹้›†ๅˆ๏ผŒๆ— ็ญ‰ๅพ…่€…ๆ—ถไธบ None + waiters = getattr(sem, "_waiters", None) # noqa: SLF001 + pending = len(waiters) if waiters else 0 snapshot[model] = { "limit": limit, - "in_use": max(limit - available, 0), + "in_use": in_use, "available": max(available, 0), + "pending": pending, } return snapshot diff --git a/src/coding/proxy/vendors/zhipu.py b/src/coding/proxy/vendors/zhipu.py index ff186cd..b6de695 100644 --- a/src/coding/proxy/vendors/zhipu.py +++ b/src/coding/proxy/vendors/zhipu.py @@ -206,6 +206,15 @@ async def _maybe_acquire_concurrency_slot( return None return await self._concurrency_limiter.acquire(mapped_model) + # โ”€โ”€ ่ฏŠๆ–ญไฟกๆฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + def get_diagnostics(self) -> dict[str, Any]: + """่ฟ”ๅ›žไพ›ๅบ”ๅ•†่ฟ่กŒๆ—ถ่ฏŠๆ–ญไฟกๆฏ๏ผŒๅŒ…ๅซๆฏๆจกๅž‹ๅนถๅ‘็Šถๆ€.""" + diagnostics = super().get_diagnostics() + if self._concurrency_limiter is not None: + diagnostics["concurrency"] = self._concurrency_limiter.get_diagnostics() + return diagnostics + # โ”€โ”€ ๅปถ่ฟŸ่ฎก็ฎ— โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def _compute_retry_delay_from_headers(