diff --git a/crates/frontend/src/api.rs b/crates/frontend/src/api.rs index 685d15c..f30e1b5 100644 --- a/crates/frontend/src/api.rs +++ b/crates/frontend/src/api.rs @@ -6132,6 +6132,8 @@ pub struct AdminLlmGatewayKeyView { #[serde(default = "default_anthropic_upstream_pool_mode")] pub kiro_anthropic_upstream_pool_mode: String, pub model_name_map: Option>, + #[serde(default)] + pub kiro_model_group_preferences: BTreeMap, pub request_max_concurrency: Option, pub request_min_start_interval_ms: Option, #[serde(default = "default_true")] @@ -9030,6 +9032,7 @@ pub async fn create_admin_llm_gateway_key( preferred_pool_strategy: default_kiro_pool_strategy(), kiro_anthropic_upstream_pool_mode: default_anthropic_upstream_pool_mode(), model_name_map: None, + kiro_model_group_preferences: BTreeMap::new(), request_max_concurrency, request_min_start_interval_ms, kiro_request_validation_enabled: true, @@ -9096,6 +9099,7 @@ pub struct PatchAdminLlmGatewayKeyRequest<'a> { pub preferred_pool_strategy: Option<&'a str>, pub kiro_anthropic_upstream_pool_mode: Option<&'a str>, pub model_name_map: Option<&'a BTreeMap>, + pub kiro_model_group_preferences: Option<&'a BTreeMap>, pub request_max_concurrency: Option, pub request_min_start_interval_ms: Option, pub codex_fast_enabled: Option, @@ -9136,6 +9140,7 @@ pub async fn patch_admin_llm_gateway_key( request.preferred_pool_strategy, request.kiro_anthropic_upstream_pool_mode, request.model_name_map, + request.kiro_model_group_preferences, request.request_max_concurrency, request.request_min_start_interval_ms, request.codex_fast_enabled, @@ -9236,6 +9241,11 @@ pub async fn patch_admin_llm_gateway_key( .map_err(|e| format!("Serialize error: {:?}", e))?; body.insert("model_name_map".to_string(), value); } + if let Some(preferences) = request.kiro_model_group_preferences { + let value = serde_json::to_value(preferences) + .map_err(|e| format!("Serialize error: {:?}", e))?; + body.insert("kiro_model_group_preferences".to_string(), value); + } if let Some(request_max_concurrency) = request.request_max_concurrency { body.insert( "request_max_concurrency".to_string(), @@ -11445,6 +11455,7 @@ pub async fn create_admin_kiro_key( preferred_pool_strategy: default_kiro_pool_strategy(), kiro_anthropic_upstream_pool_mode: default_anthropic_upstream_pool_mode(), model_name_map: None, + kiro_model_group_preferences: BTreeMap::new(), request_max_concurrency: None, request_min_start_interval_ms: None, kiro_request_validation_enabled: true, @@ -11513,6 +11524,7 @@ pub async fn patch_admin_kiro_key( request.preferred_pool_strategy, request.kiro_anthropic_upstream_pool_mode, request.model_name_map, + request.kiro_model_group_preferences, request.request_max_concurrency, request.request_min_start_interval_ms, request.kiro_request_validation_enabled, @@ -11608,6 +11620,11 @@ pub async fn patch_admin_kiro_key( .map_err(|e| format!("Serialize error: {:?}", e))?; body.insert("model_name_map".to_string(), value); } + if let Some(preferences) = request.kiro_model_group_preferences { + let value = serde_json::to_value(preferences) + .map_err(|e| format!("Serialize error: {:?}", e))?; + body.insert("kiro_model_group_preferences".to_string(), value); + } if let Some(kiro_request_validation_enabled) = request.kiro_request_validation_enabled { body.insert( "kiro_request_validation_enabled".to_string(), diff --git a/crates/frontend/src/pages/admin_kiro_gateway.rs b/crates/frontend/src/pages/admin_kiro_gateway.rs index 1c369ac..82ce3d2 100644 --- a/crates/frontend/src/pages/admin_kiro_gateway.rs +++ b/crates/frontend/src/pages/admin_kiro_gateway.rs @@ -793,6 +793,26 @@ fn kiro_key_route_summary( } } +fn kiro_model_group_preferences_summary( + preferences: &BTreeMap, + account_groups: &[AdminAccountGroupOptionView], +) -> String { + if preferences.is_empty() { + return "0 rules".to_string(); + } + let mut entries = preferences + .iter() + .take(3) + .map(|(model, group_id)| { + format!("{model} -> {}", kiro_group_name_for_id(account_groups, group_id)) + }) + .collect::>(); + if preferences.len() > entries.len() { + entries.push(format!("+{}", preferences.len() - entries.len())); + } + format!("{} rules · {}", preferences.len(), entries.join(" · ")) +} + fn kiro_pool_strategy_label(strategy: &str) -> &'static str { match llm_store::normalize_kiro_pool_strategy(strategy) { Some(llm_store::KIRO_POOL_STRATEGY_BALANCED) => "亲和 + 动态", @@ -1795,6 +1815,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let anthropic_upstream_pool_mode = use_state(|| props.key_item.kiro_anthropic_upstream_pool_mode.clone()); let model_name_map = use_state(|| props.key_item.model_name_map.clone().unwrap_or_default()); + let model_group_preferences = use_state(|| props.key_item.kiro_model_group_preferences.clone()); let kiro_request_validation_enabled = use_state(|| props.key_item.kiro_request_validation_enabled); let kiro_cache_estimation_enabled = use_state(|| props.key_item.kiro_cache_estimation_enabled); @@ -1820,6 +1841,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let billable_multiplier_settings_expanded = use_state(|| false); let route_settings_expanded = use_state(|| false); let model_mapping_expanded = use_state(|| false); + let model_group_routing_open = use_state(|| false); let saving = use_state(|| false); let feedback = use_state(|| None::); @@ -1835,6 +1857,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let preferred_pool_strategy = preferred_pool_strategy.clone(); let anthropic_upstream_pool_mode = anthropic_upstream_pool_mode.clone(); let model_name_map = model_name_map.clone(); + let model_group_preferences = model_group_preferences.clone(); let kiro_request_validation_enabled = kiro_request_validation_enabled.clone(); let kiro_cache_estimation_enabled = kiro_cache_estimation_enabled.clone(); let kiro_zero_cache_debug_enabled = kiro_zero_cache_debug_enabled.clone(); @@ -1852,6 +1875,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let key_billable_multiplier_effective_baseline = key_billable_multiplier_effective_baseline.clone(); let billable_multiplier_settings_expanded = billable_multiplier_settings_expanded.clone(); + let model_group_routing_open = model_group_routing_open.clone(); let initial_effective_billable_multiplier_json_for_effect = initial_effective_billable_multiplier_json.clone(); use_effect_with((props.key_item.clone(), props.account_groups.clone()), move |_| { @@ -1872,6 +1896,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { preferred_pool_strategy.set(key_item.preferred_pool_strategy.clone()); anthropic_upstream_pool_mode.set(key_item.kiro_anthropic_upstream_pool_mode.clone()); model_name_map.set(key_item.model_name_map.clone().unwrap_or_default()); + model_group_preferences.set(key_item.kiro_model_group_preferences.clone()); kiro_request_validation_enabled.set(key_item.kiro_request_validation_enabled); kiro_cache_estimation_enabled.set(key_item.kiro_cache_estimation_enabled); kiro_zero_cache_debug_enabled.set(key_item.kiro_zero_cache_debug_enabled); @@ -1890,6 +1915,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { key_billable_multiplier_effective_baseline .set(initial_effective_billable_multiplier_json_for_effect.clone()); billable_multiplier_settings_expanded.set(false); + model_group_routing_open.set(false); || () }); } @@ -1914,6 +1940,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let preferred_pool_strategy = preferred_pool_strategy.clone(); let anthropic_upstream_pool_mode = anthropic_upstream_pool_mode.clone(); let model_name_map = model_name_map.clone(); + let model_group_preferences = model_group_preferences.clone(); let kiro_request_validation_enabled = kiro_request_validation_enabled.clone(); let kiro_cache_estimation_enabled = kiro_cache_estimation_enabled.clone(); let kiro_zero_cache_debug_enabled = kiro_zero_cache_debug_enabled.clone(); @@ -1948,6 +1975,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let preferred_pool_strategy_value = (*preferred_pool_strategy).clone(); let anthropic_upstream_pool_mode_value = (*anthropic_upstream_pool_mode).clone(); let model_name_map_value = (*model_name_map).clone(); + let model_group_preferences_value = (*model_group_preferences).clone(); let kiro_request_validation_enabled_value = *kiro_request_validation_enabled; let kiro_cache_estimation_enabled_value = *kiro_cache_estimation_enabled; let kiro_zero_cache_debug_enabled_value = *kiro_zero_cache_debug_enabled; @@ -2028,6 +2056,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { anthropic_upstream_pool_mode_value.as_str(), ), model_name_map: Some(&model_name_map_value), + kiro_model_group_preferences: Some(&model_group_preferences_value), request_max_concurrency: None, request_min_start_interval_ms: None, codex_fast_enabled: None, @@ -2089,6 +2118,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let preferred_pool_strategy = preferred_pool_strategy.clone(); let anthropic_upstream_pool_mode = anthropic_upstream_pool_mode.clone(); let model_name_map = model_name_map.clone(); + let model_group_preferences = model_group_preferences.clone(); let kiro_cache_estimation_enabled = kiro_cache_estimation_enabled.clone(); let kiro_zero_cache_debug_enabled = kiro_zero_cache_debug_enabled.clone(); let kiro_full_request_logging_enabled = kiro_full_request_logging_enabled.clone(); @@ -2108,6 +2138,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let preferred_pool_strategy_value = (*preferred_pool_strategy).clone(); let anthropic_upstream_pool_mode_value = (*anthropic_upstream_pool_mode).clone(); let model_name_map_value = (*model_name_map).clone(); + let model_group_preferences_value = (*model_group_preferences).clone(); let kiro_cache_estimation_enabled_value = *kiro_cache_estimation_enabled; let kiro_zero_cache_debug_enabled_value = *kiro_zero_cache_debug_enabled; let kiro_full_request_logging_enabled_value = *kiro_full_request_logging_enabled; @@ -2143,6 +2174,7 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { anthropic_upstream_pool_mode_value.as_str(), ), model_name_map: Some(&model_name_map_value), + kiro_model_group_preferences: Some(&model_group_preferences_value), request_max_concurrency: None, request_min_start_interval_ms: None, codex_fast_enabled: None, @@ -2231,6 +2263,58 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { let model_name_map = model_name_map.clone(); Callback::from(move |_| model_name_map.set(BTreeMap::new())) }; + let on_reset_model_group_preferences = { + let model_group_preferences = model_group_preferences.clone(); + Callback::from(move |_| model_group_preferences.set(BTreeMap::new())) + }; + let on_save_model_group_preferences = { + let key_id = props.key_item.id.clone(); + let key_name = props.key_item.name.clone(); + let model_group_preferences = model_group_preferences.clone(); + let model_group_routing_open = model_group_routing_open.clone(); + let saving = saving.clone(); + let feedback = feedback.clone(); + let on_flash = props.on_flash.clone(); + let on_reload = props.on_reload.clone(); + Callback::from(move |_| { + if *saving { + return; + } + let key_id = key_id.clone(); + let key_name = key_name.clone(); + let preferences_value = (*model_group_preferences).clone(); + let model_group_routing_open = model_group_routing_open.clone(); + let saving = saving.clone(); + let feedback = feedback.clone(); + let on_flash = on_flash.clone(); + let on_reload = on_reload.clone(); + wasm_bindgen_futures::spawn_local(async move { + saving.set(true); + feedback.set(None); + match patch_admin_kiro_key(&key_id, PatchAdminLlmGatewayKeyRequest { + kiro_model_group_preferences: Some(&preferences_value), + ..PatchAdminLlmGatewayKeyRequest::default() + }) + .await + { + Ok(_) => { + feedback.set(Some("Saved model routing.".to_string())); + model_group_routing_open.set(false); + on_flash.emit((format!("Saved model routing for `{key_name}`."), false)); + on_reload.emit(()); + }, + Err(err) => { + feedback.set(Some(err.clone())); + on_flash.emit(( + format!("Failed to save model routing for `{key_name}`.\n{err}"), + true, + )); + }, + } + saving.set(false); + }); + }) + }; let on_restore_inherit = { let key_id = props.key_item.id.clone(); let key_name = props.key_item.name.clone(); @@ -2373,6 +2457,10 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html { .collect::>() .join(" · ") }; + let model_group_routing_summary = kiro_model_group_preferences_summary( + &model_group_preferences, + props.account_groups.as_slice(), + ); html! {
@@ -2953,6 +3041,26 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html {
} +
+
+
+
{ "Model Group Routing" }
+
+ { model_group_routing_summary.clone() } +
+
+ +
+
@@ -3034,6 +3142,126 @@ fn kiro_key_editor_card(props: &KiroKeyEditorCardProps) -> Html {
+ if *model_group_routing_open { +
+ +
+
+ if props.available_models.is_empty() { +
+ { "No model inventory loaded." } +
+ } else { +
+ { for props.available_models.iter().map(|model| { + let model_id = model.id.clone(); + let selected_group_id = (*model_group_preferences) + .get(&model_id) + .cloned() + .unwrap_or_default(); + let model_group_preferences = model_group_preferences.clone(); + let account_groups = props.account_groups.clone(); + html! { +
+
+
{ model.display_name.clone() }
+
{ model.id.clone() }
+
+ +
+ } + }) } +
+ } +
+
+ +
+ + +
+
+ +
+ } +