* stage-3634: static model-health comparison table on Insights (#691, rodboev) * stage-3634: model-health table + pt i18n keys + per-locale test (Codex fix) * Release OY: model cost/health comparison table on Insights (#691) Self-rebased rodboev's #3634 onto v0.51.437; Nathan-approved screenshot; full Codex+Opus+suite gate. Codex fix applied: added the 4 insights_model_health_* keys to the pt locale (was English-fallback) + tightened the i18n test to a per-locale check (was a weak count>=8). Co-authored-by: rodboev <rodboev@users.noreply.github.com> --------- Co-authored-by: nesquena-hermes <agent@nesquena-hermes> Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
parent
cdcede8ce7
commit
b01f3d5930
5 changed files with 180 additions and 0 deletions
|
|
@ -3,6 +3,12 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.438] — 2026-06-15 — Release OY (model cost/health comparison table on Insights)
|
||||
|
||||
### Added
|
||||
|
||||
- **The Insights panel has an opt-in "Model health comparison" reference table.** A collapsible (default-collapsed) table listing common models with their provider, input/output cost per 1M tokens, and a plain-English replacement/when-to-use guide — a quick at-a-glance cost reference alongside the usage analytics. Every cell is HTML-escaped; the data is static (no new API calls), and the four new i18n keys are present across all locales. (#691)
|
||||
|
||||
## [v0.51.437] — 2026-06-15 — Release OX (Escape blurs the composer for keyboard nav)
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -858,6 +858,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Tokens',
|
||||
insights_model_cost: 'Cost',
|
||||
insights_model_share: 'Share',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'No usage data yet',
|
||||
insights_footer: 'Showing data from the last {days} days',
|
||||
insights_skill_usage_title: 'Skill Usage',
|
||||
|
|
@ -2275,6 +2279,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Token',
|
||||
insights_model_cost: 'Costo',
|
||||
insights_model_share: 'Quota',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'Nessun dato di utilizzo',
|
||||
insights_footer: 'Dati mostrati dagli ultimi {days} giorni',
|
||||
insights_skill_usage_title: 'Utilizzo Skill',
|
||||
|
|
@ -3693,6 +3701,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'トークン',
|
||||
insights_model_cost: 'コスト',
|
||||
insights_model_share: 'シェア',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: '使用データはまだありません',
|
||||
insights_footer: '直近 {days} 日間のデータを表示',
|
||||
insights_skill_usage_title: 'スキル使用状況',
|
||||
|
|
@ -5579,6 +5591,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Tokens',
|
||||
insights_model_cost: 'Cost',
|
||||
insights_model_share: 'Share',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'No usage data yet',
|
||||
insights_footer: 'Showing data from the last {days} days', // TODO: translate
|
||||
insights_skill_usage_title: 'Skill Usage', // TODO: translate
|
||||
|
|
@ -6916,6 +6932,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Tokens',
|
||||
insights_model_cost: 'Cost',
|
||||
insights_model_share: 'Share',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'No usage data yet',
|
||||
insights_footer: 'Showing data from the last {days} days', // TODO: translate
|
||||
insights_skill_usage_title: 'Skill Usage', // TODO: translate
|
||||
|
|
@ -8269,6 +8289,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Tokens',
|
||||
insights_model_cost: 'Cost',
|
||||
insights_model_share: 'Share',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'No usage data yet',
|
||||
insights_footer: 'Showing data from the last {days} days', // TODO: translate
|
||||
insights_skill_usage_title: 'Skill Usage', // TODO: translate
|
||||
|
|
@ -9615,6 +9639,10 @@ const LOCALES = {
|
|||
insights_model_tokens: '令牌',
|
||||
insights_model_cost: '费用',
|
||||
insights_model_share: '占比',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: '暂无使用数据',
|
||||
insights_footer: '显示最近 {days} 天的数据',
|
||||
insights_skill_usage_title: '技能使用统计',
|
||||
|
|
@ -10507,6 +10535,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Token',
|
||||
insights_model_cost: '成本',
|
||||
insights_model_share: '占比',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: '尚無用量資料',
|
||||
insights_footer: '顯示最近 {days} 天的資料',
|
||||
insights_skill_usage_title: '技能使用情況',
|
||||
|
|
@ -12281,6 +12313,10 @@ const LOCALES = {
|
|||
insights_token_breakdown: 'Token Breakdown', // TODO: translate
|
||||
insights_tokens: 'Tokens', // TODO: translate
|
||||
insights_total: 'Total', // TODO: translate
|
||||
insights_model_health_title: 'Model health comparison', // TODO: translate
|
||||
insights_model_health_provider: 'Provider', // TODO: translate
|
||||
insights_model_health_replacement: 'Replacement guide', // TODO: translate
|
||||
insights_model_health_cost_per_m: 'Cost / 1M', // TODO: translate
|
||||
settings_desc_api_redact: 'Self-hosted users can disable for transparency (not recommended for shared instances).', // TODO: translate
|
||||
settings_label_api_redact: 'Redact sensitive data in API responses', // TODO: translate
|
||||
voice_error: 'Voice not supported in this browser', // TODO: translate
|
||||
|
|
@ -13662,6 +13698,10 @@ const LOCALES = {
|
|||
insights_model_tokens: '토큰',
|
||||
insights_model_cost: '비용',
|
||||
insights_model_share: '비율',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: '아직 사용 데이터가 없습니다',
|
||||
insights_footer: 'Showing data from the last {days} days', // TODO: translate
|
||||
insights_skill_usage_title: 'Skill Usage', // TODO: translate
|
||||
|
|
@ -14423,6 +14463,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Jetons',
|
||||
insights_model_cost: 'Coût',
|
||||
insights_model_share: 'Partager',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'Aucune donnée d\'utilisation pour l\'instant',
|
||||
insights_footer: 'Affichage des données des {days} derniers jours',
|
||||
insights_skill_usage_title: 'Utilisation des Skills',
|
||||
|
|
@ -16423,6 +16467,10 @@ const LOCALES = {
|
|||
insights_model_tokens: 'Jetonlar',
|
||||
insights_model_cost: 'Maliyet',
|
||||
insights_model_share: 'Paylaşmak',
|
||||
insights_model_health_title: 'Model health comparison',
|
||||
insights_model_health_provider: 'Provider',
|
||||
insights_model_health_replacement: 'Replacement guide',
|
||||
insights_model_health_cost_per_m: 'Cost / 1M',
|
||||
insights_no_usage_data: 'Henüz kullanım verisi yok',
|
||||
insights_footer: 'Son {days} günün verileri gösteriliyor',
|
||||
insights_skill_usage_title: 'Skill Usage', // TODO: translate
|
||||
|
|
@ -16754,6 +16802,10 @@ const LOCALES = {
|
|||
fork_from_here: 'Rozgałęź stąd',
|
||||
forked_from: 'Rozgałęzione z',
|
||||
subagent_children: 'Sesje subagentów',
|
||||
insights_model_health_title: 'Porównanie kondycji modeli',
|
||||
insights_model_health_provider: 'Dostawca',
|
||||
insights_model_health_replacement: 'Przewodnik zastąpienia',
|
||||
insights_model_health_cost_per_m: 'Koszt / 1 mln',
|
||||
btw_asking: 'Zadawanie pytania pobocznego…',
|
||||
btw_label: 'Pytanie poboczne — poza historią',
|
||||
btw_done: 'Odpowiedziano na pytanie poboczne',
|
||||
|
|
|
|||
|
|
@ -3441,6 +3441,30 @@ async function copyLogsAll() {
|
|||
}
|
||||
|
||||
// ── Insights panel ──
|
||||
const STATIC_MODEL_HEALTH_ROWS = [
|
||||
{id:'openai/gpt-5.4-mini', provider:'OpenAI', inputCostPerM:0.25, outputCostPerM:2.00, replacement:'Default economical general-purpose model'},
|
||||
{id:'openai/gpt-5.4', provider:'OpenAI', inputCostPerM:2.00, outputCostPerM:10.00, replacement:'Use for complex synthesis; fall back to Mini for routine turns'},
|
||||
{id:'anthropic/claude-sonnet-4.5', provider:'Anthropic', inputCostPerM:3.00, outputCostPerM:15.00, replacement:'Strong coding and analysis option; use Mini for low-risk chat'},
|
||||
{id:'google/gemini-2.5-pro', provider:'Google', inputCostPerM:1.25, outputCostPerM:10.00, replacement:'Long-context research option; use Flash for speed-sensitive work'},
|
||||
{id:'google/gemini-2.5-flash', provider:'Google', inputCostPerM:0.30, outputCostPerM:2.50, replacement:'Low-latency replacement for lighter multimodal or research turns'},
|
||||
];
|
||||
|
||||
function _renderModelHealthCost(row) {
|
||||
const input = Number(row.inputCostPerM || 0);
|
||||
const output = Number(row.outputCostPerM || 0);
|
||||
return `$${input.toFixed(2)} / $${output.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function _renderStaticModelHealthTable() {
|
||||
const rows = STATIC_MODEL_HEALTH_ROWS.map(row => `<div class="insights-table-row">
|
||||
<span class="insights-model-name" title="${esc(row.id)}">${esc(row.id)}</span>
|
||||
<span>${esc(row.provider)}</span>
|
||||
<span>${esc(_renderModelHealthCost(row))}</span>
|
||||
<span class="insights-model-health-replacement">${esc(row.replacement)}</span>
|
||||
</div>`).join('');
|
||||
return `<details class="insights-card insights-model-health-card"><summary><span class="insights-card-title">${esc(t('insights_model_health_title'))}</span></summary><div class="insights-table insights-model-health-table"><div class="insights-table-head"><span>${esc(t('insights_model_name'))}</span><span>${esc(t('insights_model_health_provider'))}</span><span>${esc(t('insights_model_health_cost_per_m'))}</span><span>${esc(t('insights_model_health_replacement'))}</span></div>${rows}</div></details>`;
|
||||
}
|
||||
|
||||
async function loadInsights(animate) {
|
||||
const box = $('insightsContent');
|
||||
const refreshBtn = $('insightsRefreshBtn');
|
||||
|
|
@ -3684,6 +3708,7 @@ function _renderInsights(d, box, wikiStatus, skillUsage) {
|
|||
} else {
|
||||
modelsHtml = `<div class="insights-card"><div class="insights-card-title">${esc(t('insights_models'))}</div><div class="insights-empty">${esc(t('insights_no_usage_data'))}</div></div>`;
|
||||
}
|
||||
const modelHealthHtml = _renderStaticModelHealthTable();
|
||||
|
||||
// Activity by day of week
|
||||
let dowHtml = '';
|
||||
|
|
@ -3737,6 +3762,7 @@ function _renderInsights(d, box, wikiStatus, skillUsage) {
|
|||
${overviewCards.map(c => `<div class="insights-stat"><div class="insights-stat-icon">${c.icon}</div><div class="insights-stat-info"><div class="insights-stat-value">${c.value}</div><div class="insights-stat-label">${esc(c.label)}</div></div></div>`).join('')}
|
||||
</div>
|
||||
${dailyHtml}
|
||||
${modelHealthHtml}
|
||||
<div class="insights-row insights-usage-grid">
|
||||
${tokenCards}
|
||||
${modelsHtml}
|
||||
|
|
|
|||
|
|
@ -5282,6 +5282,14 @@ main.main.showing-insights > #mainInsights{display:flex;overflow-y:auto;}
|
|||
.insights-table-head{display:grid;grid-template-columns:1fr 80px;padding:4px 0;border-bottom:1px solid var(--border);font-weight:600;color:var(--muted);font-size:11px;}
|
||||
.insights-table-row{display:grid;grid-template-columns:1fr 80px;padding:6px 0;border-bottom:1px solid var(--border,.05);}
|
||||
.insights-model-table .insights-table-head,.insights-model-table .insights-table-row{grid-template-columns:minmax(90px,1.5fr) 64px 76px 74px 52px;gap:8px;align-items:center;}
|
||||
.insights-model-health-card{padding:0;}
|
||||
.insights-model-health-card>summary{cursor:pointer;padding:12px 14px;}
|
||||
.insights-model-health-card>summary .insights-card-title{display:inline-block;margin-bottom:0;}
|
||||
.insights-model-health-card[open]>summary{padding-bottom:8px;}
|
||||
.insights-model-health-card .insights-model-health-table{padding:0 14px 12px;}
|
||||
.insights-model-health-table{overflow-x:auto;display:block;}
|
||||
.insights-model-health-table .insights-table-head,.insights-model-health-table .insights-table-row{grid-template-columns:minmax(130px,1.5fr) 86px 92px minmax(150px,1.4fr);gap:8px;align-items:center;min-width:540px;}
|
||||
.insights-model-health-replacement{overflow-wrap:anywhere;color:var(--muted);}
|
||||
.insights-model-cost,.insights-model-tokens{font-variant-numeric:tabular-nums;}
|
||||
.insights-model-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.insights-empty{font-size:12px;color:var(--muted);padding:12px 0;}
|
||||
|
|
|
|||
88
tests/test_issue691_model_health_table.py
Normal file
88
tests/test_issue691_model_health_table.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
PANELS_JS = (REPO / "static" / "panels.js").read_text(encoding="utf-8")
|
||||
I18N_JS = (REPO / "static" / "i18n.js").read_text(encoding="utf-8")
|
||||
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _function_body(src: str, signature: str) -> str:
|
||||
start = src.index(signature)
|
||||
brace = src.index("{", start)
|
||||
depth = 0
|
||||
for i in range(brace, len(src)):
|
||||
if src[i] == "{":
|
||||
depth += 1
|
||||
elif src[i] == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[start : i + 1]
|
||||
raise AssertionError(f"function body not found: {signature}")
|
||||
|
||||
|
||||
def test_static_model_health_metadata_and_renderer_exist():
|
||||
renderer = _function_body(PANELS_JS, "function _renderStaticModelHealthTable")
|
||||
|
||||
assert "const STATIC_MODEL_HEALTH_ROWS = [" in PANELS_JS
|
||||
assert "inputCostPerM" in PANELS_JS
|
||||
assert "outputCostPerM" in PANELS_JS
|
||||
assert "replacement:" in PANELS_JS
|
||||
assert '<details class="insights-card insights-model-health-card">' in renderer
|
||||
assert "<summary>" in renderer
|
||||
assert "insights-model-health-table" in renderer
|
||||
assert "insights_model_health_cost_per_m" in renderer
|
||||
assert "insights_model_health_replacement" in renderer
|
||||
assert "insights_model_health_quality" not in renderer
|
||||
assert "insights_model_health_hallucination" not in renderer
|
||||
|
||||
|
||||
def test_render_insights_includes_static_table_before_usage_models():
|
||||
render = _function_body(PANELS_JS, "function _renderInsights")
|
||||
|
||||
assert "const modelHealthHtml = _renderStaticModelHealthTable();" in render
|
||||
assert "${modelHealthHtml}" in render
|
||||
assert render.index("${modelHealthHtml}") < render.index("insights-usage-grid")
|
||||
assert "modelsHtml" in render
|
||||
|
||||
|
||||
def test_no_runtime_quality_or_hallucination_fetch_added():
|
||||
load = _function_body(PANELS_JS, "async function loadInsights")
|
||||
|
||||
assert "quality" not in load.lower()
|
||||
assert "hallucination" not in load.lower()
|
||||
assert "/api/model" not in load
|
||||
assert "api(`/api/insights?days=${period}`)" in load
|
||||
|
||||
|
||||
def test_model_health_i18n_keys_exist_in_locale_blocks():
|
||||
keys = [
|
||||
"insights_model_health_title",
|
||||
"insights_model_health_provider",
|
||||
"insights_model_health_replacement",
|
||||
"insights_model_health_cost_per_m",
|
||||
]
|
||||
# Split i18n.js into per-locale blocks (top-level " <code>: {" entries) and
|
||||
# assert EVERY locale carries all four keys — a `count >= N` check silently
|
||||
# tolerates a locale missing the keys (English fallback). #3634 pt gap.
|
||||
import re
|
||||
|
||||
locale_starts = [
|
||||
(m.group(1), m.start())
|
||||
for m in re.finditer(r"""^ ['"]?([A-Za-z_-]+)['"]?: \{$""", I18N_JS, re.MULTILINE)
|
||||
]
|
||||
assert len(locale_starts) >= 13, f"expected 13+ locale blocks, found {len(locale_starts)}"
|
||||
bounds = locale_starts + [("__end__", len(I18N_JS))]
|
||||
for i, (code, start) in enumerate(locale_starts):
|
||||
block = I18N_JS[start : bounds[i + 1][1]]
|
||||
for key in keys:
|
||||
assert f"{key}:" in block, f"locale '{code}' is missing {key}"
|
||||
|
||||
|
||||
def test_model_health_table_css_is_responsive_and_contained():
|
||||
assert ".insights-model-health-table" in STYLE_CSS
|
||||
assert ".insights-model-health-table .insights-table-head" in STYLE_CSS
|
||||
assert ".insights-model-health-card>summary" in STYLE_CSS
|
||||
assert "minmax(130px,1.5fr)" in STYLE_CSS
|
||||
assert "overflow-x:auto" in STYLE_CSS
|
||||
assert ".insights-model-health-replacement" in STYLE_CSS
|
||||
Loading…
Reference in a new issue