- 1
//! Provider-side account diagnostics that are safe to expose to operators. - 2
//! - 3
//! Credentials are used only for the request and are never included in the - 4
//! returned status or error text. - 5
- 6
use serde::Serialize; - 7
use std::time::Duration; - 8
- 9
use crate::error::LlmError; - 10
use crate::registry::ProviderAuth; - 11
- 12
#[derive(Debug, Clone, Serialize)] - 13
pub struct ProviderStatus { - 14
pub provider: String, - 15
pub reachable: bool, - 16
pub authenticated: bool, - 17
pub key_kind: Option<String>, - 18
pub is_free_tier: Option<bool>, - 19
pub usage_usd: Option<f64>, - 20
pub usage_daily_usd: Option<f64>, - 21
pub usage_monthly_usd: Option<f64>, - 22
pub limit_usd: Option<f64>, - 23
pub limit_remaining_usd: Option<f64>, - 24
pub credits_usd: Option<f64>, - 25
pub rate_limit_requests: Option<i64>, - 26
pub rate_limit_interval: Option<String>, - 27
} - 28
- 29
fn client() -> Result<reqwest::Client, LlmError> { - 30
reqwest::Client::builder() - 31
.timeout(Duration::from_secs(15)) - 32
.build() - 33
.map_err(|e| LlmError::Network(e.to_string())) - 34
} - 35
- 36
async fn json(response: reqwest::Response) -> Result<(u16, serde_json::Value), LlmError> { - 37
let status = response.status().as_u16(); - 38
let body = response - 39
.text() - 40
.await - 41
.map_err(|e| LlmError::Network(e.to_string()))?; - 42
let value = serde_json::from_str(&body).map_err(|e| LlmError::Parse(e.to_string()))?; - 43
Ok((status, value)) - 44
} - 45
- 46
fn status_error(status: u16, value: &serde_json::Value, secret: &str) -> LlmError { - 47
let message = value - 48
.get("error") - 49
.and_then(|error| error.get("message")) - 50
.and_then(serde_json::Value::as_str) - 51
.unwrap_or("provider status request failed") - 52
.chars() - 53
.take(400) - 54
.collect::<String>(); - 55
let message = if secret.is_empty() { - 56
message - 57
} else { - 58
message.replace(secret, "[redacted]") - 59
}; - 60
match status { - 61
401 | 403 => LlmError::Auth(message), - 62
429 => LlmError::RateLimit { - 63
message, - 64
retry_after_secs: None, - 65
}, - 66
_ => LlmError::Api { status, message }, - 67
} - 68
} - 69
- 70
fn number(value: Option<&serde_json::Value>) -> Option<f64> { - 71
value.and_then(serde_json::Value::as_f64) - 72
} - 73
- 74
/// Inspect provider account metadata when the provider publishes it. - 75
/// OpenRouter is currently the only built-in provider with a documented - 76
/// authenticated key and credit introspection endpoint. - 77
pub async fn inspect(provider: &str, auth: &ProviderAuth) -> Result<ProviderStatus, LlmError> { - 78
if provider != "openrouter" { - 79
return Err(LlmError::InvalidRequest(format!( - 80
"provider '{provider}' does not expose supported account metadata" - 81
))); - 82
} - 83
let base = auth - 84
.base_url - 85
.as_deref() - 86
.filter(|url| !url.trim().is_empty()) - 87
.unwrap_or("https://openrouter.ai/api/v1") - 88
.trim_end_matches('/'); - 89
let client = client()?; - 90
let key_response = client - 91
.get(format!("{base}/key")) - 92
.bearer_auth(&auth.api_key) - 93
.send() - 94
.await - 95
.map_err(|e| LlmError::Network(e.to_string()))?; - 96
let (key_status, key_json) = json(key_response).await?; - 97
if !(200..300).contains(&key_status) { - 98
return Err(status_error(key_status, &key_json, &auth.api_key)); - 99
} - 100
let key = key_json.get("data").unwrap_or(&key_json); - 101
- 102
let credits_response = client - 103
.get(format!("{base}/credits")) - 104
.bearer_auth(&auth.api_key) - 105
.send() - 106
.await - 107
.map_err(|e| LlmError::Network(e.to_string()))?; - 108
let (credits_status, credits_json) = json(credits_response).await?; - 109
if !(200..300).contains(&credits_status) { - 110
return Err(status_error(credits_status, &credits_json, &auth.api_key)); - 111
} - 112
let credits = credits_json.get("data").unwrap_or(&credits_json); - 113
let rate_limit = key.get("rate_limit"); - 114
Ok(ProviderStatus { - 115
provider: provider.to_string(), - 116
reachable: true, - 117
authenticated: true, - 118
key_kind: Some( - 119
if key - 120
.get("is_management_key") - 121
.and_then(serde_json::Value::as_bool) - 122
== Some(true) - 123
{ - 124
"openrouter-management-key".into() - 125
} else { - 126
"openrouter-api-key".into() - 127
}, - 128
), - 129
is_free_tier: key.get("is_free_tier").and_then(serde_json::Value::as_bool), - 130
usage_usd: number(key.get("usage")), - 131
usage_daily_usd: number(key.get("usage_daily")), - 132
usage_monthly_usd: number(key.get("usage_monthly")), - 133
limit_usd: number(key.get("limit")), - 134
limit_remaining_usd: number(key.get("limit_remaining")), - 135
credits_usd: number(credits.get("total_credits")), - 136
rate_limit_requests: rate_limit - 137
.and_then(|value| value.get("requests")) - 138
.and_then(serde_json::Value::as_i64), - 139
rate_limit_interval: rate_limit - 140
.and_then(|value| value.get("interval")) - 141
.and_then(serde_json::Value::as_str) - 142
.map(str::to_string), - 143
}) - 144
} - 145
- 146
#[cfg(test)] - 147
mod tests { - 148
use super::*; - 149
- 150
#[test] - 151
fn status_error_never_echoes_a_credential() { - 152
let error = status_error( - 153
401, - 154
&serde_json::json!({"error": {"message": "invalid key"}}), - 155
"secret", - 156
); - 157
assert_eq!(error.to_string(), "authentication failed: invalid key"); - 158
} - 159
- 160
#[test] - 161
fn status_error_redacts_an_echoed_credential() { - 162
let error = status_error( - 163
401, - 164
&serde_json::json!({"error": {"message": "rejected secret-token"}}), - 165
"secret-token", - 166
); - 167
assert!(!error.to_string().contains("secret-token")); - 168
assert!(error.to_string().contains("[redacted]")); - 169
} - 170
- 171
#[test] - 172
fn unsupported_provider_is_explicit() { - 173
let result = futures::executor::block_on(inspect( - 174
"ollama", - 175
&ProviderAuth { - 176
api_key: "not-a-secret".into(), - 177
base_url: None, - 178
credential_id: None, - 179
options: Default::default(), - 180
}, - 181
)); - 182
assert!(matches!(result, Err(LlmError::InvalidRequest(_)))); - 183
} - 184
} - 185
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.