blob: 85ca9593c93b2e5015484e824e60e413819b0e9f [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.
*/
//! Native provider sessions own wire state and use only Host-scoped transport.
mod chat_completions;
mod error;
mod events;
mod media;
mod prompt;
mod request;
use crate::{ModelError, ProviderFailureReason};
use maka_agent_facts::model::budget;
use maka_agent_ports::model::{Context, Emission, Lifetime, ProviderAdapter, Request, Session};
use maka_capabilities::http;
use maka_provider_protocol::{anthropic, chat_completions as chat, output::Event};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};
use tokio::sync::Semaphore;
use tokio_util::sync::CancellationToken;
const WINDOW: u32 = 32 * 1024 * 1024;
const RESPONSE_LIMIT: usize = 8 * 1024 * 1024;
struct Shared {
input: Arc<Semaphore>,
output: Arc<Semaphore>,
without_usage: Mutex<HashSet<(u64, String)>>,
}
impl Default for Shared {
fn default() -> Self {
Self {
input: Arc::new(Semaphore::new(WINDOW as usize)),
output: Arc::new(Semaphore::new(WINDOW as usize)),
without_usage: Mutex::default(),
}
}
}
#[derive(Default)]
pub(super) struct Adapter(Arc<Shared>);
impl ProviderAdapter for Adapter {
fn enforces_idle_timeout(&self) -> bool {
true
}
fn open(
&self,
_: Lifetime,
_: CancellationToken,
) -> futures_util::future::BoxFuture<'_, Result<Arc<dyn Session>, ModelError>> {
let shared = self.0.clone();
Box::pin(async move { Ok(Arc::new(Native(shared)) as Arc<dyn Session>) })
}
}
struct Native(Arc<Shared>);
impl Session for Native {
fn stream(
&self,
request: Request,
context: Context,
) -> futures_util::future::BoxFuture<'static, Result<(), ModelError>> {
let shared = self.0.clone();
Box::pin(async move {
tokio::select! {
biased;
_ = context.cancellation.cancelled() => Err(ModelError::Cancelled),
result = run(request, &context, shared) => result,
}
})
}
}
enum Decoder {
Anthropic(Box<anthropic::stream::Stream>),
Chat(Box<chat::Stream>),
}
impl Decoder {
fn push(&mut self, bytes: &[u8]) -> Result<Vec<Event>, ModelError> {
let mut next_id = || uuid::Uuid::new_v4().to_string();
match self {
Self::Anthropic(stream) => stream.push(bytes, &mut next_id, |_| {}).map_err(failure),
Self::Chat(stream) => {
let mut events = stream.push(bytes, &mut next_id).map_err(failure)?;
if stream.terminal() {
events.extend(stream.finish(&mut next_id).map_err(failure)?);
}
Ok(events)
}
}
}
fn finish(&mut self) -> Result<Vec<Event>, ModelError> {
match self {
Self::Anthropic(stream) => Ok(stream.finish()),
Self::Chat(stream) => stream
.finish(&mut || uuid::Uuid::new_v4().to_string())
.map_err(failure),
}
}
}
fn failure(error: maka_provider_protocol::Error) -> ModelError {
ModelError::Adapter(error.to_string())
}
async fn run(request: Request, context: &Context, shared: Arc<Shared>) -> Result<(), ModelError> {
let size = budget::reservation(&request, WINDOW)?;
let _input = shared
.input
.clone()
.acquire_many_owned(size)
.await
.map_err(|_| ModelError::Cancelled)?;
let mut prepared = request::prepare(request)?;
let route = (context.transport.identity(), prepared.url.clone());
let remembered = prepared.compatible
&& shared
.without_usage
.lock()
.expect("usage cache")
.contains(&route);
if remembered {
prepared
.body
.as_object_mut()
.expect("request object")
.remove("stream_options");
}
let mut events = events::Events::new(prepared.anthropic);
let mut retried = remembered;
loop {
let response = tokio::time::timeout(
context.idle_timeout,
context.transport.request(http::Request {
method: http::Method::Post,
url: prepared.url.clone(),
headers: prepared.headers.clone(),
body: serde_json::to_vec(&prepared.body)
.map_err(|e| ModelError::Adapter(e.to_string()))?,
}),
)
.await
.map_err(|_| ModelError::TimedOut)?
.map_err(|e| events.evidence.transport(e))?;
let body = BodyGuard(response.body);
context.events.progress();
let status = response.head.status;
let sse = header(&response.head, "content-type").is_some_and(|mime| {
mime.split(';')
.next()
.is_some_and(|v| v.trim().eq_ignore_ascii_case("text/event-stream"))
});
if !(200..300).contains(&status) || !sse {
let retry_after = retry_after(&response.head);
let read = read_body(&body.0, &response.head, context).await;
let bytes = match read {
Ok(bytes) => bytes,
Err(ModelError::Provider(failure))
if failure.reason() == ProviderFailureReason::Network
&& !(200..300).contains(&status) =>
{
return Err(events.evidence.provider(
error::http_error(status, &[]),
prepared.anthropic,
retry_after,
));
}
Err(error) => return Err(error),
};
if prepared.compatible
&& !retried
&& status == 400
&& prepared.body.get("stream_options").is_some()
{
let text = String::from_utf8_lossy(&bytes).to_ascii_lowercase();
if text.contains("stream_options") || text.contains("include_usage") {
{
let mut cache = shared.without_usage.lock().expect("usage cache");
if cache.len() >= 1024 {
cache.clear();
}
cache.insert(route.clone());
}
prepared
.body
.as_object_mut()
.expect("request object")
.remove("stream_options");
retried = true;
body.0
.close()
.await
.map_err(|e| ModelError::Adapter(e.to_string()))?;
continue;
}
}
return Err(events.evidence.provider(
error::http_error(status, &bytes),
prepared.anthropic,
retry_after,
));
}
// Release request storage after dispatch. Admission remains reserved
// through physical stream settlement; output has its own byte window.
drop(prepared.body);
loop {
let chunk = tokio::time::timeout(context.idle_timeout, body.0.next())
.await
.map_err(|_| ModelError::TimedOut)?
.map_err(|error| match error {
http::Error::Denied => ModelError::Cancelled,
http::Error::Failed(_) => events.evidence.interrupted(
ProviderFailureReason::Network,
"model HTTP transport interrupted",
),
other => ModelError::Adapter(other.to_string()),
})?;
match chunk {
Some(bytes) => {
context.events.progress();
// Bound the vector of decoded events even if an HTTP peer
// combines many records into one transport chunk.
for bytes in bytes.chunks(16 * 1024) {
let decoded = prepared.decoder.push(bytes)?;
emit(decoded, &mut events, context, &shared).await?;
if events.finished {
body.0
.close()
.await
.map_err(|e| ModelError::Adapter(e.to_string()))?;
return Ok(());
}
}
}
None => {
emit(prepared.decoder.finish()?, &mut events, context, &shared).await?;
return if events.finished {
Ok(())
} else {
Err(events.evidence.interrupted(
ProviderFailureReason::StreamTruncated,
"model stream ended without finish",
))
};
}
}
}
}
}
async fn emit(
decoded: Vec<Event>,
events: &mut events::Events,
context: &Context,
shared: &Shared,
) -> Result<(), ModelError> {
for event in decoded {
if let Some(event) = events.push(event)? {
let size = budget::bytes(&event, WINDOW)?;
let reservation = shared
.output
.clone()
.acquire_many_owned(size)
.await
.map_err(|_| ModelError::Cancelled)?;
context
.events
.emit(Emission {
event,
reservation: Some(Arc::new(reservation)),
})
.await?;
}
if events.finished {
break;
}
}
Ok(())
}
struct BodyGuard(Arc<dyn http::Body>);
impl Drop for BodyGuard {
fn drop(&mut self) {
self.0.cancel();
}
}
fn header<'a>(head: &'a http::Head, name: &str) -> Option<&'a str> {
head.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.and_then(|(_, value)| std::str::from_utf8(value).ok())
}
async fn read_body(
body: &Arc<dyn http::Body>,
head: &http::Head,
context: &Context,
) -> Result<Vec<u8>, ModelError> {
let limit = || ModelError::Adapter("provider response body exceeds 8 MiB".into());
if header(head, "content-length")
.and_then(|n| n.parse::<usize>().ok())
.is_some_and(|n| n > RESPONSE_LIMIT)
{
return Err(limit());
}
let mut bytes = Vec::new();
while let Some(chunk) = tokio::time::timeout(context.idle_timeout, body.next())
.await
.map_err(|_| ModelError::TimedOut)?
.map_err(|error| match error {
http::Error::Denied => ModelError::Cancelled,
http::Error::Failed(_) => ModelError::Provider(crate::ProviderFailure::new(
ProviderFailureReason::Network,
"error body interrupted",
true,
None,
)),
other => ModelError::Adapter(other.to_string()),
})?
{
context.events.progress();
if chunk.len() > RESPONSE_LIMIT - bytes.len() {
return Err(limit());
}
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
fn retry_after(head: &http::Head) -> Option<u64> {
let delay = if let Some(ms) = header(head, "retry-after-ms") {
ms.parse::<f64>().ok()?
} else {
let seconds = header(head, "retry-after")?;
if let Ok(seconds) = seconds.parse::<f64>() {
seconds * 1000.0
} else {
let date = jiff::fmt::rfc2822::parse(seconds).ok()?;
let now = jiff::Timestamp::now().as_millisecond();
(date.timestamp().as_millisecond() - now) as f64
}
};
(delay.is_finite() && delay > 0.0 && delay <= 2_147_483_647.0).then_some(delay.ceil() as u64)
}