mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-29 01:42:21 +00:00
feat(catalog): add OpenRouter as opt-in built-in provider
Ship a curated 17-model OpenRouter catalog that's disabled by
default. Users opt in with a single section in settings.toml:
[llm.providers.openrouter]
enabled = true
The provider reuses the existing OpenAiCompatibleAdapter via the
catalog's adapter = "openai_compatible" setting, with attribution
headers (HTTP-Referer, X-Title) injected through extra_headers.
Model IDs follow OpenRouter's vendor-namespaced slug convention
(anthropic/claude-sonnet-4-6, deepseek/deepseek-v4-flash, etc.)
so they don't collide with direct-provider entries.
OPENROUTER_API_KEY is registered as an optional vault secret across
the env var registry and secret classification, matching the
treatment of other LLM provider keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e2d1d7203c
commit
f326b9421a
4 changed files with 399 additions and 0 deletions
|
|
@ -1817,6 +1817,40 @@ enabled = true
|
|||
assert!(catalog.default_for_provider(&ollama).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_openrouter_provider_is_opt_in() {
|
||||
let openrouter = ProviderId::new("openrouter");
|
||||
let builtin = Catalog::builtin();
|
||||
|
||||
assert!(builtin.provider(&openrouter).is_none());
|
||||
assert!(builtin.list(Some(&openrouter)).is_empty());
|
||||
|
||||
let catalog = Catalog::from_builtin_with_overrides(&minimal_settings(
|
||||
r"
|
||||
[providers.openrouter]
|
||||
enabled = true
|
||||
",
|
||||
))
|
||||
.expect("enabled OpenRouter override should build from the built-in provider settings");
|
||||
|
||||
let provider = catalog
|
||||
.provider(&openrouter)
|
||||
.expect("enabled OpenRouter provider should be present");
|
||||
assert_eq!(provider.adapter, AdapterKind::OpenAiCompatible);
|
||||
assert_eq!(
|
||||
provider.base_url.as_deref(),
|
||||
Some("https://openrouter.ai/api/v1")
|
||||
);
|
||||
|
||||
let models = catalog.list(Some(&openrouter));
|
||||
assert_eq!(models.len(), 17);
|
||||
|
||||
let default = catalog
|
||||
.default_for_provider(&openrouter)
|
||||
.expect("openrouter catalog should have a default model");
|
||||
assert_eq!(default.id, "anthropic/claude-sonnet-4-6");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_get_by_id() {
|
||||
let m = Catalog::builtin().get("claude-opus-4-6").unwrap();
|
||||
|
|
|
|||
361
lib/crates/fabro-model/src/catalog/providers/openrouter.toml
Normal file
361
lib/crates/fabro-model/src/catalog/providers/openrouter.toml
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
[providers.openrouter]
|
||||
display_name = "OpenRouter"
|
||||
adapter = "openai_compatible"
|
||||
api_key_url = "https://openrouter.ai/keys"
|
||||
base_url = "https://openrouter.ai/api/v1"
|
||||
priority = 25
|
||||
enabled = false
|
||||
|
||||
[providers.openrouter.auth]
|
||||
credentials = ["env:OPENROUTER_API_KEY", "vault:OPENROUTER_API_KEY"]
|
||||
|
||||
[providers.openrouter.extra_headers]
|
||||
"HTTP-Referer" = { literal = "https://fabro.sh" }
|
||||
"X-Title" = { literal = "Fabro" }
|
||||
|
||||
# To enable OpenRouter, add the following to ~/.fabro/settings.toml:
|
||||
#
|
||||
# [llm.providers.openrouter]
|
||||
# enabled = true
|
||||
#
|
||||
# Then run `fabro provider login openrouter` to store the API key,
|
||||
# or set the OPENROUTER_API_KEY environment variable.
|
||||
|
||||
# ---------- Anthropic via OpenRouter ----------
|
||||
|
||||
[models."anthropic/claude-opus-4-7"]
|
||||
provider = "openrouter"
|
||||
api_id = "anthropic/claude-opus-4.7"
|
||||
display_name = "Claude Opus 4.7 (via OpenRouter)"
|
||||
family = "or-claude-4"
|
||||
|
||||
[models."anthropic/claude-opus-4-7".limits]
|
||||
context_window = 1000000
|
||||
max_output = 128000
|
||||
|
||||
[models."anthropic/claude-opus-4-7".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-opus-4-7".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 25.0
|
||||
cache_input_cost_per_mtok = 0.5
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6"]
|
||||
provider = "openrouter"
|
||||
api_id = "anthropic/claude-sonnet-4.6"
|
||||
display_name = "Claude Sonnet 4.6 (via OpenRouter)"
|
||||
family = "or-claude-4"
|
||||
default = true
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".limits]
|
||||
context_window = 1000000
|
||||
max_output = 64000
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-sonnet-4-6".costs]
|
||||
input_cost_per_mtok = 3.0
|
||||
output_cost_per_mtok = 15.0
|
||||
cache_input_cost_per_mtok = 0.3
|
||||
|
||||
[models."anthropic/claude-haiku-4-5"]
|
||||
provider = "openrouter"
|
||||
api_id = "anthropic/claude-haiku-4.5"
|
||||
display_name = "Claude Haiku 4.5 (via OpenRouter)"
|
||||
family = "or-claude-4"
|
||||
small_default = true
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".limits]
|
||||
context_window = 200000
|
||||
max_output = 8192
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
prompt_cache = true
|
||||
|
||||
[models."anthropic/claude-haiku-4-5".costs]
|
||||
input_cost_per_mtok = 1.0
|
||||
output_cost_per_mtok = 5.0
|
||||
cache_input_cost_per_mtok = 0.1
|
||||
|
||||
# ---------- OpenAI via OpenRouter ----------
|
||||
|
||||
[models."openai/gpt-5.4"]
|
||||
provider = "openrouter"
|
||||
api_id = "openai/gpt-5.4"
|
||||
display_name = "GPT-5.4 (via OpenRouter)"
|
||||
family = "or-gpt-5"
|
||||
|
||||
[models."openai/gpt-5.4".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[models."openai/gpt-5.4".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."openai/gpt-5.4".costs]
|
||||
input_cost_per_mtok = 2.5
|
||||
output_cost_per_mtok = 15.0
|
||||
|
||||
[models."openai/gpt-5.5"]
|
||||
provider = "openrouter"
|
||||
api_id = "openai/gpt-5.5"
|
||||
display_name = "GPT-5.5 (via OpenRouter)"
|
||||
family = "or-gpt-5"
|
||||
|
||||
[models."openai/gpt-5.5".limits]
|
||||
context_window = 1050000
|
||||
max_output = 32768
|
||||
|
||||
[models."openai/gpt-5.5".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."openai/gpt-5.5".costs]
|
||||
input_cost_per_mtok = 5.0
|
||||
output_cost_per_mtok = 30.0
|
||||
|
||||
# ---------- Google Gemini via OpenRouter ----------
|
||||
|
||||
[models."google/gemini-3.1-pro-preview"]
|
||||
provider = "openrouter"
|
||||
api_id = "google/gemini-3.1-pro-preview"
|
||||
display_name = "Gemini 3.1 Pro Preview (via OpenRouter)"
|
||||
family = "or-gemini-3"
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = true
|
||||
|
||||
[models."google/gemini-3.1-pro-preview".costs]
|
||||
input_cost_per_mtok = 2.0
|
||||
output_cost_per_mtok = 12.0
|
||||
|
||||
[models."google/gemini-3.5-flash"]
|
||||
provider = "openrouter"
|
||||
api_id = "google/gemini-3.5-flash"
|
||||
display_name = "Gemini 3.5 Flash (via OpenRouter)"
|
||||
family = "or-gemini-3"
|
||||
|
||||
[models."google/gemini-3.5-flash".limits]
|
||||
context_window = 1048576
|
||||
max_output = 65536
|
||||
|
||||
[models."google/gemini-3.5-flash".features]
|
||||
tools = true
|
||||
vision = true
|
||||
reasoning = false
|
||||
|
||||
[models."google/gemini-3.5-flash".costs]
|
||||
input_cost_per_mtok = 1.5
|
||||
output_cost_per_mtok = 9.0
|
||||
|
||||
# ---------- Open-weights models ----------
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro"]
|
||||
provider = "openrouter"
|
||||
api_id = "xiaomi/mimo-v2.5-pro"
|
||||
display_name = "Xiaomi MiMo v2.5 Pro"
|
||||
family = "or-mimo"
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."xiaomi/mimo-v2.5-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
|
||||
[models."minimax/minimax-m2.7"]
|
||||
provider = "openrouter"
|
||||
api_id = "minimax/minimax-m2.7"
|
||||
display_name = "MiniMax M2.7"
|
||||
family = "or-minimax"
|
||||
|
||||
[models."minimax/minimax-m2.7".limits]
|
||||
context_window = 200000
|
||||
max_output = 16384
|
||||
|
||||
[models."minimax/minimax-m2.7".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."minimax/minimax-m2.7".costs]
|
||||
input_cost_per_mtok = 0.28
|
||||
output_cost_per_mtok = 1.20
|
||||
|
||||
[models."deepseek/deepseek-v4-pro"]
|
||||
provider = "openrouter"
|
||||
api_id = "deepseek/deepseek-v4-pro"
|
||||
display_name = "DeepSeek V4 Pro"
|
||||
family = "or-deepseek"
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = true
|
||||
|
||||
[models."deepseek/deepseek-v4-pro".costs]
|
||||
input_cost_per_mtok = 0.435
|
||||
output_cost_per_mtok = 0.87
|
||||
|
||||
[models."deepseek/deepseek-v4-flash"]
|
||||
provider = "openrouter"
|
||||
api_id = "deepseek/deepseek-v4-flash"
|
||||
display_name = "DeepSeek V4 Flash"
|
||||
family = "or-deepseek"
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."deepseek/deepseek-v4-flash".costs]
|
||||
input_cost_per_mtok = 0.10
|
||||
output_cost_per_mtok = 0.20
|
||||
|
||||
[models."moonshotai/kimi-k2.6"]
|
||||
provider = "openrouter"
|
||||
api_id = "moonshotai/kimi-k2.6"
|
||||
display_name = "Kimi K2.6"
|
||||
family = "or-kimi"
|
||||
|
||||
[models."moonshotai/kimi-k2.6".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[models."moonshotai/kimi-k2.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."moonshotai/kimi-k2.6".costs]
|
||||
input_cost_per_mtok = 0.73
|
||||
output_cost_per_mtok = 3.49
|
||||
|
||||
[models."qwen/qwen3-coder"]
|
||||
provider = "openrouter"
|
||||
api_id = "qwen/qwen3-coder"
|
||||
display_name = "Qwen3 Coder"
|
||||
family = "or-qwen-3"
|
||||
|
||||
[models."qwen/qwen3-coder".limits]
|
||||
context_window = 1050000
|
||||
max_output = 16384
|
||||
|
||||
[models."qwen/qwen3-coder".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."qwen/qwen3-coder".costs]
|
||||
input_cost_per_mtok = 0.22
|
||||
output_cost_per_mtok = 1.80
|
||||
|
||||
[models."qwen/qwen3.6-flash"]
|
||||
provider = "openrouter"
|
||||
api_id = "qwen/qwen3.6-flash"
|
||||
display_name = "Qwen3.6 Flash"
|
||||
family = "or-qwen-3"
|
||||
|
||||
[models."qwen/qwen3.6-flash".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[models."qwen/qwen3.6-flash".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."qwen/qwen3.6-flash".costs]
|
||||
input_cost_per_mtok = 0.1875
|
||||
output_cost_per_mtok = 1.125
|
||||
|
||||
[models."z-ai/glm-4.6"]
|
||||
provider = "openrouter"
|
||||
api_id = "z-ai/glm-4.6"
|
||||
display_name = "GLM 4.6"
|
||||
family = "or-glm"
|
||||
|
||||
[models."z-ai/glm-4.6".limits]
|
||||
context_window = 203000
|
||||
max_output = 16384
|
||||
|
||||
[models."z-ai/glm-4.6".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."z-ai/glm-4.6".costs]
|
||||
input_cost_per_mtok = 0.43
|
||||
output_cost_per_mtok = 1.74
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b"]
|
||||
provider = "openrouter"
|
||||
api_id = "nvidia/nemotron-3-super-120b-a12b"
|
||||
display_name = "NVIDIA Nemotron 3 Super 120B"
|
||||
family = "or-nemotron"
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".limits]
|
||||
context_window = 1000000
|
||||
max_output = 16384
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."nvidia/nemotron-3-super-120b-a12b".costs]
|
||||
input_cost_per_mtok = 0.09
|
||||
output_cost_per_mtok = 0.45
|
||||
|
||||
[models."mistralai/devstral-2512"]
|
||||
provider = "openrouter"
|
||||
api_id = "mistralai/devstral-2512"
|
||||
display_name = "Devstral 2512"
|
||||
family = "or-devstral"
|
||||
|
||||
[models."mistralai/devstral-2512".limits]
|
||||
context_window = 262144
|
||||
max_output = 16384
|
||||
|
||||
[models."mistralai/devstral-2512".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
|
||||
[models."mistralai/devstral-2512".costs]
|
||||
input_cost_per_mtok = 0.40
|
||||
output_cost_per_mtok = 2.00
|
||||
|
|
@ -56,6 +56,7 @@ impl EnvVars {
|
|||
pub const OPENAI_PROJECT: &'static str = "OPENAI_PROJECT";
|
||||
pub const OPENAI_ORG_ID: &'static str = "OPENAI_ORG_ID";
|
||||
pub const OPENAI_PROJECT_ID: &'static str = "OPENAI_PROJECT_ID";
|
||||
pub const OPENROUTER_API_KEY: &'static str = "OPENROUTER_API_KEY";
|
||||
pub const ZAI_API_KEY: &'static str = "ZAI_API_KEY";
|
||||
|
||||
// GitHub, OAuth, and Slack
|
||||
|
|
@ -193,6 +194,7 @@ mod tests {
|
|||
EnvVars::OPENAI_PROJECT,
|
||||
EnvVars::OPENAI_ORG_ID,
|
||||
EnvVars::OPENAI_PROJECT_ID,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
EnvVars::GH_TOKEN,
|
||||
EnvVars::GITHUB_APP_CLIENT_SECRET,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const OPTIONAL_VAULT_SECRETS: &[&str] = &[
|
|||
EnvVars::KIMI_API_KEY,
|
||||
EnvVars::MINIMAX_API_KEY,
|
||||
EnvVars::OPENAI_API_KEY,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
EnvVars::DAYTONA_API_KEY,
|
||||
];
|
||||
|
|
@ -90,6 +91,7 @@ mod tests {
|
|||
EnvVars::KIMI_API_KEY,
|
||||
EnvVars::MINIMAX_API_KEY,
|
||||
EnvVars::OPENAI_API_KEY,
|
||||
EnvVars::OPENROUTER_API_KEY,
|
||||
EnvVars::ZAI_API_KEY,
|
||||
] {
|
||||
assert_eq!(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue