use std::env;
use std::sync::Arc;
use reqwest::Client;
use tracing::{info_span, instrument, Instrument};
use super::types::*;
use crate::providers::error::{CompletionError, ProviderError};
use crate::providers::http::build_http_client;
use crate::providers::zai::client::{
CompletionRequest, CompletionResponse, Message, ToolCall, Usage,
};
const DEFAULT_BASE_URL: &str = "https://api.cerebras.ai/v1";
const DEFAULT_MAX_TOKENS: u64 = 8192;
#[derive(Clone)]
pub struct CerebrasClient {
inner: Arc<CerebrasClientInner>,
}
struct CerebrasClientInner {
http_client: Client,
api_key: String,
base_url: String,
temperature: Option<f64>,
top_p: Option<f64>,
top_k: Option<u64>,
max_tokens: Option<u64>,
}
impl CerebrasClient {
pub fn from_env() -> Result<Self, ProviderError> {
let api_key = env::var("CEREBRAS_API_KEY")
.map_err(|_| ProviderError::EnvVarNotSet("CEREBRAS_API_KEY".to_string()))?;
Ok(CerebrasClientBuilder::new(&api_key).build())
}
pub fn completion_model(&self, model_id: &str) -> CerebrasCompletionModel {
CerebrasCompletionModel {
client: self.clone(),
model_id: model_id.to_string(),
}
}
}
pub struct CerebrasClientBuilder {
api_key: String,
base_url: String,
temperature: Option<f64>,
top_p: Option<f64>,
top_k: Option<u64>,
max_tokens: Option<u64>,
}
impl CerebrasClientBuilder {
pub fn new(api_key: &str) -> Self {
Self {
api_key: api_key.to_string(),
base_url: DEFAULT_BASE_URL.to_string(),
temperature: None,
top_p: None,
top_k: None,
max_tokens: None,
}
}
pub fn base_url(mut self, url: &str) -> Self {
self.base_url = url.to_string();
self
}
pub fn temperature(mut self, temp: f64) -> Self {
self.temperature = Some(temp.clamp(0.0, 1.0));
self
}
pub fn top_p(mut self, p: f64) -> Self {
self.top_p = Some(p.clamp(0.0, 1.0));
self
}
pub fn top_k(mut self, k: u64) -> Self {
self.top_k = Some(k);
self
}
pub fn max_tokens(mut self, tokens: u64) -> Self {
self.max_tokens = Some(tokens);
self
}
pub fn build(self) -> CerebrasClient {
CerebrasClient {
inner: Arc::new(CerebrasClientInner {
http_client: build_http_client(),
api_key: self.api_key,
base_url: self.base_url,
temperature: self.temperature,
top_p: self.top_p,
top_k: self.top_k,
max_tokens: self.max_tokens,
}),
}
}
}
#[derive(Clone)]
pub struct CerebrasCompletionModel {
client: CerebrasClient,
model_id: String,
}
pub fn extract_tool_result_content(content: &str) -> String {
content.to_string()
}
impl CerebrasCompletionModel {
pub fn model_id(&self) -> &str {
&self.model_id
}
pub fn provider(&self) -> &str {
"cerebras"
}
#[instrument(skip(self, request), fields(model = %self.model_id, provider = "cerebras"))]
pub async fn completion(
&self,
request: CompletionRequest,
) -> Result<CompletionResponse<CerebrasResponse>, CompletionError> {
let inner = &self.client.inner;
let mut messages = Vec::new();
if let Some(preamble) = &request.preamble {
messages.push(CerebrasMessage {
role: "system".to_string(),
content: preamble.clone(),
tool_calls: None,
tool_call_id: None,
reasoning: None,
});
}
for msg in &request.messages {
messages.push(CerebrasMessage {
role: msg.role.clone(),
content: extract_tool_result_content(&msg.content),
tool_calls: msg.tool_calls.as_ref().map(|calls| {
calls
.iter()
.map(|tc| CerebrasToolCall {
id: tc.id.clone(),
call_type: "function".to_string(),
function: CerebrasFunctionCall {
name: tc.name.clone(),
arguments: tc.arguments.clone(),
},
})
.collect()
}),
tool_call_id: msg.tool_call_id.clone(),
reasoning: msg.reasoning.clone(),
});
}
let tools = if request.tools.is_empty() {
None
} else {
Some(
request
.tools
.iter()
.map(|t| CerebrasTool {
tool_type: "function".to_string(),
function: CerebrasFunction {
name: t.name.clone(),
description: Some(t.description.clone()),
parameters: Some(t.parameters.clone()),
},
})
.collect(),
)
};
let cerebras_request = CerebrasRequest {
model: self.model_id.clone(),
messages,
temperature: request.temperature.or(inner.temperature),
max_tokens: request
.max_tokens
.or(inner.max_tokens)
.or(Some(DEFAULT_MAX_TOKENS)),
top_p: inner.top_p,
top_k: inner.top_k,
tools,
tool_choice: None,
};
let url = format!("{}/chat/completions", inner.base_url);
let response = inner
.http_client
.post(&url)
.header("Authorization", format!("Bearer {}", inner.api_key))
.header("Content-Type", "application/json")
.json(&cerebras_request)
.send()
.instrument(info_span!("cerebras_http_request"))
.await
.map_err(ProviderError::Request)?;
let status = response.status();
let response_text = response.text().await.map_err(ProviderError::Request)?;
if !status.is_success() {
return Err(CompletionError::Provider(ProviderError::Http {
status: status.as_u16(),
message: response_text,
}));
}
let cerebras_response: CerebrasResponse =
serde_json::from_str(&response_text).map_err(|e| {
tracing::error!(
"Failed to deserialize Cerebras response: {}\nRaw response: {}",
e,
&response_text[..response_text.len().min(2000)]
);
ProviderError::InvalidResponse(format!(
"JSON deserialization failed: {}. Response preview: {}",
e,
&response_text[..response_text.len().min(500)]
))
})?;
let choice = cerebras_response.choices.first().ok_or_else(|| {
CompletionError::Provider(ProviderError::InvalidResponse(
"No choices in response".to_string(),
))
})?;
let tool_calls = choice.message.tool_calls.as_ref().map(|calls| {
calls
.iter()
.map(|tc| ToolCall {
id: tc.id.clone(),
name: tc.function.name.clone(),
arguments: tc.function.arguments.clone(),
})
.collect()
});
let message = Message {
role: choice.message.role.clone(),
content: choice.message.content.clone(),
tool_calls,
tool_call_id: None,
reasoning: choice.message.reasoning.clone(),
};
let cache_read_tokens = cerebras_response
.usage
.prompt_tokens_details
.as_ref()
.and_then(|d| d.cached_tokens)
.unwrap_or(0);
Ok(CompletionResponse {
message,
usage: Usage {
prompt_tokens: cerebras_response.usage.prompt_tokens,
completion_tokens: cerebras_response.usage.completion_tokens,
total_tokens: cerebras_response.usage.total_tokens,
cache_read_tokens,
cache_creation_tokens: 0,
},
raw: cerebras_response.clone(),
reasoning_content: None,
finish_reason: choice.finish_reason.clone(),
})
}
}