use std::time::Duration;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::event_stream::AssistantMessageEventStream;
use crate::model::Model;
use crate::provider::{Provider, SimpleStreamOptions};
use crate::providers::anthropic::sse::SseEventStream;
use crate::types::{
AssistantRole, Content, Context, DoneReason, TextContent, TextContentType, Usage,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
pub endpoint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<std::collections::BTreeMap<String, String>>,
}
impl ProxyConfig {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
api_key: None,
timeout: None,
headers: None,
}
}
pub fn with_api_key(mut self, key: impl Into<String>) -> Self {
self.api_key = Some(key.into());
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let headers = self.headers.get_or_insert_with(Default::default);
headers.insert(key.into(), value.into());
self
}
}
pub struct ProxyProvider {
config: ProxyConfig,
client: reqwest::Client,
}
impl ProxyProvider {
pub fn new(config: ProxyConfig) -> Self {
let timeout = config.timeout.unwrap_or(Duration::from_secs(600));
let client = reqwest::Client::builder()
.timeout(timeout)
.build()
.expect("Failed to build HTTP client");
Self { config, client }
}
pub fn endpoint(&self) -> &str {
&self.config.endpoint
}
fn build_headers(&self, opts: &SimpleStreamOptions) -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
if let Some(api_key) = &self.config.api_key {
headers.insert(
reqwest::header::AUTHORIZATION,
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", api_key))
.expect("Invalid API key"),
);
}
if let Some(config_headers) = &self.config.headers {
for (key, value) in config_headers {
if let (Ok(name), Ok(val)) = (
reqwest::header::HeaderName::from_bytes(key.as_bytes()),
reqwest::header::HeaderValue::from_str(value),
) {
headers.insert(name, val);
}
}
}
if let Some(opts_headers) = &opts.headers {
for (key, value) in opts_headers {
if let (Ok(name), Ok(val)) = (
reqwest::header::HeaderName::from_bytes(key.as_bytes()),
reqwest::header::HeaderValue::from_str(value),
) {
headers.insert(name, val);
}
}
}
headers
}
}
#[async_trait]
impl Provider for ProxyProvider {
fn id(&self) -> &str {
"proxy"
}
fn models(&self) -> &[Model] {
&[]
}
async fn stream_simple(
&self,
model: &Model,
ctx: &Context,
opts: &SimpleStreamOptions,
) -> AssistantMessageEventStream {
use crate::event_stream::create_assistant_message_event_stream;
use crate::types::{AssistantMessage, AssistantMessageEvent, ErrorReason, StopReason};
let (mut prod, stream) = create_assistant_message_event_stream();
let url = format!(
"{}/chat/completions",
self.config.endpoint.trim_end_matches('/')
);
let headers = self.build_headers(opts);
let body = serde_json::json!({
"model": model.id,
"messages": ctx.messages,
"stream": true,
});
let client = self.client.clone();
let model = model.clone();
let opts = opts.clone();
tokio::spawn(async move {
let response = client.post(&url).headers(headers).json(&body).send().await;
match response {
Ok(resp) => {
if !resp.status().is_success() {
let status = resp.status();
let error_text = resp
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
let error_msg = AssistantMessage::terminal(
model.api.clone(),
"proxy".to_string(),
model.id.clone(),
StopReason::Error,
format!("Proxy returned {}: {}", status, error_text),
0,
);
prod.push(AssistantMessageEvent::Error {
reason: ErrorReason::Error,
error: error_msg,
});
return;
}
let mut events = SseEventStream::new(resp, opts.signal.clone());
let mut content = String::new();
let mut finish_reason = None;
loop {
match events.next_event().await {
Ok(Some(event)) if event.data.trim() == "[DONE]" => {
break;
}
Ok(Some(event)) => {
if let Ok(chunk) =
serde_json::from_str::<serde_json::Value>(&event.data)
{
if let Some(choices) =
chunk.get("choices").and_then(|c| c.as_array())
{
if let Some(choice) = choices.first() {
if let Some(delta) = choice.get("delta") {
if let Some(text) =
delta.get("content").and_then(|c| c.as_str())
{
content.push_str(text);
}
}
if let Some(reason) =
choice.get("finish_reason").and_then(|r| r.as_str())
{
finish_reason = Some(reason.to_string());
}
}
}
}
}
Ok(None) => break,
Err(e) => {
let error_msg = AssistantMessage::terminal(
model.api.clone(),
"proxy".to_string(),
model.id.clone(),
StopReason::Error,
format!("SSE stream error: {}", e),
0,
);
prod.push(AssistantMessageEvent::Error {
reason: ErrorReason::Error,
error: error_msg,
});
return;
}
}
}
let stop_reason = match finish_reason.as_deref() {
Some("stop") => StopReason::Stop,
Some("length") => StopReason::Length,
Some("tool_calls") => StopReason::ToolUse,
_ => StopReason::Stop,
};
let message = AssistantMessage {
role: AssistantRole,
content: vec![Content::Text(TextContent {
kind: TextContentType,
text: content,
text_signature: None,
})],
api: model.api.clone(),
provider: "proxy".to_string(),
model: model.id.clone(),
response_model: None,
response_id: None,
usage: Usage::zero(),
stop_reason,
deferred: None,
error_message: None,
raw_stop_reason: finish_reason,
end_turn: None,
timestamp: 0,
};
let done_reason = match stop_reason {
StopReason::Stop => DoneReason::Stop,
StopReason::Length => DoneReason::Length,
StopReason::ToolUse => DoneReason::ToolUse,
_ => DoneReason::Stop,
};
prod.push(AssistantMessageEvent::Done {
reason: done_reason,
message,
});
}
Err(e) => {
let error_msg = AssistantMessage::terminal(
model.api.clone(),
"proxy".to_string(),
model.id.clone(),
StopReason::Error,
format!("Proxy request failed: {}", e),
0,
);
prod.push(AssistantMessageEvent::Error {
reason: ErrorReason::Error,
error: error_msg,
});
}
}
});
stream
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_proxy_config_builder() {
let config = ProxyConfig::new("https://proxy.example.com")
.with_api_key("test-key")
.with_timeout(Duration::from_secs(30))
.with_header("X-Custom", "value");
assert_eq!(config.endpoint, "https://proxy.example.com");
assert_eq!(config.api_key, Some("test-key".to_string()));
assert_eq!(config.timeout, Some(Duration::from_secs(30)));
assert_eq!(
config.headers.as_ref().unwrap().get("X-Custom"),
Some(&"value".to_string())
);
}
#[test]
fn test_proxy_provider_creation() {
let config = ProxyConfig::new("https://proxy.example.com");
let provider = ProxyProvider::new(config);
assert_eq!(provider.endpoint(), "https://proxy.example.com");
assert_eq!(provider.id(), "proxy");
}
}