1use std::time::{Duration, Instant};
4
5use reqwest::Method;
6use serde_json::Value;
7
8use crate::client::Client;
9use crate::error::{Error, Result};
10use crate::http::{HttpResponse, headers};
11use crate::retry::{RetryPolicy, backoff};
12use crate::types::{CapacityInfo, HealthResponse, ModelInfo, WorkerInfo};
13
14pub(crate) fn parse_json<T: serde::de::DeserializeOwned>(
16 response: &HttpResponse,
17 owner: &str,
18) -> Result<T> {
19 serde_json::from_slice(&response.body)
20 .map_err(|err| Error::decode(format!("malformed {owner} response: {err}")))
21}
22
23impl Client {
24 pub async fn list_models(&self) -> Result<Vec<ModelInfo>> {
26 let request = self
27 .request(Method::GET, "/v1/models")?
28 .header("accept", headers::JSON_CONTENT_TYPE);
29 let response = self.send_once(request, RetryPolicy::NONE).await?;
30 let body: Value = parse_json(&response, "models")?;
31 let models = body
32 .get("models")
33 .cloned()
34 .ok_or_else(|| Error::decode("models response is missing its `models` array"))?;
35 serde_json::from_value(models)
36 .map_err(|err| Error::decode(format!("malformed models response: {err}")))
37 }
38
39 pub async fn get_model(&self, model: &str) -> Result<ModelInfo> {
41 let request = self
42 .request(Method::GET, &format!("/v1/models/{model}"))?
43 .header("accept", headers::JSON_CONTENT_TYPE);
44 let response = self.send_once(request, RetryPolicy::NONE).await?;
45 parse_json(&response, "model")
46 }
47
48 pub async fn health(&self) -> Result<HealthResponse> {
54 let request = self
55 .request(Method::GET, "/health")?
56 .header("accept", headers::JSON_CONTENT_TYPE);
57 match self.send_once(request, RetryPolicy::NONE).await {
58 Ok(response) => parse_json(&response, "health"),
59 Err(error) if error.status() == Some(404) => self.worker_health().await,
60 Err(error) => Err(error),
61 }
62 }
63
64 async fn worker_health(&self) -> Result<HealthResponse> {
66 let request = self.request(Method::GET, "/healthz")?;
67 let response = self.send_once(request, RetryPolicy::NONE).await?;
68 let status = response.text().trim().to_string();
69 Ok(HealthResponse {
70 status: if status.is_empty() {
71 "ok".to_string()
72 } else {
73 status
74 },
75 kind: "worker".to_string(),
76 ..HealthResponse::default()
77 })
78 }
79
80 pub async fn get_capacity(&self, gpu: Option<&str>) -> Result<CapacityInfo> {
84 let health = self.health().await?;
85 if health.kind != "gateway" {
86 return Err(Error::Request {
87 message: "get_capacity() requires a gateway endpoint. This appears to be a worker."
88 .to_string(),
89 code: Some("not_gateway".to_string()),
90 status: 400,
91 request: None,
92 });
93 }
94
95 let workers: Vec<WorkerInfo> = match gpu {
96 Some(gpu) => {
97 let wanted = gpu.to_ascii_lowercase();
98 health
99 .workers
100 .into_iter()
101 .filter(|worker| worker.gpu.eq_ignore_ascii_case(&wanted))
102 .collect()
103 }
104 None => health.workers,
105 };
106
107 Ok(CapacityInfo {
108 status: health.status,
109 worker_count: if gpu.is_some() {
111 workers.len() as u32
112 } else {
113 health.cluster.worker_count
114 },
115 gpu_count: health.cluster.gpu_count,
116 models_loaded: health.cluster.models_loaded,
117 configured_gpu_types: health.configured_gpu_types,
118 live_gpu_types: health.live_gpu_types,
119 workers,
120 })
121 }
122
123 pub async fn wait_for_capacity(
129 &self,
130 gpu: &str,
131 model: Option<&str>,
132 timeout: Option<Duration>,
133 poll_interval: Duration,
134 ) -> Result<CapacityInfo> {
135 let budget = timeout.unwrap_or(backoff::DEFAULT_PROVISION_TIMEOUT);
136 let start = Instant::now();
137
138 if let Some(model) = model {
139 self.encode(model, [crate::types::Item::text("warmup")])
140 .gpu(gpu)
141 .wait_for_capacity(true)
142 .provision_timeout(budget)
143 .send()
144 .await?;
145 return self.get_capacity(Some(gpu)).await;
146 }
147
148 loop {
149 if let Ok(capacity) = self.get_capacity(Some(gpu)).await
152 && capacity.worker_count > 0
153 {
154 return Ok(capacity);
155 }
156
157 let elapsed = start.elapsed();
158 if elapsed >= budget {
159 return Err(Error::Provisioning {
160 message: format!(
161 "Timeout after {:.1}s waiting for GPU '{gpu}' capacity",
162 elapsed.as_secs_f64()
163 ),
164 gpu: Some(gpu.to_string()),
165 retry_after: None,
166 });
167 }
168 tokio::time::sleep(poll_interval.min(budget.saturating_sub(elapsed))).await;
169 }
170 }
171}