| use std::collections::HashMap; |
| use std::time::Duration; |
|
|
| use bytes::Bytes; |
| use futures_util::StreamExt; |
| use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; |
| use reqwest::Method; |
| use serde::{Deserialize, Serialize}; |
| use tauri::ipc::Channel; |
|
|
| #[tauri::command] |
| pub async fn lm_ping(base_url: String) -> Result<u16, String> { |
| let trimmed = base_url.trim().trim_end_matches('/'); |
| if trimmed.is_empty() { |
| return Err("empty base url".into()); |
| } |
| let probe = format!("{trimmed}/models"); |
| let parsed = reqwest::Url::parse(&probe).map_err(|e| e.to_string())?; |
|
|
| match parsed.scheme() { |
| "http" | "https" => {} |
| s => return Err(format!("scheme not allowed: {s}")), |
| } |
|
|
| let host = parsed.host_str().ok_or_else(|| "missing host".to_string())?; |
| if is_blocked_host(host) { |
| return Err(format!("host not allowed: {host}")); |
| } |
|
|
| let client = reqwest::Client::builder() |
| .timeout(Duration::from_secs(5)) |
| .redirect(reqwest::redirect::Policy::none()) |
| .build() |
| .map_err(|e| e.to_string())?; |
| client |
| .get(parsed) |
| .send() |
| .await |
| .map(|r| r.status().as_u16()) |
| .map_err(|e| e.to_string()) |
| } |
|
|
| fn is_blocked_host(host: &str) -> bool { |
| matches!( |
| host, |
| "169.254.169.254" |
| | "fd00:ec2::254" |
| | "metadata.google.internal" |
| | "metadata.azure.com" |
| ) |
| } |
|
|
| |
| |
|
|
| #[derive(Debug, Serialize)] |
| pub struct HttpResponse { |
| pub status: u16, |
| pub headers: HashMap<String, String>, |
| pub body: Vec<u8>, |
| } |
|
|
| fn build_request( |
| client: &reqwest::Client, |
| method: &str, |
| url: &str, |
| headers: Option<HashMap<String, String>>, |
| body: Option<Vec<u8>>, |
| ) -> Result<reqwest::RequestBuilder, String> { |
| let method = Method::from_bytes(method.as_bytes()).map_err(|e| e.to_string())?; |
| let mut req = client.request(method, url); |
| if let Some(h) = headers { |
| let mut map = HeaderMap::new(); |
| for (k, v) in h { |
| let name = HeaderName::from_bytes(k.as_bytes()).map_err(|e| e.to_string())?; |
| let value = HeaderValue::from_str(&v).map_err(|e| e.to_string())?; |
| map.insert(name, value); |
| } |
| req = req.headers(map); |
| } |
| if let Some(b) = body { |
| req = req.body(b); |
| } |
| Ok(req) |
| } |
|
|
| fn header_map_to_strings(headers: &HeaderMap) -> HashMap<String, String> { |
| let mut out = HashMap::with_capacity(headers.len()); |
| for (k, v) in headers { |
| if let Ok(s) = v.to_str() { |
| out.insert(k.as_str().to_ascii_lowercase(), s.to_string()); |
| } |
| } |
| out |
| } |
|
|
| #[tauri::command] |
| pub async fn ai_http_request( |
| url: String, |
| method: String, |
| headers: Option<HashMap<String, String>>, |
| body: Option<Vec<u8>>, |
| ) -> Result<HttpResponse, String> { |
| let client = reqwest::Client::builder() |
| |
| |
| .connect_timeout(Duration::from_secs(10)) |
| .build() |
| .map_err(|e| e.to_string())?; |
|
|
| let req = build_request(&client, &method, &url, headers, body)?; |
| let resp = req.send().await.map_err(|e| e.to_string())?; |
|
|
| let status = resp.status().as_u16(); |
| let headers = header_map_to_strings(resp.headers()); |
| let body = resp.bytes().await.map_err(|e| e.to_string())?.to_vec(); |
| Ok(HttpResponse { |
| status, |
| headers, |
| body, |
| }) |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| #[serde(tag = "kind", rename_all = "camelCase")] |
| pub enum AiStreamEvent { |
| Headers { |
| status: u16, |
| headers: HashMap<String, String>, |
| }, |
| Chunk { |
| bytes: Vec<u8>, |
| }, |
| End, |
| Error { |
| message: String, |
| }, |
| } |
|
|
| #[tauri::command] |
| pub async fn ai_http_stream( |
| url: String, |
| method: String, |
| headers: Option<HashMap<String, String>>, |
| body: Option<Vec<u8>>, |
| on_event: Channel<AiStreamEvent>, |
| ) -> Result<(), String> { |
| let client = reqwest::Client::builder() |
| .connect_timeout(Duration::from_secs(10)) |
| .build() |
| .map_err(|e| e.to_string())?; |
|
|
| let req = build_request(&client, &method, &url, headers, body)?; |
| let resp = match req.send().await { |
| Ok(r) => r, |
| Err(e) => { |
| let _ = on_event.send(AiStreamEvent::Error { |
| message: e.to_string(), |
| }); |
| return Err(e.to_string()); |
| } |
| }; |
|
|
| let status = resp.status().as_u16(); |
| let headers = header_map_to_strings(resp.headers()); |
| let _ = on_event.send(AiStreamEvent::Headers { status, headers }); |
|
|
| let mut stream = resp.bytes_stream(); |
| while let Some(item) = stream.next().await { |
| match item { |
| Ok(chunk) => { |
| let bytes: Bytes = chunk; |
| if on_event |
| .send(AiStreamEvent::Chunk { |
| bytes: bytes.to_vec(), |
| }) |
| .is_err() |
| { |
| |
| return Ok(()); |
| } |
| } |
| Err(e) => { |
| let _ = on_event.send(AiStreamEvent::Error { |
| message: e.to_string(), |
| }); |
| return Err(e.to_string()); |
| } |
| } |
| } |
|
|
| let _ = on_event.send(AiStreamEvent::End); |
| Ok(()) |
| } |
|
|