246 lines
7.4 KiB
Rust
246 lines
7.4 KiB
Rust
use once_cell::sync::Lazy;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
use super::canonical::CanonicalModelRegistry;
|
|
|
|
const PROVIDER_METADATA_JSON: &str = include_str!("canonical/data/provider_metadata.json");
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
struct ProviderMetadataEntry {
|
|
pub id: String,
|
|
pub display_name: String,
|
|
pub npm: Option<String>,
|
|
pub api: Option<String>,
|
|
pub doc: Option<String>,
|
|
pub env: Vec<String>,
|
|
pub model_count: usize,
|
|
}
|
|
|
|
static PROVIDER_METADATA: Lazy<HashMap<String, ProviderMetadataEntry>> = Lazy::new(|| {
|
|
serde_json::from_str::<Vec<ProviderMetadataEntry>>(PROVIDER_METADATA_JSON)
|
|
.unwrap_or_else(|e| {
|
|
eprintln!("Failed to parse provider metadata: {}", e);
|
|
Vec::new()
|
|
})
|
|
.into_iter()
|
|
.map(|p| (p.id.clone(), p))
|
|
.collect()
|
|
});
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ProviderFormat {
|
|
OpenAI,
|
|
Anthropic,
|
|
Ollama,
|
|
}
|
|
|
|
impl ProviderFormat {
|
|
pub fn as_str(&self) -> &str {
|
|
match self {
|
|
ProviderFormat::OpenAI => "openai",
|
|
ProviderFormat::Anthropic => "anthropic",
|
|
ProviderFormat::Ollama => "ollama",
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::str::FromStr for ProviderFormat {
|
|
type Err = String;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
match s.to_lowercase().as_str() {
|
|
"openai" | "openai_compatible" => Ok(ProviderFormat::OpenAI),
|
|
"anthropic" | "anthropic_compatible" => Ok(ProviderFormat::Anthropic),
|
|
"ollama" | "ollama_compatible" => Ok(ProviderFormat::Ollama),
|
|
_ => Err(format!("unknown provider format: {}", s)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn detect_format_from_npm(npm: &str) -> Option<ProviderFormat> {
|
|
if npm.contains("openai") {
|
|
Some(ProviderFormat::OpenAI)
|
|
} else if npm.contains("anthropic") {
|
|
Some(ProviderFormat::Anthropic)
|
|
} else if npm.contains("ollama") {
|
|
Some(ProviderFormat::Ollama)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ProviderCatalogEntry {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub format: String,
|
|
pub api_url: String,
|
|
pub model_count: usize,
|
|
pub doc_url: String,
|
|
pub env_var: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ProviderTemplate {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub format: String,
|
|
pub api_url: String,
|
|
pub models: Vec<ModelTemplate>,
|
|
pub supports_streaming: bool,
|
|
pub env_var: String,
|
|
pub doc_url: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ModelTemplate {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub context_limit: usize,
|
|
pub capabilities: ModelCapabilities,
|
|
pub deprecated: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ModelCapabilities {
|
|
pub tool_call: bool,
|
|
pub reasoning: bool,
|
|
pub attachment: bool,
|
|
pub temperature: bool,
|
|
}
|
|
|
|
pub async fn get_providers_by_format(format: ProviderFormat) -> Vec<ProviderCatalogEntry> {
|
|
let native_provider_ids = super::init::providers()
|
|
.await
|
|
.into_iter()
|
|
.map(|(metadata, _)| metadata.name)
|
|
.collect::<std::collections::HashSet<_>>();
|
|
|
|
let mut entries: Vec<ProviderCatalogEntry> = PROVIDER_METADATA
|
|
.values()
|
|
.filter_map(|metadata| {
|
|
if native_provider_ids.contains(&metadata.id) {
|
|
return None;
|
|
}
|
|
|
|
let npm = metadata.npm.as_ref()?;
|
|
let detected_format = detect_format_from_npm(npm)?;
|
|
|
|
if detected_format != format {
|
|
return None;
|
|
}
|
|
|
|
let api_url = metadata.api.as_ref()?.clone();
|
|
|
|
let env_var = metadata.env.first().cloned().unwrap_or_else(|| {
|
|
format!("{}_API_KEY", metadata.id.to_uppercase().replace('-', "_"))
|
|
});
|
|
|
|
Some(ProviderCatalogEntry {
|
|
id: metadata.id.clone(),
|
|
name: metadata.display_name.clone(),
|
|
format: detected_format.as_str().to_string(),
|
|
api_url,
|
|
model_count: metadata.model_count,
|
|
doc_url: metadata.doc.clone().unwrap_or_default(),
|
|
env_var,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
// Sort by name
|
|
entries.sort_by(|a, b| a.name.cmp(&b.name));
|
|
entries
|
|
}
|
|
|
|
pub fn get_provider_template(provider_id: &str) -> Option<ProviderTemplate> {
|
|
let metadata = PROVIDER_METADATA.get(provider_id)?;
|
|
|
|
let npm = metadata.npm.as_ref()?;
|
|
let format = detect_format_from_npm(npm)?;
|
|
|
|
let api_url = metadata.api.as_ref()?.clone();
|
|
|
|
let models: Vec<ModelTemplate> = CanonicalModelRegistry::bundled()
|
|
.ok()
|
|
.map(|registry| {
|
|
registry
|
|
.get_all_models_for_provider(provider_id)
|
|
.into_iter()
|
|
.map(|model| {
|
|
// Extract just the model ID (without provider prefix)
|
|
let model_id = model
|
|
.id
|
|
.strip_prefix(&format!("{}/", provider_id))
|
|
.unwrap_or(&model.id)
|
|
.to_string();
|
|
|
|
ModelTemplate {
|
|
id: model_id,
|
|
name: model.name.clone(),
|
|
context_limit: model.limit.context,
|
|
capabilities: ModelCapabilities {
|
|
tool_call: model.tool_call,
|
|
reasoning: model.reasoning.unwrap_or(false),
|
|
attachment: model.attachment.unwrap_or(false),
|
|
temperature: model.temperature.unwrap_or(false),
|
|
},
|
|
deprecated: false, // Canonical models don't have deprecated flag
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let env_var = metadata
|
|
.env
|
|
.first()
|
|
.cloned()
|
|
.unwrap_or_else(|| format!("{}_API_KEY", provider_id.to_uppercase().replace('-', "_")));
|
|
|
|
Some(ProviderTemplate {
|
|
id: metadata.id.clone(),
|
|
name: metadata.display_name.clone(),
|
|
format: format.as_str().to_string(),
|
|
api_url,
|
|
models,
|
|
supports_streaming: true, // Default to true
|
|
env_var,
|
|
doc_url: metadata.doc.clone().unwrap_or_default(),
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn test_zai_provider() {
|
|
let openai_providers = get_providers_by_format(ProviderFormat::OpenAI).await;
|
|
let zai = openai_providers.iter().find(|p| p.id == "zai");
|
|
assert!(zai.is_some(), "z.ai should be in catalog");
|
|
|
|
let zai = zai.unwrap();
|
|
println!("Z.AI: {} models", zai.model_count);
|
|
assert!(zai.model_count > 0, "z.ai should have models");
|
|
|
|
let template = get_provider_template("zai");
|
|
assert!(template.is_some(), "z.ai should have a template");
|
|
|
|
let template = template.unwrap();
|
|
println!("Z.AI template: {} models", template.models.len());
|
|
for model in template.models.iter().take(3) {
|
|
println!(
|
|
" - {} ({}K context)",
|
|
model.name,
|
|
model.context_limit / 1000
|
|
);
|
|
}
|
|
assert!(
|
|
!template.models.is_empty(),
|
|
"z.ai template should have models"
|
|
);
|
|
}
|
|
}
|