mod request;
use crate::types::{
AgentOptions, AnthropicRequest, ApiProtocol, ContentBlock, FinishReason, Message, MessageRole,
OpenAIContent, OpenAIMessage, OpenAIRequest, StreamEvent,
};
use crate::utils::{
AnthropicAccumulator, StreamAccumulator, coalesce_text_blocks, drive,
parse_anthropic_sse_stream, parse_sse_stream,
};
use crate::{Error, Result};
use futures::stream::{Stream, StreamExt};
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
fn serialize_history_message(message: &Message) -> Result<serde_json::Value> {
serde_json::to_value(message).map_err(|error| {
Error::other(format!(
"Failed to serialize conversation history for hook: {error}"
))
})
}
fn serialize_history_snapshot(history: &[Message]) -> Result<Vec<serde_json::Value>> {
history.iter().map(serialize_history_message).collect()
}
const ANTHROPIC_VERSION: &str = "2023-06-01";
fn model_http_client_builder(options: &AgentOptions) -> reqwest::ClientBuilder {
reqwest::Client::builder()
.timeout(Duration::from_secs(options.timeout()))
.redirect(reqwest::redirect::Policy::none())
}
async fn stream_request(
http_client: &reqwest::Client,
options: &AgentOptions,
request: &OpenAIRequest,
) -> Result<EventStream> {
let protocol = options.protocol();
let url = format!("{}{}", options.base_url(), protocol.path());
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if !options.api_key().is_empty() {
let (name, value) = match protocol {
ApiProtocol::OpenAiChat => (
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", options.api_key())).map_err(|_| {
Error::config("api_key cannot be encoded as an HTTP header value")
})?,
),
ApiProtocol::Anthropic => (
HeaderName::from_static("x-api-key"),
HeaderValue::from_str(options.api_key()).map_err(|_| {
Error::config("api_key cannot be encoded as an HTTP header value")
})?,
),
};
headers.insert(name, value);
}
if protocol == ApiProtocol::Anthropic {
headers.insert(
HeaderName::from_static("anthropic-version"),
HeaderValue::from_static(ANTHROPIC_VERSION),
);
}
crate::types::http_headers::insert_all(&mut headers, options.headers())?;
let pending = http_client.post(&url).headers(headers);
let pending = match protocol {
ApiProtocol::OpenAiChat => pending.json(request),
ApiProtocol::Anthropic => pending.json(&AnthropicRequest::from_openai(request)),
};
let response = pending.send().await.map_err(Error::Http)?;
if !response.status().is_success() {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_else(|error| {
eprintln!("WARNING: Failed to read error response body: {error}");
"Unknown error (failed to read response body)".to_string()
});
return Err(Error::api_status(status, body));
}
let capture = options.include_reasoning();
Ok(match protocol {
ApiProtocol::OpenAiChat => drive(
parse_sse_stream(response),
StreamAccumulator::new().capture_reasoning(capture),
),
ApiProtocol::Anthropic => drive(
parse_anthropic_sse_stream(response),
AnthropicAccumulator::new().capture_reasoning(capture),
),
})
}
include!("client/query.rs");
include!("client/state.rs");
include!("client/setup.rs");
include!("client/send.rs");
include!("client/streaming.rs");
include!("client/send_message.rs");
include!("client/receive.rs");
include!("client/history.rs");
#[cfg(test)]
mod tests {
use super::*;
use crate::TextBlock;
include!("client/tests.rs");
}