Skip to content

Commit 42ce06a

Browse files
authored
feat(kiro): add model group routing preferences (#61)
* feat(kiro): add model group routing preferences * fix(kiro): repair model group routing CI * ci: avoid repeated postgres test migrations
1 parent afe0a64 commit 42ce06a

26 files changed

Lines changed: 887 additions & 27 deletions

File tree

crates/frontend/src/api.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6132,6 +6132,8 @@ pub struct AdminLlmGatewayKeyView {
61326132
#[serde(default = "default_anthropic_upstream_pool_mode")]
61336133
pub kiro_anthropic_upstream_pool_mode: String,
61346134
pub model_name_map: Option<BTreeMap<String, String>>,
6135+
#[serde(default)]
6136+
pub kiro_model_group_preferences: BTreeMap<String, String>,
61356137
pub request_max_concurrency: Option<u64>,
61366138
pub request_min_start_interval_ms: Option<u64>,
61376139
#[serde(default = "default_true")]
@@ -9030,6 +9032,7 @@ pub async fn create_admin_llm_gateway_key(
90309032
preferred_pool_strategy: default_kiro_pool_strategy(),
90319033
kiro_anthropic_upstream_pool_mode: default_anthropic_upstream_pool_mode(),
90329034
model_name_map: None,
9035+
kiro_model_group_preferences: BTreeMap::new(),
90339036
request_max_concurrency,
90349037
request_min_start_interval_ms,
90359038
kiro_request_validation_enabled: true,
@@ -9096,6 +9099,7 @@ pub struct PatchAdminLlmGatewayKeyRequest<'a> {
90969099
pub preferred_pool_strategy: Option<&'a str>,
90979100
pub kiro_anthropic_upstream_pool_mode: Option<&'a str>,
90989101
pub model_name_map: Option<&'a BTreeMap<String, String>>,
9102+
pub kiro_model_group_preferences: Option<&'a BTreeMap<String, String>>,
90999103
pub request_max_concurrency: Option<u64>,
91009104
pub request_min_start_interval_ms: Option<u64>,
91019105
pub codex_fast_enabled: Option<bool>,
@@ -9136,6 +9140,7 @@ pub async fn patch_admin_llm_gateway_key(
91369140
request.preferred_pool_strategy,
91379141
request.kiro_anthropic_upstream_pool_mode,
91389142
request.model_name_map,
9143+
request.kiro_model_group_preferences,
91399144
request.request_max_concurrency,
91409145
request.request_min_start_interval_ms,
91419146
request.codex_fast_enabled,
@@ -9236,6 +9241,11 @@ pub async fn patch_admin_llm_gateway_key(
92369241
.map_err(|e| format!("Serialize error: {:?}", e))?;
92379242
body.insert("model_name_map".to_string(), value);
92389243
}
9244+
if let Some(preferences) = request.kiro_model_group_preferences {
9245+
let value = serde_json::to_value(preferences)
9246+
.map_err(|e| format!("Serialize error: {:?}", e))?;
9247+
body.insert("kiro_model_group_preferences".to_string(), value);
9248+
}
92399249
if let Some(request_max_concurrency) = request.request_max_concurrency {
92409250
body.insert(
92419251
"request_max_concurrency".to_string(),
@@ -11445,6 +11455,7 @@ pub async fn create_admin_kiro_key(
1144511455
preferred_pool_strategy: default_kiro_pool_strategy(),
1144611456
kiro_anthropic_upstream_pool_mode: default_anthropic_upstream_pool_mode(),
1144711457
model_name_map: None,
11458+
kiro_model_group_preferences: BTreeMap::new(),
1144811459
request_max_concurrency: None,
1144911460
request_min_start_interval_ms: None,
1145011461
kiro_request_validation_enabled: true,
@@ -11513,6 +11524,7 @@ pub async fn patch_admin_kiro_key(
1151311524
request.preferred_pool_strategy,
1151411525
request.kiro_anthropic_upstream_pool_mode,
1151511526
request.model_name_map,
11527+
request.kiro_model_group_preferences,
1151611528
request.request_max_concurrency,
1151711529
request.request_min_start_interval_ms,
1151811530
request.kiro_request_validation_enabled,
@@ -11608,6 +11620,11 @@ pub async fn patch_admin_kiro_key(
1160811620
.map_err(|e| format!("Serialize error: {:?}", e))?;
1160911621
body.insert("model_name_map".to_string(), value);
1161011622
}
11623+
if let Some(preferences) = request.kiro_model_group_preferences {
11624+
let value = serde_json::to_value(preferences)
11625+
.map_err(|e| format!("Serialize error: {:?}", e))?;
11626+
body.insert("kiro_model_group_preferences".to_string(), value);
11627+
}
1161111628
if let Some(kiro_request_validation_enabled) = request.kiro_request_validation_enabled {
1161211629
body.insert(
1161311630
"kiro_request_validation_enabled".to_string(),

crates/frontend/src/pages/admin_kiro_gateway.rs

Lines changed: 228 additions & 0 deletions
Large diffs are not rendered by default.

crates/frontend/src/pages/admin_llm_gateway.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,7 @@ fn key_editor_card(props: &KeyEditorCardProps) -> Html {
14891489
preferred_pool_strategy: None,
14901490
kiro_anthropic_upstream_pool_mode: None,
14911491
model_name_map: None,
1492+
kiro_model_group_preferences: None,
14921493
request_max_concurrency: request_max_concurrency_value,
14931494
request_min_start_interval_ms: request_min_start_interval_ms_value,
14941495
codex_fast_enabled: Some(codex_fast_enabled_value),

crates/llm-access-core/src/store/empty.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ impl AdminKeyStore for EmptyAdminKeyStore {
431431
auto_account_names: None,
432432
preferred_pool_strategy: default_kiro_pool_strategy(),
433433
model_name_map: None,
434+
kiro_model_group_preferences: std::collections::BTreeMap::new(),
434435
request_max_concurrency: key.request_max_concurrency,
435436
request_min_start_interval_ms: key.request_min_start_interval_ms,
436437
codex_fast_enabled: true,

crates/llm-access-core/src/store/keys.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ pub struct AdminKey {
7070
pub preferred_pool_strategy: String,
7171
/// Model name mapping.
7272
pub model_name_map: Option<BTreeMap<String, String>>,
73+
/// Exact Kiro request-model to preferred account-group map.
74+
#[serde(default)]
75+
pub kiro_model_group_preferences: BTreeMap<String, String>,
7376
/// Per-key request concurrency cap.
7477
pub request_max_concurrency: Option<u64>,
7578
/// Per-key request pacing interval.
@@ -382,6 +385,8 @@ pub struct AdminKeyPatch {
382385
pub preferred_pool_strategy: Option<String>,
383386
/// New model name map.
384387
pub model_name_map: Option<Option<BTreeMap<String, String>>>,
388+
/// New exact Kiro request-model to preferred account-group map.
389+
pub kiro_model_group_preferences: Option<BTreeMap<String, String>>,
385390
/// New per-key request concurrency cap.
386391
pub request_max_concurrency: Option<Option<u64>>,
387392
/// New per-key request pacing interval.
@@ -455,6 +460,7 @@ mod tests {
455460
auto_account_names: None,
456461
preferred_pool_strategy: "balanced".to_string(),
457462
model_name_map: None,
463+
kiro_model_group_preferences: BTreeMap::new(),
458464
request_max_concurrency: None,
459465
request_min_start_interval_ms: None,
460466
codex_fast_enabled: true,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//! Kiro model -> account-group preference helpers.
2+
//!
3+
//! ```text
4+
//! raw request model
5+
//! |
6+
//! +-- exact preference hit --> preferred account set
7+
//! |
8+
//! +-- no hit ---------------> existing affinity/order rules
9+
//! ```
10+
11+
use std::collections::BTreeMap;
12+
13+
/// Per-key exact model-name to Kiro account-group preference map.
14+
pub type KiroModelGroupPreferences = BTreeMap<String, String>;
15+
16+
/// Normalize admin-supplied model preference rules.
17+
pub fn normalize_kiro_model_group_preferences(
18+
input: KiroModelGroupPreferences,
19+
) -> KiroModelGroupPreferences {
20+
input
21+
.into_iter()
22+
.filter_map(|(model, group_id)| {
23+
let model = model.trim();
24+
let group_id = group_id.trim();
25+
(!model.is_empty() && !group_id.is_empty())
26+
.then(|| (model.to_string(), group_id.to_string()))
27+
})
28+
.collect()
29+
}
30+
31+
/// Resolve one exact model-name preference.
32+
pub fn kiro_model_group_preference<'a>(
33+
preferences: &'a KiroModelGroupPreferences,
34+
model: &str,
35+
) -> Option<&'a str> {
36+
preferences.get(model.trim()).map(String::as_str)
37+
}
38+
39+
#[cfg(test)]
40+
mod tests {
41+
use super::*;
42+
43+
#[test]
44+
fn normalizes_model_group_preferences_by_trimming_and_dropping_empty_entries() {
45+
let normalized = normalize_kiro_model_group_preferences(BTreeMap::from([
46+
(" claude-sonnet-4 ".to_string(), " group-sonnet ".to_string()),
47+
("".to_string(), "group-empty-model".to_string()),
48+
("claude-opus-4".to_string(), " ".to_string()),
49+
]));
50+
51+
assert_eq!(
52+
normalized,
53+
BTreeMap::from([("claude-sonnet-4".to_string(), "group-sonnet".to_string())])
54+
);
55+
}
56+
57+
#[test]
58+
fn resolves_model_group_preferences_by_exact_model_name() {
59+
let preferences =
60+
BTreeMap::from([("claude-sonnet-4".to_string(), "group-sonnet".to_string())]);
61+
62+
assert_eq!(
63+
kiro_model_group_preference(&preferences, "claude-sonnet-4"),
64+
Some("group-sonnet")
65+
);
66+
assert_eq!(kiro_model_group_preference(&preferences, "claude-sonnet"), None);
67+
}
68+
}

crates/llm-access-core/src/store/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod empty;
1414
mod groups;
1515
mod keys;
1616
mod kiro_account;
17+
mod kiro_model_routing;
1718
mod proxy;
1819
mod public;
1920
mod routes;
@@ -71,6 +72,9 @@ pub use kiro_account::{
7172
ADMIN_KIRO_ACCOUNT_ISSUE_ABNORMAL, ADMIN_KIRO_ACCOUNT_ISSUE_AUTH_401,
7273
ADMIN_KIRO_ACCOUNT_ISSUE_DISABLED, ADMIN_KIRO_ACCOUNT_ISSUE_ERROR,
7374
};
75+
pub use kiro_model_routing::{
76+
kiro_model_group_preference, normalize_kiro_model_group_preferences, KiroModelGroupPreferences,
77+
};
7478
pub use proxy::{
7579
default_proxy_bindings, AdminProxyBinding, AdminProxyConfig, AdminProxyConfigPatch,
7680
AdminProxyEndpointCheck, AdminProxyEndpointCheckUpdate, AdminProxyTrafficSnapshot,

crates/llm-access-core/src/store/routes.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Provider routing contracts: authenticated key, provider proxy config,
22
//! Codex/Kiro route + auth-update views, and JWT/auth-error helpers.
33
4+
use std::collections::BTreeMap;
5+
46
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
57
use serde_json::Value;
68

@@ -234,6 +236,8 @@ pub struct ProviderKiroRoute {
234236
pub cctest_proxy_api_key: Option<String>,
235237
/// JSON object mapping public model names to upstream Kiro model names.
236238
pub model_name_map_json: String,
239+
/// Exact request-model to preferred account names resolved from key config.
240+
pub model_group_preferred_account_names: BTreeMap<String, Vec<String>>,
237241
/// Effective Kiro cache k-model JSON for this key.
238242
pub cache_kmodels_json: String,
239243
/// Effective Kiro cache policy JSON for this key.

crates/llm-access-migrations/migrations/postgres/0001_init.sql

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ CREATE TABLE IF NOT EXISTS llm_key_route_config (
2626
preferred_pool_strategy IN ('balanced', 'credit_first')
2727
),
2828
model_name_map_json JSONB,
29+
kiro_model_group_preferences_json JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (
30+
jsonb_typeof(kiro_model_group_preferences_json) = 'object'
31+
),
2932
request_max_concurrency BIGINT,
3033
request_min_start_interval_ms BIGINT,
3134
codex_fast_enabled BOOLEAN NOT NULL DEFAULT TRUE,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
ALTER TABLE IF EXISTS llm_key_route_config
2+
ADD COLUMN IF NOT EXISTS kiro_model_group_preferences_json JSONB NOT NULL DEFAULT '{}'::jsonb;
3+
4+
UPDATE llm_key_route_config
5+
SET kiro_model_group_preferences_json = '{}'::jsonb
6+
WHERE kiro_model_group_preferences_json IS NULL
7+
OR jsonb_typeof(kiro_model_group_preferences_json) <> 'object';
8+
9+
DO $$
10+
BEGIN
11+
IF NOT EXISTS (
12+
SELECT 1
13+
FROM pg_constraint
14+
WHERE conname = 'ck_llm_key_route_config_kiro_model_group_preferences_object'
15+
AND conrelid = 'llm_key_route_config'::regclass
16+
) THEN
17+
ALTER TABLE llm_key_route_config
18+
ADD CONSTRAINT ck_llm_key_route_config_kiro_model_group_preferences_object
19+
CHECK (jsonb_typeof(kiro_model_group_preferences_json) = 'object');
20+
END IF;
21+
END $$;

0 commit comments

Comments
 (0)