feat(routing): linked complexity-boundary sliders — your own low/medium/high
The tier boundaries lived on the router and were global for every customer of a hosted router; per-gateway tuning meant hand-editing score ranges. The Routing page now has one track with two draggable handles (the low|medium and medium|high boundaries) over three colored zones: handles push each other (low can't cross medium — it shoves it along, and vice versa), the numbers ride on the handles and the legend updates live, so nobody types ranges by hand. Unchecked = the router's own bands, untouched behavior. Under the hood the gateway re-maps the router's CONTINUOUS complexity score with the user's boundaries ([routing.bands] low/medium in gateway.toml, validated 0 < low < medium < 1) before tier chains, escalation and stats — verified live: with bands at 0.70/0.72 a score-0.27 request that used to be medium routed low, and back after restoring the defaults. 2 new strings in all 7 languages.
This commit is contained in:
@@ -931,6 +931,7 @@ async fn get_routing(State(state): State<SharedState>) -> ApiResult {
|
||||
.collect();
|
||||
ok(json!({
|
||||
"tiers": tiers_to_map(&live.cfg.routing),
|
||||
"bands": live.cfg.routing.bands,
|
||||
"tasks": tasks,
|
||||
"default": live.cfg.routing.default,
|
||||
"policy": live.cfg.routing.policy,
|
||||
@@ -978,6 +979,8 @@ struct RoutingBody {
|
||||
tiers: HashMap<String, Vec<String>>,
|
||||
#[serde(default)]
|
||||
tasks: HashMap<String, TierTargets>,
|
||||
#[serde(default)]
|
||||
bands: Option<crate::config::TierBands>,
|
||||
default: String,
|
||||
#[serde(default)]
|
||||
policy: RoutingPolicy,
|
||||
@@ -997,6 +1000,7 @@ async fn put_routing(State(state): State<SharedState>, Json(body): Json<RoutingB
|
||||
// empty rule (no models AND no forced tier) = "по сложности"
|
||||
.filter(|(_, v)| !v.models().is_empty())
|
||||
.collect(),
|
||||
bands: body.bands,
|
||||
default: body.default,
|
||||
policy: body.policy,
|
||||
};
|
||||
|
||||
@@ -404,6 +404,31 @@ pub struct RoutingCfg {
|
||||
/// called "tasks" — serde extracts it before the flattened tier map.
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub tasks: HashMap<String, TierTargets>,
|
||||
/// Local complexity bands: re-map the router's continuous score into
|
||||
/// low/medium/high with YOUR OWN boundaries. The hosted router's bands
|
||||
/// are global for all customers; these are per-gateway. `None` = keep
|
||||
/// the router's tier verdict as delivered.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub bands: Option<TierBands>,
|
||||
}
|
||||
|
||||
/// Score boundaries: `score ≤ low` → low, `≤ medium` → medium, else high.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct TierBands {
|
||||
pub low: f32,
|
||||
pub medium: f32,
|
||||
}
|
||||
|
||||
impl TierBands {
|
||||
pub fn tier_of(&self, score: f32) -> &'static str {
|
||||
if score <= self.low {
|
||||
"low"
|
||||
} else if score <= self.medium {
|
||||
"medium"
|
||||
} else {
|
||||
"high"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A routing tier is either a list of model_ids or the string `default`.
|
||||
@@ -861,6 +886,7 @@ impl Default for Config {
|
||||
default: String::new(),
|
||||
policy: RoutingPolicy::default(),
|
||||
tasks: HashMap::new(),
|
||||
bands: None,
|
||||
},
|
||||
breaker: BreakerCfg::default(),
|
||||
protocols: ProtocolsCfg {
|
||||
@@ -988,6 +1014,15 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(b) = &self.routing.bands {
|
||||
if !(b.low > 0.0 && b.low < b.medium && b.medium < 1.0) {
|
||||
anyhow::bail!(
|
||||
"config: routing.bands must satisfy 0 < low < medium < 1 (got {} / {})",
|
||||
b.low,
|
||||
b.medium
|
||||
);
|
||||
}
|
||||
}
|
||||
for (task, targets) in &self.routing.tasks {
|
||||
for id in &targets.models() {
|
||||
if !ids.contains(id.as_str()) {
|
||||
|
||||
@@ -231,6 +231,19 @@ impl Pipeline {
|
||||
let prof = profile.as_deref().unwrap_or(&live.cfg.route.profile);
|
||||
match live.router.route(&text, prof).await {
|
||||
Ok(Some(dec)) => {
|
||||
let mut dec = dec;
|
||||
// per-gateway complexity bands: re-map the
|
||||
// router's continuous score with the user's
|
||||
// own boundaries (sliders on the Routing page)
|
||||
if let Some(b) = &live.cfg.routing.bands {
|
||||
if matches!(
|
||||
dec.complexity_tier.as_str(),
|
||||
"low" | "medium" | "high"
|
||||
) {
|
||||
dec.complexity_tier =
|
||||
b.tier_of(dec.complexity_score).to_string();
|
||||
}
|
||||
}
|
||||
let src = dec.source.clone();
|
||||
let (cands, task_rule) = live
|
||||
.routing
|
||||
|
||||
Reference in New Issue
Block a user