mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-21 00:02:30 +00:00
fix: bound per-model context windows on both AI config write paths
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ced486600f
commit
19fe8cc796
@@ -210,7 +210,9 @@ pub fn validate_model_pricing_json(ai_config: &serde_json::Value) -> Result<(),
|
||||
return Err(format!("Price override for {} is not an object", key));
|
||||
};
|
||||
for field in ["input", "output", "cache_read", "cache_write"] {
|
||||
let Some(rate) = price.get(field) else { continue };
|
||||
let Some(rate) = price.get(field) else {
|
||||
continue;
|
||||
};
|
||||
let rate = rate
|
||||
.as_f64()
|
||||
.filter(|r| r.is_finite() && *r >= 0.0 && *r <= MAX_MODEL_RATE);
|
||||
@@ -223,9 +225,66 @@ pub fn validate_model_pricing_json(ai_config: &serde_json::Value) -> Result<(),
|
||||
}
|
||||
for required in ["input", "output"] {
|
||||
if !price.contains_key(required) {
|
||||
return Err(format!("Price override for {} is missing {}", key, required));
|
||||
return Err(format!(
|
||||
"Price override for {} is missing {}",
|
||||
key, required
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Context windows
|
||||
// ============================================================================
|
||||
|
||||
/// The chat compacts its history against `context_window_per_model`, so a window near
|
||||
/// or below zero would compact on every turn or drop the whole history.
|
||||
pub const MIN_CONTEXT_WINDOW: i64 = 1000;
|
||||
pub const MAX_CONTEXT_WINDOW: i64 = 10_000_000;
|
||||
|
||||
pub fn validate_context_window(key: &str, tokens: i64) -> Result<(), String> {
|
||||
if !(MIN_CONTEXT_WINDOW..=MAX_CONTEXT_WINDOW).contains(&tokens) {
|
||||
return Err(format!(
|
||||
"Context window for {key} must be between {MIN_CONTEXT_WINDOW} and {MAX_CONTEXT_WINDOW} tokens"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The untyped counterpart for the instance config, for the reason given on
|
||||
/// `validate_model_pricing_json`.
|
||||
pub fn validate_context_windows_json(ai_config: &serde_json::Value) -> Result<(), String> {
|
||||
let windows = match ai_config.get("context_window_per_model") {
|
||||
None | Some(serde_json::Value::Null) => return Ok(()),
|
||||
Some(v) => v
|
||||
.as_object()
|
||||
.ok_or_else(|| "context_window_per_model must be an object".to_string())?,
|
||||
};
|
||||
for (key, tokens) in windows {
|
||||
let tokens = tokens
|
||||
.as_i64()
|
||||
.ok_or_else(|| format!("Context window for {key} must be an integer"))?;
|
||||
validate_context_window(key, tokens)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod context_window_tests {
|
||||
use super::validate_context_windows_json;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn instance_context_windows_are_bounded() {
|
||||
let check = |w| validate_context_windows_json(&json!({ "context_window_per_model": w }));
|
||||
assert!(validate_context_windows_json(&json!({})).is_ok());
|
||||
assert!(check(json!({ "customai:m": 32000 })).is_ok());
|
||||
assert!(check(json!({ "customai:m": 0 })).is_err());
|
||||
assert!(check(json!({ "customai:m": -5 })).is_err());
|
||||
assert!(check(json!({ "customai:m": 5_000_000_000i64 })).is_err());
|
||||
assert!(check(json!({ "customai:m": "32000" })).is_err());
|
||||
assert!(check(json!([32000])).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -945,6 +945,8 @@ async fn run_setting_pre_write_hook(
|
||||
AI_CONFIG_SETTING => {
|
||||
windmill_ai::ai_types::validate_model_pricing_json(value)
|
||||
.map_err(error::Error::BadRequest)?;
|
||||
windmill_ai::ai_types::validate_context_windows_json(value)
|
||||
.map_err(error::Error::BadRequest)?;
|
||||
}
|
||||
AUTOMATE_USERNAME_CREATION_SETTING => {
|
||||
if value.as_bool().unwrap_or(false) {
|
||||
|
||||
@@ -512,6 +512,14 @@ impl AIConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_context_windows(&self) -> Result<()> {
|
||||
for (key, tokens) in self.context_window_per_model.iter().flatten() {
|
||||
windmill_ai::ai_types::validate_context_window(key, i64::from(*tokens))
|
||||
.map_err(Error::BadRequest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_sessions_retention(&self) -> Result<()> {
|
||||
match self.sessions_retention_days {
|
||||
Some(days) if !(1..=MAX_SESSIONS_RETENTION_DAYS).contains(&days) => {
|
||||
|
||||
@@ -108,6 +108,7 @@ async fn edit_copilot_config(
|
||||
}
|
||||
|
||||
ai_config.validate_model_pricing()?;
|
||||
ai_config.validate_context_windows()?;
|
||||
ai_config.validate_sessions_retention()?;
|
||||
|
||||
let mut tx = db.begin().await?;
|
||||
|
||||
Reference in New Issue
Block a user