From 5de254a34b7d09cbd575f869735c2fe04b05bb32 Mon Sep 17 00:00:00 2001 From: FabioRocha231 Date: Wed, 12 Aug 2026 23:25:31 -0300 Subject: [PATCH 1/6] fix(proxy): persist local token across restarts --- src-tauri/src/proxy/auth.rs | 93 +++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/proxy/auth.rs b/src-tauri/src/proxy/auth.rs index ec2084b..d29f115 100644 --- a/src-tauri/src/proxy/auth.rs +++ b/src-tauri/src/proxy/auth.rs @@ -1,22 +1,72 @@ use axum::http::StatusCode; use axum::{body::Body, extract::Request, middleware::Next, response::Response}; +use std::path::Path; use std::sync::OnceLock; // The proxy accepts traffic from any local process, so every endpoint needs a -// process-local secret before it can use a configured provider credential. +// user-local secret before it can use a configured provider credential. static LOCAL_TOKEN: OnceLock = OnceLock::new(); /// Shared secret required on every request to the local proxy. -/// Generated once per process from 32 random bytes (two UUIDv4s = 64 hex -/// chars; `uuid` is the only RNG crate already in Cargo.toml). +/// +/// The token is generated once and persisted under `~/.loomrouter`, so a +/// running Codex keeps the same credentials after LoomRouter restarts. +/// Regenerating it per process only worked while Codex reloaded +/// `config.toml`; an already-open app kept sending the old provider token. +#[cfg(not(test))] pub fn local_token() -> &'static str { - LOCAL_TOKEN.get_or_init(|| { - format!( - "{}{}", - uuid::Uuid::new_v4().simple(), - uuid::Uuid::new_v4().simple() - ) - }) + LOCAL_TOKEN.get_or_init(|| load_or_create_local_token(&local_token_path())) +} + +#[cfg(test)] +pub fn local_token() -> &'static str { + LOCAL_TOKEN.get_or_init(generate_local_token) +} + +#[cfg(not(test))] +fn local_token_path() -> std::path::PathBuf { + crate::config::config_dir().join("local-token") +} + +fn managed_token_from(raw: &str) -> Option { + let needle = "x-loomrouter-token\" = \""; + let start = raw.find(needle)? + needle.len(); + let end = raw[start..].find('"')?; + Some(raw[start..start + end].to_string()) +} + +fn configured_token() -> Option { + let raw = std::fs::read_to_string(crate::codex::codex_home().join("config.toml")).ok()?; + managed_token_from(&raw) +} + +fn generate_local_token() -> String { + format!( + "{}{}", + uuid::Uuid::new_v4().simple(), + uuid::Uuid::new_v4().simple() + ) +} + +fn valid_local_token(token: &str) -> bool { + token.len() == 64 && token.chars().all(|c| c.is_ascii_hexdigit()) +} + +fn load_or_create_local_token(path: &Path) -> String { + if let Ok(token) = std::fs::read_to_string(path) { + if valid_local_token(&token) { + return token; + } + } + if let Some(token) = configured_token().filter(|token| valid_local_token(token)) { + let _ = crate::secure_fs::write_private(path, token.as_bytes()); + return token; + } + let token = generate_local_token(); + if let Err(e) = crate::secure_fs::write_private(path, token.as_bytes()) { + tracing::warn!(path = %path.display(), error = %e, "failed to persist local token"); + } + token } /// Constant-time token comparison avoids leaking a valid token prefix through @@ -80,3 +130,26 @@ pub(super) async fn auth_gate(req: Request, next: Next) -> Response { )) .expect("a static unauthorized response is valid") } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn persisted_local_token_is_reused() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("local-token"); + + let first = load_or_create_local_token(&path); + let second = load_or_create_local_token(&path); + + assert_eq!(first, second); + assert_eq!(first.len(), 64); + } + + #[test] + fn managed_block_token_is_read_without_leaking_adjacent_text() { + let raw = "http_headers = { \"x-loomrouter-token\" = \"abc\", \"Authorization\" = \"Bearer abc\" }"; + assert_eq!(managed_token_from(raw).as_deref(), Some("abc")); + } +} From 28f91be41a8f66c43f15d183397ce01de4c5658d Mon Sep 17 00:00:00 2001 From: FabioRocha231 Date: Wed, 12 Aug 2026 23:25:41 -0300 Subject: [PATCH 2/6] fix(proxy): record actual upstream model in usage stats --- src-tauri/src/proxy.rs | 5 +++ src-tauri/src/proxy/dispatch.rs | 24 ++++++------ src-tauri/src/proxy/realtime.rs | 56 +++++++++++++++++++--------- src-tauri/src/proxy/tests_keys.rs | 29 ++++++++++++++ src-tauri/src/proxy/tests_routing.rs | 20 ++++++++++ 5 files changed, 105 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/proxy.rs b/src-tauri/src/proxy.rs index 5886a76..71a9e1c 100644 --- a/src-tauri/src/proxy.rs +++ b/src-tauri/src/proxy.rs @@ -225,6 +225,10 @@ struct Turn { key_id: Option, } +fn routed_stats_model(provider: &Provider, upstream_model: &str) -> String { + format!("{}/{}", provider.id, upstream_model) +} + impl Turn { fn new( provider: &str, @@ -1063,6 +1067,7 @@ fn sanitize_stateless_responses_payload(payload: &mut Value) { let Some(input) = payload.get_mut("input").and_then(Value::as_array_mut) else { return; }; + translate::repair_tool_exchange_items(input); for item in input.iter_mut() { if let Some(object) = item.as_object_mut() { object.remove("id"); diff --git a/src-tauri/src/proxy/dispatch.rs b/src-tauri/src/proxy/dispatch.rs index e88f775..80a7511 100644 --- a/src-tauri/src/proxy/dispatch.rs +++ b/src-tauri/src/proxy/dispatch.rs @@ -88,12 +88,13 @@ pub(super) async fn dispatch_routed( wire: WireApi, ) -> anyhow::Result { if is_remote_compaction_v2(payload) { - return dispatch_routed_compaction(ctx, provider, upstream_model, model, payload).await; + return dispatch_routed_compaction(ctx, provider, upstream_model, payload).await; } + let stats_model = super::routed_stats_model(provider, upstream_model); if super::routing::codex_request_kind(payload).as_deref() == Some("compaction") { record_problem( &ctx.stats, - &Turn::new(&provider.id, upstream_model, "http", None), + &Turn::new(&provider.id, &stats_model, "http", None), "compaction", &format!( "{BUILD_LABEL}: Codex sent a compaction call without a compaction_trigger item; treating it as a normal turn" @@ -140,7 +141,7 @@ pub(super) async fn dispatch_routed( return Err(anyhow::Error::new(visual_preparation_failure( &ctx.stats, &provider.id, - model, + &stats_model, "http", started, &error, @@ -168,7 +169,8 @@ pub(super) async fn dispatch_routed( build_upstream(provider, &prepared_payload, upstream_model, wire)?; let (upstream, key_id) = send(ctx, provider, path, &body).await?; - let turn = Turn::new(&provider.id, model, "http", Some(started)).with_key(key_id.as_deref()); + let turn = + Turn::new(&provider.id, &stats_model, "http", Some(started)).with_key(key_id.as_deref()); let status = StatusCode::from_u16(upstream.status().as_u16()).unwrap_or(StatusCode::BAD_GATEWAY); @@ -345,7 +347,8 @@ async fn dispatch_claude_cli( .and_then(Value::as_bool) .unwrap_or(false); let started = std::time::Instant::now(); - let turn = Turn::new(&provider.id, model, "http", Some(started)); + let stats_model = super::routed_stats_model(provider, upstream_model); + let turn = Turn::new(&provider.id, &stats_model, "http", Some(started)); let downstream_kind = wire.downstream(); let (result, id) = super::run_claude_turn(payload, upstream_model, wire).await?; tracing::debug!(%model, input_tokens = result.input_tokens, output_tokens = result.output_tokens, "claude -p turn finished"); @@ -815,11 +818,11 @@ async fn dispatch_routed_compaction( ctx: &ProxyCtx, provider: &Provider, upstream_model: &str, - model: &str, payload: &Value, ) -> anyhow::Result { let started = std::time::Instant::now(); - let turn = Turn::new(&provider.id, model, "http", Some(started)); + let stats_model = super::routed_stats_model(provider, upstream_model); + let turn = Turn::new(&provider.id, &stats_model, "http", Some(started)); let (summary, usage) = match summarize_compaction(ctx, provider, upstream_model, payload).await { Ok(ok) => ok, @@ -875,17 +878,14 @@ pub(super) async fn routed_compaction_events( upstream_model: &str, payload: &Value, ) -> anyhow::Result { - let model = payload - .get("model") - .and_then(Value::as_str) - .unwrap_or(upstream_model); + let stats_model = super::routed_stats_model(provider, upstream_model); let (summary, usage) = match summarize_compaction(ctx, provider, upstream_model, payload).await { Ok(ok) => ok, Err(error) => { record_problem( &ctx.stats, - &Turn::new(&provider.id, model, "ws", None), + &Turn::new(&provider.id, &stats_model, "ws", None), "compaction", &format!("{BUILD_LABEL}: {error}"), ); diff --git a/src-tauri/src/proxy/realtime.rs b/src-tauri/src/proxy/realtime.rs index 11be7e1..62c8351 100644 --- a/src-tauri/src/proxy/realtime.rs +++ b/src-tauri/src/proxy/realtime.rs @@ -593,7 +593,7 @@ async fn ws_session(socket: WebSocket, ctx: ProxyCtx, headers: HeaderMap) { let error = visual_preparation_failure( &ctx.stats, &provider.id, - &model, + &destination_slug, "ws", turn_start, &error, @@ -622,15 +622,15 @@ async fn ws_session(socket: WebSocket, ctx: ProxyCtx, headers: HeaderMap) { // need to be re-created on every loop iteration. let turn_fut = ws_turn_events(&ctx, &headers, payload); tokio::pin!(turn_fut); - let (mut events, final_provider, key_id) = loop { + let (mut events, final_provider, key_id, stats_model) = loop { tokio::select! { result = &mut turn_fut => { match result { Ok(v) => break v, - Err((e, final_provider, key_id)) => { + Err((e, final_provider, key_id, stats_model)) => { record_failure( &ctx.stats, - &Turn::new(&final_provider, &model, "ws", Some(turn_start)) + &Turn::new(&final_provider, &stats_model, "ws", Some(turn_start)) .with_key(key_id.as_deref()), &e.to_string(), ); @@ -706,7 +706,7 @@ async fn ws_session(socket: WebSocket, ctx: ProxyCtx, headers: HeaderMap) { // Canonical Responses frames on this transport. record_payload_usage( &ctx.stats, - &Turn::new(&final_provider, &model, "ws", Some(turn_start)) + &Turn::new(&final_provider, &stats_model, "ws", Some(turn_start)) .with_key(key_id.as_deref()), UpstreamKind::Responses, v, @@ -790,31 +790,42 @@ fn ws_error_frame(status: u16, message: &str) -> Value { /// Run one turn and return a stream of Responses event objects ready to be /// sent as WS text frames. pub(super) type WsEvents = futures::stream::BoxStream<'static, Result>; -type LabeledWsEvents = - Result<(WsEvents, String, Option), (anyhow::Error, String, Option)>; +type LabeledWsEvents = Result< + (WsEvents, String, Option, String), + (anyhow::Error, String, Option, String), +>; fn label_ws_events( result: anyhow::Result, provider_id: String, key_id: Option, + stats_model: String, ) -> LabeledWsEvents { result - .map(|events| (events, provider_id.clone(), key_id.clone())) - .map_err(|error| (error, provider_id, key_id)) + .map(|events| { + ( + events, + provider_id.clone(), + key_id.clone(), + stats_model.clone(), + ) + }) + .map_err(|error| (error, provider_id, key_id, stats_model)) } async fn ws_turn_events(ctx: &ProxyCtx, headers: &HeaderMap, payload: Value) -> LabeledWsEvents { let model = payload .get("model") .and_then(Value::as_str) + .map(str::to_string) .ok_or_else(|| { ( anyhow!("missing 'model' field"), "codex-native".to_string(), None, + String::new(), ) - })? - .to_string(); + })?; let route = { let cfg = ctx.config.read().await; @@ -826,6 +837,7 @@ async fn ws_turn_events(ctx: &ProxyCtx, headers: &HeaderMap, payload: Value) -> ws_native_events(ctx, headers, payload).await, "codex-native".to_string(), None, + model.clone(), ), EffectiveRoute::Routed { provider, @@ -842,11 +854,15 @@ async fn ws_turn_events(ctx: &ProxyCtx, headers: &HeaderMap, payload: Value) -> ws_routed_events(ctx, &provider, &upstream_model, &model, &payload).await }; let attempt = match attempt { - Ok((events, key_id)) => return label_ws_events(Ok(events), provider.id, key_id), + Ok((events, key_id)) => { + let stats_model = super::routed_stats_model(&provider, &upstream_model); + return label_ws_events(Ok(events), provider.id, key_id, stats_model); + } Err(error) => Err(error), }; if !from_fallback { - return label_ws_events(attempt, provider.id, None); + let stats_model = super::routed_stats_model(&provider, &upstream_model); + return label_ws_events(attempt, provider.id, None, stats_model); } // A failed fallback must never break a side call: retry against // the request's original destination (same rule as HTTP). @@ -870,15 +886,19 @@ async fn ws_turn_events(ctx: &ProxyCtx, headers: &HeaderMap, payload: Value) -> } else { ws_routed_events(ctx, &p, &upstream_model, &model, &payload).await }; + let stats_model = super::routed_stats_model(&p, &upstream_model); match retry { - Ok((events, key_id)) => label_ws_events(Ok(events), p.id, key_id), - Err(error) => label_ws_events(Err(error), p.id, None), + Ok((events, key_id)) => { + label_ws_events(Ok(events), p.id, key_id, stats_model.clone()) + } + Err(error) => label_ws_events(Err(error), p.id, None, stats_model), } } Err(_) => label_ws_events( ws_native_events(ctx, headers, payload).await, "codex-native".to_string(), None, + model.clone(), ), } } @@ -915,6 +935,7 @@ async fn ws_routed_events( futures::stream::BoxStream<'static, Result>, Option, )> { + let stats_model = super::routed_stats_model(provider, upstream_model); if super::dispatch::is_remote_compaction_v2(payload) { return super::dispatch::routed_compaction_events(ctx, provider, upstream_model, payload) .await @@ -923,7 +944,7 @@ async fn ws_routed_events( if super::routing::codex_request_kind(payload).as_deref() == Some("compaction") { record_problem( &ctx.stats, - &Turn::new(&provider.id, upstream_model, "ws", None), + &Turn::new(&provider.id, &stats_model, "ws", None), "compaction", &format!( "{BUILD_LABEL}: Codex sent a compaction call without a compaction_trigger item; treating it as a normal turn" @@ -969,6 +990,7 @@ async fn ws_claude_cli_events( model: &str, payload: &Value, ) -> anyhow::Result>> { + let stats_model = super::routed_stats_model(provider, upstream_model); if super::dispatch::is_remote_compaction_v2(payload) { return super::dispatch::routed_compaction_events(ctx, provider, upstream_model, payload) .await; @@ -976,7 +998,7 @@ async fn ws_claude_cli_events( if super::routing::codex_request_kind(payload).as_deref() == Some("compaction") { record_problem( &ctx.stats, - &Turn::new(&provider.id, upstream_model, "ws", None), + &Turn::new(&provider.id, &stats_model, "ws", None), "compaction", &format!( "{BUILD_LABEL}: Codex sent a compaction call without a compaction_trigger item; treating it as a normal turn" diff --git a/src-tauri/src/proxy/tests_keys.rs b/src-tauri/src/proxy/tests_keys.rs index 7a626c3..8682ae7 100644 --- a/src-tauri/src/proxy/tests_keys.rs +++ b/src-tauri/src/proxy/tests_keys.rs @@ -448,6 +448,35 @@ async fn it_011_dispatch_records_the_serving_key_id() { assert_eq!(summary.per_key[0].requests, 1); } +#[tokio::test] +async fn it_013_routed_logs_record_the_actual_upstream_model() { + let upstream = TestUpstream { + statuses: std::collections::HashMap::new(), + hits: Arc::new(Mutex::new(std::collections::HashMap::new())), + }; + let url = spawn_test_upstream(upstream).await; + let ctx = test_ctx(KeyPools::new()); + let provider = keyed_provider(format!("{url}/v1"), vec![key("key-a", "secret-a")], false); + let payload = json!({"model": "gpt-5.6-luna", "input": "hi", "stream": false}); + + let response = dispatch_routed( + &ctx, + &provider, + "deepseek-v4-flash", + "gpt-5.6-luna", + &payload, + WireApi::Responses, + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + + let logged = ctx.stats.read().await.recent(10); + assert_eq!(logged[0].provider, "test"); + assert_eq!(logged[0].model, "test/deepseek-v4-flash"); +} + #[tokio::test] async fn it_012_a_rate_limited_routed_turn_keeps_its_status_and_is_logged() { // Every key rate-limited used to reach the client as a 502 with no row diff --git a/src-tauri/src/proxy/tests_routing.rs b/src-tauri/src/proxy/tests_routing.rs index 6fcad36..1b92401 100644 --- a/src-tauri/src/proxy/tests_routing.rs +++ b/src-tauri/src/proxy/tests_routing.rs @@ -424,6 +424,26 @@ fn opencode_go_deepseek_groups_interleaved_calls_before_outputs() { assert_eq!(body["input"][5]["call_id"], "call_2"); } +#[test] +fn opencode_go_deepseek_drops_orphan_tool_output() { + let provider = multi_dialect_provider(); + let payload = json!({ + "input": [ + {"type": "message", "role": "user", "content": "continue"}, + {"type": "function_call_output", "call_id": "orphan-output", "output": "result"} + ], + "stream": true, + "tools": [] + }); + + let (_, body, _) = + build_upstream(&provider, &payload, "deepseek-v4-flash", WireApi::Responses).unwrap(); + + let items = body["input"].as_array().unwrap(); + assert_eq!(items.len(), 1, "{items:?}"); + assert_eq!(items[0]["type"], "message"); +} + #[test] fn opencode_go_deepseek_moves_interleaved_assistant_message_after_tool_output() { let provider = multi_dialect_provider(); From 172253c6fe3fe17b0ab6637cdb13ef208e2a23e1 Mon Sep 17 00:00:00 2001 From: FabioRocha231 Date: Wed, 12 Aug 2026 23:25:47 -0300 Subject: [PATCH 3/6] fix(translate): repair tool-exchange items in sanitized responses --- src-tauri/src/translate.rs | 1 + src-tauri/src/translate/request.rs | 63 ++++++++++++++++++++++++++++++ src-tauri/src/translate/tests_b.rs | 20 ++++++++++ 3 files changed, 84 insertions(+) diff --git a/src-tauri/src/translate.rs b/src-tauri/src/translate.rs index 93a629a..ec013e4 100644 --- a/src-tauri/src/translate.rs +++ b/src-tauri/src/translate.rs @@ -11,6 +11,7 @@ pub use compaction::{ encode_compaction_summary, COMPACTION_PROMPT, COMPACTION_SUMMARY_PREFIX, OPAQUE_COMPACTION_NOTE, }; +pub(crate) use request::repair_tool_exchange_items; pub use request::{chat_to_anthropic, flatten_agent_messages, responses_to_chat}; pub use response::{ anthropic_to_chat, anthropic_to_responses, apply_namespaces_to_output, diff --git a/src-tauri/src/translate/request.rs b/src-tauri/src/translate/request.rs index 64a4be7..2b253c2 100644 --- a/src-tauri/src/translate/request.rs +++ b/src-tauri/src/translate/request.rs @@ -58,6 +58,64 @@ fn flatten_content_parts(content: Option<&mut Value>, part_type: &str) -> usize touched } +/// Drop Responses tool outputs that have no matching call. A context clamp or +/// a lost history entry can leave a `function_call_output` without its +/// `function_call`; Console Go turns that into a Chat `tool` message with no +/// preceding `tool_calls` and rejects the request. +fn tool_exchange_counts(items: &[Value]) -> BTreeMap { + let mut calls: BTreeMap = BTreeMap::new(); + for item in items.iter() { + let Some(call_id) = item.get("call_id").and_then(Value::as_str) else { + continue; + }; + if is_tool_call_item(item) { + *calls.entry(call_id.to_string()).or_default() += 1; + } + } + calls +} + +fn is_tool_call_item(item: &Value) -> bool { + matches!( + item.get("type").and_then(Value::as_str), + Some("function_call") | Some("custom_tool_call") + ) +} + +fn is_tool_output_item(item: &Value) -> bool { + matches!( + item.get("type").and_then(Value::as_str), + Some("function_call_output") | Some("custom_tool_call_output") + ) +} + +fn keep_tool_exchange_item( + item: &Value, + calls: &BTreeMap, + seen_outputs: &mut BTreeMap, +) -> bool { + if is_tool_output_item(item) { + let Some(call_id) = item.get("call_id").and_then(Value::as_str) else { + return false; + }; + let seen = seen_outputs.entry(call_id.to_string()).or_default(); + if *seen < calls.get(call_id).copied().unwrap_or(0) { + *seen += 1; + true + } else { + false + } + } else { + true + } +} + +pub(crate) fn repair_tool_exchange_items(items: &mut Vec) { + let calls = tool_exchange_counts(items); + let mut seen_outputs = BTreeMap::new(); + items.retain(|item| keep_tool_exchange_item(item, &calls, &mut seen_outputs)); +} + // --------------------------------------------------------------------------- pub fn responses_to_chat(payload: &Value, model: &str, unified_reasoning: bool) -> Result { @@ -85,6 +143,8 @@ pub fn responses_to_chat(payload: &Value, model: &str, unified_reasoning: bool) messages.push(json!({"role": "user", "content": text})); } Some(Value::Array(items)) => { + let calls = tool_exchange_counts(items); + let mut seen_outputs = BTreeMap::new(); // Thinking models require prior reasoning on replay: DeepSeek/Kimi // expect reasoning_content, while MiniMax expects the raw // reasoning_details array. Responses input carries it as reasoning @@ -94,6 +154,9 @@ pub fn responses_to_chat(payload: &Value, model: &str, unified_reasoning: bool) let mut pending_reasoning = String::new(); let mut pending_minimax_details: Option = None; for item in items { + if !keep_tool_exchange_item(item, &calls, &mut seen_outputs) { + continue; + } if item.get("type").and_then(Value::as_str) == Some("reasoning") { if let Some(parts) = item.get("summary").and_then(Value::as_array) { for p in parts { diff --git a/src-tauri/src/translate/tests_b.rs b/src-tauri/src/translate/tests_b.rs index 5165831..a5d0e59 100644 --- a/src-tauri/src/translate/tests_b.rs +++ b/src-tauri/src/translate/tests_b.rs @@ -147,6 +147,26 @@ fn parallel_function_calls_merge_into_one_assistant_message() { assert_eq!(msgs[4]["role"], "assistant"); } +#[test] +fn responses_to_chat_drops_orphan_tool_output() { + // A truncated/lost conversation can replay an output without its call. + // Sending that to Console Go fails with a tool-pairing 400, so the + // translator must drop the incomplete result. + let payload = json!({ + "input": [ + {"role":"user","content":[{"type":"input_text","text":"continue"}]}, + {"type":"function_call_output","call_id":"orphan-output","output":"result"}, + {"role":"assistant","content":[{"type":"output_text","text":"done"}]} + ] + }); + let out = responses_to_chat(&payload, "kimi-k3", false).unwrap(); + let msgs = out["messages"].as_array().unwrap(); + assert_eq!(msgs.len(), 2, "{msgs:?}"); + assert!(msgs + .iter() + .all(|m| m.get("role").and_then(Value::as_str) != Some("tool"))); +} + #[test] fn interleaved_developer_message_does_not_break_tool_sequence() { // macOS Codex injects a developer (-> system) item between the From c6b38e38bbc74a32098adf616701c18f57247980 Mon Sep 17 00:00:00 2001 From: FabioRocha231 Date: Wed, 12 Aug 2026 23:25:54 -0300 Subject: [PATCH 4/6] feat(zai): quota balance, catalog key, and reset-time normalization --- src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/state.rs | 214 +++++++++++++++++++++++-- src-tauri/src/state/model_discovery.rs | 5 + 4 files changed, 207 insertions(+), 14 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 3e1dc56..e4c30b7 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2256,6 +2256,7 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", + "chrono", "dirs", "futures", "http-body-util", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2d23e47..6d37125 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,6 +22,7 @@ tauri-plugin-updater = "2" tauri-plugin-process = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +chrono = { version = "0.4", default-features = false, features = ["std"] } json5 = "0.4" tokio = { version = "1", features = ["full"] } axum = { version = "0.8", features = ["http2", "ws"] } diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index bd21f5b..a1ea799 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -205,6 +205,8 @@ pub struct QuotaBar { /// 0..100 remaining. pub percent: f64, pub detail: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub reset_at: Option, } #[derive(Debug, Clone, Serialize)] @@ -222,6 +224,22 @@ pub struct ProviderBalance { pub error: Option, } +/// Kimi resetTime can omit seconds and the trailing Z; normalize it to a +/// browser-parseable ISO UTC string instead of forwarding a partial timestamp. +fn reset_at_iso(raw: &str) -> Option { + let parsed = chrono::DateTime::parse_from_rfc3339(raw) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .or_else(|_| { + chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%SZ") + .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%MZ")) + .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S")) + .or_else(|_| chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M")) + .map(|dt| dt.and_utc()) + }) + .ok()?; + Some(parsed.format("%Y-%m-%dT%H:%M:%SZ").to_string()) +} + fn quota_bar(label: &str, detail: &serde_json::Value) -> Option { let parse = |k: &str| { detail @@ -238,24 +256,40 @@ fn quota_bar(label: &str, detail: &serde_json::Value) -> Option { if limit <= 0.0 { return None; } - let reset = detail + let reset_at = detail .get("resetTime") .and_then(serde_json::Value::as_str) - .map(|s| s.chars().take(16).collect::()) - .unwrap_or_default(); + .and_then(reset_at_iso); Some(QuotaBar { label: label.to_string(), percent: (remaining / limit * 100.0).clamp(0.0, 100.0), - detail: format!( - "{} / {} left{}", - remaining as u64, - limit as u64, - if reset.is_empty() { - String::new() - } else { - format!(" · resets {reset}") - } - ), + detail: format!("{} / {} left", remaining as u64, limit as u64), + reset_at, + }) +} + +fn zai_quota_bar(limit: &serde_json::Value) -> Option { + let unit = limit["unit"].as_i64()?; + let number = limit["number"].as_i64()?; + let usage = limit["usage"].as_f64()?; + let remaining = limit["remaining"].as_f64()?; + if usage <= 0.0 { + return None; + } + let label = match (unit, number) { + (3, 5) => "5-hour window".to_string(), + (6, 1) => "Weekly quota".to_string(), + _ => format!("{number} {unit}-unit window"), + }; + let reset_at = limit["nextResetTime"] + .as_i64() + .and_then(chrono::DateTime::from_timestamp_millis) + .map(|time| time.format("%Y-%m-%dT%H:%M:%SZ").to_string()); + Some(QuotaBar { + label, + percent: (remaining / usage * 100.0).clamp(0.0, 100.0), + detail: format!("{} / {} credits left", remaining as u64, usage as u64), + reset_at, }) } @@ -305,6 +339,36 @@ async fn fetch_balance( req }; + if p.id == "zai-coding" { + // The quota endpoint is host-relative under /api/monitor, not under + // the provider base path, and its percentage field is consumed, not + // remaining. + let origin = base + .split_once("/api/") + .map(|(origin, _)| origin) + .unwrap_or(&base); + match get(format!("{origin}/api/monitor/usage/quota/limit")) + .send() + .await + { + Ok(res) if res.status().is_success() => { + result.ok = true; + if let Ok(body) = res.json::().await { + if let Some(limits) = body["data"]["limits"].as_array() { + result.bars = limits + .iter() + .filter(|limit| limit["type"].as_str() == Some("CREDIT_LIMIT")) + .filter_map(zai_quota_bar) + .collect(); + } + } + } + Ok(res) => result.error = Some(format!("quota returned {}", res.status())), + Err(e) => result.error = Some(e.to_string()), + } + return result; + } + match crate::proxy::family_of(&provider) { // Kimi Code quota: weekly allowance + rolling 5-hour window. Only // the Coding Plan endpoint exposes /usages; other Kimi-family @@ -1187,10 +1251,73 @@ mod tests { ) } + async fn zai_balance_probe_handler( + axum::extract::Extension(server): axum::extract::Extension, + headers: axum::http::HeaderMap, + ) -> (axum::http::StatusCode, axum::Json) { + let auth = headers + .get("authorization") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split_whitespace().last()) + .unwrap_or_default(); + if !server.totals.contains_key(auth) { + return ( + axum::http::StatusCode::UNAUTHORIZED, + axum::Json(serde_json::json!({})), + ); + } + ( + axum::http::StatusCode::OK, + axum::Json(serde_json::json!({ + "code": 200, + "success": true, + "data": { + "level": "lite", + "limits": [ + { + "type": "CREDIT_LIMIT", + "unit": 3, + "number": 5, + "usage": 2000, + "currentValue": 57, + "remaining": 1942, + "percentage": 2, + "nextResetTime": 1786588392963_i64 + }, + { + "type": "CREDIT_LIMIT", + "unit": 6, + "number": 1, + "usage": 10000, + "currentValue": 57, + "remaining": 9942, + "percentage": 1, + "nextResetTime": 1787175085997_i64 + }, + { + "type": "TOKENS_LIMIT", + "unit": 1, + "number": 1, + "usage": 10000, + "currentValue": 0, + "remaining": 10000, + "percentage": 0, + "nextResetTime": 1787175085997_i64 + } + ] + } + })), + ) + } + async fn spawn_balance_server(server: BalanceProbeServer) -> String { use axum::routing::get; let app = axum::Router::new() .route("/openrouter/v1/credits", get(balance_probe_handler)) + .route( + "/api/monitor/usage/quota/limit", + get(zai_balance_probe_handler), + ) .layer(axum::Extension(server)); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); @@ -1200,8 +1327,9 @@ mod tests { format!("http://{addr}") } - fn balance_provider(base_url: String, keys: Vec) -> AppState { + fn balance_provider_with_id(id: &str, base_url: String, keys: Vec) -> AppState { let mut provider = keyed_provider(keys); + provider.id = id.into(); provider.base_url = base_url; let mut config = AppConfig::default(); config.providers.insert(provider.id.clone(), provider); @@ -1211,6 +1339,10 @@ mod tests { state } + fn balance_provider(base_url: String, keys: Vec) -> AppState { + balance_provider_with_id("test", base_url, keys) + } + #[tokio::test] async fn it_012_per_key_balances_return_rows_with_correct_key_ids() { let url = spawn_balance_server(BalanceProbeServer { @@ -1241,6 +1373,60 @@ mod tests { assert_eq!(balances[1].balance_text.as_deref(), Some("$50.00")); } + #[tokio::test] + async fn it_013_zai_balance_returns_credit_windows() { + let url = spawn_balance_server(BalanceProbeServer { + totals: std::collections::HashMap::from([("zai-secret".to_string(), 0.0)]), + in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)), + max_in_flight: Arc::new(std::sync::atomic::AtomicUsize::new(0)), + delay_ms: 0, + }) + .await; + let state = balance_provider_with_id( + "zai-coding", + format!("{url}/api/coding/paas/v4"), + vec![key("key-a", "Alpha", Some("zai-secret"))], + ); + + let balances = state.provider_balances().await; + + assert_eq!(balances.len(), 1); + let balance = &balances[0]; + assert!(balance.ok); + assert_eq!(balance.error, None); + assert_eq!(balance.bars.len(), 2); + assert_eq!(balance.bars[0].label, "5-hour window"); + assert_eq!(balance.bars[1].label, "Weekly quota"); + assert!((balance.bars[0].percent - 97.1).abs() < 0.01); + assert!((balance.bars[1].percent - 99.42).abs() < 0.01); + assert_eq!(balance.bars[0].detail, "1942 / 2000 credits left"); + assert_eq!(balance.bars[1].detail, "9942 / 10000 credits left"); + assert_eq!( + balance.bars[0].reset_at.as_deref(), + Some("2026-08-13T02:33:12Z") + ); + assert_eq!( + balance.bars[1].reset_at.as_deref(), + Some("2026-08-19T21:31:25Z") + ); + } + + #[test] + fn quota_bar_normalizes_partial_reset_time() { + let bar = quota_bar( + "Weekly quota", + &serde_json::json!({ + "limit": 100, + "remaining": 67, + "resetTime": "2026-08-13T02:33Z", + }), + ) + .unwrap(); + + assert_eq!(bar.detail, "67 / 100 left"); + assert_eq!(bar.reset_at.as_deref(), Some("2026-08-13T02:33:00Z")); + } + #[tokio::test] async fn ut_076_provider_balances_returns_one_row_per_api_key() { let url = spawn_balance_server(BalanceProbeServer { diff --git a/src-tauri/src/state/model_discovery.rs b/src-tauri/src/state/model_discovery.rs index d13ab57..429af7f 100644 --- a/src-tauri/src/state/model_discovery.rs +++ b/src-tauri/src/state/model_discovery.rs @@ -231,6 +231,10 @@ fn models_dev_key(provider_id: &str) -> &str { // The preset id follows the provider used by the coding CLI, while // models.dev publishes this catalog as `kimi-for-coding`. "kimi-for-coding" + } else if provider_id == "zai-coding" { + // The coding endpoint advertises the full Z.AI catalog, which + // models.dev publishes as `zai` rather than the provider slug. + "zai" } else { provider_id } @@ -541,6 +545,7 @@ mod tests { } assert_eq!(models_dev_key("openrouter"), "openrouter"); assert_eq!(models_dev_key("kimi-coding"), "kimi-for-coding"); + assert_eq!(models_dev_key("zai-coding"), "zai"); } #[test] From 733183fce4948096b1bad10202c0045ddc66f4db Mon Sep 17 00:00:00 2001 From: FabioRocha231 Date: Wed, 12 Aug 2026 23:26:00 -0300 Subject: [PATCH 5/6] feat(ui): analytics chart, quota reset display, and provider/model tabs --- src/assets/logos/anthropic.svg | 1 + src/assets/logos/deepseek.png | Bin 0 -> 25338 bytes src/assets/logos/glm.png | Bin 0 -> 2351 bytes src/assets/logos/grok.png | Bin 0 -> 2249 bytes src/assets/logos/hy3.png | Bin 0 -> 62176 bytes src/assets/logos/mimo.png | Bin 0 -> 1718 bytes src/assets/logos/minimax.png | Bin 0 -> 1525 bytes src/assets/logos/qwen.png | Bin 0 -> 38419 bytes src/components/AnalyticsChart.test.tsx | 94 +++++ src/components/AnalyticsChart.tsx | 459 +++++++++++++++++++++++++ src/components/modelLogo.ts | 41 +++ src/i18n/en.ts | 12 + src/i18n/es.ts | 12 + src/i18n/pt.ts | 12 + src/i18n/zh.ts | 10 + src/lib/api.ts | 4 +- src/lib/utils.test.ts | 12 +- src/lib/utils.ts | 4 + src/pages/Overview.test.tsx | 149 ++++++++ src/pages/Overview.tsx | 127 ++++++- src/pages/Providers.test.tsx | 18 + src/pages/Providers.tsx | 208 ++++++----- src/types/index.ts | 1 + 23 files changed, 1065 insertions(+), 99 deletions(-) create mode 100644 src/assets/logos/anthropic.svg create mode 100644 src/assets/logos/deepseek.png create mode 100644 src/assets/logos/glm.png create mode 100644 src/assets/logos/grok.png create mode 100644 src/assets/logos/hy3.png create mode 100644 src/assets/logos/mimo.png create mode 100644 src/assets/logos/minimax.png create mode 100644 src/assets/logos/qwen.png create mode 100644 src/components/AnalyticsChart.test.tsx create mode 100644 src/components/AnalyticsChart.tsx create mode 100644 src/components/modelLogo.ts diff --git a/src/assets/logos/anthropic.svg b/src/assets/logos/anthropic.svg new file mode 100644 index 0000000..fb2a512 --- /dev/null +++ b/src/assets/logos/anthropic.svg @@ -0,0 +1 @@ + diff --git a/src/assets/logos/deepseek.png b/src/assets/logos/deepseek.png new file mode 100644 index 0000000000000000000000000000000000000000..d2bb649b950f4e2e20d76434aebc85cf77936eb4 GIT binary patch literal 25338 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 literal 0 HcmV?d00001 diff --git a/src/assets/logos/glm.png b/src/assets/logos/glm.png new file mode 100644 index 0000000000000000000000000000000000000000..ae79ff8f231eaec6b1636f7570256673094f1894 GIT binary patch literal 2351 zcmaJ@c|6o>7k_>~Kbf)43|Yf8#!~hhktH*-%|#>GA{pClqL5saWqwV!hLkKVGRmZG zja#-NLazyB&_u=MDk3dDN==1sD)auj??3P7{p0=XY|lC8d(Ly7=X3G{HhZfm>M8;N zR2V)UfdD`l3jv%QMiQ@u1z{vMz%R(t($Z2UlNlNsmX(!JC=^{?-I9_L4u?};Utd^Q zxOVMY?4Yr+@v2p;w6(RBm6fGZ>Ehzz%a<=NUAjafky=|@$H&KcJYHyMXirZMgTZie za$>XD!^6X=si}5$c62)ZbJ+t|0B}5p2R$gWV(#3-164KzuNv+cs6xjQ_I&nkz^s0kcp*DH5QDmQ&FKV2BD0$q7vG z0s~;rK)<`Ww%0zni?B!Pc;|LPl4VEpS^pgpQ`)KI*{bbxdQtGrx$4olnDuKsZmwBw zj8KEiW6v<(zTgmoZ|mMx_CnqkE>tYJkmK$qyja^}&9XMhS*pB$V__Pec$2^5+fP+l zZJ+&Gj078)%L5!^zPK>iK@bBwGExKxu*e7frpVsSZn)T>W-lcU&T|?Q9An=c-IS%E2emEZg$(P~ zhBTk16k^Jk85vu@5%>Jj#FRIfFWFNV$TVogeY6WlK3LJ(Eh?dF4-U@{x|?w`wSf8f zFU`Kh-2zWSGs~!LKa@i3wjB}sqZiId3`SDi)nWkX*55xF}dGvh12kaoec*Th~qCM+-D_XjsnXwTTc^37AM~vb` z7F=-(k-QOo{XP&Wo}z#XWPjtWYXwgA`L@EK@fgvE% zvRPX6IBlGW!7USL6{$ryL^k+x%g9)=30O2K93 zD1}3l6kK+3av&6hh5s3PuIwT^%vjHWE-EFPh!K`4 z-7f%7w&aQlcmkZcEWiL3gA`KtlfV%>STtwzjzLhT!a~f23a>{nK?4>sig>T@*b#67 zYwpuyDQZ(gM5)O-H)t<>oCbPZ-L7+ z=y3t^0VZ@mfg)DE*T~^ig{^>(Wv&7#-i^)wVMtW5yDSsV!(a{~tQ!1t#d0u9&BI$j zFEA87aOO&SlGhOmN+)$Ah2WD(bGpl@YOLH*kV2$Rp03mW0W)4CId6jqYB6UnDoHWY z^G>HXXRBDF(L@iE7O>@mIaiEm1<(q}2R~=4TB7lox*kN2_K%l)&Pd+DN|U9CEinYl z<7aH~F%%(?st?AJx;=a#%FdIRB@o!*ujj=16GCCd$q3|@PP`2yrQ$kO2!X&6&%N<1 z*#??SFOxDw*6w&#BvOnwMm?R6sT@SmQZgNDS~Ox~jX@xaZsREG9-xPsmrsC4u@QiA zf=Ko5Qvha${YSnWBC?&?!o}hck+JRHRBF+&C?XQh{2K}eEx1ROoE0^u z-_SLpSR&H?u+KUVYdE1A2j}o-=^EIp$$Xkvr@%ye?CGN9tt2*_tw|RwAUbxCK?V=% z@b6{eK^>lw-2vwwqGB;TXrWrd-xNeSrt7mfeYjTcbUb%~-ru+q*9tJlJJ+qrykx8>G?oDHs_&8FIz9+AqNj zUfdD|_azI~g6c!1IQzmYCi*sdVf7FF?p=ldBzEA18K{@Y>Ujp;ztLZPG5_D{Q)x4C4I_{koqP&8<6}%XeBU*V%H9XSzYAULK#E15Ur%{pQ@$ zuZtSJH^^?|y|EK2%Z9NnDjqowMVyXLQwuI%3dnBHnCXR0xfZPqj@|=rXOeeT)Xjl& z!Eo_v%KP))k4H5nZci42f=aU}CxiDUSa!~&k286THNv*`Kge(NSX+YkhiWC)MDDE1 z`5L-!Tk~8D+&H%s^XxG^$Kks$zyc65;Qzleyt1~)>$4j0?a{>+8^G|~>{08^;{F@Z CoYueq literal 0 HcmV?d00001 diff --git a/src/assets/logos/grok.png b/src/assets/logos/grok.png new file mode 100644 index 0000000000000000000000000000000000000000..a50e3fd36af90b385c2a617f7d6b3cff3b99103b GIT binary patch literal 2249 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 Oz{ADI`42~S%6|YXA0M0m literal 0 HcmV?d00001 diff --git a/src/assets/logos/hy3.png b/src/assets/logos/hy3.png new file mode 100644 index 0000000000000000000000000000000000000000..e1d371df7347647dd3e5e3c64c3c3fd2b39e35ed GIT binary patch literal 62176 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 HTZR4)k>$1F literal 0 HcmV?d00001 diff --git a/src/assets/logos/mimo.png b/src/assets/logos/mimo.png new file mode 100644 index 0000000000000000000000000000000000000000..624ec66d52c37ee00bfbf839520dc5c05e7ac6c7 GIT binary patch literal 1718 zcmV;n21)seP)6ET>$^~@c(}||B*@maUcJjUjN(1|D9?7>f8Tt4F9f!|Gky}vxxt5 z6aUb<|MKbo!g@mji5qkV!;G29 z9jWd2lSQSlACjhXHj@bm1OkCTAP@)y0)apv5C{YUfea{$vN%2#MX_X56vty6XOVB? z_{xrle0^HfySA*-gesuIAL@S7OKDoQ?X14+^YbBo9hRubPm8un2q6d&!Wzx$e)U$U zE0t8c#eN%mH~w&%l>xcg{ng^1MOi3F($?3)oAq&CR~QjV%ZC`0A}B9g5A7-1)+s_d z#`hu!SC^$*^W%LyEzVT5;QchwL#shMZbijXpQgAko8n14e+?b(6-+tQBtHw022 zEt{V$FtKHRCzP(O+P`d(P4k29#adhRgmYr`@!y47Yt3jc8*UW|(t{oQ8++b66iK#r z>~~{66%H&AoZQ*p)q(M@Mee|TS%TECCm#Yw?Y}2zjlNB@%CY_QU9>O>P`h-150TuQ z{f-^rE}R-0aAuVcfB@B|04^RHP$yz2A{VaTKs`Wf24LZ-0w{rz0gkr{FjWP4vkJK2x&RT< zFO#WkQGOvEFWzzQTTXvhSTAP@uy0`*ZDcQBv`Fgb@R0O~^m?oCwzCM$+O6hI>o%*^@# zRm?OZQ2?YqP0fvC$^etq!%krU^+?9;OsfMdA8kk&pi%uZZXo^w5N)36P{hyrk!Al- zgaMR8ah8e!ywTpychS~9pRP7vZ~Aa!VJB8zC*Tifh5&y;PB~1%aQX#dEf1jFUXQ{6 zy#MZshQm4XWpAi3fO2Q3FabcTVbB%G=ASne22fscUGg8R1V9irgS+-#EDC@tRRC^h zR1N^o2L|*Ve_?=BbyR#d0}zCR&dgtV0M+hjE)ReiJpcxMfyHNO`v7b1h?oaJ=8PTy z4O@C83lJy|2(GdKXcaL!_ibemN*n1Ka0d`x*~>)_@XlU-a|ghm*5U!Yv6uUhHH<{p z5duuv%WD7t0001h{{=ukkO6>M0O`)$s{!y=x!nRNcc-NXG_M9AJ*2b+pgN?K9@JX` zP#)AvPpjAgP@h&gCNEI~>JwYyQ;T*1l&2O&CwFZFC{ON+POn=A&{*jfT;Q+`&?o~4 zE-~2#P~Y+-xX5Q4K>d8E;4-UiCsto(6I?&try7dpBxAl1Ro4-fD{r(3p zb|uXN5V`N|>$*o!ygPyJAt3Kgpna5Y^atTae-zR8VNku7Xs6L1#~uBF=+PfZ_vyxl z@-p~HtnvY1Chy$n6Jv}%HOVCAt`k_5x#Tc@`lP!3(1CpTvBt@h@A8mg$ZU!#A4$S= z#_~_Z-}qt2e$#2%91jd|Vjc(Sgn-ye*GU7KlZ2_I#k{-A z(t65TR2zPO4rM)QaB}_K2tCoVy+Mj`zPi|%6_IB74r zgZIua5XbHHvMZC@<3#tIHawA!kB9_8+RmE%;Qb>a4@GftIA5DfJ=?Wan%=(TG8Da7 zw!7Kla>}>M!fLqHRre*GXL{xD=zu^V5C{YUfj}S-2m}IwKp+|Y1B{}BI3^o?)&Kwi M07*qoM6N<$g4#kfY5)KL literal 0 HcmV?d00001 diff --git a/src/assets/logos/minimax.png b/src/assets/logos/minimax.png new file mode 100644 index 0000000000000000000000000000000000000000..1cd16498689da081e640134cd72a41a727ff496e GIT binary patch literal 1525 zcmVNH53de*z(W>NhB$jNFaU&Vt6qI=z|a12g5t)i^dn9)L={# zg*LX)2PHP9F{TN~t(y2y1F;5;mO#9>xAt_ue3+Rtd*;kl_sh)ewf9>8 z^r zE@J#yNp@Kh$;sCM5WvbEFCgC<0I#2~Y#54A=5#ost}icUKbn zvhbE!_zC8xl;qd!#4>t!?>9)2{5HXNNWvFoVWvw_6F`p4TL8QR$OJ&l(nyJ)lEU*! zSu->3AS7RR^fWL-G9Qa?{3%PJaR9WxNB)A+H)(I;gvgWpw02|6zA3{GaV?35V-87oj!l` zNu~GgQ+RKa;jy3LtE(gr?AOdQUqB1t+O>#p zTqA$uZywgx(alTHOVQ3n@_)QdxbPMXhiGmN_cAfMLmX{b4@;I}P><2-1_B{RLzm6dafqa_~JFvJ&a^%a{J@v{Gbsr2l?`myY>|Ox4Ogoe#8_fB+!7d#@XZZazYIwa(iFxuSw>j~ z!ci|_;i-?2es~XrDkatI^^t+L@dA`L*3^rqTeqO@0q&sr8RnjPkn|(B!8kOPOpos3 z>YP>6hls&6p7d-C!!hZ-x5H=zy#ybP5wo2-+X!1nR0Gu{XIcv^-tL1D{nLM9_LB$L z@!Y4$UcEs0=Z32hdo^?A{~KOyZSPA$mGxCd<$zH#}{Lv@i#(@5hPUG1Kr72i>(9|rrudHhH@`>Uise0jDGWbZ12saj~yak{v-Jteq(M-TkuuEGBAvEOwKWNXp2YlHVfW7gfrlr!%+qM7Vv&@ b{CE5Zpg*1e(004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00006VoOIv0RI600RN!9r;`8x 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
02u&_laZ&`v*bEx5f#n8> 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 literal 0 HcmV?d00001 diff --git a/src/components/AnalyticsChart.test.tsx b/src/components/AnalyticsChart.test.tsx new file mode 100644 index 0000000..d50eefb --- /dev/null +++ b/src/components/AnalyticsChart.test.tsx @@ -0,0 +1,94 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { AnalyticsChart, paretoFrontier, resolveLabelOverlaps, type ChartPoint } from './AnalyticsChart' + +interface Box { + x: number + y: number + width: number + height: number +} + +const overlaps = (a: Box, b: Box) => + a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y + +describe('paretoFrontier', () => { + it('returns only points not dominated on both cost and latency', () => { + const points = [ + { cost: 10, latency: 100 }, + { cost: 5, latency: 50 }, + { cost: 7, latency: 40 }, + { cost: 4, latency: 80 }, + ] + expect(paretoFrontier(points)).toEqual([3, 1, 2]) + }) + + it('keeps every point when cost and latency trade off monotonically', () => { + const points = [ + { cost: 1, latency: 100 }, + { cost: 2, latency: 80 }, + { cost: 3, latency: 60 }, + ] + expect(paretoFrontier(points)).toEqual([0, 1, 2]) + }) + + it('returns an empty array for no points', () => { + expect(paretoFrontier([])).toEqual([]) + }) +}) + +describe('resolveLabelOverlaps', () => { + it('nudges a label away from an earlier label it would overlap', () => { + const result = resolveLabelOverlaps( + [ + { x: 10, y: 10, width: 20, height: 13 }, + { x: 12, y: 12, width: 20, height: 13 }, + ], + [], + 0, + 100, + 6, + ) + expect(overlaps(result[0], result[1])).toBe(false) + }) + + it('does not treat a label own bubble as an obstacle', () => { + const box = { x: 10, y: 10, width: 20, height: 13 } + const result = resolveLabelOverlaps([box], [box], 0, 100, 6) + expect(result[0]).toEqual(box) + }) +}) + +describe('AnalyticsChart', () => { + const baseProps = { + locale: 'en' as const, + axisCost: 'Avg cost / request (log)', + axisSpeed: 'Speed (faster = up)', + empty: 'No plottable usage yet.', + bubbleLegend: 'Bubble size = requests', + title: 'Cost vs speed by model', + subtitle: 'Each bubble is a model; bigger means more requests.', + frontierLegend: 'Pareto frontier', + } + + it('renders one marker group per point with a logo image or monogram fallback', () => { + const data: ChartPoint[] = [ + { key: 'opencode-go:deepseek-v4-flash', label: 'model-a', sublabel: 'Acme', cost: 1, latencyMs: 100, requests: 10 }, + { key: 'gpt-5.5', label: 'model-b', sublabel: 'Acme', cost: 2, latencyMs: 200, requests: 20 }, + ] + const { container } = render() + + expect(screen.getByTestId('marker-opencode-go:deepseek-v4-flash')).toBeInTheDocument() + expect(screen.getByTestId('marker-gpt-5.5')).toBeInTheDocument() + expect(container.querySelectorAll('image')).toHaveLength(1) + expect(screen.getByText('G')).toBeInTheDocument() + expect(screen.getByText('model-a')).toBeInTheDocument() + }) + + it('renders the empty state without an SVG', () => { + const { container } = render() + + expect(screen.getByText('No plottable usage yet.')).toBeInTheDocument() + expect(container.querySelector('svg')).toBeNull() + }) +}) diff --git a/src/components/AnalyticsChart.tsx b/src/components/AnalyticsChart.tsx new file mode 100644 index 0000000..1087d22 --- /dev/null +++ b/src/components/AnalyticsChart.tsx @@ -0,0 +1,459 @@ +import { useStrings } from '@/i18n' +import type { Locale } from '@/i18n' +import { Card, CardAction, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { modelLogoSrc, modelMonogram } from './modelLogo' + +export interface ChartPoint { + key: string + label: string + sublabel?: string + cost: number + latencyMs: number + requests: number +} + +interface AnalyticsChartProps { + data: ChartPoint[] + locale: Locale + axisCost: string + axisSpeed: string + empty: string + bubbleLegend: string + title?: string + subtitle?: string + frontierLegend?: string +} + +const WIDTH = 720 +const HEIGHT = 460 +const MARGIN = { top: 16, right: 16, bottom: 44, left: 56 } +const MIN_R = 5 +const MAX_R = 18 +// sqrt so bubble area scales linearly with request volume. +const RADIUS_K = (MAX_R - MIN_R) / Math.sqrt(10_000) +const LABEL_CHAR_WIDTH = 6.5 +const LABEL_LINE_HEIGHT = 13 +const LABEL_GAP = 18 +const LABEL_OFFSET_Y = 4 +const LABEL_NUDGE_STEP = 6 + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)) +} + +function fmtLatency(ms: number): string { + return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms` +} + +function truncateModel(label: string): string { + return label.length > 14 ? `${label.slice(0, 13)}...` : label +} + +interface LabelBox { + x: number + y: number + width: number + height: number +} + +function rectsOverlap(a: LabelBox, b: LabelBox): boolean { + return ( + a.x < b.x + b.width && + a.x + a.width > b.x && + a.y < b.y + b.height && + a.y + a.height > b.y + ) +} + +// Nudge label boxes vertically so they avoid each other and every bubble but +// their own, keeping each label as close to its natural spot as possible. +// eslint-disable-next-line react-refresh/only-export-components +export function resolveLabelOverlaps( + labels: LabelBox[], + bubbles: LabelBox[], + minY: number, + maxY: number, + step: number, +): LabelBox[] { + const placed: LabelBox[] = [] + return labels.map((label, index) => { + const otherBubbles = bubbles.filter((_, other) => other !== index) + const bottom = Math.max(minY, maxY - label.height) + let resolved: LabelBox | null = null + for (let distance = 0; distance <= 240; distance += step) { + const offsets = distance === 0 ? [0] : [distance, -distance] + for (const offset of offsets) { + const candidate = { ...label, y: clamp(label.y + offset, minY, bottom) } + if ( + placed.every((box) => !rectsOverlap(candidate, box)) && + otherBubbles.every((box) => !rectsOverlap(candidate, box)) + ) { + resolved = candidate + break + } + } + if (resolved) break + } + const result = resolved ?? { ...label } + placed.push(result) + return result + }) +} + +function niceStep(range: number): number { + const rough = range / 4 + const magnitude = 10 ** Math.floor(Math.log10(rough)) + const normalized = rough / magnitude + const step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10 + return step * magnitude +} + +// eslint-disable-next-line react-refresh/only-export-components +export function paretoFrontier(points: { cost: number; latency: number }[]): number[] { + return points + .map((_, index) => index) + .filter((index) => { + const point = points[index] + return !points.some((other, otherIndex) => { + if (index === otherIndex) return false + return ( + other.cost <= point.cost && + other.latency <= point.latency && + (other.cost < point.cost || other.latency < point.latency) + ) + }) + }) + .sort((a, b) => points[a].cost - points[b].cost || points[a].latency - points[b].latency) +} + +export function AnalyticsChart({ + data, + locale, + axisCost, + axisSpeed, + empty, + bubbleLegend, + title = '', + subtitle = '', + frontierLegend = '', +}: AnalyticsChartProps) { + const s = useStrings() + + const legend = ( +
+ + + {bubbleLegend} + + + + {frontierLegend} + +
+ ) + const header = ( + +
+ {title} + {subtitle &&

{subtitle}

} +
+ {frontierLegend ? {legend} : null} +
+ ) + + if (data.length === 0) { + return ( + + {header} + +

{empty}

+
+
+ ) + } + + const currency = new Intl.NumberFormat(locale, { + style: 'currency', + currency: 'USD', + notation: 'compact', + }) + const minCost = Math.min(...data.map((point) => point.cost)) + const maxCost = Math.max(...data.map((point) => point.cost)) + const minLatency = Math.min(...data.map((point) => point.latencyMs)) + const maxLatency = Math.max(...data.map((point) => point.latencyMs)) + const plotW = WIDTH - MARGIN.left - MARGIN.right + const plotH = HEIGHT - MARGIN.top - MARGIN.bottom + + const xRange = Math.log10(maxCost) - Math.log10(minCost) + const xPad = Math.max(xRange * 0.1, 0.1) + const xMin = Math.log10(minCost) - xPad + const xMax = Math.log10(maxCost) + xPad + const x = (cost: number) => + MARGIN.left + ((Math.log10(cost) - xMin) / (xMax - xMin)) * plotW + + const yRange = maxLatency - minLatency + const yPad = Math.max(yRange * 0.1, 1) + const yMin = minLatency - yPad + const yMax = maxLatency + yPad + // Low latency (fast) at top, high latency (slow) at bottom, so "faster = up" matches the axis label. + const y = (latencyMs: number) => + MARGIN.top + ((latencyMs - yMin) / (yMax - yMin)) * plotH + + const frontier = paretoFrontier(data.map((point) => ({ cost: point.cost, latency: point.latencyMs }))) + const radiusFor = (point: ChartPoint) => + clamp(MIN_R + Math.sqrt(point.requests) * RADIUS_K, MIN_R, MAX_R) + const labelPositions = resolveLabelOverlaps( + data.map((point) => { + const label = truncateModel(point.label) + const width = label.length * LABEL_CHAR_WIDTH + return { + x: clamp(x(point.cost) + LABEL_GAP, MARGIN.left, WIDTH - MARGIN.right - width), + y: clamp( + y(point.latencyMs) + LABEL_OFFSET_Y, + MARGIN.top + 8, + HEIGHT - MARGIN.bottom - LABEL_LINE_HEIGHT, + ), + width, + height: LABEL_LINE_HEIGHT, + } + }), + data.map((point) => { + const r = radiusFor(point) + const pad = 2 + return { + x: x(point.cost) - r - pad, + y: y(point.latencyMs) - r - pad, + width: (r + pad) * 2, + height: (r + pad) * 2, + } + }), + MARGIN.top, + HEIGHT - MARGIN.bottom, + LABEL_NUDGE_STEP, + ) + const xTicks: number[] = [] + for (let exp = Math.ceil(Math.log10(minCost)); exp <= Math.floor(Math.log10(maxCost)); exp += 1) { + const value = 10 ** exp + if (value >= minCost && value <= maxCost) xTicks.push(value) + } + + const yStep = yRange > 0 ? niceStep(yRange) : 1 + const yTicks: number[] = [] + for (let value = Math.ceil(minLatency / yStep) * yStep; value <= maxLatency + yStep / 2; value += yStep) { + yTicks.push(Math.round(value)) + } + + const xTickAnchor = (pos: number): 'start' | 'middle' | 'end' => + pos <= MARGIN.left + 20 ? 'start' : pos >= WIDTH - MARGIN.right - 20 ? 'end' : 'middle' + + return ( + + {header} + + + + + + + {data.map((point) => { + const clipId = `badge-${point.key.replace(/[^a-zA-Z0-9_-]/g, '-')}` + return ( + + + + ) + })} + + {xTicks.map((value) => { + const pos = x(value) + return ( + + ) + })} + {yTicks.map((value) => { + const pos = y(value) + return ( + + ) + })} + + + + {xTicks.map((value) => { + const pos = x(value) + return ( + + + + {currency.format(value)} + + + ) + })} + + {yTicks.map((value) => { + const pos = y(value) + return ( + + + + {fmtLatency(value)} + + + ) + })} + + + {axisCost} + + + {axisSpeed} + + + {bubbleLegend} + + + {frontier.length > 1 && ( + `${x(data[index].cost)},${y(data[index].latencyMs)}`) + .join(' ')} + fill="none" + stroke="#ef4444" + strokeWidth={2} + strokeDasharray="5 4" + strokeLinecap="round" + /> + )} + + {data.map((point, index) => { + const cx = x(point.cost) + const cy = y(point.latencyMs) + const radius = radiusFor(point) + const src = modelLogoSrc(point.key) + const clipId = `badge-${point.key.replace(/[^a-zA-Z0-9_-]/g, '-')}` + const label = truncateModel(point.label) + const labelX = labelPositions[index].x + const labelY = labelPositions[index].y + const tooltip = [ + point.label, + point.sublabel ?? '', + currency.format(point.cost), + fmtLatency(point.latencyMs), + `${point.requests} ${s.overview.reqShort}`, + ] + .filter(Boolean) + .join('\n') + return ( + + + {tooltip} + + + + {src ? ( + + ) : ( + + {modelMonogram(point.key)} + + )} + + + {label} + + + ) + })} + + + + ) +} diff --git a/src/components/modelLogo.ts b/src/components/modelLogo.ts new file mode 100644 index 0000000..240c7ba --- /dev/null +++ b/src/components/modelLogo.ts @@ -0,0 +1,41 @@ +const modules = import.meta.glob('../assets/logos/*', { + eager: true, + query: '?url', + import: 'default', +}) + +const LOGOS: Record = {} +for (const [path, mod] of Object.entries(modules)) { + const key = path.split('/').pop()!.replace(/\.(png|svg)$/, '') + LOGOS[key] = mod +} + +const BRAND_PREFIXES: Array<[string, string]> = [ + ['deepseek', 'deepseek'], + ['glm', 'glm'], + ['grok', 'grok'], + ['qwen', 'qwen'], + ['minimax', 'minimax'], + ['mimo', 'mimo'], + ['hy3', 'hy3'], + ['hunyuan', 'hy3'], + ['claude', 'anthropic'], + ['opus', 'anthropic'], + ['sonnet', 'anthropic'], + ['haiku', 'anthropic'], +] + +export function modelLogoSrc(modelId: string): string | null { + const segment = modelId.split(/[/:]/).pop()!.toLowerCase() + for (const [prefix, brand] of BRAND_PREFIXES) { + if (segment.startsWith(prefix)) return LOGOS[brand] ?? null + } + return null +} + +export function modelMonogram(modelId: string): string { + const segment = modelId.split(/[/:]/).pop()?.toLowerCase() ?? '' + if (segment.startsWith('gpt')) return 'G' + if (segment.startsWith('kimi') || segment.startsWith('k3')) return 'K' + return segment.match(/[a-z0-9]/)?.[0]?.toUpperCase() ?? '?' +} diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 457a7f5..f555fd8 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -54,6 +54,17 @@ const en = { h24: '24h', d7: '7d', d30: '30d', + tabGeneral: 'General', + tabAnalytics: 'Analytics', + chartTitle: 'Cost per request vs speed by model', + chartSubtitle: + 'Each bubble is a model; bigger means more requests. The dashed line marks the best average cost per request and speed tradeoff.', + frontierLegend: 'Pareto frontier', + chartEmpty: + 'No plottable usage yet. Send a few requests and pick a model to see average cost and speed.', + axisCost: 'Avg cost / request (log)', + axisSpeed: 'Speed (faster = up)', + bubbleLegend: 'Bubble size = requests', requests: 'Requests', inputTokens: 'Input tokens', outputTokens: 'Output tokens', @@ -120,6 +131,7 @@ const en = { claudeCliMissing: 'claude CLI not found on PATH', claudeNoKey: 'No API key needed - this provider uses the login of your local Claude Code CLI.', keys: 'API keys', + modelsLabel: 'Models', addKey: 'Add key', keyName: 'Key name', keyValue: 'API key', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 12b64f3..b3adb59 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -53,6 +53,17 @@ const es: DeepPartial = { setupMissingModel: 'Activar un modelo', dismissSetupBanner: 'Descartar recordatorio de configuración', today: 'Hoy', + tabGeneral: 'General', + tabAnalytics: 'Análisis', + chartTitle: 'Coste por petición vs velocidad por modelo', + chartSubtitle: + 'Cada burbuja es un modelo; más grande significa más peticiones. La línea discontinua marca el mejor equilibrio de coste medio por petición y velocidad.', + frontierLegend: 'Frontera de Pareto', + chartEmpty: + 'Aún no hay uso graficable. Envía algunas peticiones y elige un modelo para ver coste medio y velocidad.', + axisCost: 'Coste medio / petición (log)', + axisSpeed: 'Velocidad (más rápido = arriba)', + bubbleLegend: 'Tamaño de la burbuja = peticiones', requests: 'Solicitudes', inputTokens: 'Tokens de entrada', outputTokens: 'Tokens de salida', @@ -116,6 +127,7 @@ const es: DeepPartial = { claudeCliMissing: 'claude CLI no encontrado en el PATH', claudeNoKey: 'No se necesita clave de API - este proveedor usa el inicio de sesión de tu Claude Code CLI local.', keys: 'Claves de API', + modelsLabel: 'Modelos', addKey: 'Agregar clave', keyName: 'Nombre de la clave', keyValue: 'Clave de API', diff --git a/src/i18n/pt.ts b/src/i18n/pt.ts index d84d5be..25e1ef3 100644 --- a/src/i18n/pt.ts +++ b/src/i18n/pt.ts @@ -52,6 +52,17 @@ const pt: DeepPartial = { setupMissingModel: 'Ativar um modelo', dismissSetupBanner: 'Dispensar lembrete de configuração', today: 'Hoje', + tabGeneral: 'Geral', + tabAnalytics: 'Análise', + chartTitle: 'Custo por requisição vs velocidade por modelo', + chartSubtitle: + 'Cada bolha é um modelo; maior significa mais requisições. A linha tracejada marca a melhor relação de custo médio por requisição e velocidade.', + frontierLegend: 'Fronteira de Pareto', + chartEmpty: + 'Ainda não há uso plotável. Envie algumas requisições e selecione um modelo para ver custo médio e velocidade.', + axisCost: 'Custo médio / requisição (log)', + axisSpeed: 'Velocidade (mais rápido = cima)', + bubbleLegend: 'Tamanho da bolha = requisições', requests: 'Requisições', inputTokens: 'Tokens de entrada', outputTokens: 'Tokens de saída', @@ -115,6 +126,7 @@ const pt: DeepPartial = { claudeCliMissing: 'claude CLI não encontrado no PATH', claudeNoKey: 'Sem chave de API - este provedor usa o login do seu Claude Code CLI local.', keys: 'Chaves de API', + modelsLabel: 'Modelos', addKey: 'Adicionar chave', keyName: 'Nome da chave', keyValue: 'Chave de API', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index c3c17a9..3d65541 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -54,6 +54,15 @@ const zh: DeepPartial = { h24: '24 小时', d7: '7 天', d30: '30 天', + tabGeneral: '总览', + tabAnalytics: '分析', + chartTitle: '每次请求成本与速度对比', + chartSubtitle: '每个气泡代表一个模型; 越大表示请求越多。虚线标记每次请求平均成本与速度的最佳平衡。', + frontierLegend: '帕累托前沿', + chartEmpty: '暂无可绘制的用量。发送一些请求并选择模型即可查看平均成本与速度。', + axisCost: '平均成本 / 请求 (log)', + axisSpeed: '速度 (越快越靠上)', + bubbleLegend: '气泡大小 = 请求数', requests: '请求数', inputTokens: '输入 token', outputTokens: '输出 token', @@ -116,6 +125,7 @@ const zh: DeepPartial = { claudeCliMissing: '在 PATH 中未找到 claude CLI', claudeNoKey: '无需 API 密钥 - 此提供商使用本地 Claude Code CLI 的登录状态。', keys: 'API 密钥', + modelsLabel: '模型', addKey: '添加密钥', keyName: '密钥名称', keyValue: 'API 密钥', diff --git a/src/lib/api.ts b/src/lib/api.ts index e66658d..a64bc43 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -551,7 +551,7 @@ function mock(cmd: string, args?: Record): Promise { key_name: 'Principal', ok: true, bars: [ - { label: 'Weekly quota', percent: 67, detail: '67 / 100 left · resets 2026-03-08T09:20' }, + { label: 'Weekly quota', percent: 67, detail: '67 / 100 left', reset_at: '2026-03-08T09:20:00Z' }, { label: '5-hour window', percent: 93, detail: '93 / 100 left' }, ], balance_text: null, @@ -563,7 +563,7 @@ function mock(cmd: string, args?: Record): Promise { key_name: 'Work', ok: true, bars: [ - { label: 'Weekly quota', percent: 34, detail: '34 / 100 left · resets 2026-03-08T09:20' }, + { label: 'Weekly quota', percent: 34, detail: '34 / 100 left', reset_at: '2026-03-08T09:20:00Z' }, ], balance_text: null, error: null, diff --git a/src/lib/utils.test.ts b/src/lib/utils.test.ts index 74d00df..e702513 100644 --- a/src/lib/utils.test.ts +++ b/src/lib/utils.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { formatContextWindow } from './utils' +import { avgCostPerRequest, formatContextWindow } from './utils' describe('formatContextWindow', () => { it('keeps 1M-class windows readable', () => { @@ -32,3 +32,13 @@ describe('formatContextWindow', () => { expect(formatContextWindow(163_000)).toBe('163K') }) }) + +describe('avgCostPerRequest', () => { + it('divides aggregate cost by successful request count', () => { + expect(avgCostPerRequest(3.34, 759)).toBeCloseTo(0.0044, 4) + }) + + it('returns zero instead of dividing by no requests', () => { + expect(avgCostPerRequest(1, 0)).toBe(0) + }) +}) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f856ad9..ce0f5a9 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -27,3 +27,7 @@ export function formatContextWindow(window: number): string { if (Number.isInteger(binary)) return `${binary}K` return `${Math.round(decimal)}K` } + +export function avgCostPerRequest(costUsd: number, requests: number): number { + return requests > 0 ? costUsd / requests : 0 +} diff --git a/src/pages/Overview.test.tsx b/src/pages/Overview.test.tsx index f4cb8ae..bfc8738 100644 --- a/src/pages/Overview.test.tsx +++ b/src/pages/Overview.test.tsx @@ -239,6 +239,37 @@ describe('Overview per-key dashboard', () => { expect(screen.getAllByText(/not reported/i).length).toBeGreaterThan(0) }) + it('formats quota reset_at with the active locale', async () => { + mockBalances = [ + { + provider_id: 'acme', + key_id: 'key-a', + key_name: 'Alpha', + ok: true, + bars: [ + { + label: 'Weekly quota', + percent: 67, + detail: '67 / 100 left', + reset_at: '2026-08-13T02:33:12Z', + }, + ], + balance_text: null, + error: null, + }, + ] + + renderOverview() + + const resetAt = new Intl.DateTimeFormat('en', { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(new Date('2026-08-13T02:33:12Z')) + expect(await screen.findByText(`67 / 100 left · resets ${resetAt}`)).toBeInTheDocument() + }) + it('E2E-002 shows the primary key attribution after routing', async () => { mockConfig = { port: 4180, @@ -389,3 +420,121 @@ describe('Overview per-key dashboard', () => { expect(screen.queryByText(/sk-secret/i)).not.toBeInTheDocument() }) }) + +describe('Overview Analytics tab', () => { + it('shows the empty state when stats has no plottable models', async () => { + renderOverview() + await screen.findByText(/no requests in this period/i) + await userEvent.click(screen.getByRole('tab', { name: 'Analytics' })) + + expect(await screen.findByText(/no plottable usage yet/i)).toBeInTheDocument() + }) + + it('plots average cost per request instead of aggregate cost', async () => { + mockStats = { + period_secs: 86_400, + requests: 10, + input_tokens: 1000, + output_tokens: 100, + cached_tokens: 0, + cache_ratio: 0, + cost_usd: 2, + per_provider: [ + { + provider: 'opencode-go', + requests: 10, + input_tokens: 1000, + output_tokens: 100, + cached_tokens: 0, + cost_usd: 2, + models: [ + { + model: 'opencode-go/deepseek-v4-flash', + requests: 10, + errors: 0, + input_tokens: 1000, + output_tokens: 100, + cached_tokens: 0, + cache_ratio: 0, + avg_latency_ms: 1000, + cost_usd: 2, + }, + ], + }, + ], + per_key: [], + } + const { container } = renderOverview() + await screen.findByText('opencode-go/deepseek-v4-flash') + await userEvent.click(screen.getByRole('tab', { name: 'Analytics' })) + + await screen.findByText('Avg cost / request (log)') + const tooltip = container.querySelector('circle title')?.textContent ?? '' + expect(tooltip).toContain('$0.2') + }) + + it('merges the same model served by multiple providers into one bubble', async () => { + mockStats = { + period_secs: 86_400, + requests: 15, + input_tokens: 0, + output_tokens: 0, + cached_tokens: 0, + cache_ratio: 0, + cost_usd: 3, + per_provider: [ + { + provider: 'opencode-go', + requests: 10, + input_tokens: 0, + output_tokens: 0, + cached_tokens: 0, + cost_usd: 2, + models: [ + { + model: 'opencode-go/deepseek-v4-flash', + requests: 10, + errors: 0, + input_tokens: 0, + output_tokens: 0, + cached_tokens: 0, + cache_ratio: 0, + avg_latency_ms: 1000, + cost_usd: 2, + }, + ], + }, + { + provider: 'opencode-zen', + requests: 5, + input_tokens: 0, + output_tokens: 0, + cached_tokens: 0, + cost_usd: 1, + models: [ + { + model: 'opencode-zen/deepseek-v4-flash', + requests: 5, + errors: 0, + input_tokens: 0, + output_tokens: 0, + cached_tokens: 0, + cache_ratio: 0, + avg_latency_ms: 500, + cost_usd: 1, + }, + ], + }, + ], + per_key: [], + } + const { container } = renderOverview() + await userEvent.click(screen.getByRole('tab', { name: 'Analytics' })) + await screen.findByText('Avg cost / request (log)') + + expect(container.querySelectorAll('[data-testid="marker-deepseek-v4-flash"]')).toHaveLength(1) + const tooltip = container.querySelector('circle title')?.textContent ?? '' + expect(tooltip).toContain('$0.2') + expect(tooltip).toContain('15') + }) +}) diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx index 60368e8..bf4a8f5 100644 --- a/src/pages/Overview.tsx +++ b/src/pages/Overview.tsx @@ -3,8 +3,8 @@ import { Link } from 'react-router' import { AlertTriangle, CheckCircle2, X, XCircle } from 'lucide-react' import { api } from '@/lib/api' import { useBackendState } from '@/lib/events' -import { useStrings } from '@/i18n' -import { formatContextWindow } from '@/lib/utils' +import { useLocale, useStrings, type Locale } from '@/i18n' +import { avgCostPerRequest, formatContextWindow } from '@/lib/utils' import type { AppConfig, ContextWindow, @@ -16,15 +16,35 @@ import type { } from '@/types' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { AnalyticsChart, type ChartPoint } from '@/components/AnalyticsChart' import PageShell from '@/components/PageShell' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' -import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' type PeriodKey = 'today' | 'h24' | 'd7' | 'd30' const SETUP_BANNER_DISMISSED_KEY = 'loomrouter.setup-banner.dismissed' +const QUOTA_LOCALE: Record = { + en: 'en', + pt: 'pt-BR', + es: 'es', + zh: 'zh-CN', +} + +function formatResetAt(resetAt: string | null | undefined, locale: Locale): string | null { + if (!resetAt) return null + const date = new Date(resetAt) + if (Number.isNaN(date.getTime())) return null + return new Intl.DateTimeFormat(QUOTA_LOCALE[locale], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }).format(date) +} + function periodSecs(key: PeriodKey): number { switch (key) { case 'h24': @@ -129,6 +149,7 @@ function KeyRow({ accountLevel: boolean }) { const s = useStrings() + const locale = useLocale() const tokenReported = usage != null && (usage.requests === 0 || usage.input_tokens > 0 || usage.output_tokens > 0) return ( @@ -162,16 +183,21 @@ function KeyRow({ value={usage != null && tokenReported ? fmt(usage.cached_tokens) : s.overview.notReported} /> - {balance.bars.map((bar) => ( -
-
- {bar.label} - {Math.round(bar.percent)}% + {balance.bars.map((bar) => { + const resetAt = formatResetAt(bar.reset_at, locale) + return ( +
+
+ {bar.label} + {Math.round(bar.percent)}% +
+ +

+ {resetAt ? `${bar.detail} · resets ${resetAt}` : bar.detail} +

- -

{bar.detail}

-
- ))} + ) + })} {balance.balance_text ? (

{balance.balance_text} @@ -228,6 +254,7 @@ function OverviewSkeleton() { export default function OverviewPage() { const s = useStrings() + const locale = useLocale() const [period, setPeriod] = useState('h24') const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) @@ -281,6 +308,52 @@ export default function OverviewPage() { return (id: string) => map.get(id) ?? id }, [config, s]) + const chartData = useMemo(() => { + // The stored model is a `namespace/slug`, and the namespace is not always + // the routing provider, so key by the bare slug to collapse the same + // upstream model served through several gateways into one bubble. + const byModel = new Map< + string, + { requests: number; costUsd: number; latencyWeighted: number; providers: Set } + >() + for (const provider of stats?.per_provider ?? []) { + for (const model of provider.models) { + if ( + model.cost_usd == null || + model.cost_usd <= 0 || + model.avg_latency_ms == null || + model.requests === 0 + ) { + continue + } + const label = model.model.slice(model.model.lastIndexOf('/') + 1) + const agg = byModel.get(label) ?? { + requests: 0, + costUsd: 0, + latencyWeighted: 0, + providers: new Set(), + } + agg.requests += model.requests + agg.costUsd += model.cost_usd + agg.latencyWeighted += model.avg_latency_ms * model.requests + agg.providers.add(providerName(provider.provider)) + byModel.set(label, agg) + } + } + const points: ChartPoint[] = [] + for (const [label, agg] of byModel) { + points.push({ + key: label, + label, + sublabel: [...agg.providers].join(', '), + cost: avgCostPerRequest(agg.costUsd, agg.requests), + latencyMs: Math.round(agg.latencyWeighted / agg.requests), + requests: agg.requests, + }) + } + return points + }, [stats, providerName]) + const balanceGroups = useMemo(() => { const groups = new Map() for (const balance of balances) { @@ -369,7 +442,13 @@ export default function OverviewPage() {

)} - {loading ? ( + + + {s.overview.tabGeneral} + {s.overview.tabAnalytics} + + + {loading ? ( ) : ( <> @@ -477,6 +556,28 @@ export default function OverviewPage() { )} + + + {loading ? ( + - - {fetchError &&

{fetchError}

} - - {totalCount > 8 && ( -
- setQuery(e.target.value)} - className="max-w-xs" - /> - {q && ( - - {s.providers.showingCount - .replace('{{shown}}', String(shownCount)) - .replace('{{total}}', String(totalCount))} - - )} -
- )} -
- {visibleModels.map((m) => ( -
+
+
) diff --git a/src/types/index.ts b/src/types/index.ts index 71e42b8..1fcdad1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -273,6 +273,7 @@ export interface QuotaBar { label: string percent: number detail: string + reset_at?: string | null } export interface ProviderBalance { From b21e17d17bd6eab16c47b58ad5fffd6c8403c7eb Mon Sep 17 00:00:00 2001 From: Guilherme Drumond Date: Thu, 13 Aug 2026 02:14:29 -0300 Subject: [PATCH 6/6] fix(analytics): address review regressions --- src-tauri/src/proxy.rs | 4 ++++ src-tauri/src/proxy/auth.rs | 33 +++++++++++++++++++++----- src-tauri/src/proxy/tests_routing.rs | 18 ++++++++++++++ src-tauri/src/translate/request.rs | 35 +++++++++++----------------- src-tauri/src/translate/tests_b.rs | 17 ++++++++++++++ src/i18n/en.ts | 1 + src/i18n/es.ts | 1 + src/i18n/pt.ts | 1 + src/i18n/zh.ts | 1 + src/pages/Overview.test.tsx | 11 ++++++--- src/pages/Overview.tsx | 2 +- src/pages/Providers.test.tsx | 26 +++++++++++++++++++++ src/pages/Providers.tsx | 14 +++++++---- 13 files changed, 127 insertions(+), 37 deletions(-) diff --git a/src-tauri/src/proxy.rs b/src-tauri/src/proxy.rs index 71a9e1c..a86495d 100644 --- a/src-tauri/src/proxy.rs +++ b/src-tauri/src/proxy.rs @@ -1068,6 +1068,10 @@ fn sanitize_stateless_responses_payload(payload: &mut Value) { return; }; translate::repair_tool_exchange_items(input); + if input.is_empty() { + payload["input"] = Value::String(String::new()); + return; + } for item in input.iter_mut() { if let Some(object) = item.as_object_mut() { object.remove("id"); diff --git a/src-tauri/src/proxy/auth.rs b/src-tauri/src/proxy/auth.rs index d29f115..7bc4a5f 100644 --- a/src-tauri/src/proxy/auth.rs +++ b/src-tauri/src/proxy/auth.rs @@ -29,10 +29,13 @@ fn local_token_path() -> std::path::PathBuf { } fn managed_token_from(raw: &str) -> Option { + let managed_start = raw.find(crate::codex::BEGIN_MARK)? + crate::codex::BEGIN_MARK.len(); + let managed_end = raw[managed_start..].find(crate::codex::END_MARK)? + managed_start; + let managed = &raw[managed_start..managed_end]; let needle = "x-loomrouter-token\" = \""; - let start = raw.find(needle)? + needle.len(); - let end = raw[start..].find('"')?; - Some(raw[start..start + end].to_string()) + let start = managed.find(needle)? + needle.len(); + let end = managed[start..].find('"')?; + Some(managed[start..start + end].to_string()) } fn configured_token() -> Option { @@ -59,7 +62,9 @@ fn load_or_create_local_token(path: &Path) -> String { } } if let Some(token) = configured_token().filter(|token| valid_local_token(token)) { - let _ = crate::secure_fs::write_private(path, token.as_bytes()); + if let Err(e) = crate::secure_fs::write_private(path, token.as_bytes()) { + tracing::warn!(path = %path.display(), error = %e, "failed to persist migrated local token"); + } return token; } let token = generate_local_token(); @@ -149,7 +154,23 @@ mod tests { #[test] fn managed_block_token_is_read_without_leaking_adjacent_text() { - let raw = "http_headers = { \"x-loomrouter-token\" = \"abc\", \"Authorization\" = \"Bearer abc\" }"; - assert_eq!(managed_token_from(raw).as_deref(), Some("abc")); + let raw = format!( + "# x-loomrouter-token\" = \"{}\"\n{}\nhttp_headers = {{ \"x-loomrouter-token\" = \"abc\", \"Authorization\" = \"Bearer abc\" }}\n{}", + "0".repeat(64), + crate::codex::BEGIN_MARK, + crate::codex::END_MARK, + ); + assert_eq!(managed_token_from(&raw).as_deref(), Some("abc")); + } + + #[test] + fn token_outside_managed_block_is_ignored() { + let raw = format!( + "x-loomrouter-token\" = \"{}\"\n{}\n# no token here\n{}", + "a".repeat(64), + crate::codex::BEGIN_MARK, + crate::codex::END_MARK, + ); + assert_eq!(managed_token_from(&raw), None); } } diff --git a/src-tauri/src/proxy/tests_routing.rs b/src-tauri/src/proxy/tests_routing.rs index 1b92401..cada922 100644 --- a/src-tauri/src/proxy/tests_routing.rs +++ b/src-tauri/src/proxy/tests_routing.rs @@ -327,6 +327,24 @@ fn opencode_go_deepseek_normalizes_an_empty_responses_input() { assert_eq!(body["instructions"], "prewarm"); } +#[test] +fn opencode_go_deepseek_normalizes_input_after_dropping_every_orphan_output() { + let provider = multi_dialect_provider(); + let payload = json!({ + "input": [ + {"type": "function_call_output", "call_id": "orphan-output", "output": "result"} + ], + "instructions": "continue", + "stream": true + }); + + let (_, body, _) = + build_upstream(&provider, &payload, "deepseek-v4-flash", WireApi::Responses).unwrap(); + + assert_eq!(body["input"], ""); + assert_eq!(body["instructions"], "continue"); +} + #[test] fn opencode_go_deepseek_flattens_agent_messages_before_sending_responses() { let provider = multi_dialect_provider(); diff --git a/src-tauri/src/translate/request.rs b/src-tauri/src/translate/request.rs index 2b253c2..2215aae 100644 --- a/src-tauri/src/translate/request.rs +++ b/src-tauri/src/translate/request.rs @@ -62,19 +62,6 @@ fn flatten_content_parts(content: Option<&mut Value>, part_type: &str) -> usize /// a lost history entry can leave a `function_call_output` without its /// `function_call`; Console Go turns that into a Chat `tool` message with no /// preceding `tool_calls` and rejects the request. -fn tool_exchange_counts(items: &[Value]) -> BTreeMap { - let mut calls: BTreeMap = BTreeMap::new(); - for item in items.iter() { - let Some(call_id) = item.get("call_id").and_then(Value::as_str) else { - continue; - }; - if is_tool_call_item(item) { - *calls.entry(call_id.to_string()).or_default() += 1; - } - } - calls -} - fn is_tool_call_item(item: &Value) -> bool { matches!( item.get("type").and_then(Value::as_str), @@ -91,15 +78,19 @@ fn is_tool_output_item(item: &Value) -> bool { fn keep_tool_exchange_item( item: &Value, - calls: &BTreeMap, + seen_calls: &mut BTreeMap, seen_outputs: &mut BTreeMap, ) -> bool { + let Some(call_id) = item.get("call_id").and_then(Value::as_str) else { + return !is_tool_output_item(item); + }; + if is_tool_call_item(item) { + *seen_calls.entry(call_id.to_string()).or_default() += 1; + return true; + } if is_tool_output_item(item) { - let Some(call_id) = item.get("call_id").and_then(Value::as_str) else { - return false; - }; let seen = seen_outputs.entry(call_id.to_string()).or_default(); - if *seen < calls.get(call_id).copied().unwrap_or(0) { + if *seen < seen_calls.get(call_id).copied().unwrap_or(0) { *seen += 1; true } else { @@ -111,9 +102,9 @@ fn keep_tool_exchange_item( } pub(crate) fn repair_tool_exchange_items(items: &mut Vec) { - let calls = tool_exchange_counts(items); + let mut seen_calls = BTreeMap::new(); let mut seen_outputs = BTreeMap::new(); - items.retain(|item| keep_tool_exchange_item(item, &calls, &mut seen_outputs)); + items.retain(|item| keep_tool_exchange_item(item, &mut seen_calls, &mut seen_outputs)); } // --------------------------------------------------------------------------- @@ -143,7 +134,7 @@ pub fn responses_to_chat(payload: &Value, model: &str, unified_reasoning: bool) messages.push(json!({"role": "user", "content": text})); } Some(Value::Array(items)) => { - let calls = tool_exchange_counts(items); + let mut seen_calls = BTreeMap::new(); let mut seen_outputs = BTreeMap::new(); // Thinking models require prior reasoning on replay: DeepSeek/Kimi // expect reasoning_content, while MiniMax expects the raw @@ -154,7 +145,7 @@ pub fn responses_to_chat(payload: &Value, model: &str, unified_reasoning: bool) let mut pending_reasoning = String::new(); let mut pending_minimax_details: Option = None; for item in items { - if !keep_tool_exchange_item(item, &calls, &mut seen_outputs) { + if !keep_tool_exchange_item(item, &mut seen_calls, &mut seen_outputs) { continue; } if item.get("type").and_then(Value::as_str) == Some("reasoning") { diff --git a/src-tauri/src/translate/tests_b.rs b/src-tauri/src/translate/tests_b.rs index a5d0e59..4f8c9d3 100644 --- a/src-tauri/src/translate/tests_b.rs +++ b/src-tauri/src/translate/tests_b.rs @@ -167,6 +167,23 @@ fn responses_to_chat_drops_orphan_tool_output() { .all(|m| m.get("role").and_then(Value::as_str) != Some("tool"))); } +#[test] +fn responses_to_chat_drops_tool_output_that_precedes_its_call() { + let payload = json!({ + "input": [ + {"role":"user","content":[{"type":"input_text","text":"continue"}]}, + {"type":"function_call_output","call_id":"late-call","output":"invalid"}, + {"type":"function_call","call_id":"late-call","name":"inspect","arguments":"{}"} + ] + }); + + let out = responses_to_chat(&payload, "kimi-k3", false).unwrap(); + let messages = out["messages"].as_array().unwrap(); + assert!(messages + .iter() + .all(|message| message.get("role").and_then(Value::as_str) != Some("tool"))); +} + #[test] fn interleaved_developer_message_does_not_break_tool_sequence() { // macOS Codex injects a developer (-> system) item between the diff --git a/src/i18n/en.ts b/src/i18n/en.ts index f555fd8..09cb0a4 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -65,6 +65,7 @@ const en = { axisCost: 'Avg cost / request (log)', axisSpeed: 'Speed (faster = up)', bubbleLegend: 'Bubble size = requests', + resetsAt: 'resets', requests: 'Requests', inputTokens: 'Input tokens', outputTokens: 'Output tokens', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index b3adb59..54c7388 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -64,6 +64,7 @@ const es: DeepPartial = { axisCost: 'Coste medio / petición (log)', axisSpeed: 'Velocidad (más rápido = arriba)', bubbleLegend: 'Tamaño de la burbuja = peticiones', + resetsAt: 'se reinicia el', requests: 'Solicitudes', inputTokens: 'Tokens de entrada', outputTokens: 'Tokens de salida', diff --git a/src/i18n/pt.ts b/src/i18n/pt.ts index 25e1ef3..30bfeba 100644 --- a/src/i18n/pt.ts +++ b/src/i18n/pt.ts @@ -63,6 +63,7 @@ const pt: DeepPartial = { axisCost: 'Custo médio / requisição (log)', axisSpeed: 'Velocidade (mais rápido = cima)', bubbleLegend: 'Tamanho da bolha = requisições', + resetsAt: 'reinicia em', requests: 'Requisições', inputTokens: 'Tokens de entrada', outputTokens: 'Tokens de saída', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 3d65541..e7359d6 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -63,6 +63,7 @@ const zh: DeepPartial = { axisCost: '平均成本 / 请求 (log)', axisSpeed: '速度 (越快越靠上)', bubbleLegend: '气泡大小 = 请求数', + resetsAt: '重置于', requests: '请求数', inputTokens: '输入 token', outputTokens: '输出 token', diff --git a/src/pages/Overview.test.tsx b/src/pages/Overview.test.tsx index bfc8738..47196df 100644 --- a/src/pages/Overview.test.tsx +++ b/src/pages/Overview.test.tsx @@ -3,7 +3,8 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { MemoryRouter, Route, Routes } from 'react-router' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { setLocale } from '@/i18n' import type { ProviderBalance, SetupStatus, StatsSummary } from '@/types' let setupStatus: SetupStatus = { @@ -59,6 +60,7 @@ const renderOverview = () => ) beforeEach(() => { + setLocale('en') setupStatus = { ready: true, missing: [], @@ -87,6 +89,8 @@ beforeEach(() => { } }) +afterEach(() => setLocale('en')) + describe('Overview setup banner', () => { it('UT-081 renders no banner when setup is ready', async () => { renderOverview() @@ -240,6 +244,7 @@ describe('Overview per-key dashboard', () => { }) it('formats quota reset_at with the active locale', async () => { + setLocale('pt') mockBalances = [ { provider_id: 'acme', @@ -261,13 +266,13 @@ describe('Overview per-key dashboard', () => { renderOverview() - const resetAt = new Intl.DateTimeFormat('en', { + const resetAt = new Intl.DateTimeFormat('pt-BR', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }).format(new Date('2026-08-13T02:33:12Z')) - expect(await screen.findByText(`67 / 100 left · resets ${resetAt}`)).toBeInTheDocument() + expect(await screen.findByText(`67 / 100 left · reinicia em ${resetAt}`)).toBeInTheDocument() }) it('E2E-002 shows the primary key attribution after routing', async () => { diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx index bf4a8f5..67b6be8 100644 --- a/src/pages/Overview.tsx +++ b/src/pages/Overview.tsx @@ -193,7 +193,7 @@ function KeyRow({

- {resetAt ? `${bar.detail} · resets ${resetAt}` : bar.detail} + {resetAt ? `${bar.detail} · ${s.overview.resetsAt} ${resetAt}` : bar.detail}

) diff --git a/src/pages/Providers.test.tsx b/src/pages/Providers.test.tsx index afec3ac..0e3c526 100644 --- a/src/pages/Providers.test.tsx +++ b/src/pages/Providers.test.tsx @@ -89,6 +89,32 @@ describe('Provider add flow', () => { }) }) +describe('provider tabs', () => { + it('opens Claude Code on models without rendering an empty keys tab', async () => { + const provider = keyedProvider({ + id: 'claude-code', + name: 'Claude Code', + keys: [], + models: [ + { + id: 'claude-opus-5', + label: null, + context_window: null, + protocol: null, + fast_mode: false, + enabled: true, + supports_vision: false, + }, + ], + }) + + await renderKeyedProvider(provider) + + expect(await screen.findByText('claude-opus-5')).toBeVisible() + expect(screen.queryByRole('tab', { name: /api keys/i })).not.toBeInTheDocument() + }) +}) + const keyedProvider = (over: Partial = {}): Provider => ({ id: 'acme', name: 'Acme', diff --git a/src/pages/Providers.tsx b/src/pages/Providers.tsx index 8ffe969..e91838e 100644 --- a/src/pages/Providers.tsx +++ b/src/pages/Providers.tsx @@ -1053,14 +1053,18 @@ const ProviderCard = memo(function ProviderCard({ - + - {s.providers.keys} + {provider.id !== 'claude-code' && ( + {s.providers.keys} + )} {s.providers.modelsLabel} - - - + {provider.id !== 'claude-code' && ( + + + + )}