use crate::ai::oauth::{self, Service};
use crate::ai::provider::{Provider, ProviderKind};
use crate::ai::types::AiError;
use crate::ai::{anthropic, codex};
use serde_json::Value;
use std::io::Read;
use std::time::Duration;
const TIMEOUT: Duration = Duration::from_secs(10);
const MAX_BODY: u64 = 2_000_000;
pub fn parse(body: &str) -> Result<Vec<String>, AiError> {
let value: Value = serde_json::from_str(body).map_err(|error| AiError::Decode(error.to_string()))?;
let (items, key) = match (value["data"].as_array(), value["models"].as_array()) {
(Some(items), _) => (items, "id"),
(None, Some(items)) => (items, "slug"),
_ => return Err(AiError::Decode("falta la lista de modelos".into())),
};
let mut ids: Vec<String> = Vec::new();
for item in items.iter().filter(|item| item["visibility"].as_str().is_none_or(|v| v == "list")) {
if let Some(id) = item[key].as_str().filter(|id| !ids.iter().any(|known| known == id)) {
ids.push(id.to_owned());
}
}
Ok(ids)
}
fn endpoint(provider: &Provider) -> Result<(String, Vec<(String, String)>), AiError> {
Ok(match provider.kind {
ProviderKind::ClaudeSubscription => {
let session = oauth::fresh(Service::Claude)?;
(format!("{}/v1/models?limit=100", provider.base_url), anthropic::subscription_headers(&session.access))
}
ProviderKind::ChatGptSubscription => {
let session = oauth::fresh(Service::ChatGpt)?;
let headers = codex::headers(&session.access, session.account_id.as_deref().unwrap_or_default());
(codex::models_url(), headers)
}
_ => provider.models_endpoint(),
})
}
pub fn fetch(provider: &Provider) -> Result<Vec<String>, AiError> {
let (url, headers) = endpoint(provider)?;
let agent: ureq::Agent =
ureq::Agent::config_builder().timeout_global(Some(TIMEOUT)).http_status_as_error(false).build().into();
let mut request = agent.get(&url);
for (name, value) in &headers {
request = request.header(name, value);
}
let response = request.call().map_err(|error| AiError::Transport(error.to_string()))?;
let code = response.status().as_u16();
let mut text = String::new();
response
.into_body()
.into_reader()
.take(MAX_BODY)
.read_to_string(&mut text)
.map_err(|error| AiError::Transport(error.to_string()))?;
if code >= 400 {
return Err(AiError::Status { code, body: text.chars().take(500).collect() });
}
let mut ids = parse(&text)?;
if provider.kind != ProviderKind::Anthropic {
ids.sort();
}
Ok(ids)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lee_los_ids_sin_repetidos() {
let body = r#"{"data":[{"id":"a","type":"model"},{"id":"b"},{"id":"a"},{"nombre":"x"}]}"#;
assert_eq!(parse(body).unwrap(), ["a", "b"]);
}
#[test]
fn chatgpt_lista_slugs_y_oculta_los_que_no_se_ofrecen() {
let body = r#"{"models":[{"slug":"a","visibility":"list"},{"slug":"b","visibility":"hide"},{"slug":"c"}]}"#;
assert_eq!(parse(body).unwrap(), ["a", "c"]);
}
#[test]
fn rechaza_lo_que_no_es_una_lista() {
assert!(parse(r#"{"error":"x"}"#).is_err());
assert!(parse("no es json").is_err());
}
}