synaps_cli/runtime/openai/
ping.rs1use std::collections::BTreeMap;
7use std::time::{Duration, Instant};
8
9use serde_json::json;
10
11use super::registry;
12use super::types::ProviderConfig;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PingStatus {
16 Online,
17 RateLimited,
18 Unauthorized,
19 NotFound,
20 Error,
21 Timeout,
22}
23
24impl PingStatus {
25 pub fn icon(&self) -> &'static str {
26 match self {
27 PingStatus::Online => "✅",
28 PingStatus::RateLimited => "⏳",
29 PingStatus::Unauthorized => "🔒",
30 PingStatus::NotFound => "❌",
31 PingStatus::Error => "⚠️",
32 PingStatus::Timeout => "⌛",
33 }
34 }
35
36 pub fn label(&self) -> &'static str {
37 match self {
38 PingStatus::Online => "online",
39 PingStatus::RateLimited => "429 rate limited",
40 PingStatus::Unauthorized => "401 unauthorized",
41 PingStatus::NotFound => "404 not found",
42 PingStatus::Error => "error",
43 PingStatus::Timeout => "timeout",
44 }
45 }
46}
47
48#[derive(Debug, Clone)]
49pub struct PingResult {
50 pub provider_key: String,
51 pub model_id: String,
52 pub status: PingStatus,
53 pub latency_ms: u64,
54}
55
56const TIMEOUT: Duration = Duration::from_secs(10);
57
58pub async fn ping_model(
59 client: &reqwest::Client,
60 cfg: &ProviderConfig,
61 provider_key: &str,
62) -> PingResult {
63 let url = format!("{}/chat/completions", cfg.base_url.trim_end_matches('/'));
64 let body = json!({
65 "model": cfg.model,
66 "messages": [{"role": "user", "content": "hi"}],
67 "max_tokens": 1,
68 "stream": false,
69 });
70
71 let start = Instant::now();
72 let fut = client
73 .post(&url)
74 .bearer_auth(&cfg.api_key)
75 .json(&body)
76 .send();
77
78 let status = match tokio::time::timeout(TIMEOUT, fut).await {
79 Err(_) => PingStatus::Timeout,
80 Ok(Err(_)) => PingStatus::Error,
81 Ok(Ok(resp)) => {
82 let code = resp.status().as_u16();
83 match code {
84 200..=299 => PingStatus::Online,
85 401 | 403 => PingStatus::Unauthorized,
86 404 => PingStatus::NotFound,
87 429 => PingStatus::RateLimited,
88 _ => PingStatus::Error,
89 }
90 }
91 };
92
93 PingResult {
94 provider_key: provider_key.to_string(),
95 model_id: cfg.model.clone(),
96 status,
97 latency_ms: start.elapsed().as_millis() as u64,
98 }
99}
100
101pub async fn ping_all_configured(
104 client: &reqwest::Client,
105 overrides: &BTreeMap<String, String>,
106 tx: tokio::sync::mpsc::UnboundedSender<(String, PingStatus, u64)>,
107) {
108 let specs = registry::providers();
109 let mut handles = Vec::new();
110
111 for spec in specs {
112 let Some(base_cfg) = registry::resolve_provider_model(spec.key, spec.default_model, overrides) else {
113 continue;
114 };
115 for (model_id, _label, _tier) in spec.models {
116 let cfg = ProviderConfig {
117 base_url: base_cfg.base_url.clone(),
118 api_key: base_cfg.api_key.clone(),
119 model: (*model_id).to_string(),
120 provider: base_cfg.provider.clone(),
121 };
122 let client = client.clone();
123 let key = spec.key.to_string();
124 let tx = tx.clone();
125 handles.push(tokio::spawn(async move {
126 let result = ping_model(&client, &cfg, &key).await;
127 let full_key = format!("{}/{}", result.provider_key, result.model_id);
128 let _ = tx.send((full_key, result.status, result.latency_ms));
129 }));
130 }
131 }
132
133 for h in handles {
134 let _ = h.await;
135 }
136 }