pub mod anthropic;
pub mod gemini;
pub mod ollama;
pub mod openai_compat;
pub(crate) mod stream_bridge;
use std::sync::Arc;
use async_trait::async_trait;
use crate::domain::{ChatRequest, TurnId};
use crate::models::{ModelError, Result, TokenUsage};
use super::capabilities::Capabilities;
use super::ctx::{FinalResponse, StreamContext, StreamEvent};
#[async_trait]
pub trait ModelProvider: Send + Sync {
fn capabilities(&self) -> &Capabilities;
async fn chat(&self, request: ChatRequest, ctx: StreamContext) -> Result<FinalResponse>;
}
pub(crate) async fn collect_text(
provider: Arc<dyn ModelProvider>,
turn: TurnId,
request: ChatRequest,
token: tokio_util::sync::CancellationToken,
) -> Result<(String, Option<TokenUsage>)> {
let (stream_tx, mut stream_rx) = tokio::sync::mpsc::channel::<StreamEvent>(128);
let ctx = StreamContext::new(token, stream_tx, turn);
let collector = tokio::task::spawn(async move {
let mut text = String::new();
let mut usage = None;
while let Some(event) = stream_rx.recv().await {
match event {
StreamEvent::Text(chunk) => text.push_str(&chunk),
StreamEvent::Done {
usage: done_usage, ..
} => usage = done_usage,
StreamEvent::Reasoning(_)
| StreamEvent::ToolCall(_)
| StreamEvent::ThinkingSignature(_) => {},
}
}
(text, usage)
});
let response = provider.chat(request, ctx).await;
let (text, stream_usage) = collector.await.map_err(|err| {
ModelError::StreamError(format!("collect_text collector failed: {}", err))
})?;
match response {
Ok(final_response) => Ok((text, final_response.usage.or(stream_usage))),
Err(err) => Err(err),
}
}
pub use anthropic::AnthropicProvider;
pub use gemini::GeminiProvider;
pub use ollama::OllamaProvider;
pub use openai_compat::OpenAICompatProvider;