use super::native_inline::compact_history_native_inline;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompactionStrategy {
NativeStandalone,
NativeInline,
Local,
}
pub fn manual_compaction_strategy(provider: &dyn LLMProvider, model: &str) -> CompactionStrategy {
if provider.supports_manual_openai_compaction(model) {
CompactionStrategy::NativeStandalone
} else if provider.supports_native_inline_compaction(model) {
CompactionStrategy::NativeInline
} else {
CompactionStrategy::Local
}
}
#[must_use]
pub fn compacted_history_shrinks(original_len: usize, compacted_len: usize, mode: CompactionMode) -> bool {
let envelope_messages = match mode {
CompactionMode::Local => 1,
CompactionMode::Provider | CompactionMode::Unknown => 0,
};
compacted_len.saturating_add(envelope_messages) < original_len
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ManualCompactionOptions {
pub instructions: Option<String>,
pub max_output_tokens: Option<u32>,
pub reasoning_effort: Option<ReasoningEffortLevel>,
pub allow_reasoning_effort_downgrade: bool,
pub verbosity: Option<VerbosityLevel>,
}
impl From<ManualCompactionOptions> for ResponsesCompactionOptions {
fn from(options: ManualCompactionOptions) -> Self {
Self {
instructions: options.instructions,
max_output_tokens: options.max_output_tokens,
reasoning_effort: options.reasoning_effort,
verbosity: options.verbosity,
responses_include: None,
response_store: None,
service_tier: None,
prompt_cache_key: None,
}
}
}
impl CompactionConfig {
pub(crate) fn with_manual_overrides(self, options: &ManualCompactionOptions) -> Self {
let summary_prompt = options
.instructions
.clone()
.map(|instructions| instructions.trim().to_string())
.filter(|instructions| !instructions.is_empty())
.unwrap_or(self.summary_prompt);
Self { summary_prompt, ..self }
}
}
#[cfg_attr(feature = "profiling", hotpath::measure)]
pub async fn compact_history_manual(
provider: &dyn LLMProvider,
model: &str,
history: &[Message],
config: &CompactionConfig,
options: &ManualCompactionOptions,
) -> Result<(Vec<Message>, CompactionMode)> {
compact_history_manual_with_budget(provider, model, history, config, options, None).await
}
pub async fn compact_history_manual_with_budget(
provider: &dyn LLMProvider,
model: &str,
history: &[Message],
config: &CompactionConfig,
options: &ManualCompactionOptions,
context_budget: Option<usize>,
) -> Result<(Vec<Message>, CompactionMode)> {
compact_history_manual_with_parent_context(provider, model, history, config, options, context_budget, None).await
}
pub async fn compact_history_manual_with_parent_context(
provider: &dyn LLMProvider,
model: &str,
history: &[Message],
config: &CompactionConfig,
options: &ManualCompactionOptions,
context_budget: Option<usize>,
parent: Option<&CompactionParentContext>,
) -> Result<(Vec<Message>, CompactionMode)> {
if history.is_empty() {
return Ok((Vec::new(), CompactionMode::Local));
}
let options = resolve_manual_compaction_options(provider, model, options)?;
match manual_compaction_strategy(provider, model) {
CompactionStrategy::NativeStandalone => {
let responses_options: ResponsesCompactionOptions = options.clone().into();
let native_source = bound_history_for_native_compaction(
history,
options.instructions.as_deref().unwrap_or(""),
compaction_history_budget(provider, model, context_budget),
);
let compacted = provider
.compact_history_with_options(model, &native_source, &responses_options)
.await
.context("Failed to compact history via provider-native compaction")?;
Ok((compacted, CompactionMode::Provider))
}
CompactionStrategy::NativeInline => {
compact_history_native_inline(provider, model, history, config, &options, context_budget, parent).await
}
CompactionStrategy::Local => {
let compacted =
summarize_locally(provider, model, history, config, &options, context_budget, parent).await?;
Ok((compacted, CompactionMode::Local))
}
}
}
fn resolve_manual_compaction_options(
provider: &dyn LLMProvider,
model: &str,
options: &ManualCompactionOptions,
) -> Result<ManualCompactionOptions> {
let Some(requested) = options.reasoning_effort else {
return Ok(options.clone());
};
let mapping = ReasoningEffortMapper::resolve(provider, model, requested, options.allow_reasoning_effort_downgrade)
.with_context(|| {
format!("Failed to resolve compaction reasoning effort for {} / {}", provider.name(), model)
})?;
if mapping.degraded() {
tracing::warn!(
provider = provider.name(),
model,
requested = %mapping.requested,
effective = %mapping.effective,
"Compaction reasoning effort explicitly downgraded"
);
}
let mut resolved = options.clone();
resolved.reasoning_effort = Some(mapping.effective);
Ok(resolved)
}