llm: drop model/agent scope matching; add instance-wide compaction model picker
Nightly Build / build (push) Successful in 6m50s

Remove the scope system end-to-end (llm_models.scope column, agent meta
scope field, scope-based tier in model selection, UI checkboxes/pills):
it was only a soft ranking hint, had drifted (6 UI scopes vs 3 used by
agents, 'general' not even selectable) and duplicated what strength
already decides. Strength stays the single AUTO-selection axis.

Compaction: the summary model is now pickable from the Settings page
via a new PropertyType::LlmModel config property (registry key
compaction_model), instance-wide and live (no restart). Fallback chain:
explicit pick -> compaction.strength from config.yml -> priority order;
a deleted configured model degrades to AUTO. ContextCompactor reads the
key at compact time through GlobalConfigManager.
This commit is contained in:
2026-07-25 10:48:09 +01:00
parent 9dafc4bfaa
commit 5081ec2afe
39 changed files with 174 additions and 160 deletions
+7 -15
View File
@@ -93,7 +93,6 @@ struct ModelRow {
model_id: String,
name: String,
strength: Option<String>,
scope: String,
is_default: i64,
priority: i64,
extra_params: Option<String>,
@@ -106,7 +105,7 @@ struct ModelRow {
pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
let rows = sqlx::query_as::<_, ModelRow>(
"SELECT id, provider_id, model_id, name, strength, scope, is_default, priority, extra_params,
"SELECT id, provider_id, model_id, name, strength, is_default, priority, extra_params,
context_length, max_output_tokens, knowledge_cutoff, capabilities, reasoning
FROM llm_models
WHERE removed_at IS NULL
@@ -120,7 +119,6 @@ pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
}
pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64> {
let scope = serde_json::to_string(&r.scope)?;
let extra_params = r.extra_params.as_ref().map(|v| v.to_string());
let capabilities = serde_json::to_string(&r.capabilities)?;
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
@@ -132,14 +130,13 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
// soft-deleted row. Upsert on `name` so that existing row is revived
// (removed_at cleared) and every field overwritten.
let id = sqlx::query_scalar::<_, i64>(
"INSERT INTO llm_models (provider_id, model_id, name, strength, scope, is_default, priority, extra_params,
"INSERT INTO llm_models (provider_id, model_id, name, strength, is_default, priority, extra_params,
context_length, max_output_tokens, knowledge_cutoff, capabilities, reasoning)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
ON CONFLICT(name) DO UPDATE SET
provider_id = excluded.provider_id,
model_id = excluded.model_id,
strength = excluded.strength,
scope = excluded.scope,
is_default = excluded.is_default,
priority = excluded.priority,
extra_params = excluded.extra_params,
@@ -155,7 +152,6 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
.bind(&r.model_id)
.bind(&r.name)
.bind(r.strength.map(strength_str))
.bind(scope)
.bind(r.is_default as i64)
.bind(r.priority as i64)
.bind(extra_params)
@@ -172,23 +168,21 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
}
pub async fn update_model(pool: &SqlitePool, id: i64, r: &LlmModelRecord) -> Result<()> {
let scope = serde_json::to_string(&r.scope)?;
let extra_params = r.extra_params.as_ref().map(|v| v.to_string());
let capabilities = serde_json::to_string(&r.capabilities)?;
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
sqlx::query(
"UPDATE llm_models
SET provider_id=?1, model_id=?2, name=?3, strength=?4,
scope=?5, is_default=?6, priority=?7, extra_params=?8,
context_length=?9, max_output_tokens=?10, knowledge_cutoff=?11, capabilities=?12,
reasoning=?13
WHERE id=?14",
is_default=?5, priority=?6, extra_params=?7,
context_length=?8, max_output_tokens=?9, knowledge_cutoff=?10, capabilities=?11,
reasoning=?12
WHERE id=?13",
)
.bind(r.provider_id)
.bind(&r.model_id)
.bind(&r.name)
.bind(r.strength.map(strength_str))
.bind(scope)
.bind(r.is_default as i64)
.bind(r.priority as i64)
.bind(extra_params)
@@ -267,7 +261,6 @@ fn provider_row_to_record(r: ProviderRow) -> Result<LlmProviderRecord> {
}
fn model_row_to_record(r: ModelRow) -> Result<LlmModelRecord> {
let scope: Vec<String> = serde_json::from_str(&r.scope).unwrap_or_default();
let extra_params = r.extra_params
.as_deref()
.and_then(|s| serde_json::from_str(s).ok());
@@ -281,7 +274,6 @@ fn model_row_to_record(r: ModelRow) -> Result<LlmModelRecord> {
model_id: r.model_id,
name: r.name,
strength: r.strength.as_deref().and_then(parse_strength),
scope,
is_default: r.is_default != 0,
priority: r.priority as i32,
extra_params,
+9 -20
View File
@@ -100,12 +100,11 @@ impl LlmManager {
pub async fn resolve(
&self,
client_name: Option<&str>,
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let name = match client_name {
None | Some(AUTO_CLIENT) => {
let (name, entry) = self.select(required_scope, required_strength).await?;
let (name, entry) = self.select(required_strength).await?;
self.maybe_refresh_meta(&name).await;
return Ok((name, entry));
}
@@ -368,7 +367,6 @@ impl LlmManager {
model_id: slot.model.model_id.clone(),
name: slot.model.name.clone(),
strength: slot.model.strength,
scope: slot.model.scope.clone(),
is_default: slot.model.is_default,
priority: slot.model.priority,
extra_params: slot.model.extra_params.clone(),
@@ -391,7 +389,6 @@ impl LlmManager {
pub async fn select_excluding(
&self,
excluded: &[&str],
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let state = self.state.read().await;
@@ -401,7 +398,7 @@ impl LlmManager {
if slots.is_empty() {
anyhow::bail!("no alternative LLM models available");
}
sort_slots_for_agent(&mut slots, required_scope, required_strength);
sort_slots_for_agent(&mut slots, required_strength);
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
return Ok((name.to_string(), slot.entry.clone()));
}
@@ -414,7 +411,6 @@ impl LlmManager {
async fn select(
&self,
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let state = self.state.read().await;
@@ -424,7 +420,7 @@ impl LlmManager {
}
let mut slots: Vec<(&String, &ModelSlot)> = state.models.iter().collect();
sort_slots_for_agent(&mut slots, required_scope, required_strength);
sort_slots_for_agent(&mut slots, required_strength);
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
return Ok((name.to_string(), slot.entry.clone()));
@@ -526,7 +522,6 @@ fn build_entry(
model: model.model_id.clone(),
model_db_id,
strength: model.strength,
scope: model.scope.clone(),
extra_params: extra,
context_length: model.context_length,
prompt_cache,
@@ -548,28 +543,24 @@ fn build_entry(
pub fn sort_models_for_agent(
mut models: Vec<LlmModelInfo>,
scope: Option<&str>,
strength: Option<LlmStrength>,
) -> Vec<LlmModelInfo> {
models.sort_by_key(|m| (model_tier(m.strength, m.scope.as_slice(), scope, strength), m.priority));
models.sort_by_key(|m| (model_tier(m.strength, strength), m.priority));
models
}
fn sort_slots_for_agent(
slots: &mut Vec<(&String, &ModelSlot)>,
scope: Option<&str>,
strength: Option<LlmStrength>,
) {
slots.sort_by_key(|(_, s)| (
model_tier(s.model.strength, s.model.scope.as_slice(), scope, strength),
model_tier(s.model.strength, strength),
s.model.priority,
));
}
fn model_tier(
model_strength: Option<LlmStrength>,
model_scope: &[String],
req_scope: Option<&str>,
req_strength: Option<LlmStrength>,
) -> u8 {
let strength_ok = match (req_strength, model_strength) {
@@ -583,11 +574,9 @@ fn model_tier(
(Some(req), Some(avail)) => avail == req,
_ => true,
};
let scope_ok = req_scope.map_or(true, |sc| model_scope.iter().any(|x| x == sc));
match (strength_ok && scope_ok, exact_match && scope_ok, strength_ok) {
(true, true, _) => 0, // exact strength + scope ok
(true, false, _) => 1, // over-qualified but scope ok
(false, _, true) => 2, // strength ok, scope mismatch
_ => 3, // doesn't meet minimum bar
match (strength_ok, exact_match) {
(true, true) => 0, // exact strength
(true, false) => 1, // over-qualified
_ => 3, // doesn't meet minimum bar
}
}
-2
View File
@@ -17,7 +17,6 @@ pub struct LlmEntry {
pub model: String,
pub model_db_id: i64,
pub strength: Option<LlmStrength>,
pub scope: Vec<String>,
pub extra_params: Option<serde_json::Value>,
/// Max input context window in tokens, if known.
pub context_length: Option<i64>,
@@ -96,7 +95,6 @@ pub struct LlmModelInfo {
pub model_id: String,
pub name: String,
pub strength: Option<LlmStrength>,
pub scope: Vec<String>,
pub is_default: bool,
pub priority: i32,
pub extra_params: Option<serde_json::Value>,