use async_trait::async_trait;
use futures::stream::Stream;
use std::pin::Pin;
use crate::error::Result;
use crate::utils::ConversationMessage;
pub use crate::types::{CostInfo, Provider, TokenUsage};
pub mod anthropic;
pub(crate) mod anthropic_streaming;
pub mod bedrock;
pub mod google;
#[cfg(feature = "google-adc")]
pub mod google_anthropic;
pub mod openai;
pub mod openai_responses;
pub(crate) mod openai_responses_streaming;
pub(crate) mod openai_streaming;
pub mod openrouter;
pub mod openrouter_responses;
pub mod tracing_client;
pub use anthropic::AnthropicClient;
#[cfg(feature = "bedrock")]
pub use bedrock::BedrockClient;
pub use google::{FileState, GoogleGenAIClient, UploadedFile};
#[cfg(feature = "google-adc")]
pub use google_anthropic::GoogleAnthropicClient;
pub use openai::OAIClient;
pub use openai_responses::OpenAIResponsesClient;
pub use openrouter::OpenRouterClient;
pub use openrouter_responses::OpenRouterResponsesClient;
pub use tracing_client::TracingInferenceClient;
#[derive(Debug, Clone)]
pub struct GenerationConfig {
pub model: String,
pub max_tokens: Option<u32>,
pub temperature: Option<f32>,
pub top_p: Option<f32>,
pub tools: Option<Vec<serde_json::Value>>,
pub native_tools: bool,
pub reasoning_effort: Option<String>,
pub thinking_budget: Option<u32>,
pub output_schema: Option<serde_json::Value>,
pub output_type_name: Option<String>,
pub timeout: Option<std::time::Duration>,
}
impl Default for GenerationConfig {
fn default() -> Self {
Self {
model: String::new(),
max_tokens: Some(4096),
temperature: None,
top_p: None,
tools: None,
native_tools: false,
reasoning_effort: None,
thinking_budget: None,
output_schema: None,
output_type_name: None,
timeout: None,
}
}
}
impl GenerationConfig {
pub fn new(model: impl Into<String>) -> Self {
Self {
model: model.into(),
..Default::default()
}
}
pub fn with_max_tokens(mut self, max_tokens: u32) -> Self {
self.max_tokens = Some(max_tokens);
self
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
self.temperature = Some(temperature);
self
}
pub fn with_tools(mut self, tools: Vec<serde_json::Value>) -> Self {
self.tools = Some(tools);
self
}
pub fn with_native_tools(mut self, enabled: bool) -> Self {
self.native_tools = enabled;
self
}
pub fn with_reasoning_effort(mut self, effort: impl Into<String>) -> Self {
self.reasoning_effort = Some(effort.into());
self
}
pub fn with_thinking_budget(mut self, budget: u32) -> Self {
self.thinking_budget = Some(budget);
self
}
pub fn with_output_schema(
mut self,
schema: serde_json::Value,
type_name: impl Into<String>,
) -> Self {
self.output_schema = Some(schema);
self.output_type_name = Some(type_name.into());
self
}
pub fn with_timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
}
#[derive(Debug, Clone)]
pub struct GenerationResponse {
pub content: String,
pub reasoning: Option<String>,
pub tool_calls: Vec<serde_json::Value>,
pub reasoning_segments: Vec<serde_json::Value>,
pub usage: TokenUsage,
pub provider_cost_dollars: Option<f64>,
pub raw: Option<serde_json::Value>,
}
impl GenerationResponse {
pub fn text(content: impl Into<String>) -> Self {
Self {
content: content.into(),
reasoning: None,
tool_calls: Vec::new(),
reasoning_segments: Vec::new(),
usage: TokenUsage::default(),
provider_cost_dollars: None,
raw: None,
}
}
pub fn has_tool_calls(&self) -> bool {
!self.tool_calls.is_empty()
}
pub fn with_provider_cost(mut self, cost_dollars: f64) -> Self {
self.provider_cost_dollars = Some(cost_dollars);
self
}
}
#[derive(Debug, Clone)]
pub enum StreamChunk {
Reasoning(String),
Signature(String),
Content(String),
ToolCallPartial(serde_json::Value),
ToolCallComplete(serde_json::Value),
Usage {
input_tokens: u64,
output_tokens: u64,
cached_tokens: u64,
},
}
pub type TraceCallback = std::sync::Arc<
dyn Fn(
u64,
Vec<serde_json::Value>,
&GenerationResponse,
&CostInfo,
) -> Pin<Box<dyn futures::Future<Output = ()> + Send>>
+ Send
+ Sync,
>;
#[async_trait]
pub trait CostStore: Send + Sync {
async fn record_cost(&self, model: &str, cost: &CostInfo) -> Result<()>;
async fn get_credits_used(&self) -> Result<u64>;
}
pub struct MemoryCostStore {
credits_used: std::sync::atomic::AtomicU64,
}
impl MemoryCostStore {
pub fn new() -> Self {
Self {
credits_used: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn credits(&self) -> u64 {
self.credits_used.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for MemoryCostStore {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CostStore for MemoryCostStore {
async fn record_cost(&self, _model: &str, cost: &CostInfo) -> Result<()> {
self.credits_used.fetch_add(
cost.total_microdollars(),
std::sync::atomic::Ordering::Relaxed,
);
Ok(())
}
async fn get_credits_used(&self) -> Result<u64> {
Ok(self.credits())
}
}
#[async_trait]
pub trait InferenceClient: Send + Sync {
async fn get_generation(
&self,
messages: &[ConversationMessage],
config: &GenerationConfig,
) -> Result<GenerationResponse>;
async fn connect_and_listen(
&self,
messages: &[ConversationMessage],
config: &GenerationConfig,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk>> + Send>>>;
fn provider(&self) -> Provider;
fn supports_native_tools(&self) -> bool {
self.provider().supports_native_tools()
}
fn set_trace_callback(&mut self, _callback: TraceCallback) {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generation_config_builder() {
let config = GenerationConfig::new("gpt-4")
.with_max_tokens(2048)
.with_temperature(0.5)
.with_native_tools(true);
assert_eq!(config.model, "gpt-4");
assert_eq!(config.max_tokens, Some(2048));
assert_eq!(config.temperature, Some(0.5));
assert!(config.native_tools);
}
#[test]
fn test_generation_response_text() {
let response = GenerationResponse::text("Hello, world!");
assert_eq!(response.content, "Hello, world!");
assert!(!response.has_tool_calls());
}
#[test]
fn test_generation_response_with_tool_calls() {
let mut response = GenerationResponse::text("Using tools...");
response
.tool_calls
.push(serde_json::json!({"name": "get_weather"}));
assert!(response.has_tool_calls());
}
}