From e6c92aeb27cb9196b96150d9c3f7f1bb54d06c99 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 23 Jul 2026 06:50:26 -0400 Subject: [PATCH] Add current GPT and Claude models to OpenRouter catalog --- lib/crates/fabro-model/src/catalog.rs | 125 +++++++++++++++++ .../src/catalog/providers/openrouter.toml | 130 ++++++++++++++++++ 2 files changed, 255 insertions(+) diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index 9ae07fc8b..ce586679e 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -2135,6 +2135,131 @@ enabled = true ); } + #[test] + fn builtin_openrouter_includes_gpt_5_6_and_current_claude_models_when_enabled() { + 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 expected = [ + ( + "openai/gpt-5.6-sol", + "openai/gpt-5.6-sol", + "gpt-5", + 1_050_000, + 5.0, + 30.0, + 0.5, + ReasoningEffortFeature::Levels, + false, + BillingPolicy::OpenAi, + ), + ( + "openai/gpt-5.6-terra", + "openai/gpt-5.6-terra", + "gpt-5", + 1_050_000, + 2.5, + 15.0, + 0.25, + ReasoningEffortFeature::Levels, + false, + BillingPolicy::OpenAi, + ), + ( + "openai/gpt-5.6-luna", + "openai/gpt-5.6-luna", + "gpt-5", + 1_050_000, + 1.0, + 6.0, + 0.1, + ReasoningEffortFeature::Levels, + false, + BillingPolicy::OpenAi, + ), + ( + "anthropic/claude-opus-4-8", + "anthropic/claude-opus-4.8", + "claude-4", + 1_000_000, + 5.0, + 25.0, + 0.5, + ReasoningEffortFeature::Levels, + false, + BillingPolicy::Anthropic, + ), + ( + "anthropic/claude-fable-5", + "anthropic/claude-fable-5", + "claude-5", + 1_000_000, + 10.0, + 50.0, + 1.0, + ReasoningEffortFeature::AlwaysAdaptive, + false, + BillingPolicy::Anthropic, + ), + ]; + + for ( + id, + api_id, + family, + context_window, + input_cost, + output_cost, + cache_input_cost, + reasoning_effort, + sampling_params, + billing_policy, + ) in expected + { + let model = catalog + .get(id) + .unwrap_or_else(|| panic!("OpenRouter model '{id}' should be present")); + assert_eq!(model.provider, ProviderId::new("openrouter"), "{id}"); + assert_eq!(model.family, family, "{id}"); + assert_eq!(model.limits.context_window, context_window, "{id}"); + assert_eq!(model.limits.max_output, Some(128_000), "{id}"); + assert!(model.features.tools, "{id}"); + assert!(model.features.vision, "{id}"); + assert!(model.features.reasoning, "{id}"); + assert!(model.features.prompt_cache, "{id}"); + assert_eq!(model.features.reasoning_effort, reasoning_effort, "{id}"); + assert_eq!(model.features.sampling_params, sampling_params, "{id}"); + assert_eq!(model.costs.input_cost_per_mtok, Some(input_cost), "{id}"); + assert_eq!(model.costs.output_cost_per_mtok, Some(output_cost), "{id}"); + assert_eq!( + model.costs.cache_input_cost_per_mtok, + Some(cache_input_cost), + "{id}" + ); + + let settings = catalog + .model_settings(id) + .unwrap_or_else(|| panic!("OpenRouter settings for '{id}' should be present")); + assert_eq!(settings.api_id, api_id, "{id}"); + assert_eq!(settings.billing_policy, billing_policy, "{id}"); + assert_eq!( + catalog.get(api_id).map(|model| model.id.as_str()), + Some(id), + "{id}" + ); + assert_eq!( + settings.controls.reasoning_effort, + ReasoningEffort::VARIANTS, + "{id}" + ); + } + } + #[test] fn builtin_openrouter_includes_glm_5_2_when_enabled() { let catalog = Catalog::from_builtin_with_overrides(&minimal_settings( diff --git a/lib/crates/fabro-model/src/catalog/providers/openrouter.toml b/lib/crates/fabro-model/src/catalog/providers/openrouter.toml index 86fd43319..d01fcb3dc 100644 --- a/lib/crates/fabro-model/src/catalog/providers/openrouter.toml +++ b/lib/crates/fabro-model/src/catalog/providers/openrouter.toml @@ -33,6 +33,57 @@ credentials = ["env:OPENROUTER_API_KEY", "vault:OPENROUTER_API_KEY"] # best-effort estimates; OpenRouter returns the authoritative usage.cost # in-band on every response. +[models."anthropic/claude-fable-5"] +provider = "openrouter" +api_id = "anthropic/claude-fable-5" +display_name = "Claude Fable 5 (via OpenRouter)" +family = "claude-5" +billing_policy = "anthropic" + +[models."anthropic/claude-fable-5".limits] +context_window = 1000000 +max_output = 128000 + +[models."anthropic/claude-fable-5".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "always_adaptive" +prompt_cache = true +sampling_params = false + +[models."anthropic/claude-fable-5".costs] +input_cost_per_mtok = 10.0 +output_cost_per_mtok = 50.0 +cache_input_cost_per_mtok = 1.0 + +[models."anthropic/claude-opus-4-8"] +provider = "openrouter" +api_id = "anthropic/claude-opus-4.8" +display_name = "Claude Opus 4.8 (via OpenRouter)" +family = "claude-4" +billing_policy = "anthropic" +training = "2026-01-01" +knowledge_cutoff = "Jan 2026" +aliases = ["anthropic/claude-opus-4.8"] + +[models."anthropic/claude-opus-4-8".limits] +context_window = 1000000 +max_output = 128000 + +[models."anthropic/claude-opus-4-8".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +prompt_cache = true +sampling_params = false + +[models."anthropic/claude-opus-4-8".costs] +input_cost_per_mtok = 5.0 +output_cost_per_mtok = 25.0 +cache_input_cost_per_mtok = 0.5 + [models."anthropic/claude-opus-4-7"] provider = "openrouter" api_id = "anthropic/claude-opus-4.7" @@ -103,6 +154,85 @@ cache_input_cost_per_mtok = 0.1 # ---------- OpenAI via OpenRouter ---------- +# GPT-5.6 entries use OpenRouter's base rates below the 272k-token +# long-context threshold. Authoritative in-band usage.cost covers +# long-context and cache-write pricing. + +[models."openai/gpt-5.6-sol"] +provider = "openrouter" +api_id = "openai/gpt-5.6-sol" +display_name = "GPT-5.6 Sol (via OpenRouter)" +family = "gpt-5" +training = "2026-02-16" +knowledge_cutoff = "February 16, 2026" + +[models."openai/gpt-5.6-sol".limits] +context_window = 1050000 +max_output = 128000 + +[models."openai/gpt-5.6-sol".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +prompt_cache = true +sampling_params = false + +[models."openai/gpt-5.6-sol".costs] +input_cost_per_mtok = 5.0 +output_cost_per_mtok = 30.0 +cache_input_cost_per_mtok = 0.5 + +[models."openai/gpt-5.6-terra"] +provider = "openrouter" +api_id = "openai/gpt-5.6-terra" +display_name = "GPT-5.6 Terra (via OpenRouter)" +family = "gpt-5" +training = "2026-02-16" +knowledge_cutoff = "February 16, 2026" + +[models."openai/gpt-5.6-terra".limits] +context_window = 1050000 +max_output = 128000 + +[models."openai/gpt-5.6-terra".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +prompt_cache = true +sampling_params = false + +[models."openai/gpt-5.6-terra".costs] +input_cost_per_mtok = 2.5 +output_cost_per_mtok = 15.0 +cache_input_cost_per_mtok = 0.25 + +[models."openai/gpt-5.6-luna"] +provider = "openrouter" +api_id = "openai/gpt-5.6-luna" +display_name = "GPT-5.6 Luna (via OpenRouter)" +family = "gpt-5" +training = "2026-02-16" +knowledge_cutoff = "February 16, 2026" + +[models."openai/gpt-5.6-luna".limits] +context_window = 1050000 +max_output = 128000 + +[models."openai/gpt-5.6-luna".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "levels" +prompt_cache = true +sampling_params = false + +[models."openai/gpt-5.6-luna".costs] +input_cost_per_mtok = 1.0 +output_cost_per_mtok = 6.0 +cache_input_cost_per_mtok = 0.1 + [models."openai/gpt-5.4"] provider = "openrouter" api_id = "openai/gpt-5.4"