blob: ef72b662fe8c0d70b9198a1d335e920e9d8acb35 [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::provider_stream::request;
use futures_util::future::BoxFuture;
use maka_agent_ports::model::*;
use maka_model::{ModelExecutor, ProviderKind};
use std::{
sync::{Arc, Mutex},
time::Duration,
};
use tokio_util::sync::CancellationToken;
struct Controlled {
release: CancellationToken,
retained: Arc<Mutex<Option<Context>>>,
}
impl ProviderAdapter for Controlled {
fn open(
&self,
_: Lifetime,
_: CancellationToken,
) -> BoxFuture<'_, Result<Arc<dyn Session>, Error>> {
let session = Controlled {
release: self.release.clone(),
retained: self.retained.clone(),
};
Box::pin(async { Ok(Arc::new(session) as Arc<dyn Session>) })
}
}
impl Session for Controlled {
fn stream(&self, _: Request, context: Context) -> BoxFuture<'static, Result<(), Error>> {
let release = self.release.clone();
*self.retained.lock().unwrap() = Some(context.clone());
Box::pin(async move {
context
.events
.emit(
ModelEvent::ResponseMetadata {
id: Some("accepted".into()),
model: None,
timestamp: None,
}
.into(),
)
.await?;
tokio::select! {
_ = release.cancelled() => {}
_ = context.cancellation.cancelled() => return Err(Error::Cancelled),
}
context
.events
.emit(
ModelEvent::Finished {
reason: maka_agent_facts::model::ModelFinishReason::Stop,
usage: Default::default(),
provider_options: None,
}
.into(),
)
.await
})
}
}
fn binding(
release: CancellationToken,
retained: Arc<Mutex<Option<Context>>>,
revision: &str,
) -> Binding {
let source = maka_agent_facts::composition::SourceRevision {
kind: maka_agent_facts::composition::SourceKind::ModelAdapter,
name: "chat-completions".into(),
package_id: "fixture.adapter".into(),
entry_id: "fixture.adapter".into(),
activation: "fixture.activation".into(),
revision: revision.into(),
};
Binding::new(Arc::new(Controlled { release, retained }), source)
}
#[tokio::test]
async fn captured_adapter_survives_executor_replacement_and_bounds_idle_work() {
let release = CancellationToken::new();
let retained = Arc::default();
let original = binding(release.clone(), Arc::clone(&retained), "1");
let executor = ModelExecutor::new(Duration::from_millis(100))
.unwrap()
.with_adapters(std::collections::BTreeMap::from([(
"chat-completions".into(),
original.clone(),
)]));
let input = || {
request(
ProviderKind::OpenaiChatCompletions,
"http://unused.example/v1".into(),
)
};
let original = executor.binding(&input().provider).unwrap();
let source = original.source().clone();
let mut accepted = executor
.stream_with_adapter(input(), CancellationToken::new(), None, original.clone())
.await
.unwrap();
assert!(matches!(
accepted.next().await.unwrap().unwrap().event,
ModelEvent::ResponseMetadata { .. }
));
let executor = executor.with_adapters(std::collections::BTreeMap::from([(
"chat-completions".into(),
binding(CancellationToken::new(), Arc::default(), "2"),
)]));
let replacement = executor.binding(&input().provider).unwrap();
assert_ne!(source.revision, replacement.source().revision);
assert!(!original.same_registration(&replacement));
assert_eq!(original.source(), &source);
release.cancel();
assert!(matches!(
accepted.next().await.unwrap().unwrap().event,
ModelEvent::Finished { .. }
));
assert!(accepted.next().await.is_none());
accepted
.cancel_and_wait()
.await
.expect("actual stream cleanup");
let context = retained.lock().unwrap().take().unwrap();
assert!(matches!(
context
.transport
.request(maka_capabilities::http::Request {
method: maka_capabilities::http::Method::Get,
url: "http://unused.example/".into(),
headers: vec![],
body: vec![],
})
.await,
Err(Error::Cancelled)
));
let mut stalled = executor
.stream(input(), CancellationToken::new())
.await
.unwrap();
assert!(matches!(
stalled.next().await.unwrap().unwrap().event,
ModelEvent::ResponseMetadata { .. }
));
assert!(matches!(
stalled.next().await.unwrap(),
Err(Error::TimedOut)
));
stalled
.cancel_and_wait()
.await
.expect("actual stream cleanup");
}