use std::error::Error; use std::sync::LazyLock; use tracing::{debug, warn}; use crate::{ client::EndpointConfig, error::{AiError, AiResult}, }; #[derive(Debug, serde::Deserialize)] struct ModelsListResponse { data: Vec, } #[derive(Debug, Clone, serde::Deserialize)] pub struct UpstreamModel { pub id: String, #[serde(default)] pub name: Option, #[serde(default)] pub owned_by: Option, #[serde(default)] pub context_length: Option, #[serde(default)] pub max_output_tokens: Option, #[serde(default)] pub capabilities: Option, #[serde(default)] pub pricing: Option, } #[derive(Debug, Clone, serde::Deserialize)] pub struct UpstreamCapabilities { #[serde(default)] pub vision: Option, #[serde(default)] pub tool_call: Option, #[serde(default)] pub reasoning: Option, } #[derive(Debug, Clone, serde::Deserialize)] pub struct UpstreamPricing { #[serde(default)] pub prompt: Option, #[serde(default)] pub completion: Option, #[serde(default)] pub input: Option, #[serde(default)] pub output: Option, #[serde(default)] pub cache_read: Option, #[serde(default)] pub unit: Option, #[serde(default)] pub currency: Option, } static HTTP_CLIENT: LazyLock = LazyLock::new(|| { let mut builder = reqwest::Client::builder(); let proxy_url = std::env::var("HTTPS_PROXY") .or_else(|_| std::env::var("https_proxy")) .or_else(|_| std::env::var("HTTP_PROXY")) .or_else(|_| std::env::var("http_proxy")) .ok(); if let Some(raw) = &proxy_url { let url = raw.trim().trim_matches('"').trim_matches('\''); match reqwest::Proxy::all(url) { Ok(proxy) => { debug!(proxy_url = %url, "sync: using proxy"); builder = builder.proxy(proxy); } Err(e) => { warn!(proxy_url = %url, error = %e, "sync: invalid proxy URL, skipping"); } } } #[allow(clippy::expect_used)] builder.build().expect("failed to build reqwest HTTP client — check system TLS configuration") }); pub async fn list_models( config: &EndpointConfig, ) -> AiResult> { let base = config.base_url.trim_end_matches('/'); let url = if base.ends_with("/v1") { format!("{}/models", base) } else { format!("{}/v1/models", base) }; debug!(url = %url, "listing models from upstream"); let resp = HTTP_CLIENT .get(&url) .header("Authorization", format!("Bearer {}", config.api_key.trim())) .send() .await .map_err(|e| { tracing::error!( error = %e, source = ?e.source(), "list_models: request failed with full cause chain" ); AiError::Response(format!("failed to list models: {}", e)) })?; let body = resp .text() .await .map_err(|e| AiError::Response(format!("failed to read models body: {}", e)))?; if let Ok(parsed) = serde_json::from_str::(&body) { debug!(count = parsed.data.len(), "parsed models in standard format"); return Ok(parsed.data); } if let Ok(parsed) = serde_json::from_str::>(&body) { debug!(count = parsed.len(), "parsed models in array format"); return Ok(parsed); } warn!( body = %body.chars().take(500).collect::(), "list_models: unknown response format" ); Err(AiError::Response(format!( "unexpected /v1/models response format (first 200 chars): {}", body.chars().take(200).collect::() ))) }