From e12c3b4534e3c7d4af62e0470e755c479ab1d2a6 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:44:30 +0700 Subject: [PATCH 1/2] Accept Mac-shaped Wayfinder savings and read decision latency from /metrics --- apps/desktop-tauri/src/types/bridge.ts | 2 + rust/src/core/usage_snapshot.rs | 4 + .../fixtures/wayfinder/mac-pack-savings.json | 1 + rust/src/providers/wayfinder.rs | 185 ++++++++++++++++-- 4 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 rust/src/providers/fixtures/wayfinder/mac-pack-savings.json diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index dbb975a02d..a85a7b8a2f 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -825,6 +825,8 @@ export interface WayfinderUsageSnapshot { unit: string; priced: boolean; routes: WayfinderRouteSummary[]; + /** Mean router decision latency in ms, from the gateway's /metrics. */ + avgDecisionMs?: number; } /** Line item cost for one UTC day; descending by cost, then name. */ diff --git a/rust/src/core/usage_snapshot.rs b/rust/src/core/usage_snapshot.rs index 210178900b..b80185a2ec 100755 --- a/rust/src/core/usage_snapshot.rs +++ b/rust/src/core/usage_snapshot.rs @@ -66,6 +66,10 @@ pub struct WayfinderUsageSnapshot { pub unit: String, pub priced: bool, pub routes: Vec, + /// Mean router decision latency from `/metrics`, in milliseconds. `None` + /// when the gateway exposes no latency histogram or has routed nothing. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avg_decision_ms: Option, } /// Per-route savings data reported by Wayfinder. diff --git a/rust/src/providers/fixtures/wayfinder/mac-pack-savings.json b/rust/src/providers/fixtures/wayfinder/mac-pack-savings.json new file mode 100644 index 0000000000..a95c1b9934 --- /dev/null +++ b/rust/src/providers/fixtures/wayfinder/mac-pack-savings.json @@ -0,0 +1 @@ +{"priced":true,"requests":1200,"tokens":350000,"realized":4.2,"baseline":9.8,"saved":5.6,"saved_pct":57.1,"by_route":{"cheap":{"requests":900,"saved":3.1,"tokens":260000},"premium":{"requests":300,"saved":2.5,"tokens":90000}}} diff --git a/rust/src/providers/wayfinder.rs b/rust/src/providers/wayfinder.rs index 909b0afbe0..08d955273d 100644 --- a/rust/src/providers/wayfinder.rs +++ b/rust/src/providers/wayfinder.rs @@ -23,6 +23,7 @@ const HEALTH_PATH: &str = "healthz"; const MODELS_PATH: &str = "router/models"; const SAVINGS_PATH: &str = "v1/savings?period=30d"; const METRICS_PATH: &str = "metrics"; +const DECISION_LATENCY_METRIC: &str = "wayfinder_router_decision_latency_seconds"; #[derive(Debug, Clone, Deserialize)] pub struct HealthResponse { @@ -62,9 +63,14 @@ pub struct ModelResponse { pub key_ok: bool, } +/// `/v1/savings`. Upstream `WayfinderSavingsResponse` requires only the +/// totals and `by_route`; `period_days` and `unit` are optional extras here, so +/// a gateway that omits them still yields a card. #[derive(Debug, Clone, Deserialize)] pub struct SavingsResponse { + #[serde(default = "default_period_days")] pub period_days: u32, + #[serde(default = "default_unit")] pub unit: String, pub priced: bool, pub requests: u64, @@ -79,15 +85,60 @@ pub struct SavingsResponse { pub by_route: BTreeMap, } +/// Upstream `RouteBucket` requires only `requests`, `saved` and `tokens`. #[derive(Debug, Clone, Deserialize)] pub struct RouteResponse { pub requests: u64, + #[serde(default)] pub realized: f64, + #[serde(default)] pub baseline: f64, pub saved: f64, pub tokens: u64, } +/// The savings query asks for `period=30d`. +fn default_period_days() -> u32 { + 30 +} + +fn default_unit() -> String { + "usd".to_string() +} + +/// Upstream `averageDecisionMilliseconds(fromPrometheusText:)`: the last +/// `_sum` and `_count` samples, as sum / count * 1000. A sample +/// line starts with the series name followed by a space or `{`, and its value +/// is the last space-separated token. +pub fn average_decision_ms(metrics_text: &str) -> Option { + let sum_name = format!("{DECISION_LATENCY_METRIC}_sum"); + let count_name = format!("{DECISION_LATENCY_METRIC}_count"); + let mut sum = None; + let mut count = None; + for line in metrics_text.split('\n') { + if let Some(value) = metric_value(line, &sum_name) { + sum = Some(value); + } else if let Some(value) = metric_value(line, &count_name) { + count = Some(value); + } + } + let (sum, count) = (sum?, count?); + let average = sum / count * 1000.0; + (count > 0.0 && average.is_finite()).then_some(average) +} + +fn metric_value(line: &str, name: &str) -> Option { + let line = line.trim_end_matches('\r'); + let rest = line.strip_prefix(name)?; + if !(rest.starts_with(' ') || rest.starts_with('{')) { + return None; + } + rest.split(' ') + .rfind(|token| !token.is_empty())? + .parse() + .ok() +} + pub fn parse_gateway_url(raw: &str) -> Result { let trimmed = raw.trim(); let url = Url::parse(trimmed) @@ -166,7 +217,26 @@ fn usage_snapshot( health: HealthResponse, models: ModelsResponse, savings: SavingsResponse, + metrics_text: Option<&str>, ) -> WayfinderUsageSnapshot { + let mut routes: Vec = savings + .by_route + .into_iter() + .map(|(name, route)| WayfinderRouteSummary { + name, + requests: route.requests, + tokens: route.tokens, + realized: route.realized, + baseline: route.baseline, + saved: route.saved, + }) + .collect(); + // Upstream order: busiest route first, then by name. + routes.sort_by(|a, b| { + b.requests + .cmp(&a.requests) + .then_with(|| a.name.cmp(&b.name)) + }); WayfinderUsageSnapshot { gateway_status: health.status, offline: health.offline, @@ -184,18 +254,8 @@ fn usage_snapshot( period_days: savings.period_days, unit: savings.unit, priced: savings.priced, - routes: savings - .by_route - .into_iter() - .map(|(name, route)| WayfinderRouteSummary { - name, - requests: route.requests, - tokens: route.tokens, - realized: route.realized, - baseline: route.baseline, - saved: route.saved, - }) - .collect(), + routes, + avg_decision_ms: metrics_text.and_then(average_decision_ms), } } @@ -239,12 +299,12 @@ impl WayfinderProvider { .map_err(|_| ProviderError::Parse("Invalid Wayfinder gateway response".to_string())) } - async fn fetch_metrics(client: &reqwest::Client, url: &Url) -> Result<(), ProviderError> { - let response = client.get(url.clone()).send().await?; - if response.status().is_success() { - let _ = response.text().await?; + async fn fetch_metrics(client: &reqwest::Client, url: &Url) -> Option { + let response = client.get(url.clone()).send().await.ok()?; + if !response.status().is_success() { + return None; } - Ok(()) + response.text().await.ok() } } @@ -282,9 +342,9 @@ impl Provider for WayfinderProvider { let savings: SavingsResponse = Self::fetch_json(&client, &endpoint_url(&base, SAVINGS_PATH)?).await?; // Metrics are best-effort enrichment; a failure must not fail the usage fetch. - let _metrics = Self::fetch_metrics(&client, &endpoint_url(&base, METRICS_PATH)?).await; + let metrics = Self::fetch_metrics(&client, &endpoint_url(&base, METRICS_PATH)?).await; - let wayfinder_usage = usage_snapshot(health, models, savings); + let wayfinder_usage = usage_snapshot(health, models, savings, metrics.as_deref()); Ok( ProviderFetchResult::new(UsageSnapshot::new(RateWindow::new(0.0)), "gateway") @@ -365,6 +425,7 @@ mod tests { parse_health(include_str!("fixtures/wayfinder/pack-health.json")).unwrap(), parse_models(include_str!("fixtures/wayfinder/pack-models.json")).unwrap(), parse_savings(include_str!("fixtures/wayfinder/pack-savings.json")).unwrap(), + Some(PACK_METRICS), ); let json = serde_json::to_value(&snapshot).unwrap(); @@ -392,6 +453,7 @@ mod tests { "unit", "priced", "routes", + "avgDecisionMs", ]; actual.sort_unstable(); expected.sort_unstable(); @@ -404,6 +466,8 @@ mod tests { assert_eq!(json["savedPercent"], 57.1); assert_eq!(json["periodDays"], 30); assert_eq!(json["estimatedRequests"], 0); + let avg = json["avgDecisionMs"].as_f64().unwrap(); + assert!((avg - 0.35).abs() < 1e-9, "{avg}"); let route = json["routes"][0].as_object().unwrap(); let mut route_keys: Vec<&str> = route.keys().map(String::as_str).collect(); @@ -418,6 +482,89 @@ mod tests { assert_eq!(json["routes"][1]["name"], "premium"); } + /// `/metrics` body from the Mac-parity Wayfinder pack. + const PACK_METRICS: &str = "# HELP wayfinder_router_decision_latency_seconds Router decision latency\n\ +# TYPE wayfinder_router_decision_latency_seconds histogram\n\ +wayfinder_router_decision_latency_seconds_sum 0.42\n\ +wayfinder_router_decision_latency_seconds_count 1200\n"; + + /// The pack's `/v1/savings` as the Mac rig serves it: no `period_days`, + /// no `unit`, and route buckets with only requests/saved/tokens. + #[test] + fn mac_pack_savings_without_optional_fields_parses() { + let savings = + parse_savings(include_str!("fixtures/wayfinder/mac-pack-savings.json")).unwrap(); + assert_eq!(savings.period_days, 30); + assert_eq!(savings.unit, "usd"); + assert_eq!(savings.requests, 1200); + assert_eq!(savings.saved_pct, 57.1); + assert_eq!(savings.by_route["cheap"].requests, 900); + assert_eq!(savings.by_route["cheap"].realized, 0.0); + assert_eq!(savings.by_route["premium"].saved, 2.5); + + // A missing upstream-required field still fails the parse. + assert!(parse_savings(r#"{"priced":true,"requests":1,"tokens":1}"#).is_err()); + assert!( + parse_savings( + r#"{"priced":true,"requests":1,"tokens":1,"realized":0,"baseline":0,"saved":0,"saved_pct":0,"by_route":{"a":{"requests":1,"tokens":1}}}"# + ) + .is_err() + ); + } + + #[test] + fn average_decision_latency_follows_upstream_prometheus_parse() { + let average = average_decision_ms(PACK_METRICS).unwrap(); + assert!((average - 0.35).abs() < 1e-9, "{average}"); + + // Labelled series, CRLF line ends, and the last sample wins. + let labelled = "wayfinder_router_decision_latency_seconds_sum{route=\"cheap\"} 1\r\n\ +wayfinder_router_decision_latency_seconds_count{route=\"cheap\"} 4\r\n\ +wayfinder_router_decision_latency_seconds_count{route=\"all\"} 2\r\n"; + assert_eq!(average_decision_ms(labelled), Some(500.0)); + + // A longer metric name sharing the prefix is not a match. + assert_eq!( + average_decision_ms( + "wayfinder_router_decision_latency_seconds_sum_total 5\n\ +wayfinder_router_decision_latency_seconds_count 1\n" + ), + None + ); + // No samples or a zero count gives no latency. + assert_eq!( + average_decision_ms( + "wayfinder_router_decision_latency_seconds_sum 0\n\ +wayfinder_router_decision_latency_seconds_count 0\n" + ), + None + ); + assert_eq!(average_decision_ms("up 1\n"), None); + } + + #[test] + fn routes_sort_by_requests_then_name() { + let savings = parse_savings( + r#"{"priced":true,"requests":30,"tokens":3,"realized":0,"baseline":0,"saved":0,"saved_pct":0, + "by_route":{"alpha":{"requests":5,"saved":0,"tokens":1}, + "zulu":{"requests":20,"saved":0,"tokens":1}, + "beta":{"requests":5,"saved":0,"tokens":1}}}"#, + ) + .unwrap(); + let snapshot = usage_snapshot( + parse_health(include_str!("fixtures/wayfinder/pack-health.json")).unwrap(), + parse_models(include_str!("fixtures/wayfinder/pack-models.json")).unwrap(), + savings, + None, + ); + let names: Vec<&str> = snapshot.routes.iter().map(|r| r.name.as_str()).collect(); + assert_eq!(names, ["zulu", "alpha", "beta"]); + assert_eq!(snapshot.avg_decision_ms, None); + // No latency: the key stays off the bridge payload. + let json = serde_json::to_value(&snapshot).unwrap(); + assert!(json.get("avgDecisionMs").is_none()); + } + #[test] fn malformed_payloads_are_rejected() { assert!(parse_health("{\"status\":").is_err()); From 52c4337c6606579400700d66209a608b2bbd2f86 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sun, 11 Oct 2026 12:20:59 +0700 Subject: [PATCH 2/2] Ignore Prometheus timestamps and skip label blocks in Wayfinder metrics --- rust/src/providers/wayfinder.rs | 59 +++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/rust/src/providers/wayfinder.rs b/rust/src/providers/wayfinder.rs index 08d955273d..02eb76ab2f 100644 --- a/rust/src/providers/wayfinder.rs +++ b/rust/src/providers/wayfinder.rs @@ -127,16 +127,41 @@ pub fn average_decision_ms(metrics_text: &str) -> Option { (count > 0.0 && average.is_finite()).then_some(average) } +/// The sample value is the first token after the optional `{...}` label block. +/// Any trailing token is the optional timestamp and is ignored. fn metric_value(line: &str, name: &str) -> Option { let line = line.trim_end_matches('\r'); - let rest = line.strip_prefix(name)?; - if !(rest.starts_with(' ') || rest.starts_with('{')) { + let mut rest = line.strip_prefix(name)?; + if let Some(labels) = rest.strip_prefix('{') { + rest = after_label_block(labels)?; + } + if !rest.starts_with(' ') { return None; } - rest.split(' ') - .rfind(|token| !token.is_empty())? - .parse() - .ok() + rest.split_whitespace().next()?.parse().ok() +} + +/// Returns the text after the `}` closing a label block whose `{` is already +/// consumed. Quoted label values may hold `}`, spaces and `\"` escapes. +fn after_label_block(text: &str) -> Option<&str> { + let mut in_quotes = false; + let mut escaped = false; + for (index, ch) in text.char_indices() { + if in_quotes { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == '"' { + in_quotes = false; + } + } else if ch == '"' { + in_quotes = true; + } else if ch == '}' { + return Some(&text[index + ch.len_utf8()..]); + } + } + None } pub fn parse_gateway_url(raw: &str) -> Result { @@ -542,6 +567,28 @@ wayfinder_router_decision_latency_seconds_count 0\n" assert_eq!(average_decision_ms("up 1\n"), None); } + #[test] + fn metric_value_ignores_timestamps_and_quoted_label_blocks() { + // Two-line sample with a trailing timestamp on each series: 0.0007 / 2 * 1000 = 0.35 ms. + let stamped = "wayfinder_router_decision_latency_seconds_sum 0.0007 1700000000000\n\ +wayfinder_router_decision_latency_seconds_count 2 1700000000000\n"; + let average = average_decision_ms(stamped).unwrap(); + assert!((average - 0.35).abs() < 1e-9, "{average}"); + + // A label value with a space, a `}` and an escaped quote does not end the block early. + assert_eq!( + metric_value(r#"x{route="fast } lane \"a\""} 0.0007 1700000000000"#, "x"), + Some(0.0007) + ); + let labelled = "wayfinder_router_decision_latency_seconds_sum{route=\"fast } lane\"} 0.0007 1700000000000\n\ +wayfinder_router_decision_latency_seconds_count{route=\"fast } lane\"} 2 1700000000000\n"; + let average = average_decision_ms(labelled).unwrap(); + assert!((average - 0.35).abs() < 1e-9, "{average}"); + + // An unterminated label block yields no value. + assert_eq!(metric_value("x{route=\"a 3", "x"), None); + } + #[test] fn routes_sort_by_requests_then_name() { let savings = parse_savings(