diff --git a/cli/src/builtins/common.rs b/cli/src/builtins/common.rs index 374fc26..b5d73bd 100644 --- a/cli/src/builtins/common.rs +++ b/cli/src/builtins/common.rs @@ -13,13 +13,16 @@ use crate::llm::{LLMSampler, Sampler}; use crate::metrics_store::MetricsStore; /// Fields shared by every builtin benchmark config. When adding a new builtin, -/// embed this with `#[serde(flatten)]` so that `limit`, `model`, and +/// embed this with `#[serde(flatten)]` so that `samples`, `model`, and /// `max_workers` are automatically supported without duplication. #[derive(Debug, Default, Deserialize)] pub(crate) struct BuiltinConfig { /// Number of dataset rows to evaluate. If omitted, the entire dataset is used. - #[serde(default)] - pub(crate) limit: Option, + /// + /// This field used to be called `limit`, so we are aliasing it to provide + /// backward compatibility. + #[serde(default, alias = "limit")] + pub(crate) samples: Option, /// Which model sampler to use. If omitted, the builtin chooses a sensible default. #[serde(default)] pub(crate) model: Option, @@ -228,6 +231,13 @@ mod tests { use super::*; use rstest::rstest; + #[test] + fn builtin_config_accepts_samples() { + let config: BuiltinConfig = serde_json::from_str(r#"{"samples":10}"#).unwrap(); + + assert_eq!(config.samples, Some(10)); + } + #[rstest] #[case("hello", Some("hello"))] #[case("", Some(""))] diff --git a/cli/src/builtins/custom_nocode/mod.rs b/cli/src/builtins/custom_nocode/mod.rs index a18152a..64c5e80 100644 --- a/cli/src/builtins/custom_nocode/mod.rs +++ b/cli/src/builtins/custom_nocode/mod.rs @@ -46,10 +46,10 @@ impl BuiltinWorkflow for CustomNoCodeBuiltin { config.dataset.config_name.as_deref(), config.dataset.split.as_deref(), config.dataset.revision.as_deref(), - config.limit, + config.samples, ) .await?; - config.limit = Some(limit); + config.samples = Some(limit); crate::db::set_run_input(ctx.db, ctx.run_id, &serde_json::to_string(&config)?).await?; let db = ctx.db.clone(); @@ -214,7 +214,7 @@ mod tests { "dataset": {"name": "fixture/qa"}, "model": "random", "prompt_template_file": template_path.to_str().unwrap(), - "limit": 2, + "samples": 2, })) .unwrap(); @@ -257,7 +257,10 @@ mod tests { let recorded_input: serde_json::Value = serde_json::from_str(recorded_run.input.as_deref().unwrap()).unwrap(); assert_eq!(recorded_input["style"]["type"], "exact_match"); - assert_eq!(recorded_input["limit"], 2); + assert_eq!(recorded_input["samples"], 2); + let recorded_output: serde_json::Value = + serde_json::from_str(recorded_run.output.as_deref().unwrap()).unwrap(); + assert_eq!(recorded_output, json!({"samples": 2})); let all_metrics = metrics_store.list_for_run(run_id).await.unwrap(); let is_correct_count = all_metrics diff --git a/cli/src/builtins/custom_nocode/runtime.rs b/cli/src/builtins/custom_nocode/runtime.rs index c538ba4..e20862a 100644 --- a/cli/src/builtins/custom_nocode/runtime.rs +++ b/cli/src/builtins/custom_nocode/runtime.rs @@ -30,8 +30,8 @@ pub(super) fn parse_input(input: Option<&str>) -> Result 0"); + if config.samples == Some(0) { + bail!("samples must be > 0"); } Ok(config) @@ -65,7 +65,7 @@ pub(super) async fn resolve_dataset_limit( let total = info .total_rows - .context("could not determine dataset size; pass an explicit limit")?; + .context("could not determine dataset size; pass an explicit samples value")?; let limit = limit.unwrap_or(total).min(total); Ok((manager, info, limit)) diff --git a/cli/src/builtins/input.rs b/cli/src/builtins/input.rs index 7d443b5..fe7546d 100644 --- a/cli/src/builtins/input.rs +++ b/cli/src/builtins/input.rs @@ -6,7 +6,7 @@ use crate::llm::Sampler; #[derive(Serialize)] pub(crate) struct BuiltinRunInput { pub(crate) model: String, - pub(crate) num_samples: usize, + pub(crate) samples: usize, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) max_workers: Option, } @@ -16,12 +16,12 @@ pub(crate) async fn set_builtin_run_input( db: &sea_orm::DatabaseConnection, run_id: i64, model: Option<&Sampler>, - num_samples: usize, + samples: usize, max_workers: Option, ) -> anyhow::Result<()> { let input = serde_json::to_string(&BuiltinRunInput { model: builtin_model_name(model), - num_samples, + samples, max_workers, })?; crate::db::set_run_input(db, run_id, &input).await @@ -35,3 +35,22 @@ fn builtin_model_name(model: Option<&Sampler>) -> String { Some(other) => other.to_string(), } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalized_input_uses_samples() { + let input = BuiltinRunInput { + model: "demo-builtin".to_owned(), + samples: 10, + max_workers: None, + }; + + assert_eq!( + serde_json::to_value(input).unwrap(), + serde_json::json!({"model": "demo-builtin", "samples": 10}) + ); + } +} diff --git a/cli/src/builtins/output.rs b/cli/src/builtins/output.rs index 541dbbb..fffc197 100644 --- a/cli/src/builtins/output.rs +++ b/cli/src/builtins/output.rs @@ -3,15 +3,30 @@ use serde::Serialize; /// Normalized run output schema for all builtins. #[derive(Serialize)] pub(crate) struct BuiltinRunOutput { - pub(crate) samples_completed: usize, + pub(crate) samples: usize, } /// Rewrite the run record output to the normalized builtin shape. pub(crate) async fn set_builtin_run_output( db: &sea_orm::DatabaseConnection, run_id: i64, - samples_completed: usize, + samples: usize, ) -> anyhow::Result<()> { - let output = serde_json::to_string(&BuiltinRunOutput { samples_completed })?; + let output = serde_json::to_string(&BuiltinRunOutput { samples })?; crate::db::set_run_output(db, run_id, &output).await } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalized_output_uses_samples() { + let output = BuiltinRunOutput { samples: 10 }; + + assert_eq!( + serde_json::to_value(output).unwrap(), + serde_json::json!({"samples": 10}) + ); + } +} diff --git a/cli/src/builtins/pubmedqa/config.rs b/cli/src/builtins/pubmedqa/config.rs index 8577c40..15edd0a 100644 --- a/cli/src/builtins/pubmedqa/config.rs +++ b/cli/src/builtins/pubmedqa/config.rs @@ -4,7 +4,7 @@ use crate::builtins::common::BuiltinConfig; /// Parsed user input for the builtin. /// -/// All common fields (`limit`, `model`, `max_workers`) live in [`BuiltinConfig`] +/// All common fields (`samples`, `model`, `max_workers`) live in [`BuiltinConfig`] /// and are flattened during deserialization so that the TOML surface stays flat. #[derive(Debug, Default, Deserialize)] pub(crate) struct PubMedQAConfig { diff --git a/cli/src/builtins/pubmedqa/mod.rs b/cli/src/builtins/pubmedqa/mod.rs index 4e0df77..41c667e 100644 --- a/cli/src/builtins/pubmedqa/mod.rs +++ b/cli/src/builtins/pubmedqa/mod.rs @@ -37,8 +37,8 @@ impl BuiltinWorkflow for PubmedqaBuiltin { .context("invalid builtin input JSON")? .unwrap_or_default(); - if config.base.limit == Some(0) { - bail!("limit must be > 0"); + if config.base.samples == Some(0) { + bail!("samples must be > 0"); } let max_workers = config.base.max_workers.unwrap_or_else(get_max_workers); @@ -55,8 +55,8 @@ impl BuiltinWorkflow for PubmedqaBuiltin { let total = info .total_rows - .context("could not determine dataset size; pass an explicit limit")?; - let limit = config.base.limit.unwrap_or(total).min(total); + .context("could not determine dataset size; pass an explicit samples value")?; + let limit = config.base.samples.unwrap_or(total).min(total); set_builtin_run_input( ctx.db, diff --git a/cli/src/builtins/similarity.rs b/cli/src/builtins/similarity.rs index 1c8fe0f..1a8a7bc 100644 --- a/cli/src/builtins/similarity.rs +++ b/cli/src/builtins/similarity.rs @@ -18,7 +18,7 @@ use crate::similarity::{ /// Configuration shared by all similarity-based builtins. /// -/// All common fields (`limit`, `model`, `max_workers`) live in [`BuiltinConfig`] +/// All common fields (`samples`, `model`, `max_workers`) live in [`BuiltinConfig`] /// and are flattened during deserialization so that the TOML surface stays flat. #[derive(Debug, Default, Deserialize)] struct SimilarityConfig { @@ -80,8 +80,8 @@ impl BuiltinWorkflow for SimilarityBenchmark { .context("invalid builtin input JSON")? .unwrap_or_default(); - if config.base.limit == Some(0) { - bail!("limit must be > 0"); + if config.base.samples == Some(0) { + bail!("samples must be > 0"); } let metric: Box = match config.metric { @@ -98,8 +98,8 @@ impl BuiltinWorkflow for SimilarityBenchmark { let total = info .total_rows - .context("could not determine dataset size; pass an explicit limit")?; - let limit = config.base.limit.unwrap_or(total).min(total); + .context("could not determine dataset size; pass an explicit samples value")?; + let limit = config.base.samples.unwrap_or(total).min(total); let db = ctx.db; let run_id = ctx.run_id; diff --git a/cli/src/commands/resume.rs b/cli/src/commands/resume.rs index ef8033b..9e7e46f 100644 --- a/cli/src/commands/resume.rs +++ b/cli/src/commands/resume.rs @@ -237,7 +237,7 @@ mod tests { }, model: Some(qt::llm::Sampler::Random {}), prompt_template_file: file.path().to_str().unwrap().to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: qt::config::CustomNoCodeStyleConfig::ExactMatch { @@ -319,7 +319,7 @@ style = {{ type = "exact_match", golden_column = "answer" }} dataset = {{ name = "fixture/qa" }} model = "random" prompt_template_file = "{}" -limit = 2 +samples = 2 "#, template_path.to_str().unwrap() ), @@ -330,7 +330,7 @@ limit = 2 // reconstruct the executable configuration from quantiles.toml. let input_json = serde_json::to_string(&serde_json::json!({ "model": "demo-builtin", - "num_samples": 2, + "samples": 2, })) .unwrap(); diff --git a/cli/src/commands/run.rs b/cli/src/commands/run.rs index aeb0f07..f1af9a7 100644 --- a/cli/src/commands/run.rs +++ b/cli/src/commands/run.rs @@ -136,7 +136,7 @@ fn assemble_builtin_input( } let input = BuiltinConfigInput { - limit: bench.samples, + samples: bench.samples, model: bench.model.clone(), max_workers: bench.max_workers, }; @@ -176,10 +176,10 @@ pub(super) fn assemble_custom_nocode_input( .with_context(|| "invalid `model` in custom_nocode --input")?, ); } - "limit" => { - params.limit = Some( + "samples" => { + params.samples = Some( serde_json::from_value(value) - .with_context(|| "invalid `limit` in custom_nocode --input")?, + .with_context(|| "invalid `samples` in custom_nocode --input")?, ); } "prompt_template_file" => { @@ -188,7 +188,7 @@ pub(super) fn assemble_custom_nocode_input( )?; } _ => bail!( - "unsupported custom_nocode --input field `{key}`; only `model`, `limit`, and `prompt_template_file` may be overridden" + "unsupported custom_nocode --input field `{key}`; only `model`, `samples`, and `prompt_template_file` may be overridden" ), } } @@ -479,7 +479,7 @@ struct BuiltinRunJsonOutput { #[derive(Serialize, Default)] struct BuiltinConfigInput { #[serde(skip_serializing_if = "Option::is_none")] - limit: Option, + samples: Option, #[serde(skip_serializing_if = "Option::is_none")] model: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -740,7 +740,7 @@ mod tests { }; let (input, _) = super::assemble_builtin_input(Some(&bench), None); let parsed: serde_json::Value = serde_json::from_str(&input.unwrap()).unwrap(); - assert_eq!(parsed["limit"], 5); + assert_eq!(parsed["samples"], 5); assert_eq!(parsed["model"], "random"); assert_eq!(parsed["max_workers"], 8); } @@ -782,7 +782,7 @@ mod tests { }, model: Some(qt::llm::Sampler::Random {}), prompt_template_file: "prompts/qa.txt".to_owned(), - limit: Some(10), + samples: Some(10), max_workers: Some(4), metrics: Vec::new(), style: qt::config::CustomNoCodeStyleConfig::ExactMatch { @@ -800,11 +800,11 @@ mod tests { assert_eq!(parsed["model"], "random"); assert_eq!(parsed["prompt_template_file"], "prompts/qa.txt"); assert_eq!(parsed["style"]["golden_column"], "answer"); - assert_eq!(parsed["limit"], 10); + assert_eq!(parsed["samples"], 10); assert_eq!(parsed["max_workers"], 4); } - /// When `model`, `limit`, and `max_workers` are absent from the config, the assembled + /// When `model`, `samples`, and `max_workers` are absent from the config, the assembled /// JSON should omit those keys entirely rather than emit null values. #[test] fn assemble_custom_nocode_input_omits_none_fields() { @@ -819,7 +819,7 @@ mod tests { }, model: None, prompt_template_file: "prompts/qa.txt".to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: qt::config::CustomNoCodeStyleConfig::ExactMatch { @@ -830,7 +830,7 @@ mod tests { let input = super::assemble_custom_nocode_input(&bench, None).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&input).unwrap(); assert!(parsed.get("model").is_none()); - assert!(parsed.get("limit").is_none()); + assert!(parsed.get("samples").is_none()); assert!(parsed.get("max_workers").is_none()); } @@ -849,7 +849,7 @@ mod tests { }, model: None, prompt_template_file: "prompts/qa.txt".to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: qt::config::CustomNoCodeStyleConfig::ExactMatch { @@ -860,7 +860,7 @@ mod tests { let input = super::assemble_custom_nocode_input( &bench, Some( - r#"{"model":"openai:gpt-5.6-luna","limit":5,"prompt_template_file":"prompts/other.txt"}"#, + r#"{"model":"openai:gpt-5.6-luna","samples":5,"prompt_template_file":"prompts/other.txt"}"#, ), ) .unwrap(); @@ -869,7 +869,7 @@ mod tests { assert_eq!(parsed["style"]["type"], "exact_match"); assert_eq!(parsed["style"]["golden_column"], "answer"); assert_eq!(parsed["model"], "openai:gpt-5.6-luna"); - assert_eq!(parsed["limit"], 5); + assert_eq!(parsed["samples"], 5); assert_eq!(parsed["prompt_template_file"], "prompts/other.txt"); } @@ -894,7 +894,8 @@ mod tests { #[test] fn assemble_custom_nocode_input_rejects_non_object() { let bench = custom_nocode_benchmark_for_override_tests(); - let err = super::assemble_custom_nocode_input(&bench, Some(r#"["limit", 5]"#)).unwrap_err(); + let err = + super::assemble_custom_nocode_input(&bench, Some(r#"["samples", 5]"#)).unwrap_err(); assert!( err.to_string() .contains("failed to parse custom_nocode --input as a JSON object") @@ -906,10 +907,10 @@ mod tests { fn assemble_custom_nocode_input_rejects_null_override() { let bench = custom_nocode_benchmark_for_override_tests(); let err = - super::assemble_custom_nocode_input(&bench, Some(r#"{"limit":null}"#)).unwrap_err(); + super::assemble_custom_nocode_input(&bench, Some(r#"{"samples":null}"#)).unwrap_err(); assert!( err.to_string() - .contains("invalid `limit` in custom_nocode --input") + .contains("invalid `samples` in custom_nocode --input") ); } @@ -925,7 +926,7 @@ mod tests { }, model: None, prompt_template_file: "prompts/qa.txt".to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: qt::config::CustomNoCodeStyleConfig::ExactMatch { diff --git a/cli/src/config/custom_nocode.rs b/cli/src/config/custom_nocode.rs index 1869bb1..fa33d72 100644 --- a/cli/src/config/custom_nocode.rs +++ b/cli/src/config/custom_nocode.rs @@ -95,8 +95,8 @@ pub struct CustomNoCodeParams { /// Path to a Jinja template file for rendering prompts. pub prompt_template_file: String, /// Number of dataset rows to evaluate. - #[serde(skip_serializing_if = "Option::is_none")] - pub limit: Option, + #[serde(alias = "limit", skip_serializing_if = "Option::is_none")] + pub samples: Option, /// Maximum number of concurrent workers. #[serde(skip_serializing_if = "Option::is_none")] pub max_workers: Option, @@ -290,7 +290,7 @@ mod tests { dataset = { name = "quantiles/simpleqa-verified" } model = "random" prompt_template_file = "prompts/qa.txt" - limit = 10 + samples = 10 "#; let config: WorkspaceConfig = toml::from_str(toml).unwrap(); let bench = config.benchmarks.get("nocode_custom").unwrap(); @@ -298,7 +298,7 @@ mod tests { if let BenchmarkConfig::CustomNoCode(c) = bench { assert_eq!(c.params.dataset.name, "quantiles/simpleqa-verified"); assert_eq!(c.params.model, Some(Sampler::Random)); - assert_eq!(c.params.limit, Some(10)); + assert_eq!(c.params.samples, Some(10)); let CustomNoCodeStyleConfig::ExactMatch { golden_column } = &c.params.style else { panic!("expected exact-match task"); }; @@ -347,7 +347,7 @@ mod tests { }, model: Some(Sampler::Random), prompt_template_file: "does_not_exist.txt".to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: CustomNoCodeStyleConfig::ExactMatch { @@ -373,7 +373,7 @@ mod tests { }, model: Some(Sampler::Random), prompt_template_file: file.path().to_str().unwrap().to_owned(), - limit: None, + samples: None, max_workers: None, metrics: Vec::new(), style: CustomNoCodeStyleConfig::ExactMatch { @@ -496,7 +496,7 @@ mod tests { }, model: None, prompt_template_file: file.path().to_string_lossy().into_owned(), - limit: None, + samples: None, max_workers: None, metrics: vec![ CustomNoCodeMetricSelection::Name(CustomNoCodeMetricName::F1), @@ -541,7 +541,7 @@ mod tests { }, model: None, prompt_template_file: file.path().to_string_lossy().into_owned(), - limit: None, + samples: None, max_workers: None, metrics: vec![CustomNoCodeMetricSelection::Name( CustomNoCodeMetricName::F1, diff --git a/custom-nocode-examples/quantiles.toml b/custom-nocode-examples/quantiles.toml index e44533a..6e1ec60 100644 --- a/custom-nocode-examples/quantiles.toml +++ b/custom-nocode-examples/quantiles.toml @@ -27,7 +27,7 @@ style = { type = "exact_match", golden_column = "answer" } dataset = { name = "quantiles/simpleqa-verified" } model = "random" prompt_template_file = "prompts/qa.txt" -limit = 10 +samples = 10 # The MedQA benchmark configured to use the random model. # @@ -39,7 +39,7 @@ type = "custom_nocode" dataset = { name = "quantiles/MedQA-USMLE-4-options", config_name = "default", split = "test" } model = "random" prompt_template_file = "prompts/medqa.txt" -limit = 10 +samples = 10 [benchmarks.medqa.style] type = "multiple_choice" @@ -57,7 +57,7 @@ type = "custom_nocode" dataset = { name = "quantiles/medmcqa", config_name = "default", split = "validation" } model = "random" prompt_template_file = "prompts/medmcqa.txt" -limit = 100 +samples = 100 [benchmarks.medmcqa.style] type = "multiple_choice" @@ -75,7 +75,7 @@ type = "custom_nocode" dataset = { name = "quantiles/MMLU-Pro", config_name = "default", split = "test" } model = "random" prompt_template_file = "prompts/mmlu-pro.txt" -limit = 1000 +samples = 1000 [benchmarks.mmlu-pro.style] type = "multiple_choice"