diff --git a/README.md b/README.md index 7429841..8eb0591 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,10 @@ gate: # enabled: false model: name: deepseek/deepseek-v4-pro - cascade: [anthropic/claude-sonnet-4.6] + cascade: + - mistralai/mistral-small-3.2-24b-instruct + - google/gemma-3-27b-it + - qwen/qwen3-32b apiBase: https://openrouter.ai/api/v1 # ignored from config by default; see note below consensus: 1 # >1: only findings multiple models agree on survive ``` diff --git a/src/config.rs b/src/config.rs index 107a60e..bd7761f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,6 +16,11 @@ use serde::{Deserialize, Serialize}; use crate::envelope::Severity; pub const DEFAULT_MODEL: &str = "deepseek/deepseek-v4-pro"; +pub const DEFAULT_CASCADE: [&str; 3] = [ + "mistralai/mistral-small-3.2-24b-instruct", + "google/gemma-3-27b-it", + "qwen/qwen3-32b", +]; pub const DEFAULT_API_BASE: &str = "https://openrouter.ai/api/v1"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -118,7 +123,7 @@ impl Default for Config { gate_fail_on: GateLevel::Severity(Severity::Error), gate_on_error: OnError::Block, model: DEFAULT_MODEL.to_string(), - cascade: Vec::new(), + cascade: DEFAULT_CASCADE.iter().map(|m| (*m).to_string()).collect(), api_base: DEFAULT_API_BASE.to_string(), consensus: 1, guardrails: None, @@ -464,8 +469,10 @@ gate: model: name: deepseek/deepseek-v4-pro - # cascade: - # - anthropic/claude-sonnet-4.6 + cascade: + - mistralai/mistral-small-3.2-24b-instruct + - google/gemma-3-27b-it + - qwen/qwen3-32b # apiBase: https://openrouter.ai/api/v1 # any OpenAI-compatible endpoint (Ollama, vLLM, Azure). # # Ignored from config by default (a repo could redirect # # the inference credential). Prefer POSTIL_API_BASE; to @@ -546,6 +553,10 @@ mod tests { f.write_all(content.as_bytes()).unwrap(); } + fn default_cascade() -> Vec { + DEFAULT_CASCADE.iter().map(|m| (*m).to_string()).collect() + } + #[test] fn defaults_are_low_noise() { let c = Config::default(); @@ -557,6 +568,22 @@ mod tests { )); } + #[test] + fn defaults_keep_primary_and_retry_roster_order() { + let c = Config::default(); + assert_eq!(c.model, DEFAULT_MODEL); + assert_eq!(c.cascade, default_cascade()); + assert_eq!( + c.model_chain(), + vec![ + "deepseek/deepseek-v4-pro", + "mistralai/mistral-small-3.2-24b-instruct", + "google/gemma-3-27b-it", + "qwen/qwen3-32b", + ] + ); + } + #[test] fn postil_yaml_wins_over_coderabbit() { let dir = tempfile::tempdir().unwrap(); @@ -598,6 +625,8 @@ mod tests { let mut c = Config::default(); c.apply_file(f).unwrap(); assert_eq!(c.max_findings, 20); + assert_eq!(c.model, DEFAULT_MODEL); + assert_eq!(c.cascade, default_cascade()); } #[test] diff --git a/tests/e2e.rs b/tests/e2e.rs index b834ad6..5afe37d 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -662,10 +662,33 @@ async fn garbage_output_fails_closed_after_repair_attempt() { let env: Value = serde_json::from_str(&String::from_utf8(out.get_output().stdout.clone()).unwrap()).unwrap(); assert_eq!(env["findings"][0]["path"], ".postil/model-output"); - assert_eq!(env["usage"]["promptTokens"], 160); - assert_eq!(env["usage"]["completionTokens"], 60); - // Initial call + repair call. - assert_eq!(server.received_requests().await.unwrap().len(), 2); + assert_eq!(env["usage"]["promptTokens"], 640); + assert_eq!(env["usage"]["completionTokens"], 240); + // Initial call + repair call for each default model in the retry roster. + let requests = server.received_requests().await.unwrap(); + assert_eq!(requests.len(), 8); + let models: Vec = requests + .iter() + .map(|request| { + request.body_json::().unwrap()["model"] + .as_str() + .unwrap() + .to_string() + }) + .collect(); + assert_eq!( + models, + vec![ + "deepseek/deepseek-v4-pro", + "deepseek/deepseek-v4-pro", + "mistralai/mistral-small-3.2-24b-instruct", + "mistralai/mistral-small-3.2-24b-instruct", + "google/gemma-3-27b-it", + "google/gemma-3-27b-it", + "qwen/qwen3-32b", + "qwen/qwen3-32b", + ] + ); } #[tokio::test]