use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::Stream;
use futures_util::StreamExt;
use stream_rs::accumulators::openai::OpenAiAccumulator;
use stream_rs::sse::SseEvent;
use stream_rs::stream::SseStream;
use crate::error::{Error, Result};
use crate::types::{ChatChunk, ChatRequest, ChatResponse};
pub const DEFAULT_BASE_URL: &str = "https://api.x.ai/v1";
#[derive(Debug, Clone)]
pub struct GrokClient {
http: reqwest::Client,
base_url: String,
api_key: String,
}
impl GrokClient {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
Self::builder().api_key(api_key).build()
}
pub fn from_env() -> Result<Self> {
let key = std::env::var("XAI_API_KEY").map_err(|_| Error::MissingApiKey)?;
Self::new(key)
}
#[must_use]
pub fn builder() -> GrokClientBuilder {
GrokClientBuilder::default()
}
pub async fn chat(&self, mut request: ChatRequest) -> Result<ChatResponse> {
request.stream = Some(false);
let resp = self.post("/chat/completions", &request).await?;
let resp = error_for_status(resp).await?;
Ok(resp.json::<ChatResponse>().await?)
}
pub async fn chat_stream(&self, mut request: ChatRequest) -> Result<ChatStream> {
request.stream = Some(true);
let resp = self.post("/chat/completions", &request).await?;
let resp = error_for_status(resp).await?;
let byte_stream: ByteStream = resp.bytes_stream().boxed();
let sse = SseStream::new(byte_stream);
Ok(ChatStream {
inner: Box::pin(sse),
done: false,
})
}
pub async fn chat_stream_collect<F>(
&self,
request: ChatRequest,
mut on_token: F,
) -> Result<OpenAiAccumulator>
where
F: FnMut(&str),
{
let mut stream = self.chat_stream(request).await?;
let mut acc = OpenAiAccumulator::new();
while let Some(event) = stream.next().await {
let chunk = event?;
for choice in &chunk.choices {
let i = choice.index as usize;
if let Some(role) = &choice.delta.role {
acc.push_role(i, role);
}
if let Some(content) = &choice.delta.content {
on_token(content);
acc.push_content(i, content);
}
if let Some(tool_calls) = &choice.delta.tool_calls {
for tc in tool_calls {
let (name, args) = tc.function.as_ref().map_or((None, None), |f| {
(f.name.as_deref(), f.arguments.as_deref())
});
acc.push_tool_call(i, tc.index as usize, tc.id.as_deref(), name, args);
}
}
if let Some(reason) = &choice.finish_reason {
acc.set_finish_reason(i, reason);
}
}
}
Ok(acc)
}
async fn post<T: serde::Serialize>(&self, path: &str, body: &T) -> Result<reqwest::Response> {
let url = format!("{}{}", self.base_url, path);
Ok(self
.http
.post(url)
.bearer_auth(&self.api_key)
.json(body)
.send()
.await?)
}
}
async fn error_for_status(resp: reqwest::Response) -> Result<reqwest::Response> {
let status = resp.status();
if status.is_success() {
return Ok(resp);
}
let body = resp.text().await.unwrap_or_default();
Err(Error::Api {
status: status.as_u16(),
body,
})
}
#[derive(Debug, Default)]
pub struct GrokClientBuilder {
api_key: Option<String>,
base_url: Option<String>,
http: Option<reqwest::Client>,
}
impl GrokClientBuilder {
#[must_use]
pub fn api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
#[must_use]
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = Some(url.into());
self
}
#[must_use]
pub fn http_client(mut self, client: reqwest::Client) -> Self {
self.http = Some(client);
self
}
pub fn build(self) -> Result<GrokClient> {
let api_key = self
.api_key
.filter(|k| !k.is_empty())
.ok_or(Error::MissingApiKey)?;
Ok(GrokClient {
http: self.http.unwrap_or_default(),
base_url: self
.base_url
.unwrap_or_else(|| DEFAULT_BASE_URL.to_string()),
api_key,
})
}
}
type ByteStream =
futures_util::stream::BoxStream<'static, std::result::Result<bytes::Bytes, reqwest::Error>>;
pub struct ChatStream {
inner: Pin<Box<SseStream<ByteStream, bytes::Bytes, reqwest::Error>>>,
done: bool,
}
impl Stream for ChatStream {
type Item = Result<ChatChunk>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
if self.done {
return Poll::Ready(None);
}
match self.inner.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(event))) => {
if let Some(item) = decode_event(&event) {
match item {
DecodedEvent::Done => {
self.done = true;
return Poll::Ready(None);
}
DecodedEvent::Chunk(result) => return Poll::Ready(Some(result)),
}
}
}
Poll::Ready(Some(Err(e))) => {
self.done = true;
return Poll::Ready(Some(Err(Error::Transport(e))));
}
Poll::Ready(None) => {
self.done = true;
return Poll::Ready(None);
}
Poll::Pending => return Poll::Pending,
}
}
}
}
enum DecodedEvent {
Chunk(Result<ChatChunk>),
Done,
}
fn decode_event(event: &SseEvent) -> Option<DecodedEvent> {
let data = event.data.trim();
if data.is_empty() {
return None;
}
if data == "[DONE]" {
return Some(DecodedEvent::Done);
}
Some(DecodedEvent::Chunk(
serde_json::from_str::<ChatChunk>(data).map_err(Error::Decode),
))
}