kernel/profiles/
context_budget.rs1use crate::profiles::configuration::normalized_param_values;
5use crate::records::{JsonValue, ModelRecord, RuntimeId, SourceKind};
6
7pub const COMPLETION_FLOOR: i64 = 256;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Verdict {
16 Fits {
19 clamped_max_tokens: Option<i64>,
21 },
22 Exceeds {
24 estimated: i64,
26 window: i64,
28 },
29}
30
31pub fn estimated_tokens(characters: i64) -> i64 {
34 (characters + 3) / 4
35}
36
37fn reserved_for_completion(window: i64) -> i64 {
45 COMPLETION_FLOOR.min(window / 2).max(1)
46}
47
48pub fn assess(prompt_characters: i64, window: i64, requested_max_tokens: Option<i64>) -> Verdict {
51 let estimated = estimated_tokens(prompt_characters);
52 if window <= 0 || estimated + reserved_for_completion(window) > window {
53 return Verdict::Exceeds { estimated, window };
54 }
55 let available = window - estimated;
56 let clamped = requested_max_tokens.unwrap_or(available).min(available);
57 Verdict::Fits {
58 clamped_max_tokens: Some(clamped),
59 }
60}
61
62pub const BUILTIN_CONTEXT_WINDOW: i64 = 4096;
67
68pub fn effective_window(
72 record: &ModelRecord,
73 requested_context_length: Option<i64>,
74) -> Option<i64> {
75 if record.source.kind == SourceKind::builtin() {
76 return Some(BUILTIN_CONTEXT_WINDOW);
77 }
78 let window = record_policy_window(record, requested_context_length)?;
79 (window > 0).then_some(window)
80}
81
82fn record_policy_window(record: &ModelRecord, requested: Option<i64>) -> Option<i64> {
83 let id = record.runtime.id.as_ref()?;
84 if *id == RuntimeId::ollama() {
85 requested.or(record.context_length)
86 } else if *id == RuntimeId::llama_cpp()
87 || *id == RuntimeId::mlx_swift()
88 || *id == RuntimeId::mlx_lm()
89 {
90 record.context_length
91 } else {
92 None
93 }
94}
95
96pub fn stored_context_length(record: &ModelRecord) -> Option<i64> {
98 normalized_param_values(record)
99 .get("context_length")
100 .and_then(JsonValue::as_i64)
101}
102
103pub fn prompt_characters(payload: &JsonValue) -> i64 {
106 let JsonValue::Object(object) = payload else {
107 return 0;
108 };
109 let mut total = 0i64;
110 if let Some(JsonValue::Array(messages)) = object.get("messages") {
111 for message in messages {
112 if let JsonValue::Object(fields) = message
113 && let Some(JsonValue::String(content)) = fields.get("content")
114 {
115 total += content.chars().count() as i64;
116 }
117 }
118 }
119 if let Some(JsonValue::String(prompt)) = object.get("prompt") {
120 total += prompt.chars().count() as i64;
121 }
122 total
123}