mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
Simplified custom model flow with canonical models (#6934)
This commit is contained in:
@@ -163,14 +163,14 @@ pub async fn run_model_list<C: Connection>() {
|
||||
"o3-mini-2025-01-31",
|
||||
"o1",
|
||||
"o1-2024-12-17",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"o4-mini-deep-research",
|
||||
"o4-mini-deep-research-2025-06-26",
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"gpt-4o-2024-11-20",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
"o4-mini-deep-research",
|
||||
"o4-mini-deep-research-2025-06-26",
|
||||
"gpt-4",
|
||||
"gpt-4-0613",
|
||||
"gpt-4-turbo",
|
||||
|
||||
@@ -1969,6 +1969,7 @@ fn add_provider() -> anyhow::Result<()> {
|
||||
supports_streaming: Some(supports_streaming),
|
||||
headers,
|
||||
requires_auth,
|
||||
catalog_provider_id: None,
|
||||
})?;
|
||||
|
||||
cliclack::outro(format!("Custom provider added: {}", display_name))?;
|
||||
|
||||
@@ -354,6 +354,8 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::get_custom_provider,
|
||||
super::routes::config_management::update_custom_provider,
|
||||
super::routes::config_management::remove_custom_provider,
|
||||
super::routes::config_management::get_provider_catalog,
|
||||
super::routes::config_management::get_provider_catalog_template,
|
||||
super::routes::config_management::check_provider,
|
||||
super::routes::config_management::set_config_provider,
|
||||
super::routes::config_management::configure_provider_oauth,
|
||||
@@ -450,6 +452,10 @@ derive_utoipa!(Icon as IconSchema);
|
||||
super::routes::config_management::ToolPermission,
|
||||
super::routes::config_management::UpsertPermissionsQuery,
|
||||
super::routes::config_management::UpdateCustomProviderRequest,
|
||||
goose::providers::catalog::ProviderCatalogEntry,
|
||||
goose::providers::catalog::ProviderTemplate,
|
||||
goose::providers::catalog::ModelTemplate,
|
||||
goose::providers::catalog::ModelCapabilities,
|
||||
super::routes::config_management::CheckProviderRequest,
|
||||
super::routes::config_management::SetProviderRequest,
|
||||
super::routes::config_management::ModelInfoQuery,
|
||||
|
||||
@@ -15,6 +15,10 @@ use goose::model::ModelConfig;
|
||||
use goose::providers::auto_detect::detect_provider_from_api_key;
|
||||
use goose::providers::base::{ProviderMetadata, ProviderType};
|
||||
use goose::providers::canonical::maybe_get_canonical_model;
|
||||
use goose::providers::catalog::{
|
||||
get_provider_template, get_providers_by_format, ProviderCatalogEntry, ProviderFormat,
|
||||
ProviderTemplate,
|
||||
};
|
||||
use goose::providers::create_with_default_model;
|
||||
use goose::providers::providers as get_providers;
|
||||
use goose::{
|
||||
@@ -94,6 +98,8 @@ pub struct UpdateCustomProviderRequest {
|
||||
pub headers: Option<std::collections::HashMap<String, String>>,
|
||||
#[serde(default = "default_requires_auth")]
|
||||
pub requires_auth: bool,
|
||||
#[serde(default)]
|
||||
pub catalog_provider_id: Option<String>,
|
||||
}
|
||||
|
||||
fn default_requires_auth() -> bool {
|
||||
@@ -648,6 +654,7 @@ pub async fn create_custom_provider(
|
||||
supports_streaming: request.supports_streaming,
|
||||
headers: request.headers,
|
||||
requires_auth: request.requires_auth,
|
||||
catalog_provider_id: request.catalog_provider_id,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -718,6 +725,7 @@ pub async fn update_custom_provider(
|
||||
supports_streaming: request.supports_streaming,
|
||||
headers: request.headers,
|
||||
requires_auth: request.requires_auth,
|
||||
catalog_provider_id: request.catalog_provider_id,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -770,6 +778,54 @@ pub async fn set_config_provider(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/config/provider-catalog",
|
||||
params(
|
||||
("format" = Option<String>, Query, description = "Filter by provider format (openai, anthropic, ollama)")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Provider catalog retrieved successfully", body = [ProviderCatalogEntry]),
|
||||
(status = 400, description = "Invalid format parameter")
|
||||
)
|
||||
)]
|
||||
pub async fn get_provider_catalog(
|
||||
axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
|
||||
) -> Result<Json<Vec<ProviderCatalogEntry>>, ErrorResponse> {
|
||||
let format_str = params.get("format").map(|s| s.as_str()).unwrap_or("openai");
|
||||
|
||||
let format = format_str.parse::<ProviderFormat>().map_err(|_| {
|
||||
ErrorResponse::bad_request(format!(
|
||||
"Invalid format '{}'. Must be one of: openai, anthropic, ollama",
|
||||
format_str
|
||||
))
|
||||
})?;
|
||||
|
||||
let providers = get_providers_by_format(format).await;
|
||||
Ok(Json(providers))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/config/provider-catalog/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Provider ID from models.dev")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Provider template retrieved successfully", body = ProviderTemplate),
|
||||
(status = 404, description = "Provider not found in catalog")
|
||||
)
|
||||
)]
|
||||
pub async fn get_provider_catalog_template(
|
||||
Path(id): Path<String>,
|
||||
) -> Result<Json<ProviderTemplate>, ErrorResponse> {
|
||||
let template = get_provider_template(&id).ok_or_else(|| {
|
||||
ErrorResponse::not_found(format!("Provider '{}' not found in catalog", id))
|
||||
})?;
|
||||
|
||||
Ok(Json(template))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/config/providers/{name}/oauth",
|
||||
@@ -836,6 +892,11 @@ pub fn routes(state: Arc<AppState>) -> Router {
|
||||
.route("/config/extensions/{name}", delete(remove_extension))
|
||||
.route("/config/providers", get(providers))
|
||||
.route("/config/providers/{name}/models", get(get_provider_models))
|
||||
.route("/config/provider-catalog", get(get_provider_catalog))
|
||||
.route(
|
||||
"/config/provider-catalog/{id}",
|
||||
get(get_provider_catalog_template),
|
||||
)
|
||||
.route("/config/detect-provider", post(detect_provider))
|
||||
.route("/config/slash_commands", get(get_slash_commands))
|
||||
.route(
|
||||
|
||||
@@ -42,6 +42,8 @@ pub struct DeclarativeProviderConfig {
|
||||
pub supports_streaming: Option<bool>,
|
||||
#[serde(default = "default_requires_auth")]
|
||||
pub requires_auth: bool,
|
||||
#[serde(default)]
|
||||
pub catalog_provider_id: Option<String>,
|
||||
}
|
||||
|
||||
fn default_requires_auth() -> bool {
|
||||
@@ -102,6 +104,7 @@ pub struct CreateCustomProviderParams {
|
||||
pub supports_streaming: Option<bool>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub requires_auth: bool,
|
||||
pub catalog_provider_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -115,6 +118,7 @@ pub struct UpdateCustomProviderParams {
|
||||
pub supports_streaming: Option<bool>,
|
||||
pub headers: Option<HashMap<String, String>>,
|
||||
pub requires_auth: bool,
|
||||
pub catalog_provider_id: Option<String>,
|
||||
}
|
||||
|
||||
pub fn create_custom_provider(
|
||||
@@ -154,6 +158,7 @@ pub fn create_custom_provider(
|
||||
timeout_seconds: None,
|
||||
supports_streaming: params.supports_streaming,
|
||||
requires_auth: params.requires_auth,
|
||||
catalog_provider_id: params.catalog_provider_id,
|
||||
};
|
||||
|
||||
let custom_providers_dir = custom_providers_dir();
|
||||
@@ -215,6 +220,7 @@ pub fn update_custom_provider(params: UpdateCustomProviderParams) -> Result<()>
|
||||
timeout_seconds: existing_config.timeout_seconds,
|
||||
supports_streaming: params.supports_streaming,
|
||||
requires_auth: params.requires_auth,
|
||||
catalog_provider_id: params.catalog_provider_id,
|
||||
};
|
||||
|
||||
let file_path = custom_providers_dir().join(format!("{}.json", updated_config.name));
|
||||
|
||||
@@ -18,6 +18,17 @@ use serde_json::Value;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ProviderMetadata {
|
||||
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,
|
||||
}
|
||||
|
||||
const MODELS_DEV_API_URL: &str = "https://models.dev/api.json";
|
||||
const DEFAULT_CONTEXT_LIMIT: usize = 128_000;
|
||||
const SEPARATOR: &str =
|
||||
@@ -25,21 +36,9 @@ const SEPARATOR: &str =
|
||||
const SUBSEPARATOR: &str =
|
||||
"--------------------------------------------------------------------------------";
|
||||
|
||||
const ALLOWED_PROVIDERS: &[&str] = &[
|
||||
"anthropic",
|
||||
"google",
|
||||
"openai",
|
||||
"openrouter",
|
||||
"llama",
|
||||
"mistral",
|
||||
"xai",
|
||||
"deepseek",
|
||||
"cohere",
|
||||
"azure",
|
||||
"amazon-bedrock",
|
||||
"venice",
|
||||
"google-vertex",
|
||||
];
|
||||
fn is_compatible_provider(npm: &str) -> bool {
|
||||
npm.contains("openai") || npm.contains("anthropic") || npm.contains("ollama")
|
||||
}
|
||||
|
||||
fn normalize_provider_name(provider: &str) -> &str {
|
||||
match provider {
|
||||
@@ -366,12 +365,7 @@ fn process_model(
|
||||
model_id: &str,
|
||||
model_data: &Value,
|
||||
normalized_provider: &str,
|
||||
) -> Result<Option<(String, CanonicalModel)>> {
|
||||
let cost_data = match model_data.get("cost") {
|
||||
Some(c) if !c.is_null() => c,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
) -> Result<(String, CanonicalModel)> {
|
||||
let name = model_data["name"]
|
||||
.as_str()
|
||||
.with_context(|| format!("Model {} missing name", model_id))?;
|
||||
@@ -383,11 +377,19 @@ fn process_model(
|
||||
output: parse_modalities(model_data, "output"),
|
||||
};
|
||||
|
||||
let cost = Pricing {
|
||||
input: cost_data.get("input").and_then(|v| v.as_f64()),
|
||||
output: cost_data.get("output").and_then(|v| v.as_f64()),
|
||||
cache_read: cost_data.get("cache_read").and_then(|v| v.as_f64()),
|
||||
cache_write: cost_data.get("cache_write").and_then(|v| v.as_f64()),
|
||||
let cost = match model_data.get("cost") {
|
||||
Some(c) if !c.is_null() => Pricing {
|
||||
input: c.get("input").and_then(|v| v.as_f64()),
|
||||
output: c.get("output").and_then(|v| v.as_f64()),
|
||||
cache_read: c.get("cache_read").and_then(|v| v.as_f64()),
|
||||
cache_write: c.get("cache_write").and_then(|v| v.as_f64()),
|
||||
},
|
||||
_ => Pricing {
|
||||
input: None,
|
||||
output: None,
|
||||
cache_read: None,
|
||||
cache_write: None,
|
||||
},
|
||||
};
|
||||
|
||||
let limit = Limit {
|
||||
@@ -428,7 +430,73 @@ fn process_model(
|
||||
.unwrap_or(model_id)
|
||||
.to_string();
|
||||
|
||||
Ok(Some((model_name, canonical_model)))
|
||||
Ok((model_name, canonical_model))
|
||||
}
|
||||
|
||||
fn collect_provider_metadata(
|
||||
providers_obj: &serde_json::Map<String, Value>,
|
||||
) -> Vec<ProviderMetadata> {
|
||||
let mut metadata_list = Vec::new();
|
||||
|
||||
for (provider_id, provider_data) in providers_obj {
|
||||
let npm = match provider_data.get("npm").and_then(|v| v.as_str()) {
|
||||
Some(n) if !n.is_empty() => n,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if !is_compatible_provider(npm) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let api = provider_data
|
||||
.get("api")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
|
||||
if api.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let normalized_provider = normalize_provider_name(provider_id).to_string();
|
||||
let doc = provider_data
|
||||
.get("doc")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
let env = provider_data
|
||||
.get("env")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let display_name = provider_data
|
||||
.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(provider_id)
|
||||
.to_string();
|
||||
let model_count = provider_data
|
||||
.get("models")
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|models| models.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
metadata_list.push(ProviderMetadata {
|
||||
id: normalized_provider,
|
||||
display_name,
|
||||
npm: Some(npm.to_string()),
|
||||
api,
|
||||
doc,
|
||||
env,
|
||||
model_count,
|
||||
});
|
||||
|
||||
println!(" Added {} ({}) - {} models", provider_id, npm, model_count);
|
||||
}
|
||||
|
||||
metadata_list
|
||||
}
|
||||
|
||||
async fn build_canonical_models() -> Result<()> {
|
||||
@@ -441,28 +509,25 @@ async fn build_canonical_models() -> Result<()> {
|
||||
let mut registry = CanonicalModelRegistry::new();
|
||||
let mut total_models = 0;
|
||||
|
||||
for provider_key in ALLOWED_PROVIDERS {
|
||||
if let Some(provider_data) = providers_obj.get(*provider_key) {
|
||||
let models = provider_data["models"]
|
||||
.as_object()
|
||||
.with_context(|| format!("Provider {} missing models object", provider_key))?;
|
||||
for (provider_key, provider_data) in providers_obj {
|
||||
let models = match provider_data.get("models").and_then(|v| v.as_object()) {
|
||||
Some(m) => m,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let normalized_provider = normalize_provider_name(provider_key);
|
||||
let normalized_provider = normalize_provider_name(provider_key);
|
||||
|
||||
println!(
|
||||
"\nProcessing {} ({} models)...",
|
||||
normalized_provider,
|
||||
models.len()
|
||||
);
|
||||
println!(
|
||||
"\nProcessing {} ({} models)...",
|
||||
normalized_provider,
|
||||
models.len()
|
||||
);
|
||||
|
||||
for (model_id, model_data) in models {
|
||||
if let Some((model_name, canonical_model)) =
|
||||
process_model(model_id, model_data, normalized_provider)?
|
||||
{
|
||||
registry.register(normalized_provider, &model_name, canonical_model);
|
||||
total_models += 1;
|
||||
}
|
||||
}
|
||||
for (model_id, model_data) in models {
|
||||
let (model_name, canonical_model) =
|
||||
process_model(model_id, model_data, normalized_provider)?;
|
||||
registry.register(normalized_provider, &model_name, canonical_model);
|
||||
total_models += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,6 +539,18 @@ async fn build_canonical_models() -> Result<()> {
|
||||
output_path.display()
|
||||
);
|
||||
|
||||
println!("\n\nCollecting provider metadata from models.dev...");
|
||||
let provider_metadata_list = collect_provider_metadata(providers_obj);
|
||||
|
||||
let provider_metadata_path = data_file_path("provider_metadata.json");
|
||||
let provider_metadata_json = serde_json::to_string_pretty(&provider_metadata_list)?;
|
||||
std::fs::write(&provider_metadata_path, provider_metadata_json)?;
|
||||
println!(
|
||||
"✓ Wrote {} providers metadata to {}",
|
||||
provider_metadata_list.len(),
|
||||
provider_metadata_path.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,807 @@
|
||||
[
|
||||
{
|
||||
"id": "evroc",
|
||||
"display_name": "evroc",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://models.think.evroc.com/v1",
|
||||
"doc": "https://docs.evroc.com/products/think/overview.html",
|
||||
"env": [
|
||||
"EVROC_API_KEY"
|
||||
],
|
||||
"model_count": 13
|
||||
},
|
||||
{
|
||||
"id": "privatemode-ai",
|
||||
"display_name": "Privatemode AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "http://localhost:8080/v1",
|
||||
"doc": "https://docs.privatemode.ai/api/overview",
|
||||
"env": [
|
||||
"PRIVATEMODE_API_KEY",
|
||||
"PRIVATEMODE_ENDPOINT"
|
||||
],
|
||||
"model_count": 5
|
||||
},
|
||||
{
|
||||
"id": "stepfun",
|
||||
"display_name": "StepFun",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.stepfun.com/v1",
|
||||
"doc": "https://platform.stepfun.com/docs/zh/overview/concept",
|
||||
"env": [
|
||||
"STEPFUN_API_KEY"
|
||||
],
|
||||
"model_count": 3
|
||||
},
|
||||
{
|
||||
"id": "moonshotai-cn",
|
||||
"display_name": "Moonshot AI (China)",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.moonshot.cn/v1",
|
||||
"doc": "https://platform.moonshot.cn/docs/api/chat",
|
||||
"env": [
|
||||
"MOONSHOT_API_KEY"
|
||||
],
|
||||
"model_count": 6
|
||||
},
|
||||
{
|
||||
"id": "firmware",
|
||||
"display_name": "Firmware",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://app.firmware.ai/api/v1",
|
||||
"doc": "https://docs.firmware.ai",
|
||||
"env": [
|
||||
"FIRMWARE_API_KEY"
|
||||
],
|
||||
"model_count": 16
|
||||
},
|
||||
{
|
||||
"id": "nova",
|
||||
"display_name": "Nova",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.nova.amazon.com/v1",
|
||||
"doc": "https://nova.amazon.com/dev/documentation",
|
||||
"env": [
|
||||
"NOVA_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "lucidquery",
|
||||
"display_name": "LucidQuery AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://lucidquery.com/api/v1",
|
||||
"doc": "https://lucidquery.com/api/docs",
|
||||
"env": [
|
||||
"LUCIDQUERY_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "moonshotai",
|
||||
"display_name": "Moonshot AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.moonshot.ai/v1",
|
||||
"doc": "https://platform.moonshot.ai/docs/api/chat",
|
||||
"env": [
|
||||
"MOONSHOT_API_KEY"
|
||||
],
|
||||
"model_count": 6
|
||||
},
|
||||
{
|
||||
"id": "302ai",
|
||||
"display_name": "302.AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.302.ai/v1",
|
||||
"doc": "https://doc.302.ai",
|
||||
"env": [
|
||||
"302AI_API_KEY"
|
||||
],
|
||||
"model_count": 64
|
||||
},
|
||||
{
|
||||
"id": "qihang-ai",
|
||||
"display_name": "QiHang",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.qhaigc.net/v1",
|
||||
"doc": "https://www.qhaigc.net/docs",
|
||||
"env": [
|
||||
"QIHANG_API_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "zai-coding-plan",
|
||||
"display_name": "Z.AI Coding Plan",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.z.ai/api/coding/paas/v4",
|
||||
"doc": "https://docs.z.ai/devpack/overview",
|
||||
"env": [
|
||||
"ZHIPU_API_KEY"
|
||||
],
|
||||
"model_count": 10
|
||||
},
|
||||
{
|
||||
"id": "ollama-cloud",
|
||||
"display_name": "Ollama Cloud",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://ollama.com/v1",
|
||||
"doc": "https://docs.ollama.com/cloud",
|
||||
"env": [
|
||||
"OLLAMA_API_KEY"
|
||||
],
|
||||
"model_count": 33
|
||||
},
|
||||
{
|
||||
"id": "xiaomi",
|
||||
"display_name": "Xiaomi",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.xiaomimimo.com/v1",
|
||||
"doc": "https://platform.xiaomimimo.com/#/docs",
|
||||
"env": [
|
||||
"XIAOMI_API_KEY"
|
||||
],
|
||||
"model_count": 1
|
||||
},
|
||||
{
|
||||
"id": "alibaba",
|
||||
"display_name": "Alibaba",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
"doc": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"env": [
|
||||
"DASHSCOPE_API_KEY"
|
||||
],
|
||||
"model_count": 41
|
||||
},
|
||||
{
|
||||
"id": "vultr",
|
||||
"display_name": "Vultr",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.vultrinference.com/v1",
|
||||
"doc": "https://api.vultrinference.com/",
|
||||
"env": [
|
||||
"VULTR_API_KEY"
|
||||
],
|
||||
"model_count": 5
|
||||
},
|
||||
{
|
||||
"id": "nvidia",
|
||||
"display_name": "Nvidia",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://integrate.api.nvidia.com/v1",
|
||||
"doc": "https://docs.api.nvidia.com/nim/",
|
||||
"env": [
|
||||
"NVIDIA_API_KEY"
|
||||
],
|
||||
"model_count": 71
|
||||
},
|
||||
{
|
||||
"id": "upstage",
|
||||
"display_name": "Upstage",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.upstage.ai/v1/solar",
|
||||
"doc": "https://developers.upstage.ai/docs/apis/chat",
|
||||
"env": [
|
||||
"UPSTAGE_API_KEY"
|
||||
],
|
||||
"model_count": 3
|
||||
},
|
||||
{
|
||||
"id": "bailing",
|
||||
"display_name": "Bailing",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.tbox.cn/api/llm/v1/chat/completions",
|
||||
"doc": "https://alipaytbox.yuque.com/sxs0ba/ling/intro",
|
||||
"env": [
|
||||
"BAILING_API_TOKEN"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "github-copilot",
|
||||
"display_name": "GitHub Copilot",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.githubcopilot.com",
|
||||
"doc": "https://docs.github.com/en/copilot",
|
||||
"env": [
|
||||
"GITHUB_TOKEN"
|
||||
],
|
||||
"model_count": 22
|
||||
},
|
||||
{
|
||||
"id": "jiekou",
|
||||
"display_name": "Jiekou.AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.jiekou.ai/openai",
|
||||
"doc": "https://docs.jiekou.ai/docs/support/quickstart?utm_source=github_models.dev",
|
||||
"env": [
|
||||
"JIEKOU_API_KEY"
|
||||
],
|
||||
"model_count": 61
|
||||
},
|
||||
{
|
||||
"id": "cloudferro-sherlock",
|
||||
"display_name": "CloudFerro Sherlock",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api-sherlock.cloudferro.com/openai/v1/",
|
||||
"doc": "https://docs.sherlock.cloudferro.com/",
|
||||
"env": [
|
||||
"CLOUDFERRO_SHERLOCK_API_KEY"
|
||||
],
|
||||
"model_count": 4
|
||||
},
|
||||
{
|
||||
"id": "abacus",
|
||||
"display_name": "Abacus",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://routellm.abacus.ai/v1",
|
||||
"doc": "https://abacus.ai/help/api",
|
||||
"env": [
|
||||
"ABACUS_API_KEY"
|
||||
],
|
||||
"model_count": 55
|
||||
},
|
||||
{
|
||||
"id": "nebius",
|
||||
"display_name": "Nebius Token Factory",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.tokenfactory.nebius.com/v1",
|
||||
"doc": "https://docs.tokenfactory.nebius.com/",
|
||||
"env": [
|
||||
"NEBIUS_API_KEY"
|
||||
],
|
||||
"model_count": 46
|
||||
},
|
||||
{
|
||||
"id": "deepseek",
|
||||
"display_name": "DeepSeek",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.deepseek.com",
|
||||
"doc": "https://platform.deepseek.com/api-docs/pricing",
|
||||
"env": [
|
||||
"DEEPSEEK_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "alibaba-cn",
|
||||
"display_name": "Alibaba (China)",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
"doc": "https://www.alibabacloud.com/help/en/model-studio/models",
|
||||
"env": [
|
||||
"DASHSCOPE_API_KEY"
|
||||
],
|
||||
"model_count": 65
|
||||
},
|
||||
{
|
||||
"id": "novita-ai",
|
||||
"display_name": "NovitaAI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.novita.ai/openai",
|
||||
"doc": "https://novita.ai/docs/guides/introduction",
|
||||
"env": [
|
||||
"NOVITA_API_KEY"
|
||||
],
|
||||
"model_count": 84
|
||||
},
|
||||
{
|
||||
"id": "siliconflow-cn",
|
||||
"display_name": "SiliconFlow (China)",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.siliconflow.cn/v1",
|
||||
"doc": "https://cloud.siliconflow.com/models",
|
||||
"env": [
|
||||
"SILICONFLOW_CN_API_KEY"
|
||||
],
|
||||
"model_count": 72
|
||||
},
|
||||
{
|
||||
"id": "vivgrid",
|
||||
"display_name": "Vivgrid",
|
||||
"npm": "@ai-sdk/openai",
|
||||
"api": "https://api.vivgrid.com/v1",
|
||||
"doc": "https://docs.vivgrid.com/models",
|
||||
"env": [
|
||||
"VIVGRID_API_KEY"
|
||||
],
|
||||
"model_count": 8
|
||||
},
|
||||
{
|
||||
"id": "chutes",
|
||||
"display_name": "Chutes",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://llm.chutes.ai/v1",
|
||||
"doc": "https://llm.chutes.ai/v1/models",
|
||||
"env": [
|
||||
"CHUTES_API_KEY"
|
||||
],
|
||||
"model_count": 67
|
||||
},
|
||||
{
|
||||
"id": "kimi-for-coding",
|
||||
"display_name": "Kimi For Coding",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://api.kimi.com/coding/v1",
|
||||
"doc": "https://www.kimi.com/coding/docs/en/third-party-agents.html",
|
||||
"env": [
|
||||
"KIMI_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "kuae-cloud-coding-plan",
|
||||
"display_name": "KUAE Cloud Coding Plan",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://coding-plan-endpoint.kuaecloud.net/v1",
|
||||
"doc": "https://docs.mthreads.com/kuaecloud/kuaecloud-doc-online/coding_plan/",
|
||||
"env": [
|
||||
"KUAE_API_KEY"
|
||||
],
|
||||
"model_count": 1
|
||||
},
|
||||
{
|
||||
"id": "cortecs",
|
||||
"display_name": "Cortecs",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.cortecs.ai/v1",
|
||||
"doc": "https://api.cortecs.ai/v1/models",
|
||||
"env": [
|
||||
"CORTECS_API_KEY"
|
||||
],
|
||||
"model_count": 21
|
||||
},
|
||||
{
|
||||
"id": "github-models",
|
||||
"display_name": "GitHub Models",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://models.github.ai/inference",
|
||||
"doc": "https://docs.github.com/en/github-models",
|
||||
"env": [
|
||||
"GITHUB_TOKEN"
|
||||
],
|
||||
"model_count": 55
|
||||
},
|
||||
{
|
||||
"id": "baseten",
|
||||
"display_name": "Baseten",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://inference.baseten.co/v1",
|
||||
"doc": "https://docs.baseten.co/development/model-apis/overview",
|
||||
"env": [
|
||||
"BASETEN_API_KEY"
|
||||
],
|
||||
"model_count": 8
|
||||
},
|
||||
{
|
||||
"id": "moark",
|
||||
"display_name": "Moark",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://moark.com/v1",
|
||||
"doc": "https://moark.com/docs/openapi/v1#tag/%E6%96%87%E6%9C%AC%E7%94%9F%E6%88%90",
|
||||
"env": [
|
||||
"MOARK_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "siliconflow",
|
||||
"display_name": "SiliconFlow",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.siliconflow.com/v1",
|
||||
"doc": "https://cloud.siliconflow.com/models",
|
||||
"env": [
|
||||
"SILICONFLOW_API_KEY"
|
||||
],
|
||||
"model_count": 70
|
||||
},
|
||||
{
|
||||
"id": "helicone",
|
||||
"display_name": "Helicone",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://ai-gateway.helicone.ai/v1",
|
||||
"doc": "https://helicone.ai/models",
|
||||
"env": [
|
||||
"HELICONE_API_KEY"
|
||||
],
|
||||
"model_count": 91
|
||||
},
|
||||
{
|
||||
"id": "huggingface",
|
||||
"display_name": "Hugging Face",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://router.huggingface.co/v1",
|
||||
"doc": "https://huggingface.co/docs/inference-providers",
|
||||
"env": [
|
||||
"HF_TOKEN"
|
||||
],
|
||||
"model_count": 20
|
||||
},
|
||||
{
|
||||
"id": "opencode",
|
||||
"display_name": "OpenCode Zen",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://opencode.ai/zen/v1",
|
||||
"doc": "https://opencode.ai/docs/zen",
|
||||
"env": [
|
||||
"OPENCODE_API_KEY"
|
||||
],
|
||||
"model_count": 37
|
||||
},
|
||||
{
|
||||
"id": "meganova",
|
||||
"display_name": "Meganova",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.meganova.ai/v1",
|
||||
"doc": "https://docs.meganova.ai",
|
||||
"env": [
|
||||
"MEGANOVA_API_KEY"
|
||||
],
|
||||
"model_count": 19
|
||||
},
|
||||
{
|
||||
"id": "fastrouter",
|
||||
"display_name": "FastRouter",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://go.fastrouter.ai/api/v1",
|
||||
"doc": "https://fastrouter.ai/models",
|
||||
"env": [
|
||||
"FASTROUTER_API_KEY"
|
||||
],
|
||||
"model_count": 14
|
||||
},
|
||||
{
|
||||
"id": "minimax",
|
||||
"display_name": "MiniMax (minimax.io)",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://api.minimax.io/anthropic/v1",
|
||||
"doc": "https://platform.minimax.io/docs/guides/quickstart",
|
||||
"env": [
|
||||
"MINIMAX_API_KEY"
|
||||
],
|
||||
"model_count": 4
|
||||
},
|
||||
{
|
||||
"id": "cloudflare-workers-ai",
|
||||
"display_name": "Cloudflare Workers AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1",
|
||||
"doc": "https://developers.cloudflare.com/workers-ai/models/",
|
||||
"env": [
|
||||
"CLOUDFLARE_ACCOUNT_ID",
|
||||
"CLOUDFLARE_API_KEY"
|
||||
],
|
||||
"model_count": 39
|
||||
},
|
||||
{
|
||||
"id": "inception",
|
||||
"display_name": "Inception",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.inceptionlabs.ai/v1/",
|
||||
"doc": "https://platform.inceptionlabs.ai/docs",
|
||||
"env": [
|
||||
"INCEPTION_API_KEY"
|
||||
],
|
||||
"model_count": 2
|
||||
},
|
||||
{
|
||||
"id": "wandb",
|
||||
"display_name": "Weights & Biases",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.inference.wandb.ai/v1",
|
||||
"doc": "https://weave-docs.wandb.ai/guides/integrations/inference/",
|
||||
"env": [
|
||||
"WANDB_API_KEY"
|
||||
],
|
||||
"model_count": 10
|
||||
},
|
||||
{
|
||||
"id": "zhipuai-coding-plan",
|
||||
"display_name": "Zhipu AI Coding Plan",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
"doc": "https://docs.bigmodel.cn/cn/coding-plan/overview",
|
||||
"env": [
|
||||
"ZHIPU_API_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "minimax-cn",
|
||||
"display_name": "MiniMax (minimaxi.com)",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://api.minimaxi.com/anthropic/v1",
|
||||
"doc": "https://platform.minimaxi.com/docs/guides/quickstart",
|
||||
"env": [
|
||||
"MINIMAX_API_KEY"
|
||||
],
|
||||
"model_count": 4
|
||||
},
|
||||
{
|
||||
"id": "zenmux",
|
||||
"display_name": "ZenMux",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://zenmux.ai/api/anthropic/v1",
|
||||
"doc": "https://docs.zenmux.ai",
|
||||
"env": [
|
||||
"ZENMUX_API_KEY"
|
||||
],
|
||||
"model_count": 67
|
||||
},
|
||||
{
|
||||
"id": "minimax-coding-plan",
|
||||
"display_name": "MiniMax Coding Plan (minimax.io)",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://api.minimax.io/anthropic/v1",
|
||||
"doc": "https://platform.minimax.io/docs/coding-plan/intro",
|
||||
"env": [
|
||||
"MINIMAX_API_KEY"
|
||||
],
|
||||
"model_count": 4
|
||||
},
|
||||
{
|
||||
"id": "ovhcloud",
|
||||
"display_name": "OVHcloud AI Endpoints",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1",
|
||||
"doc": "https://www.ovhcloud.com/en/public-cloud/ai-endpoints/catalog//",
|
||||
"env": [
|
||||
"OVHCLOUD_API_KEY"
|
||||
],
|
||||
"model_count": 13
|
||||
},
|
||||
{
|
||||
"id": "iflowcn",
|
||||
"display_name": "iFlow",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://apis.iflow.cn/v1",
|
||||
"doc": "https://platform.iflow.cn/en/docs",
|
||||
"env": [
|
||||
"IFLOW_API_KEY"
|
||||
],
|
||||
"model_count": 14
|
||||
},
|
||||
{
|
||||
"id": "synthetic",
|
||||
"display_name": "Synthetic",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.synthetic.new/v1",
|
||||
"doc": "https://synthetic.new/pricing",
|
||||
"env": [
|
||||
"SYNTHETIC_API_KEY"
|
||||
],
|
||||
"model_count": 26
|
||||
},
|
||||
{
|
||||
"id": "zhipuai",
|
||||
"display_name": "Zhipu AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://open.bigmodel.cn/api/paas/v4",
|
||||
"doc": "https://docs.z.ai/guides/overview/pricing",
|
||||
"env": [
|
||||
"ZHIPU_API_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "kilo",
|
||||
"display_name": "Kilo Gateway",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.kilo.ai/api/gateway",
|
||||
"doc": "https://kilo.ai",
|
||||
"env": [
|
||||
"KILO_API_KEY"
|
||||
],
|
||||
"model_count": 263
|
||||
},
|
||||
{
|
||||
"id": "submodel",
|
||||
"display_name": "submodel",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://llm.submodel.ai/v1",
|
||||
"doc": "https://submodel.gitbook.io",
|
||||
"env": [
|
||||
"SUBMODEL_INSTAGEN_ACCESS_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "nano-gpt",
|
||||
"display_name": "NanoGPT",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://nano-gpt.com/api/v1",
|
||||
"doc": "https://docs.nano-gpt.com",
|
||||
"env": [
|
||||
"NANO_GPT_API_KEY"
|
||||
],
|
||||
"model_count": 33
|
||||
},
|
||||
{
|
||||
"id": "zai",
|
||||
"display_name": "Z.AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.z.ai/api/paas/v4",
|
||||
"doc": "https://docs.z.ai/guides/overview/pricing",
|
||||
"env": [
|
||||
"ZHIPU_API_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "berget",
|
||||
"display_name": "Berget.AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.berget.ai/v1",
|
||||
"doc": "https://api.berget.ai",
|
||||
"env": [
|
||||
"BERGET_API_KEY"
|
||||
],
|
||||
"model_count": 8
|
||||
},
|
||||
{
|
||||
"id": "stackit",
|
||||
"display_name": "STACKIT",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.openai-compat.model-serving.eu01.onstackit.cloud/v1",
|
||||
"doc": "https://docs.stackit.cloud/products/data-and-ai/ai-model-serving/basics/available-shared-models",
|
||||
"env": [
|
||||
"STACKIT_API_KEY"
|
||||
],
|
||||
"model_count": 8
|
||||
},
|
||||
{
|
||||
"id": "inference",
|
||||
"display_name": "Inference",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://inference.net/v1",
|
||||
"doc": "https://inference.net/models",
|
||||
"env": [
|
||||
"INFERENCE_API_KEY"
|
||||
],
|
||||
"model_count": 9
|
||||
},
|
||||
{
|
||||
"id": "requesty",
|
||||
"display_name": "Requesty",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://router.requesty.ai/v1",
|
||||
"doc": "https://requesty.ai/solution/llm-routing/models",
|
||||
"env": [
|
||||
"REQUESTY_API_KEY"
|
||||
],
|
||||
"model_count": 20
|
||||
},
|
||||
{
|
||||
"id": "morph",
|
||||
"display_name": "Morph",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.morphllm.com/v1",
|
||||
"doc": "https://docs.morphllm.com/api-reference/introduction",
|
||||
"env": [
|
||||
"MORPH_API_KEY"
|
||||
],
|
||||
"model_count": 3
|
||||
},
|
||||
{
|
||||
"id": "qiniu-ai",
|
||||
"display_name": "Qiniu",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.qnaigc.com.com/v1",
|
||||
"doc": "https://developer.qiniu.com/aitokenapi",
|
||||
"env": [
|
||||
"Qiniu_API_KEY"
|
||||
],
|
||||
"model_count": 76
|
||||
},
|
||||
{
|
||||
"id": "lmstudio",
|
||||
"display_name": "LMStudio",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "http://127.0.0.1:1234/v1",
|
||||
"doc": "https://lmstudio.ai/models",
|
||||
"env": [
|
||||
"LMSTUDIO_API_KEY"
|
||||
],
|
||||
"model_count": 3
|
||||
},
|
||||
{
|
||||
"id": "friendli",
|
||||
"display_name": "Friendli",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.friendli.ai/serverless/v1",
|
||||
"doc": "https://friendli.ai/docs/guides/serverless_endpoints/introduction",
|
||||
"env": [
|
||||
"FRIENDLI_TOKEN"
|
||||
],
|
||||
"model_count": 8
|
||||
},
|
||||
{
|
||||
"id": "aihubmix",
|
||||
"display_name": "AIHubMix",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://aihubmix.com/v1",
|
||||
"doc": "https://docs.aihubmix.com",
|
||||
"env": [
|
||||
"AIHUBMIX_API_KEY"
|
||||
],
|
||||
"model_count": 43
|
||||
},
|
||||
{
|
||||
"id": "fireworks-ai",
|
||||
"display_name": "Fireworks AI",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.fireworks.ai/inference/v1/",
|
||||
"doc": "https://fireworks.ai/docs/",
|
||||
"env": [
|
||||
"FIREWORKS_API_KEY"
|
||||
],
|
||||
"model_count": 13
|
||||
},
|
||||
{
|
||||
"id": "io-net",
|
||||
"display_name": "IO.NET",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.intelligence.io.solutions/api/v1",
|
||||
"doc": "https://io.net/docs/guides/intelligence/io-intelligence",
|
||||
"env": [
|
||||
"IOINTELLIGENCE_API_KEY"
|
||||
],
|
||||
"model_count": 17
|
||||
},
|
||||
{
|
||||
"id": "modelscope",
|
||||
"display_name": "ModelScope",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api-inference.modelscope.cn/v1",
|
||||
"doc": "https://modelscope.cn/docs/model-service/API-Inference/intro",
|
||||
"env": [
|
||||
"MODELSCOPE_API_KEY"
|
||||
],
|
||||
"model_count": 7
|
||||
},
|
||||
{
|
||||
"id": "meta-llama",
|
||||
"display_name": "Llama",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.llama.com/compat/v1/",
|
||||
"doc": "https://llama.developer.meta.com/docs/models",
|
||||
"env": [
|
||||
"LLAMA_API_KEY"
|
||||
],
|
||||
"model_count": 7
|
||||
},
|
||||
{
|
||||
"id": "scaleway",
|
||||
"display_name": "Scaleway",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.scaleway.ai/v1",
|
||||
"doc": "https://www.scaleway.com/en/docs/generative-apis/",
|
||||
"env": [
|
||||
"SCALEWAY_API_KEY"
|
||||
],
|
||||
"model_count": 14
|
||||
},
|
||||
{
|
||||
"id": "poe",
|
||||
"display_name": "Poe",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"api": "https://api.poe.com/v1",
|
||||
"doc": "https://creator.poe.com/docs/external-applications/openai-compatible-api",
|
||||
"env": [
|
||||
"POE_API_KEY"
|
||||
],
|
||||
"model_count": 114
|
||||
},
|
||||
{
|
||||
"id": "minimax-cn-coding-plan",
|
||||
"display_name": "MiniMax Coding Plan (minimaxi.com)",
|
||||
"npm": "@ai-sdk/anthropic",
|
||||
"api": "https://api.minimaxi.com/anthropic/v1",
|
||||
"doc": "https://platform.minimaxi.com/docs/coding-plan/intro",
|
||||
"env": [
|
||||
"MINIMAX_API_KEY"
|
||||
],
|
||||
"model_count": 4
|
||||
}
|
||||
]
|
||||
@@ -83,6 +83,14 @@ impl CanonicalModelRegistry {
|
||||
self.models.get(&(provider.to_string(), model.to_string()))
|
||||
}
|
||||
|
||||
pub fn get_all_models_for_provider(&self, provider: &str) -> Vec<CanonicalModel> {
|
||||
self.models
|
||||
.iter()
|
||||
.filter(|((p, _), _)| p == provider)
|
||||
.map(|(_, model)| model.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn all_models(&self) -> Vec<&CanonicalModel> {
|
||||
self.models.values().collect()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod azureauth;
|
||||
pub mod base;
|
||||
pub mod bedrock;
|
||||
pub mod canonical;
|
||||
pub mod catalog;
|
||||
pub mod chatgpt_codex;
|
||||
pub mod claude_code;
|
||||
pub(crate) mod cli_common;
|
||||
|
||||
Reference in New Issue
Block a user