Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3440519
feat(install): implement Windows service installation via kardianos/s…
yeerliin Mar 30, 2026
965ec05
feat(mcp): implement Windows MCP stdio shim with LockFileEx
yeerliin Mar 30, 2026
72c8ee3
docs: add Windows installation instructions to README
yeerliin Mar 30, 2026
f7685a8
feat(benchmark): per-model evaluation for multi-model agents
yeerliin Mar 31, 2026
705b86f
feat(benchmark): add composite score for quantitative model comparison
yeerliin Mar 31, 2026
4965667
feat(benchmark): add pairwise model comparison with recommendations
yeerliin Mar 31, 2026
997fd56
fix(benchmark): normalize model names to prevent duplicates
yeerliin Mar 31, 2026
9b2c099
feat(store): add per-model benchmark queries
yeerliin Mar 31, 2026
d5580df
feat(runner): per-model benchmark pipeline
yeerliin Mar 31, 2026
d75c5d3
feat(tui): per-model benchmark display with ranked comparison panel
yeerliin Mar 31, 2026
563e7af
feat(web): add browser-based benchmark dashboard
yeerliin Mar 31, 2026
1cf8d22
feat(web): add tracking tab, i18n, and detail panel improvements
yeerliin Mar 31, 2026
1346a01
docs: add web dashboard section to README
yeerliin Mar 31, 2026
815a717
feat(web): on-demand benchmark run from dashboard
yeerliin Mar 31, 2026
cf7545c
fix(install): add Windows SCM auto-recovery on service failure
yeerliin Mar 31, 2026
5dd0a24
feat(daemon): embed web dashboard into daemon service
yeerliin Mar 31, 2026
69af0e1
fix(decision): ignore ROI when TotalCostUSD==0
kiosvantra Apr 1, 2026
1979819
fix: reconcile rebased benchmark and TUI changes
github-actions[bot] Apr 2, 2026
dbb592d
Merge origin/main into pr-4 resolving conflicts for #4
abdfayad100 Apr 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions configs/thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"max_error_rate": 0.30,
"max_cost_spike_multiplier": 3.0
},
"score_weights": {
"accuracy": 0.40,
"latency": 0.20,
"tool_success_rate": 0.20,
"roi_score": 0.20
},
"per_agent": {},
"model_pricing": {
"note": "Output price per 1M tokens in USD. A value of 0 means the model is free — ROI/cost checks are skipped. Update when prices change.",
Expand Down
228 changes: 228 additions & 0 deletions internal/benchmark/comparison.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
package benchmark

import (
"fmt"
"math"

"github.com/kiosvantra/metronous/internal/store"
)

// MetricDelta represents the difference between two models for a single metric.
type MetricDelta struct {
// MetricName is the human-readable metric name.
MetricName string

// ModelAValue is the metric value for model A.
ModelAValue float64

// ModelBValue is the metric value for model B.
ModelBValue float64

// Delta is B - A (positive = B is higher).
Delta float64

// DeltaPct is the percentage change: (B-A)/A * 100; 0 if A is 0.
DeltaPct float64

// BetterModel is "A", "B", or "tie".
BetterModel string
}

// ModelComparison holds the full side-by-side comparison of two benchmark runs.
type ModelComparison struct {
// AgentID is the agent being compared.
AgentID string

// ModelA is the first model name.
ModelA string

// ModelB is the second model name.
ModelB string

// ScoreA is the composite score for run A.
ScoreA float64

// ScoreB is the composite score for run B.
ScoreB float64

// ScoreDelta is ScoreA - ScoreB (positive = A better).
ScoreDelta float64

// Deltas contains per-metric deltas (Score, Accuracy, P95Latency, ToolSuccess, Cost).
Deltas []MetricDelta

// Winner is "A", "B", or "tie" based on CompositeScore comparison.
Winner string

// BetterModel is the model identifier of the winner, or "" when tied.
BetterModel string

// Recommendation is a generated human-readable sentence.
Recommendation string

// AHasInsufficient is true when runA.Verdict == INSUFFICIENT_DATA.
AHasInsufficient bool

// BHasInsufficient is true when runB.Verdict == INSUFFICIENT_DATA.
BHasInsufficient bool

// CostDelta is A.TotalCostUSD - B.TotalCostUSD.
CostDelta float64

// CostDeltaPct is ((A-B)/B)*100; 0 when B cost is 0.
CostDeltaPct float64

// AccuracyDelta is A.Accuracy - B.Accuracy.
AccuracyDelta float64

// LatencyDeltaMs is A.P95LatencyMs - B.P95LatencyMs (negative = A faster).
LatencyDeltaMs float64

// ToolSuccessDelta is A.ToolSuccessRate - B.ToolSuccessRate.
ToolSuccessDelta float64

// ROIDelta is A.ROIScore - B.ROIScore.
ROIDelta float64
}

// tieMargin is the absolute score margin within which two models are considered tied.
// Spec FR-COMP-EC-03 says |delta| < 0.01. We add a small float64 epsilon to handle
// cases like 0.76 - 0.75 which in binary floating point is slightly above 0.01.
const tieMargin = 0.01 + 1e-10

// CompareModels produces a pairwise comparison between two BenchmarkRun records.
// runA and runB may belong to any agent (the caller is responsible for filtering).
// This is a PURE FUNCTION: no I/O, no DB calls, no side effects.
func CompareModels(runA, runB store.BenchmarkRun) ModelComparison {
scoreDelta := runA.CompositeScore - runB.CompositeScore

// Determine winner by composite score.
var winner, betterModel string
if math.Abs(scoreDelta) <= tieMargin {
winner = "tie"
betterModel = ""
} else if scoreDelta > 0 {
winner = "A"
betterModel = runA.Model
} else {
winner = "B"
betterModel = runB.Model
}

// Compute individual metric deltas.
costDelta := runA.TotalCostUSD - runB.TotalCostUSD
var costDeltaPct float64
if runB.TotalCostUSD != 0 {
costDeltaPct = (costDelta / runB.TotalCostUSD) * 100
}

accuracyDelta := runA.Accuracy - runB.Accuracy
latencyDelta := runA.P95LatencyMs - runB.P95LatencyMs
toolDelta := runA.ToolSuccessRate - runB.ToolSuccessRate
roiDelta := runA.ROIScore - runB.ROIScore

deltas := []MetricDelta{
buildDelta("Composite Score", runA.CompositeScore, runB.CompositeScore, true, tieMargin),
buildDelta("Accuracy", runA.Accuracy, runB.Accuracy, true, 0.02),
buildDelta("P95 Latency", runA.P95LatencyMs, runB.P95LatencyMs, false, 0),
buildDelta("Tool Success", runA.ToolSuccessRate, runB.ToolSuccessRate, true, 0.02),
buildDelta("Cost", runA.TotalCostUSD, runB.TotalCostUSD, false, 0),
}

// Generate recommendation sentence.
recommendation := buildRecommendation(winner, betterModel, scoreDelta, runA, runB)

return ModelComparison{
AgentID: runA.AgentID,
ModelA: runA.Model,
ModelB: runB.Model,
ScoreA: runA.CompositeScore,
ScoreB: runB.CompositeScore,
ScoreDelta: scoreDelta,
Deltas: deltas,
Winner: winner,
BetterModel: betterModel,
Recommendation: recommendation,
AHasInsufficient: runA.Verdict == store.VerdictInsufficientData,
BHasInsufficient: runB.Verdict == store.VerdictInsufficientData,
CostDelta: costDelta,
CostDeltaPct: costDeltaPct,
AccuracyDelta: accuracyDelta,
LatencyDeltaMs: latencyDelta,
ToolSuccessDelta: toolDelta,
ROIDelta: roiDelta,
}
}

// buildDelta constructs a MetricDelta for one metric.
// higherIsBetter indicates whether a higher value is preferred.
// tiePct is the absolute margin within which the metric is considered tied (0 = no tie logic).
func buildDelta(name string, aVal, bVal float64, higherIsBetter bool, tiePct float64) MetricDelta {
delta := bVal - aVal // positive = B higher
var deltaPct float64
if aVal != 0 {
deltaPct = (delta / math.Abs(aVal)) * 100
}

var better string
if tiePct > 0 && math.Abs(delta) <= tiePct {
better = "tie"
} else if delta == 0 {
better = "tie"
} else if higherIsBetter {
if delta > 0 {
better = "B"
} else {
better = "A"
}
} else {
// Lower is better (latency, cost).
if delta < 0 {
better = "B" // B is lower → B wins
} else {
better = "A"
}
}

return MetricDelta{
MetricName: name,
ModelAValue: aVal,
ModelBValue: bVal,
Delta: delta,
DeltaPct: deltaPct,
BetterModel: better,
}
}

// buildRecommendation generates the human-readable recommendation sentence.
func buildRecommendation(winner, betterModel string, scoreDelta float64, runA, runB store.BenchmarkRun) string {
if winner == "tie" {
return "Both models are equivalent (score delta < 0.01)"
}

scoreDeltaPts := math.Abs(scoreDelta) * 100

var winnerRun, loserRun store.BenchmarkRun
if winner == "A" {
winnerRun = runA
loserRun = runB
} else {
winnerRun = runB
loserRun = runA
}

// Accuracy delta relative to loser.
var accPct float64
if loserRun.Accuracy != 0 {
accPct = (winnerRun.Accuracy - loserRun.Accuracy) / loserRun.Accuracy * 100
}

// Cost delta relative to winner (positive = winner is more expensive).
var costPct float64
if winnerRun.TotalCostUSD != 0 {
costPct = (winnerRun.TotalCostUSD - loserRun.TotalCostUSD) / math.Abs(loserRun.TotalCostUSD+1e-10) * 100
}

return fmt.Sprintf("%s scores %.1fpts higher overall (%+.1f%% accuracy, %+.1f%% cost)",
betterModel, scoreDeltaPts, accPct, costPct)
}
Loading
Loading