返回 CodeWhale
cache_format.rs
根目录 / crates / tui / src / commands / groups / debug / cache_format.rs
1 //! Pure cache inspection, history and status formatting for `/cache`.
2 //! Route resolution, pricing classes and elapsed observation stay host-owned.
3
4 use crate::diagnostics_reports::format_cost_amount_precise;
5 use codewhale_command_contract::facets::{
6 CommandPresentationContext, DebugCacheTelemetry, DebugCacheTurn, DebugCostProjection,
7 DebugPromptInspection, DebugWarmupKey,
8 };
9 use std::collections::{BTreeMap, BTreeSet};
10
11 pub(crate) fn format_warmup_status(
12 last_warmup: Option<&DebugWarmupKey>,
13 current: &DebugWarmupKey,
14 last_hash_short: Option<&str>,
15 current_hash_short: &str,
16 ) -> String {
17 match last_warmup {
18 None => format!(
19 "Warmup status: no previous warmup (current key: {})\n",
20 current_hash_short
21 ),
22 Some(previous) if previous == current => {
23 format!(
24 "Warmup status: valid (key {} matches)\n",
25 current_hash_short
26 )
27 }
28 Some(previous) => {
29 let mut reasons = Vec::new();
30 if previous.provider != current.provider {
31 reasons.push("provider changed");
32 }
33 if previous.model != current.model {
34 reasons.push("model changed");
35 }
36 if previous.base_url != current.base_url {
37 reasons.push("base URL changed");
38 }
39 if previous.static_prefix_hash != current.static_prefix_hash {
40 reasons.push("static prefix changed");
41 }
42 if previous.tool_catalog_hash != current.tool_catalog_hash {
43 reasons.push("tool catalog changed");
44 }
45 if previous.project_pack_hash != current.project_pack_hash {
46 reasons.push("project pack changed");
47 }
48 if previous.skills_hash != current.skills_hash {
49 reasons.push("skills changed");
50 }
51 let reason_text = if reasons.is_empty() {
52 "unknown prefix input changed".to_string()
53 } else {
54 reasons.join(", ")
55 };
56 format!(
57 "Warmup status: invalid ({} -> {}; {})\n",
58 last_hash_short.unwrap_or(""),
59 current_hash_short,
60 reason_text
61 )
62 }
63 }
64 }
65
66 pub(super) fn format_verbose_diff(
67 previous: &DebugPromptInspection,
68 current: &DebugPromptInspection,
69 ) -> String {
70 let mut out = String::new();
71 let max_len = previous.layers.len().max(current.layers.len());
72 for index in 0..max_len {
73 match (previous.layers.get(index), current.layers.get(index)) {
74 (Some(prev), Some(curr)) if prev == curr => {
75 out.push_str(&format!(" [{index}] {} unchanged\n", curr.name));
76 }
77 (Some(prev), Some(curr)) => {
78 out.push_str(&format!(" [{index}] {} changed\n", curr.name));
79 if prev.name != curr.name {
80 out.push_str(&format!(" name: {} -> {}\n", prev.name, curr.name));
81 }
82 if prev.stability != curr.stability {
83 out.push_str(&format!(
84 " stability: {} -> {}\n",
85 prev.stability.label(),
86 curr.stability.label()
87 ));
88 }
89 if prev.char_len != curr.char_len {
90 out.push_str(&format!(
91 " chars: {} -> {} ({:+})\n",
92 prev.char_len,
93 curr.char_len,
94 curr.char_len as i64 - prev.char_len as i64
95 ));
96 }
97 if prev.sha256 != curr.sha256 {
98 out.push_str(&format!(
99 " hash: {} -> {}\n",
100 short_hash(&prev.sha256),
101 short_hash(&curr.sha256)
102 ));
103 }
104 }
105 (None, Some(curr)) => {
106 out.push_str(&format!(" [{index}] {} added\n", curr.name));
107 }
108 (Some(prev), None) => {
109 out.push_str(&format!(" [{index}] {} removed\n", prev.name));
110 }
111 (None, None) => unreachable!("index is within max_len"),
112 }
113 }
114 out
115 }
116
117 fn short_hash(hash: &str) -> &str {
118 &hash[..hash.len().min(12)]
119 }
120
121 /// Render a prefix-cache stability and health summary for `/cache stats`.
122 ///
123 /// Surfaces the current prefix fingerprint, stability ratio, change history,
124 /// and an aggregated cache-hit summary from per-turn telemetry. When the
125 /// prefix has changed, a prominent warning is included so users can
126 /// correlate cache misses with prefix drift.
127 pub(super) fn format_cache_stats(telemetry: &DebugCacheTelemetry) -> String {
128 let mut out = String::new();
129 out.push_str("Cache Stats\n");
130
131 // ── Prefix stability ──────────────────────────────────────────────
132 out.push_str("\n── Prefix Stability\n");
133 match telemetry.prefix_stability_pct {
134 Some(pct) => {
135 let checks = telemetry.prefix_checks_total;
136 let changes = telemetry.prefix_change_count;
137 let stable_checks = checks.saturating_sub(changes);
138
139 let drift = telemetry.prefix_drift_count;
140 if changes == 0 {
141 out.push_str(&format!(
142 " Stability: {pct}% ({stable_checks}/{checks} checks)\n"
143 ));
144 out.push_str(" Status: stable (no prefix changes this session)\n");
145 if telemetry.prefix_context_updates > 0 {
146 out.push_str(&format!(
147 " Context updates: {} (workspace drift delivered as history, header unchanged)\n",
148 telemetry.prefix_context_updates
149 ));
150 }
151 } else {
152 out.push_str(&format!(
153 " Stability: {pct}% ({stable_checks}/{checks} checks, {changes} change{})\n",
154 if changes == 1 { "" } else { "s" }
155 ));
156 if drift == 0 {
157 out.push_str(
158 " Status: stable (all changes were declared header changes)\n",
159 );
160 } else {
161 out.push_str(&format!(
162 " Status: WARNING — {drift} undeclared drift{}\n",
163 if drift == 1 { "" } else { "s" }
164 ));
165 }
166 if let Some(ref reason) = telemetry.prefix_pin_reason {
167 out.push_str(&format!(" Pin reason: {reason}\n"));
168 }
169 if telemetry.prefix_context_updates > 0 {
170 out.push_str(&format!(
171 " Context updates: {} (workspace drift delivered as history, header unchanged)\n",
172 telemetry.prefix_context_updates
173 ));
174 }
175 if let Some(ref reason) = telemetry.prefix_last_miss_reason {
176 out.push_str(&format!(" Last miss: {reason}\n"));
177 }
178 if let Some(ref desc) = telemetry.last_prefix_change_desc {
179 out.push_str(&format!(" Last change: {desc}\n"));
180 }
181 }
182 }
183 None => {
184 out.push_str(" Stability: unknown (no checks recorded yet)\n");
185 out.push_str(" Run a turn first to collect prefix stability data.\n");
186 }
187 }
188
189 // ── Prefix fingerprint ────────────────────────────────────────────
190 out.push_str("\n── Prefix Fingerprint\n");
191 match &telemetry.last_pinned_prefix_hash {
192 Some(hash) => {
193 out.push_str(&format!(" Pinned hash: {hash}\n"));
194 let short = if hash.len() >= 12 { &hash[..12] } else { hash };
195 out.push_str(&format!(" Short id: {short}\n"));
196 if telemetry.prefix_drift_count > 0 {
197 out.push_str(" Drift: WARNING — undeclared hash change this session\n");
198 out.push_str(&format!(
199 " ({change} change{plural} detected, {drift} undeclared)\n",
200 change = telemetry.prefix_change_count,
201 plural = if telemetry.prefix_change_count == 1 {
202 ""
203 } else {
204 "s"
205 },
206 drift = telemetry.prefix_drift_count,
207 ));
208 } else if telemetry.prefix_change_count > 0 {
209 out.push_str(" Drift: none (all changes were declared)\n");
210 out.push_str(&format!(
211 " ({change} change{plural} detected)\n",
212 change = telemetry.prefix_change_count,
213 plural = if telemetry.prefix_change_count == 1 {
214 ""
215 } else {
216 "s"
217 },
218 ));
219 } else {
220 out.push_str(" Drift: none (hash stable)\n");
221 }
222 }
223 None => {
224 out.push_str(" Pinned hash: unavailable\n");
225 out.push_str(" Run a turn first, or use /cache inspect.\n");
226 }
227 }
228
229 // ── Cache hit-rate summary ────────────────────────────────────────
230 out.push_str("\n── Cache Hit Rate\n");
231 let history = &telemetry.history;
232 if history.is_empty() {
233 out.push_str(" No turn telemetry recorded yet.\n");
234 } else {
235 // Aggregate only cache-aware turns; skip turns where the provider
236 // did not report cache telemetry (cache_hit_tokens is None).
237 // When cache_miss_tokens is None, infer it as
238 // input_tokens − cache_hit_tokens (matches /cache table logic).
239 let mut turns = 0u64;
240 let (hit, miss, input) =
241 telemetry
242 .history
243 .iter()
244 .fold((0u64, 0u64, 0u64), |(hit, miss, input), rec| {
245 let Some(hit_tokens) = rec.cache_hit_tokens else {
246 return (hit, miss, input);
247 };
248 let h = u64::from(hit_tokens);
249 let m = u64::from(
250 rec.cache_miss_tokens
251 .unwrap_or(rec.input_tokens.saturating_sub(hit_tokens)),
252 );
253 turns += 1;
254 (hit + h, miss + m, input + u64::from(rec.input_tokens))
255 });
256 let total_cache = hit + miss;
257 let avg_pct = if total_cache > 0 {
258 (hit as f64 / total_cache as f64 * 100.0).clamp(0.0, 100.0)
259 } else {
260 0.0
261 };
262 out.push_str(&format!(" Turns recorded: {turns}\n"));
263 out.push_str(&format!(
264 " Cache hit tokens: {hit} ({avg_pct:.1}% of {total_cache} cache-aware tokens)\n",
265 hit = format_tokens(hit),
266 total_cache = format_tokens(total_cache),
267 ));
268 out.push_str(&format!(
269 " Cache miss tokens: {miss}\n",
270 miss = format_tokens(miss),
271 ));
272 out.push_str(&format!(
273 " Total input tokens: {input}\n",
274 input = format_tokens(input),
275 ));
276 if avg_pct < 80.0 {
277 out.push_str(" NOTE: cache hit rate is low (< 80%). Check prefix stability above or consider /compact.\n");
278 }
279 }
280
281 out
282 }
283
284 /// Render three-zone prefix contract status for `/cache zones` (#2264).
285 ///
286 /// Displays the PinnedPrefix fingerprint, AppendLog size, and TurnScratch
287 /// state. PinnedPrefix is frozen and checked for drift each turn, and
288 /// AppendLog is the backing store for the engine's session history
289 /// (`core::session::Session::messages`). TurnScratch is still type
290 /// scaffolding: nothing on the request path populates it.
291 pub(super) fn format_cache_zones(telemetry: &DebugCacheTelemetry) -> String {
292 let mut out = String::new();
293 out.push_str("Cache Zones (#2264 three-zone contract)\n");
294
295 // ── PinnedPrefix ─────────────────────────────────────────────────
296 out.push_str("\n── PinnedPrefix (system + tools, frozen baseline)\n");
297 match &telemetry.last_pinned_prefix_hash {
298 Some(hash) => {
299 let short = if hash.len() >= 12 { &hash[..12] } else { hash };
300 out.push_str(&format!(" Short id: {short}\n"));
301 if telemetry.prefix_change_count > 0 {
302 out.push_str(&format!(
303 " Status: WARNING — {change} drift{plural} detected\n",
304 change = telemetry.prefix_change_count,
305 plural = if telemetry.prefix_change_count == 1 {
306 ""
307 } else {
308 "s"
309 }
310 ));
311 } else {
312 out.push_str(" Status: stable (no drift this session)\n");
313 }
314 if let Some(pct) = telemetry.prefix_stability_pct {
315 out.push_str(&format!(" Stability: {pct}%\n"));
316 }
317 }
318 None => {
319 out.push_str(" Status: unavailable (not yet frozen)\n");
320 out.push_str(" Run a turn first to freeze the baseline.\n");
321 }
322 }
323
324 // ── AppendLog ────────────────────────────────────────────────────
325 out.push_str("\n── AppendLog (conversation history, append-only)\n");
326 out.push_str(" Status: wired — backs the engine session history\n");
327 let msg_count = telemetry.api_message_count;
328 out.push_str(&format!(" Messages: {msg_count}\n"));
329 let history_count = telemetry.non_system_message_count;
330 out.push_str(&format!(" History msgs: {history_count}\n"));
331
332 // ── TurnScratch ──────────────────────────────────────────────────
333 out.push_str("\n── TurnScratch (per-turn ephemeral data)\n");
334 out.push_str(" Status: not wired — type scaffolding, unused by requests\n");
335
336 // ── Zone contract summary ────────────────────────────────────────
337 out.push_str("\n── Contract Status\n");
338 let has_drift = telemetry.prefix_change_count > 0;
339 out.push_str(&format!(
340 " PinnedPrefix: {}\n",
341 if telemetry.last_pinned_prefix_hash.is_some() {
342 if has_drift {
343 "WARNING — drifted"
344 } else {
345 "OK"
346 }
347 } else {
348 "not frozen"
349 }
350 ));
351 out.push_str(" AppendLog: wired (session history)\n");
352 out.push_str(" TurnScratch: not wired\n");
353
354 out
355 }
356
357 /// Formats a u64 token count with a compact suffix: K for thousands,
358 /// M for millions. Never returns scientific notation.
359 pub(crate) fn format_tokens(n: u64) -> String {
360 if n >= 1_000_000 {
361 format!("{:.1}M", n as f64 / 1_000_000.0)
362 } else if n >= 1_000 {
363 format!("{:.1}K", n as f64 / 1_000.0)
364 } else {
365 n.to_string()
366 }
367 }
368
369 pub(super) fn format_static_prefix_status(
370 previous: Option<&DebugPromptInspection>,
371 current: &DebugPromptInspection,
372 ) -> String {
373 let Some(previous) = previous else {
374 return "Static base prefix stability: no previous request\n".to_string();
375 };
376 if previous.base_static_prefix_hash == current.base_static_prefix_hash {
377 return "Static base prefix stability: OK\n".to_string();
378 }
379
380 let changed = changed_static_layers(previous, current);
381 if changed.is_empty() {
382 "Static base prefix stability: WARNING (base hash changed)\n".to_string()
383 } else {
384 format!(
385 "Static base prefix stability: WARNING changed layers: {}\n",
386 changed.join(", ")
387 )
388 }
389 }
390
391 pub(super) fn format_first_divergence(
392 previous: Option<&DebugPromptInspection>,
393 current: &DebugPromptInspection,
394 ) -> String {
395 let Some(previous) = previous else {
396 return "First divergence from previous request: unavailable\n".to_string();
397 };
398 let max_len = previous.layers.len().max(current.layers.len());
399 for index in 0..max_len {
400 match (previous.layers.get(index), current.layers.get(index)) {
401 (Some(prev), Some(curr)) if prev.name == curr.name && prev.sha256 == curr.sha256 => {}
402 (Some(prev), Some(curr)) if prev.name == curr.name => {
403 return format!("First divergence from previous request: {}\n", curr.name);
404 }
405 (Some(_), Some(curr)) => {
406 return format!("First divergence from previous request: {}\n", curr.name);
407 }
408 (None, Some(curr)) => {
409 return format!("First divergence from previous request: {}\n", curr.name);
410 }
411 (Some(prev), None) => {
412 return format!(
413 "First divergence from previous request: {} removed\n",
414 prev.name
415 );
416 }
417 (None, None) => break,
418 }
419 }
420 "First divergence from previous request: none\n".to_string()
421 }
422
423 fn changed_static_layers(
424 previous: &DebugPromptInspection,
425 current: &DebugPromptInspection,
426 ) -> Vec<String> {
427 current
428 .layers
429 .iter()
430 .filter(|layer| layer.stability.label() == "static")
431 .filter(|layer| {
432 previous
433 .layers
434 .iter()
435 .find(|previous_layer| previous_layer.name == layer.name)
436 .is_none_or(|previous_layer| previous_layer.sha256 != layer.sha256)
437 })
438 .map(|layer| layer.name.clone())
439 .collect()
440 }
441
442 /// Column header for the per-turn cache/cost table. The widths here must match
443 /// the row format strings below.
444 const TURN_CACHE_ROW_HEADER: &str = "turn route in out hit miss write replay ratio cost age";
445
446 /// Rule width for the table. Sized to the header above.
447 const TURN_CACHE_TABLE_WIDTH: usize = 106;
448
449 /// Render one turn's cost cell, collecting the reason when it has none.
450 ///
451 /// A turn with no route provenance (legacy or synthetic record) and a turn on a
452 /// route that is not money-metered both render as `—` — neither is a real
453 /// zero-dollar charge.
454 fn turn_cost_cell(
455 rec: &DebugCacheTurn,
456 cost: &DebugCostProjection,
457 unpriced_reasons: &mut BTreeMap<u8, String>,
458 unpriced_classes: &mut BTreeSet<String>,
459 ) -> String {
460 if let Some(amount) = rec.priced_amount {
461 return format_cost_amount_precise(amount, cost.currency);
462 }
463 if let (Some(reason), Some(rank)) = (&rec.unpriced_reason_key, rec.unpriced_reason_sort_rank) {
464 unpriced_reasons.insert(rank, reason.clone());
465 }
466 unpriced_classes.extend(rec.unpriced_classes.iter().cloned());
467 "—".to_string()
468 }
469
470 pub(crate) fn format_cache_history(
471 telemetry: &DebugCacheTelemetry,
472 count: usize,
473 cost: &DebugCostProjection,
474 presentation: &dyn CommandPresentationContext,
475 ) -> Result<String, String> {
476 let total = telemetry.history.len();
477 let start = total.saturating_sub(count);
478 let rows: Vec<&DebugCacheTurn> = telemetry.history.iter().skip(start).collect();
479
480 let mut totals_input: u64 = 0;
481 let mut totals_hit: u64 = 0;
482 let mut totals_miss: u64 = 0;
483 let mut totals_write: u64 = 0;
484 let mut totals_reasoning: u64 = 0;
485 // Preserve the host enum's reason order rather than sorting its labels.
486 let mut unpriced_reasons: BTreeMap<u8, String> = BTreeMap::new();
487 let mut unpriced_classes: BTreeSet<String> = BTreeSet::new();
488 let mut header = presentation.translate(
489 "cmd_cache_header",
490 &[
491 ("count", &rows.len().to_string()),
492 ("total", &total.to_string()),
493 ("model", &telemetry.model),
494 ],
495 )?;
496 header.push_str(&"─".repeat(TURN_CACHE_TABLE_WIDTH));
497 header.push('\n');
498 header.push_str(TURN_CACHE_ROW_HEADER);
499 header.push('\n');
500 header.push_str(&"─".repeat(TURN_CACHE_TABLE_WIDTH));
501 header.push('\n');
502
503 let mut body = String::new();
504 let absolute_start = total.saturating_sub(rows.len());
505 for (i, rec) in rows.iter().enumerate() {
506 let turn_index = absolute_start + i + 1;
507 totals_input += u64::from(rec.input_tokens);
508
509 let replay_cell = rec
510 .reasoning_replay_tokens
511 .map_or_else(|| "—".to_string(), |t| t.to_string());
512 let write = u32::try_from(rec.priced_cache_write).unwrap_or(u32::MAX);
513 let write_cell = rec
514 .cache_write_tokens
515 .map_or_else(|| "—".to_string(), |_| write.to_string());
516 totals_write += rec.priced_cache_write;
517 totals_reasoning += u64::from(rec.reasoning_tokens.unwrap_or(0));
518 let cost_cell = turn_cost_cell(rec, cost, &mut unpriced_reasons, &mut unpriced_classes);
519 let route_cell = format_turn_cache_route(rec);
520 let age = crate::elapsed::format_elapsed_secs(rec.age_seconds);
521
522 // No cache telemetry → render `—` everywhere and don't pollute totals
523 // with inferred zeros. Some providers (and some routes inside DeepSeek)
524 // skip the cache fields; including a synthesized 0/N for those turns
525 // would make every aggregate ratio look broken.
526 if rec.cache_hit_tokens.is_none()
527 && rec.cache_miss_tokens.is_none()
528 && rec.cache_write_tokens.is_none()
529 {
530 body.push_str(&format!(
531 "{turn:>4} {route:<24} {input:>5} {output:>5} {hit:>5} {miss:>5} {write:>5} {replay:>6} {ratio:>6} {cost:>9} {age}\n",
532 turn = turn_index,
533 route = route_cell,
534 input = rec.input_tokens,
535 output = rec.output_tokens,
536 hit = "—",
537 miss = "—",
538 write = write_cell,
539 replay = replay_cell,
540 ratio = "—",
541 cost = cost_cell,
542 age = age,
543 ));
544 continue;
545 }
546
547 let miss_reported = rec.cache_miss_tokens;
548 let hit = u32::try_from(rec.priced_cache_read).unwrap_or(u32::MAX);
549 let miss = u32::try_from(rec.priced_cache_miss).unwrap_or(u32::MAX);
550 // Use the same mutually-exclusive hit/miss/write partition as pricing.
551 // Inferring `input - hit` here and then adding write counted creation
552 // tokens twice in exactly the turns with a write premium.
553 let accounted = u64::from(hit) + u64::from(miss) + u64::from(write);
554 let ratio = if accounted == 0 {
555 " —".to_string()
556 } else {
557 format!("{:>5.1}%", 100.0 * f64::from(hit) / accounted as f64)
558 };
559 totals_hit += u64::from(hit);
560 totals_miss += u64::from(miss);
561
562 let miss_cell = match miss_reported {
563 Some(_) => format!("{miss}"),
564 None => format!("{miss}*"),
565 };
566
567 body.push_str(&format!(
568 "{turn:>4} {route:<24} {input:>5} {output:>5} {hit:>5} {miss:>5} {write:>5} {replay:>6} {ratio} {cost:>9} {age}\n",
569 turn = turn_index,
570 route = route_cell,
571 input = rec.input_tokens,
572 output = rec.output_tokens,
573 hit = hit,
574 miss = miss_cell,
575 write = write_cell,
576 replay = replay_cell,
577 ratio = ratio,
578 cost = cost_cell,
579 age = age,
580 ));
581 }
582
583 // Anthropic-normalized aggregate: hit / (hit + miss + write).
584 let totals_accounted = totals_hit + totals_miss + totals_write;
585 let avg_ratio = if totals_accounted == 0 {
586 "—".to_string()
587 } else {
588 format!(
589 "{:.1}%",
590 100.0 * totals_hit as f64 / totals_accounted as f64
591 )
592 };
593
594 let mut footer = String::new();
595 footer.push_str(&"─".repeat(TURN_CACHE_TABLE_WIDTH));
596 footer.push('\n');
597 // Reasoning is reported separately from `sum_out` on purpose: providers
598 // count it *inside* the completion tokens they bill, so adding the two
599 // would double-count it.
600 footer.push_str(&format!(
601 "sum_write: {totals_write} sum_reasoning: {totals_reasoning} (already inside out)\n"
602 ));
603 footer.push_str(&presentation.translate(
604 "cmd_cache_totals",
605 &[
606 ("sum_in", &totals_input.to_string()),
607 ("sum_hit", &totals_hit.to_string()),
608 ("sum_miss", &totals_miss.to_string()),
609 ("avg", &avg_ratio),
610 ],
611 )?);
612 footer.push_str(&presentation.translate("cmd_cache_footnote", &[])?);
613 if !unpriced_reasons.is_empty() || !unpriced_classes.is_empty() {
614 // Reasons are localized prose; token-class labels are key names and
615 // stay raw, the same split `/cost` uses.
616 let mut notes = Vec::new();
617 for reason in unpriced_reasons.values() {
618 notes.push(presentation.translate(&format!("cost_reason_{reason}"), &[])?);
619 }
620 notes.extend(unpriced_classes);
621 footer.push_str(
622 &presentation.translate("cmd_cache_unpriced_note", &[("notes", &notes.join(", "))])?,
623 );
624 }
625 footer.push_str(&presentation.translate("cmd_cache_advice", &[])?);
626 if let Some(line) = session_cache_rates_line(telemetry, presentation)? {
627 footer.push_str("\n\n");
628 footer.push_str(&line);
629 }
630
631 Ok(format!("{header}{body}{footer}"))
632 }
633
634 pub(crate) fn format_turn_cache_route(rec: &DebugCacheTurn) -> String {
635 let Some(model) = rec.model.as_deref().filter(|model| !model.is_empty()) else {
636 return "—".to_string();
637 };
638 let provider = rec
639 .provider_identity
640 .as_deref()
641 .filter(|provider| !provider.trim().is_empty())
642 .or(rec.provider.as_deref())
643 .unwrap_or("?");
644 let route = if rec.auto_model {
645 format!("auto:{provider}/{model}")
646 } else {
647 format!("{provider}/{model}")
648 };
649 truncate_route_cell(&route, 24)
650 }
651
652 fn truncate_route_cell(route: &str, max_chars: usize) -> String {
653 if route.chars().count() <= max_chars {
654 return route.to_string();
655 }
656 if max_chars <= 3 {
657 return route.chars().take(max_chars).collect();
658 }
659 let mut out: String = route.chars().take(max_chars - 3).collect();
660 out.push_str("...");
661 out
662 }
663
664 /// Preserve the upstream session-rate labels without exposing host state.
665 pub(super) fn session_cache_rates_line(
666 telemetry: &DebugCacheTelemetry,
667 presentation: &dyn CommandPresentationContext,
668 ) -> Result<Option<String>, String> {
669 let rates = telemetry.session_cache_rates;
670 if rates.agents.is_none() {
671 return Ok(None);
672 }
673 let labelled = rates.labelled(
674 &presentation.translate("cmd_cache_rate_parent", &[])?,
675 &presentation.translate("cmd_cache_rate_agents", &[])?,
676 &presentation.translate("cmd_cache_rate_combined", &[])?,
677 );
678 labelled
679 .map(|rates| presentation.translate("cmd_cache_session_rates", &[("rates", &rates)]))
680 .transpose()
681 }
682
682 lines RUST