Skip to content

Commit 56c4096

Browse files
committed
Add provider info to home usage view
1 parent 165fe3e commit 56c4096

5 files changed

Lines changed: 156 additions & 34 deletions

File tree

src-tauri/src/shared/local_usage_core.rs

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ struct OpencodeDbSessionState {
3535
cached_read: i64,
3636
}
3737

38+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39+
struct UsageModelKey {
40+
model: String,
41+
provider: Option<String>,
42+
}
43+
44+
impl UsageModelKey {
45+
fn unknown() -> Self {
46+
Self {
47+
model: "unknown".to_string(),
48+
provider: None,
49+
}
50+
}
51+
}
52+
3853
const MAX_ACTIVITY_GAP_MS: i64 = 2 * 60 * 1000;
3954

4055
pub(crate) async fn local_usage_snapshot_core(
@@ -78,7 +93,7 @@ fn scan_local_usage(
7893
.iter()
7994
.map(|key| (key.clone(), DailyTotals::default()))
8095
.collect();
81-
let mut model_totals: HashMap<String, i64> = HashMap::new();
96+
let mut model_totals: HashMap<UsageModelKey, i64> = HashMap::new();
8297

8398
if !sessions_roots.is_empty() {
8499
for root in sessions_roots {
@@ -117,7 +132,7 @@ fn build_snapshot(
117132
updated_at: i64,
118133
day_keys: Vec<String>,
119134
daily: HashMap<String, DailyTotals>,
120-
model_totals: HashMap<String, i64>,
135+
model_totals: HashMap<UsageModelKey, i64>,
121136
) -> LocalUsageSnapshot {
122137
let mut days: Vec<LocalUsageDay> = Vec::with_capacity(day_keys.len());
123138
let mut total_tokens = 0;
@@ -163,9 +178,10 @@ fn build_snapshot(
163178

164179
let mut top_models: Vec<LocalUsageModel> = model_totals
165180
.into_iter()
166-
.filter(|(model, tokens)| model != "unknown" && *tokens > 0)
167-
.map(|(model, tokens)| LocalUsageModel {
168-
model,
181+
.filter(|(key, tokens)| key.model != "unknown" && *tokens > 0)
182+
.map(|(key, tokens)| LocalUsageModel {
183+
model: key.model,
184+
provider: key.provider,
169185
tokens,
170186
share_percent: if total_tokens > 0 {
171187
((tokens as f64) / (total_tokens as f64) * 1000.0).round() / 10.0
@@ -218,7 +234,7 @@ fn scan_opencode_db_usage(
218234
.iter()
219235
.map(|key| (key.clone(), DailyTotals::default()))
220236
.collect();
221-
let mut model_totals: HashMap<String, i64> = HashMap::new();
237+
let mut model_totals: HashMap<UsageModelKey, i64> = HashMap::new();
222238
let mut cache_state_by_session: HashMap<String, OpencodeDbSessionState> = HashMap::new();
223239
let cutoff_ms = earliest_day_start_ms(days).unwrap_or(0);
224240

@@ -325,7 +341,7 @@ fn scan_opencode_db_usage(
325341

326342
if total_for_model > 0 {
327343
let model = extract_model_from_opencode_message(&value)
328-
.unwrap_or_else(|| "unknown".to_string());
344+
.unwrap_or_else(UsageModelKey::unknown);
329345
*model_totals.entry(model).or_insert(0) += total_for_model;
330346
}
331347
}
@@ -399,8 +415,8 @@ fn extract_opencode_message_cwd(value: &Value) -> Option<String> {
399415
.map(|cwd| cwd.to_string())
400416
}
401417

402-
fn extract_model_from_opencode_message(value: &Value) -> Option<String> {
403-
value
418+
fn extract_model_from_opencode_message(value: &Value) -> Option<UsageModelKey> {
419+
let model = value
404420
.get("modelID")
405421
.and_then(|v| v.as_str())
406422
.or_else(|| {
@@ -409,13 +425,24 @@ fn extract_model_from_opencode_message(value: &Value) -> Option<String> {
409425
.and_then(|model| model.get("modelID"))
410426
.and_then(|v| v.as_str())
411427
})
412-
.map(|model| model.to_string())
428+
.and_then(normalize_non_empty_string)?;
429+
let provider = value
430+
.get("providerID")
431+
.and_then(|v| v.as_str())
432+
.or_else(|| {
433+
value
434+
.get("model")
435+
.and_then(|model| model.get("providerID"))
436+
.and_then(|v| v.as_str())
437+
})
438+
.and_then(normalize_non_empty_string);
439+
Some(UsageModelKey { model, provider })
413440
}
414441

415442
fn scan_file(
416443
path: &Path,
417444
daily: &mut HashMap<String, DailyTotals>,
418-
model_totals: &mut HashMap<String, i64>,
445+
model_totals: &mut HashMap<UsageModelKey, i64>,
419446
workspace_path: Option<&Path>,
420447
) -> Result<(), String> {
421448
let file = match File::open(path) {
@@ -426,7 +453,7 @@ fn scan_file(
426453
};
427454
let reader = BufReader::new(file);
428455
let mut previous_totals: Option<UsageTotals> = None;
429-
let mut current_model: Option<String> = None;
456+
let mut current_model: Option<UsageModelKey> = None;
430457
let mut last_activity_ms: Option<i64> = None;
431458
let mut seen_runs: HashSet<i64> = HashSet::new();
432459
let mut match_known = workspace_path.is_none();
@@ -602,7 +629,7 @@ fn scan_file(
602629
let model = current_model
603630
.clone()
604631
.or_else(|| extract_model_from_token_count(&value))
605-
.unwrap_or_else(|| "unknown".to_string());
632+
.unwrap_or_else(UsageModelKey::unknown);
606633
*model_totals.entry(model).or_insert(0) += delta.input + delta.output;
607634
}
608635
}
@@ -645,18 +672,38 @@ fn scan_file(
645672
Ok(())
646673
}
647674

648-
fn extract_model_from_turn_context(value: &Value) -> Option<String> {
675+
fn extract_model_from_turn_context(value: &Value) -> Option<UsageModelKey> {
649676
let payload = value.get("payload").and_then(|value| value.as_object())?;
650-
if let Some(model) = payload.get("model").and_then(|value| value.as_str()) {
651-
return Some(model.to_string());
677+
if let Some(model) = payload
678+
.get("model")
679+
.and_then(|value| value.as_str())
680+
.and_then(normalize_non_empty_string)
681+
{
682+
let provider = payload
683+
.get("provider")
684+
.or_else(|| payload.get("provider_id"))
685+
.or_else(|| payload.get("providerID"))
686+
.and_then(|value| value.as_str())
687+
.and_then(normalize_non_empty_string);
688+
return Some(UsageModelKey { model, provider });
652689
}
653690
let info = payload.get("info").and_then(|value| value.as_object())?;
654-
info.get("model")
691+
let model = info
692+
.get("model")
693+
.or_else(|| info.get("model_id"))
694+
.or_else(|| info.get("modelID"))
695+
.and_then(|value| value.as_str())
696+
.and_then(normalize_non_empty_string)?;
697+
let provider = info
698+
.get("provider")
699+
.or_else(|| info.get("provider_id"))
700+
.or_else(|| info.get("providerID"))
655701
.and_then(|value| value.as_str())
656-
.map(|value| value.to_string())
702+
.and_then(normalize_non_empty_string);
703+
Some(UsageModelKey { model, provider })
657704
}
658705

659-
fn extract_model_from_token_count(value: &Value) -> Option<String> {
706+
fn extract_model_from_token_count(value: &Value) -> Option<UsageModelKey> {
660707
let payload = value.get("payload").and_then(|value| value.as_object())?;
661708
let info = payload.get("info").and_then(|value| value.as_object());
662709
let model = info
@@ -666,8 +713,21 @@ fn extract_model_from_token_count(value: &Value) -> Option<String> {
666713
.and_then(|value| value.as_str())
667714
})
668715
.or_else(|| payload.get("model").and_then(|value| value.as_str()))
669-
.or_else(|| value.get("model").and_then(|value| value.as_str()));
670-
model.map(|value| value.to_string())
716+
.or_else(|| value.get("model").and_then(|value| value.as_str()))
717+
.and_then(normalize_non_empty_string)?;
718+
719+
let provider = info
720+
.and_then(|info| {
721+
info.get("provider")
722+
.or_else(|| info.get("provider_id"))
723+
.or_else(|| info.get("providerID"))
724+
.and_then(|value| value.as_str())
725+
})
726+
.or_else(|| payload.get("provider").and_then(|value| value.as_str()))
727+
.or_else(|| value.get("provider").and_then(|value| value.as_str()))
728+
.and_then(normalize_non_empty_string);
729+
730+
Some(UsageModelKey { model, provider })
671731
}
672732

673733
fn find_usage_map<'a>(
@@ -689,6 +749,15 @@ fn read_i64(map: &serde_json::Map<String, Value>, keys: &[&str]) -> i64 {
689749
.unwrap_or(0)
690750
}
691751

752+
fn normalize_non_empty_string(value: &str) -> Option<String> {
753+
let trimmed = value.trim();
754+
if trimmed.is_empty() {
755+
None
756+
} else {
757+
Some(trimmed.to_string())
758+
}
759+
}
760+
692761
fn read_timestamp_ms(value: &Value) -> Option<i64> {
693762
let raw = value.get("timestamp")?;
694763
if let Some(text) = raw.as_str() {

src-tauri/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ pub(crate) struct LocalUsageTotals {
173173
#[serde(rename_all = "camelCase")]
174174
pub(crate) struct LocalUsageModel {
175175
pub(crate) model: String,
176+
#[serde(default)]
177+
pub(crate) provider: Option<String>,
176178
pub(crate) tokens: i64,
177179
pub(crate) share_percent: f64,
178180
}

src/features/home/components/Home.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,25 @@ export function Home({
130130
}).format(date);
131131
};
132132

133+
const formatProviderLabel = (value: string | null | undefined) => {
134+
if (!value) {
135+
return null;
136+
}
137+
const normalized = value.trim();
138+
if (!normalized) {
139+
return null;
140+
}
141+
const lower = normalized.toLowerCase();
142+
if (lower === "openai") return "OpenAI";
143+
if (lower === "anthropic") return "Anthropic";
144+
if (lower === "google") return "Google";
145+
if (lower === "openrouter") return "OpenRouter";
146+
if (lower === "xai") return "xAI";
147+
if (lower === "deepseek") return "DeepSeek";
148+
if (lower === "mistral") return "Mistral";
149+
return normalized;
150+
};
151+
133152
const usageTotals = localUsageSnapshot?.totals ?? null;
134153
const usageDays = localUsageSnapshot?.days ?? [];
135154
const last7Days = usageDays.slice(-7);
@@ -504,18 +523,24 @@ export function Home({
504523
</div>
505524
<div className="home-usage-models-list">
506525
{localUsageSnapshot?.topModels?.length ? (
507-
localUsageSnapshot.topModels.map((model) => (
508-
<span
509-
className="home-usage-model-chip"
510-
key={model.model}
511-
title={`${model.model}: ${formatCount(model.tokens)} tokens`}
512-
>
513-
{model.model}
514-
<span className="home-usage-model-share">
515-
{model.sharePercent.toFixed(1)}%
526+
localUsageSnapshot.topModels.map((model) => {
527+
const providerLabel = formatProviderLabel(model.provider);
528+
return (
529+
<span
530+
className="home-usage-model-chip"
531+
key={`${model.provider ?? "unknown"}:${model.model}`}
532+
title={`${providerLabel ? `${providerLabel} · ` : ""}${model.model}: ${formatCount(model.tokens)} tokens`}
533+
>
534+
{providerLabel && (
535+
<span className="home-usage-model-provider">{providerLabel}</span>
536+
)}
537+
<span className="home-usage-model-name">{model.model}</span>
538+
<span className="home-usage-model-share">
539+
{model.sharePercent.toFixed(1)}%
540+
</span>
516541
</span>
517-
</span>
518-
))
542+
);
543+
})
519544
) : (
520545
<span className="home-usage-model-empty">No models yet</span>
521546
)}

src/styles/home.css

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,38 @@
371371
border: 1px solid var(--border-subtle);
372372
max-width: 100%;
373373
min-width: 0;
374-
overflow-wrap: anywhere;
375-
word-break: break-word;
374+
overflow: hidden;
375+
}
376+
377+
.home-usage-model-provider {
378+
display: inline-flex;
379+
align-items: center;
380+
padding: 1px 6px;
381+
border-radius: 999px;
382+
border: 1px solid rgba(255, 255, 255, 0.08);
383+
background: rgba(255, 255, 255, 0.03);
384+
color: var(--text-subtle);
385+
font-size: 9px;
386+
font-weight: 600;
387+
line-height: 1.2;
388+
letter-spacing: 0.05em;
389+
text-transform: uppercase;
390+
white-space: nowrap;
391+
flex-shrink: 0;
392+
}
393+
394+
.home-usage-model-name {
395+
min-width: 0;
396+
overflow: hidden;
397+
text-overflow: ellipsis;
398+
white-space: nowrap;
376399
}
377400

378401
.home-usage-model-share {
379402
font-size: 10px;
380403
color: var(--text-subtle);
404+
white-space: nowrap;
405+
flex-shrink: 0;
381406
}
382407

383408
.home-usage-model-empty {

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ export type LocalUsageTotals = {
522522

523523
export type LocalUsageModel = {
524524
model: string;
525+
provider?: string | null;
525526
tokens: number;
526527
sharePercent: number;
527528
};

0 commit comments

Comments
 (0)