diff --git a/crates/kebab-core/src/lib.rs b/crates/kebab-core/src/lib.rs index 4512bf3..6da7a53 100644 --- a/crates/kebab-core/src/lib.rs +++ b/crates/kebab-core/src/lib.rs @@ -51,7 +51,7 @@ pub use metadata::{ }; pub use search::{ DocFilter, DocSummary, RetrievalDetail, SearchFilters, SearchHit, - SearchMode, SearchQuery, + SearchMode, SearchOpts, SearchQuery, }; pub use answer::{ Answer, AnswerCitation, AnswerRetrievalSummary, ModelRef, RefusalReason, TokenUsage, diff --git a/crates/kebab-core/src/search.rs b/crates/kebab-core/src/search.rs index 6e49b5a..9d6527b 100644 --- a/crates/kebab-core/src/search.rs +++ b/crates/kebab-core/src/search.rs @@ -96,6 +96,18 @@ pub struct DocSummary { pub chunker_version: ChunkerVersion, } +/// p9-fb-34: caller-supplied output budget knobs for `App::search_with_opts`. +/// All `None` = no enforcement (existing behavior). +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct SearchOpts { + /// chars/4 approximation of wire JSON token cost. None = no cap. + pub max_tokens: Option, + /// Per-hit snippet character cap. None = use config default. + pub snippet_chars: Option, + /// Opaque base64 cursor from a previous response. None = first page. + pub cursor: Option, +} + #[cfg(test)] mod tests { use super::*; @@ -135,4 +147,12 @@ mod tests { assert_eq!(v["indexed_at"], "2026-05-09T12:00:00Z"); assert_eq!(v["stale"], true); } + + #[test] + fn search_opts_default_is_all_none() { + let opts = SearchOpts::default(); + assert!(opts.max_tokens.is_none()); + assert!(opts.snippet_chars.is_none()); + assert!(opts.cursor.is_none()); + } }