use bytes::Bytes;
use http::HeaderMap;
use lellm_core::{ChatRequest, ChatResponse, LlmError};
use std::borrow::Cow;
use super::stream::sse_frame::SseFrame;
pub use super::stream::tool_call_accumulator::ToolCallDelta;
#[derive(Debug)]
pub struct CodecRequest {
pub path: Cow<'static, str>,
pub headers: HeaderMap,
pub body: Bytes,
}
#[derive(Debug)]
pub enum StreamChunk {
TextDelta(String),
ThinkingDelta {
thinking: String,
redacted: Option<String>,
},
ToolCallDelta(ToolCallDelta),
Usage(lellm_core::TokenUsage),
InputTokens(u32),
OutputTokens(u32),
Done,
}
#[derive(Debug)]
pub struct StreamParseResult {
pub chunks: Vec<StreamChunk>,
}
impl StreamParseResult {
pub fn empty() -> Self {
Self { chunks: Vec::new() }
}
pub fn chunk(c: StreamChunk) -> Self {
Self { chunks: vec![c] }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Capabilities {
pub supports_image_input: bool,
pub supports_reasoning: bool,
pub supports_tool_call: bool,
#[allow(dead_code)]
pub supports_prefill: bool,
pub supports_stream_thinking: bool,
}
pub fn validate_capabilities(req: &ChatRequest, caps: &Capabilities) -> Result<(), LlmError> {
if !caps.supports_image_input {
for msg in &req.messages {
for block in msg.content() {
if let lellm_core::ContentBlock::Image { .. } = block {
return Err(LlmError::UnsupportedFeature {
feature: "image input".into(),
});
}
}
}
}
if let Some(ref reasoning) = req.reasoning {
if !reasoning.is_disabled() && !caps.supports_reasoning {
return Err(LlmError::UnsupportedFeature {
feature: "reasoning".into(),
});
}
}
if req.tools.is_some() && !caps.supports_tool_call {
return Err(LlmError::UnsupportedFeature {
feature: "tool call".into(),
});
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub enum AuthStyle {
Bearer,
CustomHeader(&'static str),
None,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ProviderEnvError {
MissingEnv { name: String },
EmptyEnv { name: String },
}
impl std::fmt::Display for ProviderEnvError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingEnv { name } => write!(f, "missing environment variable: {}", name),
Self::EmptyEnv { name } => write!(f, "environment variable is empty: {}", name),
}
}
}
impl std::error::Error for ProviderEnvError {}
#[derive(Debug)]
#[non_exhaustive]
pub enum ProviderBuildError {
Env(ProviderEnvError),
Url(url::ParseError),
InvalidHeader { field: String, value: String },
Validation { message: String },
}
impl std::fmt::Display for ProviderBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Env(e) => write!(f, "{}", e),
Self::Url(e) => write!(f, "{}", e),
Self::InvalidHeader { field, value } => {
write!(f, "invalid header {}: {}", field, value)
}
Self::Validation { message } => write!(f, "validation error: {}", message),
}
}
}
impl std::error::Error for ProviderBuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Env(e) => Some(e),
Self::Url(e) => Some(e),
_ => None,
}
}
}
impl From<ProviderEnvError> for ProviderBuildError {
fn from(err: ProviderEnvError) -> Self {
Self::Env(err)
}
}
impl From<url::ParseError> for ProviderBuildError {
fn from(err: url::ParseError) -> Self {
Self::Url(err)
}
}
pub type BuildResult<T> = Result<T, ProviderBuildError>;
pub trait ChatCodec: Send + Sync {
fn encode(&self, req: &ChatRequest, stream: bool) -> Result<CodecRequest, LlmError>;
fn decode(&self, body: &[u8]) -> Result<ChatResponse, LlmError>;
fn decode_sse(&self, frame: &SseFrame) -> Result<StreamParseResult, LlmError>;
}
pub trait ModelCapabilities: Send + Sync {
fn capabilities_for(&self, _model: &str) -> Capabilities {
Capabilities::default()
}
}
pub trait ProviderMeta: Send + Sync {
fn provider_id(&self) -> &str;
fn default_base_url(&self) -> &'static str;
fn auth_style(&self) -> AuthStyle;
fn default_headers(&self) -> HeaderMap {
HeaderMap::new()
}
fn api_key_env(&self) -> Cow<'static, str> {
format!("{}_API_KEY", self.provider_id().to_ascii_uppercase()).into()
}
}
pub trait ProviderExtension: ChatCodec + ModelCapabilities + ProviderMeta {}
impl<T> ProviderExtension for T where T: ChatCodec + ModelCapabilities + ProviderMeta {}