Release OY (v0.51.438): model cost/health comparison table on Insights (#691) (#4260)

* 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:
nesquena-hermes 2026-06-15 12:12:16 -07:00 committed by GitHub
parent cdcede8ce7
commit b01f3d5930
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 180 additions and 0 deletions

View file

@ -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

View file

@ -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',

View file

@ -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}

View file

@ -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;}

View 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