git.iko.soy/opencode-android
on d518ef7 help sign in
60 lines 2.9 kB history raw
1package soy.iko.opencode.ui.chat
3import soy.iko.opencode.data.model.Tokens
5// NumberFormat.getNumberInstance performs an expensive ICU locale lookup + object
6// construction on every call. Reuse a thread-local instance so repeated calls (e.g.
7// when the message list re-seeds after a reconnect and every visible assistant bubble
8// recomposes at once) don't each pay that cost. Thread-local because NumberFormat is
9// not thread-safe.
10internal val tokenNumberFormat: ThreadLocal<java.text.NumberFormat> = ThreadLocal.withInitial {
11 java.text.NumberFormat.getNumberInstance(java.util.Locale.US)
14internal fun formatTokens(tokens: Tokens, format: String): String {
15 val nf = tokenNumberFormat.get()!!
16 return format.format(nf.format(tokens.input), nf.format(tokens.output))
19internal fun formatCost(cost: Double, shortFormat: String, longFormat: String): String =
20 // Locale.US so the formatting is stable regardless of device locale (avoids
21 // non-ASCII digits or comma decimal separators in a dollar amount). The format
22 // string itself is localized via strings.xml so the currency symbol can be
23 // adapted by translators.
24 if (cost < 0.01) String.format(java.util.Locale.US, longFormat, cost)
25 else String.format(java.util.Locale.US, shortFormat, cost)
27/** Assemble the single-line cost/tokens summary for an assistant bubble. Returns null when the
28 * message isn't complete, when neither cost nor tokens were reported, or when every figure is
29 * zero (so the bubble doesn't show a meaningless "0 in · 0 out • $0.0000"). Extracted from
30 * [AssistantBlock] to keep that function's cyclomatic complexity under the detekt threshold.
31 * Reasoning and cache segments are added only when non-zero. */
32internal fun buildCostSummary(
33 isComplete: Boolean,
34 tokens: Tokens?,
35 cost: Double?,
36 tokenFormat: String,
37 reasoningFormat: String,
38 cacheReadFormat: String,
39 cacheWriteFormat: String,
40 costShort: String,
41 costLong: String,
42): String? {
43 if (!isComplete || (cost == null && tokens == null)) return null
44 val nf = tokenNumberFormat.get()!!
45 return buildList {
46 // Skip an all-zero token count (e.g. a completed message that reported no usage) so
47 // the bubble doesn't show a meaningless "0 in · 0 out".
48 tokens?.takeIf { it.input > 0 || it.output > 0 }?.let { add(formatTokens(it, tokenFormat)) }
49 tokens?.takeIf { it.reasoning > 0 }?.let {
50 add(reasoningFormat.format(nf.format(it.reasoning)))
51 }
52 tokens?.cache?.takeIf { it.read > 0 }?.let {
53 add(cacheReadFormat.format(nf.format(it.read)))
54 }
55 tokens?.cache?.takeIf { it.write > 0 }?.let {
56 add(cacheWriteFormat.format(nf.format(it.write)))
57 }
58 cost?.takeIf { it > 0 }?.let { add(formatCost(it, costShort, costLong)) }
59 }.takeIf { it.isNotEmpty() }?.joinToString(" • ")