fix(cli): format stats with Locale.ROOT so output is locale-independent

renderStats used the default-locale String.format, so under a comma-decimal
locale (e.g. ru_RU) percentages/rates rendered as "58,3" — both producing
locale-dependent CLI output and failing StatsRenderTest. Format all five
numeric rows with Locale.ROOT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 18:59:38 +00:00
parent 33baaed903
commit 7909357747
@@ -13,6 +13,7 @@ import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import java.util.Locale
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -110,9 +111,10 @@ fun renderStats(report: StatsReportDto): String {
lines += " calls: ${report.inferenceCount}" lines += " calls: ${report.inferenceCount}"
lines += " tokens: ${report.promptTokens} prompt + ${report.completionTokens} completion = " + lines += " tokens: ${report.promptTokens} prompt + ${report.completionTokens} completion = " +
"${report.promptTokens + report.completionTokens}" "${report.promptTokens + report.completionTokens}"
lines += " time: ${report.inferenceMs} ms (%.1f tok/s)".format(report.tokensPerSecond) lines += " time: ${report.inferenceMs} ms (%.1f tok/s)".format(Locale.ROOT, report.tokensPerSecond)
for (p in report.perProvider) { for (p in report.perProvider) {
lines += " %-20s %4d calls %6d tok %8d ms (%.1f tok/s)".format( lines += " %-20s %4d calls %6d tok %8d ms (%.1f tok/s)".format(
Locale.ROOT,
p.provider, p.completedCount, p.promptTokens + p.completionTokens, p.totalLatencyMs, p.tokensPerSecond, p.provider, p.completedCount, p.promptTokens + p.completionTokens, p.totalLatencyMs, p.tokensPerSecond,
) )
} }
@@ -121,7 +123,7 @@ fun renderStats(report: StatsReportDto): String {
lines += "Tools" lines += "Tools"
lines += " calls: ${report.toolCount} time: ${report.toolMs} ms" lines += " calls: ${report.toolCount} time: ${report.toolMs} ms"
for (t in report.perTool) { for (t in report.perTool) {
lines += " %-24s %4d %8d ms".format(t.toolName, t.completedCount, t.totalDurationMs) lines += " %-24s %4d %8d ms".format(Locale.ROOT, t.toolName, t.completedCount, t.totalDurationMs)
} }
lines += "" lines += ""
@@ -131,6 +133,7 @@ fun renderStats(report: StatsReportDto): String {
lines += " total wait: ${report.approvalWaitMs} ms (avg ${report.avgApprovalWaitMs} ms)" lines += " total wait: ${report.approvalWaitMs} ms (avg ${report.avgApprovalWaitMs} ms)"
for (tier in report.perTier) { for (tier in report.perTier) {
lines += " %-4s req %d res %d wait %d ms (avg %d ms)".format( lines += " %-4s req %d res %d wait %d ms (avg %d ms)".format(
Locale.ROOT,
tier.tier, tier.requestedCount, tier.resolvedCount, tier.totalWaitMs, tier.avgWaitMs, tier.tier, tier.requestedCount, tier.resolvedCount, tier.totalWaitMs, tier.avgWaitMs,
) )
} }
@@ -145,6 +148,7 @@ fun renderStats(report: StatsReportDto): String {
lines += "Time accounting (% of session wall time)" lines += "Time accounting (% of session wall time)"
lines += " inference: %.1f%% tools: %.1f%% approval-wait: %.1f%% other: %.1f%%".format( lines += " inference: %.1f%% tools: %.1f%% approval-wait: %.1f%% other: %.1f%%".format(
Locale.ROOT,
report.inferencePct, report.toolPct, report.approvalWaitPct, otherPct(report), report.inferencePct, report.toolPct, report.approvalWaitPct, otherPct(report),
) )