diff --git a/lib/crates/fabro-llm/tests/integration.rs b/lib/crates/fabro-llm/tests/integration.rs index 898004e63..18d6ef6af 100644 --- a/lib/crates/fabro-llm/tests/integration.rs +++ b/lib/crates/fabro-llm/tests/integration.rs @@ -5,7 +5,10 @@ use std::sync::Arc; +use fabro_auth::ApiCredential; +use fabro_llm::client::Client; use fabro_llm::error::ProviderErrorKind; +use fabro_llm::model_test::{ModelTestStatus, run_model_test}; use fabro_llm::provider::ProviderAdapter; use fabro_llm::providers::{ AnthropicAdapter, BedrockAdapter, GeminiAdapter, OpenAiAdapter, OpenAiCompatibleAdapter, @@ -13,7 +16,8 @@ use fabro_llm::providers::{ use fabro_llm::types::{ CostSource, FinishReason, Message, ReasoningEffort, Request, ToolChoice, ToolDefinition, }; -use fabro_model::Catalog; +use fabro_model::catalog::{LlmCatalogSettings, ProviderCatalogSettings}; +use fabro_model::{Catalog, ModelTestMode, ProviderId}; use fabro_static::EnvVars; fn make_request(model: &str) -> Request { @@ -332,6 +336,42 @@ async fn openrouter_complete() { assert_eq!(response.cost_source, Some(CostSource::Authoritative)); } +#[fabro_macros::e2e_test(live("OPENROUTER_API_KEY"))] +async fn openrouter_kimi_k3_deep_tool_round_trip() { + let api_key = + std::env::var(EnvVars::OPENROUTER_API_KEY).expect("OPENROUTER_API_KEY must be set"); + let provider = ProviderId::new("openrouter"); + let mut settings = LlmCatalogSettings::default(); + settings + .providers + .insert(provider.to_string(), ProviderCatalogSettings { + enabled: Some(true), + ..ProviderCatalogSettings::default() + }); + let catalog = Arc::new( + Catalog::from_builtin_with_overrides(&settings) + .expect("enabled OpenRouter catalog should build"), + ); + let credential = ApiCredential::from_api_key(provider, api_key, &catalog) + .expect("OpenRouter credential should resolve from the catalog"); + let client = Arc::new( + Client::from_credentials(vec![credential], Arc::clone(&catalog)) + .await + .expect("OpenRouter client should build from the catalog"), + ); + let model = catalog + .get("moonshotai/kimi-k3") + .expect("OpenRouter Kimi K3 should be present"); + + let outcome = run_model_test(model, ModelTestMode::Deep, client).await; + assert_eq!( + outcome.status, + ModelTestStatus::Ok, + "OpenRouter Kimi K3 deep test failed: {:?}", + outcome.error_message + ); +} + async fn run_multi_turn_cache_test( adapter: &dyn ProviderAdapter, model: &str, diff --git a/lib/crates/fabro-model/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs index 2bd6b932f..811978968 100644 --- a/lib/crates/fabro-model/src/catalog.rs +++ b/lib/crates/fabro-model/src/catalog.rs @@ -2056,6 +2056,71 @@ enabled = true ); } + #[test] + fn builtin_openrouter_includes_kimi_k3_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 model = catalog + .get("moonshotai/kimi-k3") + .expect("OpenRouter Kimi K3 should be present"); + insta::assert_debug_snapshot!(model, @r#" + Model { + id: "moonshotai/kimi-k3", + provider: openrouter, + family: "kimi-k3", + display_name: "Kimi K3 (via OpenRouter)", + limits: ModelLimits { + context_window: 1048576, + max_output: Some( + 131072, + ), + }, + training: None, + knowledge_cutoff: None, + features: ModelFeatures { + tools: true, + vision: true, + reasoning: true, + reasoning_effort: AlwaysAdaptive, + prompt_cache: true, + sampling_params: false, + }, + costs: ModelCosts { + input_cost_per_mtok: Some( + 3.0, + ), + output_cost_per_mtok: Some( + 15.0, + ), + cache_input_cost_per_mtok: Some( + 0.3, + ), + }, + estimated_output_tps: None, + aliases: [], + default: false, + small_default: false, + configured: false, + } + "#); + + let settings = catalog + .model_settings("moonshotai/kimi-k3") + .expect("OpenRouter Kimi K3 settings should be present"); + assert_eq!(settings.api_id, "moonshotai/kimi-k3"); + assert_eq!(settings.controls.reasoning_effort, vec![ + ReasoningEffort::Low, + ReasoningEffort::High, + ReasoningEffort::Max, + ]); + } + #[test] fn builtin_ollama_provider_is_opt_in() { let ollama = ProviderId::new("ollama"); diff --git a/lib/crates/fabro-model/src/catalog/providers/openrouter.toml b/lib/crates/fabro-model/src/catalog/providers/openrouter.toml index 4759b8996..0c3393a22 100644 --- a/lib/crates/fabro-model/src/catalog/providers/openrouter.toml +++ b/lib/crates/fabro-model/src/catalog/providers/openrouter.toml @@ -278,6 +278,32 @@ reasoning = false input_cost_per_mtok = 0.73 output_cost_per_mtok = 3.49 +[models."moonshotai/kimi-k3"] +provider = "openrouter" +api_id = "moonshotai/kimi-k3" +display_name = "Kimi K3 (via OpenRouter)" +family = "kimi-k3" + +[models."moonshotai/kimi-k3".limits] +context_window = 1048576 +max_output = 131072 + +[models."moonshotai/kimi-k3".features] +tools = true +vision = true +reasoning = true +reasoning_effort = "always_adaptive" +prompt_cache = true +sampling_params = false + +[models."moonshotai/kimi-k3".controls] +reasoning_effort = ["low", "high", "max"] + +[models."moonshotai/kimi-k3".costs] +input_cost_per_mtok = 3.0 +output_cost_per_mtok = 15.0 +cache_input_cost_per_mtok = 0.3 + [models."qwen/qwen3-coder"] provider = "openrouter" api_id = "qwen/qwen3-coder"