Files
cortiq-gateway/src/router_client.rs
T
infosave2007 04580196ad
Docker / publish (push) Canceled after 0s
Release / crates-io (push) Canceled after 0s
Release / release (macos-14, aarch64-apple-darwin) (push) Canceled after 0s
Release / release (ubuntu-latest, x86_64-unknown-linux-gnu) (push) Canceled after 0s
Release / release (windows-latest, x86_64-pc-windows-msvc) (push) Canceled after 0s
fix: add promotion + router-status modules referenced by the CMF integration
The v0.2.3 CMF-integration commit referenced crate::promotion (main.rs),
router_client::RouterLastStatus (state.rs) and router_client::classify_status
(admin), but those definitions were not part of the commit, so a clean build
failed (E0583/E0432/E0425). Add the two source files; cargo build --release now
compiles.
2026-07-08 16:59:58 +03:00

123 lines
4.2 KiB
Rust

//! Client for cortiq-router. Issues `POST /v1/route` and (optionally) `POST /v1/feedback`.
//! Returns the decision as a [`RouteDecision`]. When the router is unavailable the caller
//! falls back to `routing.default` (graceful degradation).
use crate::config::RouterCfg;
use crate::model::RouteDecision;
use crate::secrets::SecretStore;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::Arc;
/// Outcome of the most recent real `/v1/route` call, shared with the admin health
/// endpoint so the panel can tell "no money / bad key" apart from "router down".
/// 0 = ok (or no calls yet); [`STATUS_NETWORK`] / [`STATUS_TIMEOUT`] for transport
/// failures; otherwise the upstream HTTP status code.
pub type RouterLastStatus = Arc<AtomicU16>;
pub const STATUS_NETWORK: u16 = 1;
pub const STATUS_TIMEOUT: u16 = 2;
/// Error kind for the admin panel.
pub fn classify_status(code: u16) -> &'static str {
match code {
STATUS_NETWORK => "unreachable",
STATUS_TIMEOUT => "timeout",
401 | 403 => "auth",
402 => "payment",
429 => "quota",
_ => "error",
}
}
pub struct RouterClient {
http: reqwest::Client,
url: String,
api_key: Option<String>,
taxonomy_id: Option<String>,
last_status: RouterLastStatus,
}
impl RouterClient {
/// The key is resolved through the secret store (admin panel value first,
/// then the environment variable) so a key pasted in the console works
/// without a restart.
pub fn new(
cfg: &RouterCfg,
secrets: &SecretStore,
last_status: RouterLastStatus,
) -> anyhow::Result<Self> {
let mut builder =
reqwest::Client::builder().timeout(std::time::Duration::from_millis(cfg.timeout_ms));
if !cfg.verify_tls {
builder = builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let http = builder.build()?;
let api_key = cfg
.api_key_env
.as_ref()
.and_then(|name| secrets.resolve(name));
Ok(Self {
http,
url: cfg.url.trim_end_matches('/').to_string(),
api_key,
taxonomy_id: cfg.taxonomy_id.clone(),
last_status,
})
}
/// Classify text. Returns `Ok(None)` on network error/timeout —
/// the caller treats this as "router unavailable" and uses the default tier.
pub async fn route(&self, text: &str, profile: &str) -> anyhow::Result<Option<RouteDecision>> {
let mut body = serde_json::json!({
"input": { "text": text },
"options": { "policy_profile": profile, "allow_oracle": true }
});
if let Some(tax) = &self.taxonomy_id {
body["taxonomy_id"] = serde_json::Value::String(tax.clone());
}
let mut req = self.http.post(format!("{}/v1/route", self.url)).json(&body);
if let Some(key) = &self.api_key {
req = req.bearer_auth(key);
}
let resp = match req.send().await {
Ok(r) => r,
Err(e) => {
let code = if e.is_timeout() {
STATUS_TIMEOUT
} else {
STATUS_NETWORK
};
self.last_status.store(code, Ordering::Relaxed);
return Ok(None); // degradation
}
};
if !resp.status().is_success() {
self.last_status
.store(resp.status().as_u16(), Ordering::Relaxed);
return Ok(None);
}
self.last_status.store(0, Ordering::Relaxed);
let v: serde_json::Value = resp.json().await?;
let d = &v["decision"];
Ok(Some(RouteDecision {
task_label: d["task_label"]
.as_str()
.unwrap_or("__unknown__")
.to_string(),
complexity_score: d["complexity"]["score"].as_f64().unwrap_or(0.5) as f32,
complexity_tier: d["complexity"]["tier"]
.as_str()
.unwrap_or("medium")
.to_string(),
router_request_id: v["request_id"].as_str().map(|s| s.to_string()),
source: "router".into(),
}))
}
}