aether-llm 0.9.18

Multi-provider LLM abstraction layer for the Aether AI agent framework
Documentation
use aws_sdk_bedrockruntime::error::SdkError;
use aws_sdk_bedrockruntime::operation::converse_stream::ConverseStreamError;
use aws_sdk_bedrockruntime::operation::invoke_model_with_response_stream::InvokeModelWithResponseStreamError;
use aws_sdk_bedrockruntime::primitives::event_stream::EventReceiver;
use aws_sdk_bedrockruntime::types::error::{ConverseStreamOutputError, ResponseStreamError};
use aws_smithy_types::event_stream::RawMessage;
use futures::{Stream, stream};
use std::fmt::Debug;
use std::time::Duration;
use tracing::error;

use super::provider::ModelTarget;
use crate::providers::response_stream::{OpenedStream, StreamAssembler, response_stream};
use crate::{LlmError, LlmResponse, LlmResponseStream, ProviderError, Result};

pub(super) trait BedrockException {
    fn kind(&self) -> ExceptionKind;
}

pub(super) enum ExceptionKind {
    Throttled,
    Transient,
    Other,
}

pub(super) fn stream<T, U, V>(
    send: impl Future<Output = Result<EventReceiver<T, U>>> + Send + 'static,
    decode: impl FnMut(T, &mut StreamAssembler<V>) -> Result<Vec<LlmResponse>> + Send + 'static,
    idle_timeout: Duration,
) -> LlmResponseStream
where
    T: Send + 'static,
    U: BedrockException + Send + 'static,
    V: Ord + Debug + Send + 'static,
{
    response_stream(async move { Ok(OpenedStream::new(events(send.await?))) }, decode, idle_timeout)
}

pub(super) fn request_error<T: BedrockException + Debug>(target: &ModelTarget, error: &SdkError<T>) -> LlmError {
    error!(model = %target.model, error = ?error, "Bedrock API error");
    let message = format!("Bedrock API error: {error}");
    let status = error.raw_response().map(|response| response.status().as_u16());
    let request_id =
        error.raw_response().and_then(|response| response.headers().get("x-amzn-requestid")).map(str::to_string);
    let provider = match error {
        SdkError::TimeoutError(_) => ProviderError::timeout(message),
        SdkError::DispatchFailure(_) => ProviderError::network(message),
        SdkError::ResponseError(_) => ProviderError::server(message),
        SdkError::ServiceError(service) => match service.err().kind() {
            ExceptionKind::Throttled => ProviderError::rate_limit(message),
            ExceptionKind::Transient => ProviderError::server(message),
            ExceptionKind::Other => ProviderError::api(message),
        },
        _ => ProviderError::api(message),
    };
    provider.with_http_metadata(status, request_id).into()
}

impl BedrockException for ConverseStreamError {
    fn kind(&self) -> ExceptionKind {
        match self {
            Self::ThrottlingException(_) => ExceptionKind::Throttled,
            Self::ServiceUnavailableException(_)
            | Self::InternalServerException(_)
            | Self::ModelStreamErrorException(_)
            | Self::ModelTimeoutException(_)
            | Self::ModelNotReadyException(_) => ExceptionKind::Transient,
            _ => ExceptionKind::Other,
        }
    }
}

impl BedrockException for InvokeModelWithResponseStreamError {
    fn kind(&self) -> ExceptionKind {
        match self {
            Self::ThrottlingException(_) | Self::ServiceQuotaExceededException(_) => ExceptionKind::Throttled,
            Self::ServiceUnavailableException(_)
            | Self::InternalServerException(_)
            | Self::ModelStreamErrorException(_)
            | Self::ModelTimeoutException(_)
            | Self::ModelNotReadyException(_) => ExceptionKind::Transient,
            _ => ExceptionKind::Other,
        }
    }
}

impl BedrockException for ConverseStreamOutputError {
    fn kind(&self) -> ExceptionKind {
        match self {
            Self::ThrottlingException(_) => ExceptionKind::Throttled,
            Self::ServiceUnavailableException(_)
            | Self::InternalServerException(_)
            | Self::ModelStreamErrorException(_) => ExceptionKind::Transient,
            _ => ExceptionKind::Other,
        }
    }
}

impl BedrockException for ResponseStreamError {
    fn kind(&self) -> ExceptionKind {
        match self {
            Self::ThrottlingException(_) => ExceptionKind::Throttled,
            Self::ServiceUnavailableException(_)
            | Self::InternalServerException(_)
            | Self::ModelStreamErrorException(_)
            | Self::ModelTimeoutException(_) => ExceptionKind::Transient,
            _ => ExceptionKind::Other,
        }
    }
}

fn stream_error<T: BedrockException>(error: &SdkError<T, RawMessage>) -> LlmError {
    let message = format!("Bedrock stream error: {error}");
    let provider = match error {
        SdkError::ServiceError(service) => match service.err().kind() {
            ExceptionKind::Throttled => ProviderError::rate_limit(message),
            ExceptionKind::Transient => ProviderError::stream_interrupted(message),
            ExceptionKind::Other => ProviderError::api(message),
        },
        _ => ProviderError::stream_interrupted(message),
    };
    provider.into()
}

fn events<T, U>(receiver: EventReceiver<T, U>) -> impl Stream<Item = Result<T>> + Send
where
    T: Send,
    U: BedrockException + Send,
{
    stream::unfold(receiver, |mut receiver| async move {
        let event = receiver.recv().await.map_err(|e| {
            error!("Bedrock stream recv error: {e}");
            stream_error(&e)
        });
        event.transpose().map(|event| (event, receiver))
    })
}