use std::time::Duration;
use serde_json::json;
use super::files::read_environment_value;
use super::{
ClientError, ClientKind, ClientManager, DOCTOR_MAX_TOKENS, compact_body, doctor_model,
};
impl ClientManager {
pub async fn doctor(&self, client: ClientKind) -> Result<String, ClientError> {
if let Some(limitation) = client.setup_limitation() {
return Err(ClientError::message(limitation));
}
let status = self.status(client)?;
let base_url = status.base_url.ok_or_else(|| {
ClientError::message(format!(
"{} is not configured; run `clients setup {client}`",
client.display_name()
))
})?;
let token_env = client
.token_env()
.ok_or_else(|| ClientError::message("client has no router token environment"))?;
let token = self
.environment_var(token_env)
.or_else(|| {
read_environment_value(&self.environment_path(client), token_env)
.ok()
.flatten()
})
.ok_or_else(|| {
ClientError::message(format!(
"{token_env} is unset and no managed credential exists; run `clients setup {client}`"
))
})?;
let catalog = self.catalog(&base_url, &token).await?;
let model = doctor_model(client, &catalog)?;
let (url, body) = probe_request(client, &base_url, model);
let response = reqwest::Client::new()
.post(&url)
.bearer_auth(token)
.json(&body)
.timeout(Duration::from_secs(30))
.send()
.await
.map_err(|error| {
ClientError::message(format!("router is not reachable at {url}: {error}"))
})?;
let code = response.status();
let response_body = response.text().await.unwrap_or_default();
if code.is_success() {
return Ok(format!(
"{} reached {url} successfully ({code})",
client.display_name()
));
}
if code.as_u16() == 401 || code.as_u16() == 403 {
return Err(ClientError::message(format!(
"router rejected {token_env} ({code}); the token is invalid, expired, or revoked"
)));
}
if code.as_u16() == 503 {
return Err(ClientError::message(format!(
"router reached, but its upstream credential is unavailable ({code}): {}",
compact_body(&response_body)
)));
}
if code.as_u16() == 404 {
return Err(ClientError::message(format!(
"router reached, but catalog model '{model}' is unavailable ({code}): {}",
compact_body(&response_body)
)));
}
Err(ClientError::message(format!(
"router request failed at {url} ({code}): {}",
compact_body(&response_body)
)))
}
}
fn probe_request(client: ClientKind, base_url: &str, model: &str) -> (String, serde_json::Value) {
let base = base_url.trim_end_matches('/');
match client {
ClientKind::Codex => (
format!("{base}/responses"),
json!({
"model": model,
"input": "Reply OK",
"max_output_tokens": DOCTOR_MAX_TOKENS,
"reasoning": {"effort": "low"}
}),
),
ClientKind::ClaudeCode => (
format!("{base}/v1/messages"),
json!({
"model": model,
"max_tokens": DOCTOR_MAX_TOKENS,
"messages": [{"role":"user", "content":"Reply OK"}]
}),
),
ClientKind::GrokCli | ClientKind::Opencode | ClientKind::QwenCode | ClientKind::Agent => (
format!("{base}/chat/completions"),
json!({
"model": model,
"max_tokens": DOCTOR_MAX_TOKENS,
"reasoning_effort": "low",
"messages": [{"role":"user", "content":"Reply OK"}]
}),
),
ClientKind::Cursor | ClientKind::GeminiCli => unreachable!(),
}
}
#[cfg(test)]
mod tests {
use super::{DOCTOR_MAX_TOKENS, probe_request};
use crate::clients::ClientKind;
#[test]
fn every_probe_asks_at_the_floor_not_the_ceiling() {
assert_eq!(DOCTOR_MAX_TOKENS, 64, "a reachability check is not a task");
for client in ClientKind::ALL {
if matches!(client, ClientKind::Cursor | ClientKind::GeminiCli) {
continue;
}
let (url, body) = probe_request(client, "https://router.example/", "a-model");
assert!(
!url.contains("//chat") && !url.contains("//v1") && !url.contains("//responses"),
"{client}: a trailing slash must not double: {url}"
);
assert_eq!(body["model"], "a-model", "{client} probes the given model");
let budget = body
.get("max_tokens")
.or_else(|| body.get("max_output_tokens"))
.and_then(serde_json::Value::as_u64)
.unwrap_or_else(|| panic!("{client} must bound its output"));
assert_eq!(
budget,
u64::from(DOCTOR_MAX_TOKENS),
"{client} must probe at the floor"
);
let effort = body
.get("reasoning_effort")
.or_else(|| body.pointer("/reasoning/effort"));
if let Some(effort) = effort {
assert_eq!(effort, "low", "{client} must not buy deep reasoning");
}
}
}
}