use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::message::Message;
use crate::tool::ToolDefinition;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct DriverId(String);
impl DriverId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn openai() -> Self {
Self::new("openai")
}
pub fn anthropic() -> Self {
Self::new("anthropic")
}
pub fn llmsim() -> Self {
Self::new("llmsim")
}
}
impl fmt::Display for DriverId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<&str> for DriverId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct ReasoningConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub budget_tokens: Option<u32>,
}
#[derive(Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ModelSpec {
pub driver: DriverId,
pub model: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningConfig>,
#[serde(default, skip_serializing_if = "serde_json::Value::is_null")]
pub metadata: serde_json::Value,
}
impl std::fmt::Debug for ModelSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModelSpec")
.field("driver", &self.driver)
.field("model", &self.model)
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
.field("base_url", &self.base_url)
.field("reasoning", &self.reasoning)
.field(
"metadata",
&(!self.metadata.is_null()).then_some("<redacted>"),
)
.finish()
}
}
impl ModelSpec {
pub fn new(driver: impl Into<DriverId>, model: impl Into<String>) -> Self {
Self {
driver: driver.into(),
model: model.into(),
api_key: None,
base_url: None,
reasoning: None,
metadata: serde_json::Value::Null,
}
}
pub fn openai(model: impl Into<String>) -> Self {
Self::new(DriverId::openai(), model)
}
pub fn anthropic(model: impl Into<String>) -> Self {
Self::new(DriverId::anthropic(), model)
}
pub fn llmsim() -> Self {
Self::new(DriverId::llmsim(), "llmsim")
}
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn reasoning_effort(mut self, effort: impl Into<String>) -> Self {
let mut reasoning = self.reasoning.unwrap_or_default();
reasoning.effort = Some(effort.into());
self.reasoning = Some(reasoning);
self
}
pub fn thinking_budget(mut self, budget_tokens: u32) -> Self {
let mut reasoning = self.reasoning.unwrap_or_default();
reasoning.budget_tokens = Some(budget_tokens);
self.reasoning = Some(reasoning);
self
}
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ChatRequest {
pub model: ModelSpec,
pub system_prompt: Option<String>,
pub messages: Vec<Message>,
pub tools: Vec<ToolDefinition>,
}
impl ChatRequest {
pub fn new(model: ModelSpec, messages: Vec<Message>) -> Self {
Self {
model,
system_prompt: None,
messages,
tools: Vec::new(),
}
}
pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
self.system_prompt = Some(prompt.into());
self
}
pub fn maybe_system_prompt(mut self, prompt: Option<String>) -> Self {
self.system_prompt = prompt;
self
}
pub fn tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.tools = tools;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Usage {
pub input_tokens: u64,
pub output_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
#[serde(default)]
pub reasoning_tokens: u64,
}
impl Usage {
pub fn new(input_tokens: u64, output_tokens: u64) -> Self {
Self {
input_tokens,
output_tokens,
..Self::default()
}
}
pub fn with_cache(mut self, read: u64, creation: u64) -> Self {
self.cache_read_input_tokens = read;
self.cache_creation_input_tokens = creation;
self
}
pub fn with_reasoning(mut self, reasoning_tokens: u64) -> Self {
self.reasoning_tokens = reasoning_tokens;
self
}
pub fn add(&mut self, other: Usage) {
self.input_tokens += other.input_tokens;
self.output_tokens += other.output_tokens;
self.cache_read_input_tokens += other.cache_read_input_tokens;
self.cache_creation_input_tokens += other.cache_creation_input_tokens;
self.reasoning_tokens += other.reasoning_tokens;
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ChatResponse {
pub message: Message,
pub usage: Usage,
}
impl ChatResponse {
pub fn new(message: Message, usage: Usage) -> Self {
Self { message, usage }
}
}
#[async_trait]
pub trait DeltaSink: Send {
async fn delta(&mut self, delta: &str, accumulated: &str) -> Result<()>;
}
#[async_trait]
pub trait ChatDriver: Send + Sync {
fn id(&self) -> DriverId;
async fn complete(&self, request: ChatRequest) -> Result<ChatResponse>;
async fn complete_streaming(
&self,
request: ChatRequest,
sink: &mut dyn DeltaSink,
) -> Result<ChatResponse> {
let response = self.complete(request).await?;
let text = response.message.text();
if !text.is_empty() {
sink.delta(&text, &text).await?;
}
Ok(response)
}
}
#[derive(Default, Clone)]
pub struct DriverRegistry {
drivers: HashMap<DriverId, Arc<dyn ChatDriver>>,
}
impl DriverRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, driver: impl ChatDriver + 'static) {
self.register_arc(Arc::new(driver));
}
pub fn register_arc(&mut self, driver: Arc<dyn ChatDriver>) {
self.drivers.insert(driver.id(), driver);
}
pub fn get(&self, id: &DriverId) -> Option<Arc<dyn ChatDriver>> {
self.drivers.get(id).cloned()
}
pub fn contains(&self, id: &DriverId) -> bool {
self.drivers.contains_key(id)
}
}
#[cfg(test)]
mod tests {
use super::ModelSpec;
#[test]
fn model_debug_redacts_credentials() {
let model = ModelSpec::openai("test-model").api_key("super-secret");
let debug = format!("{model:?}");
assert!(debug.contains("<redacted>"));
assert!(!debug.contains("super-secret"));
}
}