blob: cf5f6bbcd2633a68315080bb91a4c6892c33cd01 [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 super::cache::CACHE_LIMIT;
use crate::budget;
use serde_json::Value;
use std::{collections::BTreeMap, sync::Arc};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
/// Only finalized provider items are replayable. Added items track unfinished
/// slots without retaining their partial contents. This is one request's cache.
#[derive(Default)]
pub(super) struct Output(BTreeMap<u64, Item>);
enum Item {
Started(String),
Done(Value),
}
impl Output {
pub fn observe(
&mut self,
event: &mut Value,
permit: &mut OwnedSemaphorePermit,
budget: &Arc<Semaphore>,
) -> Option<()> {
let done = match event["type"].as_str() {
Some("response.output_item.added") => false,
Some("response.output_item.done") => true,
_ => return Some(()),
};
let index = event["output_index"].as_u64()?;
let item = &event["item"];
let id = item["id"].as_str().filter(|id| !id.is_empty())?;
item["type"].as_str()?;
match self.0.get(&index) {
Some(Item::Started(previous)) if done && previous == id => {}
Some(_) => return None,
None => {}
}
// Charge before retaining the item. Include a small slot allowance;
// old Started storage stays conservatively reserved until completion.
let bytes = if done {
budget::bytes(item, CACHE_LIMIT).ok()?
} else {
budget::bytes(&id, CACHE_LIMIT).ok()?
}
.checked_add(64)?;
(permit.num_permits().checked_add(bytes as usize)? <= CACHE_LIMIT as usize).then_some(())?;
permit.merge(budget.clone().try_acquire_many_owned(bytes).ok()?);
let item = if done {
Item::Done(event["item"].take())
} else {
Item::Started(id.to_owned())
};
self.0.insert(index, item);
Some(())
}
pub fn finish(self) -> Option<Vec<Value>> {
if self.0.is_empty() {
return None;
}
self.0
.into_iter()
.enumerate()
.map(|(expected, (index, item))| {
if index != expected as u64 {
return None;
}
match item {
Item::Done(item) => Some(item),
Item::Started(_) => None,
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::super::cache::Prepared;
use super::*;
use serde_json::json;
fn event(index: u64, id: &str, done: bool) -> Value {
json!({"type": if done {"response.output_item.done"} else {"response.output_item.added"},
"output_index":index, "item":{"type":"message","id":id,"role":"assistant",
"status":"completed","phase":"final_answer",
"content":[{"type":"output_text","text":"cached","annotations":[]}]}})
}
#[test]
fn finalized_items_are_ordered_complete_and_budgeted_without_duplicate_terminal_output() {
let budget = Arc::new(Semaphore::new(4096));
let body = json!({"input":[{"role":"user","content":"first"}]});
let original = [
event(0, "a", true)["item"].clone(),
event(1, "b", true)["item"].clone(),
];
for full_terminal in [false, true] {
let mut baseline = Prepared::new(body.clone(), None)
.unwrap()
.cache(&budget)
.unwrap();
for (index, id, done) in [
(0, "a", false),
(1, "b", false),
(1, "b", true),
(0, "a", true),
] {
baseline
.observe(&mut event(index, id, done), &budget)
.unwrap();
}
let mut terminal =
json!({"id":"resp","output":if full_terminal {json!(original)} else {json!([])}});
let baseline = baseline.complete(&mut terminal, &budget).unwrap();
assert_eq!(baseline.response_id.as_deref(), Some("resp"));
assert_eq!(&baseline.body["input"].as_array().unwrap()[1..], &original);
assert_eq!(baseline.body["input"].as_array().unwrap().len(), 3);
drop(baseline);
assert_eq!(budget.available_permits(), 4096);
}
// Partial, duplicate, misidentified and gapped output must never be
// advertised to semantic confirmation as a reconstructable response.
for events in [
vec![event(0, "a", false)],
vec![event(1, "a", true)],
vec![event(0, "a", true), event(0, "a", true)],
vec![event(0, "a", false), event(0, "b", true)],
vec![event(0, "a", true), event(1, "b", false)],
] {
let mut baseline = Prepared::new(body.clone(), None).unwrap().cache(&budget);
for mut event in events {
baseline = baseline.and_then(|mut value| {
value.observe(&mut event, &budget)?;
Some(value)
});
}
let completed = baseline
.and_then(|value| value.complete(&mut json!({"id":"resp","output":[]}), &budget));
assert!(completed.is_none());
assert_eq!(budget.available_permits(), 4096);
}
let mut baseline = Prepared::new(body, None).unwrap().cache(&budget).unwrap();
baseline
.observe(&mut event(0, "a", false), &budget)
.unwrap();
let remaining = budget.available_permits() as u32;
let competing = budget.clone().try_acquire_many_owned(remaining).unwrap();
let mut done = event(0, "a", true);
assert!(baseline.observe(&mut done, &budget).is_none());
assert_eq!(
done["item"]["id"], "a",
"failed reservation must not move the raw item"
);
drop(baseline);
drop(competing);
assert_eq!(budget.available_permits(), 4096);
}
}