Skip to main content

kernel/profiles/
configuration.rs

1//! Merging a model's saved configuration into a request: keeping only parameter
2//! values the current schema still recognizes, and seeding the system prompt.
3
4use std::collections::BTreeMap;
5
6use crate::records::{Capability, JsonValue, ModelRecord};
7
8/// The record's parameter values, each run through its spec: a value is kept
9/// only when its key still has a spec in the current schema AND the value
10/// coerces/clamps to that spec (see `ParamSpec::normalized`). Values for
11/// vanished parameters and wrong-typed / unnormalizable values are dropped, so
12/// nothing out-of-range or off-type can reach a runtime.
13pub fn normalized_param_values(record: &ModelRecord) -> BTreeMap<String, JsonValue> {
14    let mut kept = BTreeMap::new();
15    for (key, value) in &record.param_values {
16        if let Some(spec) = record.params.iter().find(|spec| &spec.key == key)
17            && let Some(normalized) = spec.normalized(value)
18        {
19            kept.insert(key.clone(), normalized);
20        }
21    }
22    kept
23}
24
25/// A copy of `record` with parameter values for vanished parameters removed.
26///
27/// This is an optional at-rest cleanup, not a correctness requirement: request
28/// handling normalizes through [`merged`]/[`normalized_param_values`] on every
29/// call, so a stale stored value never reaches a runtime. A caller uses this only
30/// to tidy the persisted shelf (e.g. before displaying or re-saving a record).
31pub fn dropping_vanished_param_values(record: &ModelRecord) -> ModelRecord {
32    let mut record = record.clone();
33    record.param_values = normalized_param_values(&record);
34    record
35}
36
37/// Merge the record's configuration into a request `payload`.
38///
39/// For a chat capability, a system prompt (session override, else the record's
40/// own, else `fallback_prompt`) is seeded into the `messages` array, and any
41/// `appended_block` is folded in. Saved parameter values fill in keys the payload
42/// does not already set. Non-object, non-null payloads pass through untouched.
43pub fn merged(
44    record: &ModelRecord,
45    capability: &Capability,
46    payload: JsonValue,
47    fallback_prompt: Option<&str>,
48    session_prompt: Option<&str>,
49    appended_block: Option<&str>,
50) -> JsonValue {
51    let overrides = normalized_param_values(record);
52    let (prompt, block) = if *capability == Capability::chat() {
53        let prompt = match session_prompt {
54            Some(session) => trimmed(Some(session)),
55            None => trimmed(record.system_prompt.as_deref()).or_else(|| trimmed(fallback_prompt)),
56        };
57        (prompt, trimmed(appended_block))
58    } else {
59        (None, None)
60    };
61
62    if overrides.is_empty() && prompt.is_none() && block.is_none() {
63        return payload;
64    }
65
66    let mut fields = match payload {
67        JsonValue::Object(fields) => fields,
68        JsonValue::Null => BTreeMap::new(),
69        other => return other,
70    };
71
72    for (key, value) in overrides {
73        fields.entry(key).or_insert(value);
74    }
75
76    if (prompt.is_some() || block.is_some())
77        && let Some(messages) = fields.get_mut("messages")
78        && let JsonValue::Array(turns) = messages
79    {
80        *messages = seeded(prompt.as_deref(), block.as_deref(), std::mem::take(turns));
81    }
82
83    JsonValue::Object(fields)
84}
85
86fn trimmed(prompt: Option<&str>) -> Option<String> {
87    let cleaned = prompt?.trim();
88    (!cleaned.is_empty()).then(|| cleaned.to_owned())
89}
90
91fn seeded(prompt: Option<&str>, block: Option<&str>, mut turns: Vec<JsonValue>) -> JsonValue {
92    let system_index = turns.iter().position(is_system_turn);
93
94    if let Some(index) = system_index {
95        let Some(block) = block else {
96            return JsonValue::Array(turns);
97        };
98        if let JsonValue::Object(fields) = &mut turns[index] {
99            let existing = match fields.get("content") {
100                Some(JsonValue::String(content)) => Some(content.clone()),
101                _ => None,
102            };
103            if let Some(existing) = existing {
104                let joined = [existing.as_str(), block]
105                    .into_iter()
106                    .filter(|part| !part.is_empty())
107                    .collect::<Vec<_>>()
108                    .join("\n\n");
109                fields.insert("content".to_owned(), JsonValue::String(joined));
110            }
111        }
112        return JsonValue::Array(turns);
113    }
114
115    let content = [prompt, block]
116        .into_iter()
117        .flatten()
118        .collect::<Vec<_>>()
119        .join("\n\n");
120    if content.is_empty() {
121        return JsonValue::Array(turns);
122    }
123    let mut system = BTreeMap::new();
124    system.insert("role".to_owned(), JsonValue::String("system".to_owned()));
125    system.insert("content".to_owned(), JsonValue::String(content));
126    let mut updated = Vec::with_capacity(turns.len() + 1);
127    updated.push(JsonValue::Object(system));
128    updated.extend(turns);
129    JsonValue::Array(updated)
130}
131
132fn is_system_turn(turn: &JsonValue) -> bool {
133    matches!(turn, JsonValue::Object(fields)
134        if fields.get("role") == Some(&JsonValue::String("system".to_owned())))
135}