Skip to content

Commit 9869aad

Browse files
committed
feat(provider): provider support client
1 parent 5bb3a44 commit 9869aad

9 files changed

Lines changed: 497 additions & 0 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ futures = "0.3.32"
2222
futures-util = "0.3.32"
2323
serde = { version = "1.0.228", features = ["derive"] }
2424
serde_json = "1.0.150"
25+
serde_urlencoded = "0.7.1"
2526
rmcp = "1.7.0"
2627
rusqlite = { version = "0.40.0", features = ["bundled"] }
2728
ratatui = "0.30.0"

‎src/core/client/ai_client.rs‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use anyhow::{anyhow, Result};
2+
use bytes::Bytes;
3+
use futures_util::Stream;
4+
use reqwest::Method;
5+
use serde::{de::DeserializeOwned, Serialize};
6+
use std::collections::HashMap;
7+
use std::pin::Pin;
8+
9+
use crate::core::client::base_client::BaseClient;
10+
use crate::core::client::config::ClientConfig;
11+
use crate::core::transport::http_transoprt::HttpTransport;
12+
use crate::core::transport::retry::RetryPolicy;
13+
use crate::core::transport::TransportRequest;
14+
15+
#[derive(Clone)]
16+
pub struct AiClient {
17+
transport: HttpTransport,
18+
}
19+
20+
impl AiClient {
21+
pub fn new(config: ClientConfig, retry: RetryPolicy) -> Result<Self> {
22+
let base_client = BaseClient::new(config)?;
23+
let transport = HttpTransport::new(base_client, retry);
24+
25+
Ok(Self { transport })
26+
}
27+
28+
pub async fn post_json<T>(
29+
&self,
30+
url: &str,
31+
headers: HashMap<String, String>,
32+
body: &impl Serialize,
33+
) -> Result<T>
34+
where
35+
T: DeserializeOwned,
36+
{
37+
let body_bytes = serde_json::to_vec(body)
38+
.map_err(|e| anyhow!("Failed to serialize request body: {}", e))?;
39+
40+
let request = TransportRequest {
41+
method: Method::POST,
42+
url: url.to_string(),
43+
headers,
44+
body: Some(Bytes::from(body_bytes)),
45+
};
46+
47+
let response = self.transport.execute(request).await?;
48+
49+
serde_json::from_slice(&response.body)
50+
.map_err(|e| anyhow!("Failed to deserialize response: {}", e))
51+
}
52+
53+
pub async fn get_json<T>(
54+
&self,
55+
url: &str,
56+
headers: HashMap<String, String>,
57+
query: &impl Serialize,
58+
) -> Result<T>
59+
where
60+
T: DeserializeOwned,
61+
{
62+
let query_string = serde_urlencoded::to_string(query)
63+
.map_err(|e| anyhow!("Failed to serialize query: {}", e))?;
64+
65+
let url_with_query = if query_string.is_empty() {
66+
url.to_string()
67+
} else {
68+
format!("{}?{}", url, query_string)
69+
};
70+
71+
let request = TransportRequest {
72+
method: Method::GET,
73+
url: url_with_query,
74+
headers,
75+
body: None,
76+
};
77+
78+
let response = self.transport.execute(request).await?;
79+
80+
serde_json::from_slice(&response.body)
81+
.map_err(|e| anyhow!("Failed to deserialize response: {}", e))
82+
}
83+
84+
pub async fn post_stream<T>(
85+
&self,
86+
url: &str,
87+
headers: HashMap<String, String>,
88+
body: &impl Serialize,
89+
) -> Result<Pin<Box<dyn Stream<Item = Result<T>> + Send + 'static>>>
90+
where
91+
T: DeserializeOwned + Send + 'static,
92+
{
93+
let body_bytes = serde_json::to_vec(body)
94+
.map_err(|e| anyhow!("Failed to serialize request body: {}", e))?;
95+
96+
let request = TransportRequest {
97+
method: Method::POST,
98+
url: url.to_string(),
99+
headers,
100+
body: Some(Bytes::from(body_bytes)),
101+
};
102+
103+
self.transport.execute_stream(request).await.map_err(|e| anyhow!(e))
104+
}
105+
106+
pub fn transport(&self) -> &HttpTransport {
107+
&self.transport
108+
}
109+
}

‎src/core/providers/gemini/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod provider;
2+
pub use provider::GeminiProvider;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use anyhow::Result;
2+
use async_trait::async_trait;
3+
use std::collections::HashMap;
4+
use crate::core::client::{AiClient, ClientConfig};
5+
use crate::core::transport::retry::RetryPolicy;
6+
use crate::core::providers::provider::Provider;
7+
use crate::core::providers::types::{CompletionRequest, CompletionResponse};
8+
9+
pub struct GeminiProvider {
10+
client: AiClient,
11+
api_key: String,
12+
model: String,
13+
}
14+
15+
impl GeminiProvider {
16+
pub fn new(api_key: String, model: String) -> Result<Self> {
17+
let config = ClientConfig::default();
18+
let retry = RetryPolicy::default();
19+
let client = AiClient::new(config, retry)?;
20+
21+
Ok(Self {
22+
client,
23+
api_key,
24+
model,
25+
})
26+
}
27+
}
28+
29+
#[async_trait]
30+
impl Provider for GeminiProvider {
31+
fn client(&self) -> &AiClient {
32+
&self.client
33+
}
34+
35+
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
36+
let mut headers = HashMap::new();
37+
headers.insert("Content-Type".to_string(), "application/json".to_string());
38+
39+
let url = format!("https://generativelanguage.googleapis.com/v1/models/{model}:generateContent?key={api_key}",
40+
model = self.model,
41+
api_key = self.api_key
42+
);
43+
44+
let response: serde_json::Value = self.client
45+
.post_json(&url, headers, &serde_json::json!({
46+
"contents": request.messages.iter().map(|m| {
47+
serde_json::json!({
48+
"role": match m.role {
49+
crate::core::providers::types::Role::System => "system",
50+
crate::core::providers::types::Role::User => "user",
51+
crate::core::providers::types::Role::Assistant => "model",
52+
},
53+
"parts": [serde_json::json!({"text": m.content})]
54+
})
55+
}).collect::<Vec<_>>(),
56+
"generationConfig": {
57+
"temperature": request.temperature.unwrap_or(0.7),
58+
"maxOutputTokens": request.max_tokens,
59+
}
60+
}))
61+
.await
62+
.map_err(|e| anyhow::anyhow!("Gemini API error: {}", e))?;
63+
64+
let content = response["candidates"][0]["content"]["parts"][0]["text"].as_str().unwrap_or("").to_string();
65+
66+
Ok(CompletionResponse {
67+
content,
68+
finish_reason: None,
69+
usage: None,
70+
})
71+
}
72+
}

‎src/core/providers/ollama/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod provider;
2+
pub use provider::OllamaProvider;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use anyhow::Result;
2+
use async_trait::async_trait;
3+
use std::collections::HashMap;
4+
use crate::core::client::{AiClient, ClientConfig};
5+
use crate::core::transport::retry::RetryPolicy;
6+
use crate::core::providers::provider::Provider;
7+
use crate::core::providers::types::{CompletionRequest, CompletionResponse};
8+
9+
pub struct OllamaProvider {
10+
client: AiClient,
11+
base_url: String,
12+
}
13+
14+
impl OllamaProvider {
15+
pub fn new(base_url: String) -> Result<Self> {
16+
let config = ClientConfig::default();
17+
let retry = RetryPolicy::default();
18+
let client = AiClient::new(config, retry)?;
19+
20+
Ok(Self {
21+
client,
22+
base_url,
23+
})
24+
}
25+
}
26+
27+
#[async_trait]
28+
impl Provider for OllamaProvider {
29+
fn client(&self) -> &AiClient {
30+
&self.client
31+
}
32+
33+
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
34+
let mut headers = HashMap::new();
35+
headers.insert("Content-Type".to_string(), "application/json".to_string());
36+
37+
let url = format!("{}/api/chat", self.base_url);
38+
39+
let response: serde_json::Value = self.client
40+
.post_json(&url, headers, &serde_json::json!({
41+
"model": request.model,
42+
"messages": request.messages,
43+
"stream": request.stream,
44+
"temperature": request.temperature,
45+
"max_tokens": request.max_tokens,
46+
}))
47+
.await
48+
.map_err(|e| anyhow::anyhow!("Ollama API error: {}", e))?;
49+
50+
let content = response["message"]["content"].as_str().unwrap_or("").to_string();
51+
let finish_reason = match response["done"].as_bool() {
52+
Some(true) => Some(crate::core::providers::types::FinishReason::Stop),
53+
_ => None,
54+
};
55+
56+
Ok(CompletionResponse {
57+
content,
58+
finish_reason,
59+
usage: None,
60+
})
61+
}
62+
}

‎src/core/transport/http_transoprt.rs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
use std::time::Duration;
2+
use std::pin::Pin;
23

4+
use futures_util::Stream;
35
use tokio::time::sleep;
46
use crate::core::transport::errors::TransportError;
57
use crate::core::transport::request::TransportRequest;
68
use crate::core::transport::response::TransportResponse;
79
use crate::core::transport::retry::RetryPolicy;
10+
use crate::core::transport::sse::{SseStream, sse_stream_to_json};
811
use crate::core::client::base_client::BaseClient;
12+
use serde::de::DeserializeOwned;
13+
914
#[derive(Clone)]
1015
pub struct HttpTransport {
1116
client: BaseClient,
@@ -100,4 +105,26 @@ impl HttpTransport {
100105
fn backoff(&self, retry: usize) -> Duration {
101106
self.retry.base_delay * retry as u32
102107
}
108+
109+
pub async fn execute_stream<T>(
110+
&self,
111+
req: TransportRequest,
112+
) -> Result<Pin<Box<dyn Stream<Item = anyhow::Result<T>> + Send + 'static>>, TransportError>
113+
where
114+
T: DeserializeOwned + Send + 'static,
115+
{
116+
let request = self.build_request(req)?;
117+
118+
let response = self.client.execute(request).await?;
119+
120+
let status = response.status();
121+
if status.is_client_error() || status.is_server_error() {
122+
return Err(TransportError::Server(status.to_string()));
123+
}
124+
125+
let sse_stream = SseStream::new(response);
126+
let json_stream = sse_stream_to_json(sse_stream).await;
127+
128+
Ok(json_stream)
129+
}
103130
}

0 commit comments

Comments
 (0)