blob: 602829f65a5a19ac0617e88c2d8172cc3d0369ec [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{Inner, RunContext, RunError, compact};
use maka_agent_facts::context::CheckpointMode;
use maka_agent_facts::context::ModelRequestContext;
use maka_agent_facts::event::EventWrite;
use maka_event_log::context::{LatestMainContext, ModelContextSource};
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
/// A cap on this request, never a mutation of the configured model limit.
pub(super) fn output_limit(
input: &RunContext,
source: &ModelContextSource,
) -> Result<Option<u64>, RunError> {
let limit = input.main_output_limit.unwrap_or(8000);
let Some(window) = input
.context
.as_ref()
.and_then(|context| context.model_context_window)
else {
return Ok(Some(limit));
};
let LatestMainContext::Selected(latest) = &source.latest_main else {
return Ok(Some(limit));
};
let connection = input
.configuration
.model
.as_ref()
.map(|model| model.connection_id.as_str());
if connection.is_none()
|| latest.connection_id.as_deref() != connection
|| latest.model_id != input.provider.model
|| latest.route_identity != crate::model_attempt::route_identity(input)?
|| !latest.projection_current
|| latest.checkpoint_event_id.as_deref()
!= source
.baseline
.as_ref()
.map(|baseline| baseline.event_id.as_str())
{
return Ok(Some(limit));
}
let Some(tokens) = latest.usage.input_tokens.filter(|tokens| *tokens > 0) else {
return Ok(Some(limit));
};
let Some(output) = latest.usage.output_tokens else {
return Ok(Some(limit));
};
let retained = tokens.saturating_add(output);
let thinking = input
.provider_options
.pointer("/anthropic/thinking")
.filter(|thinking| thinking["type"] == "enabled")
.and_then(|thinking| thinking["budgetTokens"].as_u64())
.unwrap_or(0);
Ok(Some(cap_output(limit, window, retained, thinking)))
}
fn cap_output(limit: u64, window: u64, retained: u64, thinking: u64) -> u64 {
// Leave room for newly appended input. This is not a tokenizer or a proof
// the next request fits: a useful floor preserves reactive recovery.
let available = window
.saturating_sub(retained)
.saturating_sub(8000)
.saturating_sub(thinking);
available.min(limit).max(limit.min(8000))
}
pub(super) fn due(input: &RunContext, source: &ModelContextSource) -> bool {
let Some(ModelRequestContext {
declared_window: Some(window),
..
}) = &input.context
else {
return false;
};
let LatestMainContext::Selected(latest) = &source.latest_main else {
return false;
};
let Some(connection) = input
.configuration
.model
.as_ref()
.map(|binding| binding.connection_id.as_str())
else {
return false;
};
if !latest.projection_current
|| latest.model_id != input.provider.model
|| crate::model_attempt::route_identity(input).as_ref().ok() != Some(&latest.route_identity)
|| latest.connection_id.as_deref() != Some(connection)
|| latest.checkpoint_event_id.as_deref()
!= source
.baseline
.as_ref()
.map(|baseline| baseline.event_id.as_str())
{
return false;
}
threshold(
latest.usage.input_tokens,
latest.usage.output_tokens,
*window,
)
}
fn threshold(input: Option<u64>, output: Option<u64>, window: u64) -> bool {
let Some(input) = input.filter(|tokens| *tokens > 0) else {
return false;
};
input >= window || output.is_some_and(|output| input.saturating_add(output) >= window)
}
pub(super) async fn start(
inner: &Arc<Inner>,
input: &mut RunContext,
opening_id: &str,
mid_turn: bool,
cancellation: &CancellationToken,
continuation_base: Option<u64>,
) -> Result<Option<Pending>, RunError> {
let mode = if mid_turn {
CheckpointMode::MidTurn {
anchor_event_id: opening_id.to_owned(),
}
} else {
CheckpointMode::PreTurn
};
let job = match compact::capture(inner, input, &mode, cancellation, continuation_base).await {
Ok(Some(job)) => job,
Ok(None) => {
return Ok(None);
}
Err(error) => return Err(error),
};
let cancellation = cancellation.child_token();
let worker_cancel = cancellation.clone();
let inner = inner.clone();
let cleanup = crate::cleanup::current();
let worker =
tokio::spawn(async move { cleanup.scope(job.execute(&inner, &worker_cancel)).await });
Ok(Some(Pending {
cancellation,
worker: Some(worker),
}))
}
pub(super) struct Pending {
cancellation: CancellationToken,
worker: Option<tokio::task::JoinHandle<Result<compact::Candidate, RunError>>>,
}
impl Pending {
pub fn is_finished(&self) -> bool {
self.worker
.as_ref()
.is_some_and(|worker| worker.is_finished())
}
async fn join(mut self) -> Result<Option<maka_agent_facts::event::RuntimeEvent>, RunError> {
match self
.worker
.take()
.expect("candidate has one owner")
.await
.map_err(|_| crate::cleanup::unconfirmed())?
{
Ok((_, checkpoint)) => Ok(checkpoint),
Err(RunError::Model(_) | RunError::Cancelled) => Ok(None),
Err(error) => Err(error),
}
}
pub async fn adopt(
self,
inner: &Inner,
cancellation: &CancellationToken,
) -> Result<bool, RunError> {
let checkpoint = self.join().await?;
if cancellation.is_cancelled() {
return Err(RunError::Cancelled);
}
let Some(checkpoint) = checkpoint else {
return Ok(false);
};
let session = checkpoint.invocation.session_id.clone();
inner.log.append(&EventWrite::plain(checkpoint)?).await?;
inner.clear_loaded(&session);
Ok(true)
}
}
impl Drop for Pending {
fn drop(&mut self) {
self.cancellation.cancel();
}
}
pub(super) async fn drain(pending: &mut Option<Pending>) -> Result<(), RunError> {
if let Some(pending) = pending.take() {
pending.cancellation.cancel();
pending.join().await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{cap_output, threshold};
#[test]
fn output_reserve_keeps_a_useful_floor_without_exceeding_the_selected_limit() {
assert_eq!(cap_output(128_000, 200_000, 100_000, 0), 92_000);
assert_eq!(cap_output(128_000, 200_000, 100_000, 1024), 90_976);
assert_eq!(cap_output(128_000, 200_000, 191_999, 0), 8000);
assert_eq!(cap_output(128_000, 200_000, u64::MAX, u64::MAX), 8000);
assert_eq!(cap_output(4096, 200_000, 199_999, 0), 4096);
}
#[test]
fn actual_usage_threshold_preserves_unknown_without_reserving_output_twice() {
assert!(!threshold(None, Some(5000), 1));
assert!(!threshold(Some(0), Some(5000), 1));
assert!(threshold(Some(100), None, 100));
assert!(!threshold(Some(100), None, 101));
assert!(threshold(Some(100), Some(20), 120));
assert!(!threshold(Some(100), Some(20), 121));
assert!(threshold(Some(100), Some(5000), 5100));
assert!(!threshold(Some(100), Some(5000), 5101));
}
}