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)]
pub enum ProviderEnvError {
MissingApiKey { provider: String, env_var: String },
InvalidUrl { url: String, reason: String },
}
impl std::fmt::Display for ProviderEnvError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProviderEnvError::MissingApiKey { provider, env_var } => {
write!(
f,
"Missing API key for provider '{}' (env: {})",
provider, env_var
)
}
ProviderEnvError::InvalidUrl { url, reason } => {
write!(f, "Invalid URL '{}': {}", url, reason)
}
}
}
}
impl std::error::Error for ProviderEnvError {}
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 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 {}