use std::time::Duration;
use open_agent::ApiProtocol;
use serde::Deserialize;
use thiserror::Error;
const TIMEOUT: Duration = Duration::from_secs(10);
const ANTHROPIC_VERSION: &str = "2023-06-01";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Model {
pub id: String,
pub display_name: Option<String>,
}
impl Model {
pub fn label(&self) -> String {
match &self.display_name {
Some(name) if name != &self.id => format!("{} ({name})", self.id),
_ => self.id.clone(),
}
}
}
#[derive(Debug, Error)]
pub enum ListError {
#[error("this endpoint does not offer a model list")]
Unsupported,
#[error("the endpoint rejected the key (HTTP {0})")]
Unauthorized(u16),
#[error("could not reach the endpoint: {0}")]
Transport(String),
#[error("the endpoint's model list could not be read: {0}")]
Malformed(String),
}
pub trait ModelSource {
#[allow(async_fn_in_trait)]
async fn list(
&self,
endpoint: &str,
api_key: &str,
protocol: ApiProtocol,
) -> Result<Vec<Model>, ListError>;
}
const MAX_LISTING_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Debug, Clone, Copy)]
pub struct Http {
max_bytes: u64,
}
impl Http {
pub fn new() -> Self {
Self {
max_bytes: MAX_LISTING_BYTES,
}
}
#[cfg(test)]
pub fn with_max_bytes(mut self, max_bytes: u64) -> Self {
self.max_bytes = max_bytes;
self
}
}
impl Default for Http {
fn default() -> Self {
Self::new()
}
}
impl ModelSource for Http {
async fn list(
&self,
endpoint: &str,
api_key: &str,
protocol: ApiProtocol,
) -> Result<Vec<Model>, ListError> {
let client = crate::http::client(TIMEOUT).map_err(ListError::Transport)?;
let request = client.get(url(endpoint));
let request = match protocol {
ApiProtocol::Anthropic => request
.header("x-api-key", api_key)
.header("anthropic-version", ANTHROPIC_VERSION),
_ => request.header("Authorization", format!("Bearer {api_key}")),
};
let response = request
.send()
.await
.map_err(|err| ListError::Transport(err.to_string()))?;
let status = response.status().as_u16();
if !response.status().is_success() {
return Err(classify(status));
}
let body = crate::http::read_bounded(response, self.max_bytes)
.await
.map_err(|err| match err {
crate::http::ReadError::Transport(msg) => ListError::Transport(msg),
crate::http::ReadError::Malformed(msg) => ListError::Malformed(msg),
})?;
parse(&body)
}
}
fn url(endpoint: &str) -> String {
format!("{}/models", endpoint.trim_end_matches('/'))
}
fn classify(status: u16) -> ListError {
match status {
404 | 405 | 501 => ListError::Unsupported,
401 | 403 => ListError::Unauthorized(status),
other => ListError::Transport(format!("HTTP {other}")),
}
}
#[derive(Debug, Deserialize)]
struct Listing {
data: Vec<Entry>,
}
#[derive(Debug, Deserialize)]
struct Entry {
id: String,
#[serde(default)]
display_name: Option<String>,
}
fn parse(body: &str) -> Result<Vec<Model>, ListError> {
let listing: Listing = serde_json::from_str(body)
.map_err(|err| ListError::Malformed(crate::text::excerpt(&err.to_string(), 120)))?;
let models: Vec<Model> = listing
.data
.into_iter()
.filter(|entry| !entry.id.is_empty())
.map(|entry| Model {
id: entry.id,
display_name: entry.display_name,
})
.collect();
if models.is_empty() {
return Err(ListError::Unsupported);
}
Ok(models)
}
#[cfg(test)]
mod tests;