diff --git a/Cargo.lock b/Cargo.lock index 36ed228c1..d14944a41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,6 +1248,7 @@ dependencies = [ "fabro-config", "fabro-llm", "fabro-mcp", + "fabro-model", "fabro-sandbox", "fabro-util", "futures", @@ -1287,6 +1288,7 @@ dependencies = [ "fabro-hooks", "fabro-interview", "fabro-llm", + "fabro-model", "fabro-retro", "fabro-sandbox", "fabro-types", @@ -1360,6 +1362,7 @@ dependencies = [ "fabro-interview", "fabro-llm", "fabro-mcp", + "fabro-model", "fabro-openai-oauth", "fabro-retro", "fabro-sandbox", @@ -1487,6 +1490,7 @@ dependencies = [ "fabro-agent", "fabro-config", "fabro-llm", + "fabro-model", "fabro-util", "mockito", "regex", @@ -1524,6 +1528,7 @@ dependencies = [ "cli-table", "dialoguer", "dotenvy", + "fabro-model", "fabro-util", "futures", "http", @@ -1556,6 +1561,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "fabro-model" +version = "0.176.2" +dependencies = [ + "insta", + "serde", + "serde_json", +] + [[package]] name = "fabro-openai-oauth" version = "0.176.2" @@ -1715,7 +1729,7 @@ name = "fabro-validate" version = "0.176.2" dependencies = [ "fabro-graphviz", - "fabro-llm", + "fabro-model", "serde", "thiserror 2.0.18", ] @@ -1741,6 +1755,7 @@ dependencies = [ "fabro-interview", "fabro-llm", "fabro-mcp", + "fabro-model", "fabro-retro", "fabro-sandbox", "fabro-util", diff --git a/lib/crates/fabro-agent/Cargo.toml b/lib/crates/fabro-agent/Cargo.toml index 8f23c6819..6f8f57370 100644 --- a/lib/crates/fabro-agent/Cargo.toml +++ b/lib/crates/fabro-agent/Cargo.toml @@ -23,6 +23,7 @@ anyhow.workspace = true dotenvy.workspace = true fabro-config = { path = "../fabro-config", features = ["clap"] } fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } fabro-mcp = { path = "../fabro-mcp" } fabro-sandbox = { path = "../fabro-sandbox" } fabro-util = { path = "../fabro-util" } diff --git a/lib/crates/fabro-agent/src/cli.rs b/lib/crates/fabro-agent/src/cli.rs index 4b7a1ba94..c15c48ddb 100644 --- a/lib/crates/fabro-agent/src/cli.rs +++ b/lib/crates/fabro-agent/src/cli.rs @@ -6,7 +6,7 @@ use crate::{ }; use clap::{Args, Parser}; use fabro_llm::client::Client; -use fabro_llm::provider::{ModelId, Provider}; +use fabro_model::{ModelId, Provider}; use fabro_util::terminal::Styles; use std::io::{IsTerminal, Write}; use std::path::PathBuf; @@ -386,8 +386,8 @@ pub async fn run_with_args_and_client( // Resolve model and build profile let model = args.model.unwrap_or_else(|| { - fabro_llm::catalog::default_model_for_provider(provider.as_str()) - .unwrap_or_else(fabro_llm::catalog::default_model_from_env) + fabro_model::default_model_for_provider(provider.as_str()) + .unwrap_or_else(fabro_model::default_model_from_env) .id }); eprintln!("{}", styles.dim.apply_to(format!("Using model: {model}"))); @@ -634,7 +634,7 @@ pub async fn run() -> anyhow::Result<()> { #[cfg(test)] mod tests { use super::*; - use fabro_llm::provider::Provider; + use fabro_model::Provider; use serde_json::json; static NO_COLOR: std::sync::LazyLock = std::sync::LazyLock::new(|| Styles::new(false)); diff --git a/lib/crates/fabro-agent/src/memory.rs b/lib/crates/fabro-agent/src/memory.rs index 8c0e3a9e3..982572b10 100644 --- a/lib/crates/fabro-agent/src/memory.rs +++ b/lib/crates/fabro-agent/src/memory.rs @@ -1,5 +1,5 @@ use crate::sandbox::Sandbox; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use std::collections::HashSet; use tracing::{debug, info, warn}; diff --git a/lib/crates/fabro-agent/src/profiles/anthropic.rs b/lib/crates/fabro-agent/src/profiles/anthropic.rs index 55920409b..7bceb3470 100644 --- a/lib/crates/fabro-agent/src/profiles/anthropic.rs +++ b/lib/crates/fabro-agent/src/profiles/anthropic.rs @@ -6,7 +6,7 @@ use crate::sandbox::Sandbox; use crate::skills::Skill; use crate::tool_registry::ToolRegistry; use crate::tools::{make_edit_file_tool, register_core_tools, WebFetchSummarizer}; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use super::EnvContext; @@ -164,7 +164,7 @@ in the project. Keep changes minimal and focused on the task."; } fn capabilities(&self) -> ProfileCapabilities { - let context_window_size = fabro_llm::catalog::get_model_info(self.model()) + let context_window_size = fabro_model::get_model_info(self.model()) .map(|info| info.limits.context_window as usize) .unwrap_or_else(|| { if self.model().contains("opus-4-6") { diff --git a/lib/crates/fabro-agent/src/profiles/gemini.rs b/lib/crates/fabro-agent/src/profiles/gemini.rs index d70c45569..5f93ecbcf 100644 --- a/lib/crates/fabro-agent/src/profiles/gemini.rs +++ b/lib/crates/fabro-agent/src/profiles/gemini.rs @@ -9,7 +9,7 @@ use crate::tools::{ make_edit_file_tool, make_list_dir_tool, make_read_many_files_tool, register_core_tools, WebFetchSummarizer, }; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use super::EnvContext; @@ -199,7 +199,7 @@ in the project."; } fn capabilities(&self) -> ProfileCapabilities { - let context_window_size = fabro_llm::catalog::get_model_info(self.model()) + let context_window_size = fabro_model::get_model_info(self.model()) .map(|info| info.limits.context_window as usize) .unwrap_or(1_000_000); ProfileCapabilities { diff --git a/lib/crates/fabro-agent/src/profiles/mod.rs b/lib/crates/fabro-agent/src/profiles/mod.rs index f5f240947..283b00112 100644 --- a/lib/crates/fabro-agent/src/profiles/mod.rs +++ b/lib/crates/fabro-agent/src/profiles/mod.rs @@ -9,7 +9,7 @@ pub use openai::OpenAiProfile; use crate::sandbox::Sandbox; use crate::skills::{format_skills_prompt_section, Skill}; use crate::tool_registry::ToolRegistry; -use fabro_llm::provider::Provider; +use fabro_model::Provider; /// Common fields shared by all provider profiles. /// diff --git a/lib/crates/fabro-agent/src/profiles/openai.rs b/lib/crates/fabro-agent/src/profiles/openai.rs index 74fc645dc..d5eb54d46 100644 --- a/lib/crates/fabro-agent/src/profiles/openai.rs +++ b/lib/crates/fabro-agent/src/profiles/openai.rs @@ -7,7 +7,7 @@ use crate::skills::Skill; use crate::tool_registry::ToolRegistry; use crate::tools::{register_core_tools, WebFetchSummarizer}; use crate::v4a_patch::make_apply_patch_tool; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use super::EnvContext; @@ -191,7 +191,7 @@ in the project."; } fn capabilities(&self) -> ProfileCapabilities { - let context_window_size = fabro_llm::catalog::get_model_info(self.model()) + let context_window_size = fabro_model::get_model_info(self.model()) .map(|info| info.limits.context_window as usize) .unwrap_or(128_000); ProfileCapabilities { diff --git a/lib/crates/fabro-agent/src/provider_profile.rs b/lib/crates/fabro-agent/src/provider_profile.rs index 9aa816123..93c57f716 100644 --- a/lib/crates/fabro-agent/src/provider_profile.rs +++ b/lib/crates/fabro-agent/src/provider_profile.rs @@ -6,8 +6,8 @@ use crate::subagent::{ SubAgentManager, }; use crate::tool_registry::ToolRegistry; -use fabro_llm::provider::Provider; use fabro_llm::types::ToolDefinition; +use fabro_model::Provider; use std::sync::Arc; /// Static capabilities of a provider profile. @@ -82,7 +82,7 @@ pub trait ProviderProfile: Send + Sync { mod tests { use super::*; use crate::test_support::{MockSandbox, TestProfile}; - use fabro_llm::provider::Provider; + use fabro_model::Provider; #[test] fn profile_provider_and_model() { diff --git a/lib/crates/fabro-agent/src/session.rs b/lib/crates/fabro-agent/src/session.rs index 41de33cc0..4fd442dc4 100644 --- a/lib/crates/fabro-agent/src/session.rs +++ b/lib/crates/fabro-agent/src/session.rs @@ -901,7 +901,7 @@ impl Session { temperature: None, top_p: None, max_tokens: self.config.max_tokens.or_else(|| { - fabro_llm::catalog::get_model_info(self.provider_profile.model()) + fabro_model::get_model_info(self.provider_profile.model()) .and_then(|m| m.limits.max_output) }), stop_sequences: None, diff --git a/lib/crates/fabro-agent/src/test_support.rs b/lib/crates/fabro-agent/src/test_support.rs index 705ce2641..91c97a616 100644 --- a/lib/crates/fabro-agent/src/test_support.rs +++ b/lib/crates/fabro-agent/src/test_support.rs @@ -10,8 +10,9 @@ use crate::tool_registry::ToolRegistry; use async_trait::async_trait; use fabro_llm::client::Client; use fabro_llm::error::SdkError; -use fabro_llm::provider::{Provider, ProviderAdapter, StreamEventStream}; +use fabro_llm::provider::{ProviderAdapter, StreamEventStream}; use fabro_llm::types::{ContentPart, FinishReason, Message, Request, Response, StreamEvent, Usage}; +use fabro_model::Provider; use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; diff --git a/lib/crates/fabro-agent/src/tools.rs b/lib/crates/fabro-agent/src/tools.rs index d6e8f110e..f204dadb4 100644 --- a/lib/crates/fabro-agent/src/tools.rs +++ b/lib/crates/fabro-agent/src/tools.rs @@ -2,8 +2,8 @@ use crate::config::SessionConfig; use crate::sandbox::GrepOptions; use crate::tool_registry::RegisteredTool; use fabro_llm::client::Client; -use fabro_llm::provider::ModelId; use fabro_llm::types::{Message, Request, ToolDefinition}; +use fabro_model::ModelId; use std::borrow::Cow; use std::fmt::Write; use std::sync::Arc; @@ -1249,7 +1249,7 @@ mod tests { let client = make_client(provider).await; let summarizer = WebFetchSummarizer { client, - model_id: ModelId::new(fabro_llm::provider::Provider::Anthropic, "mock-model"), + model_id: ModelId::new(fabro_model::Provider::Anthropic, "mock-model"), }; let tool = make_web_fetch_tool(Some(summarizer)); @@ -1342,7 +1342,7 @@ mod tests { let summarizer = WebFetchSummarizer { client, - model_id: ModelId::new(fabro_llm::provider::Provider::Anthropic, "target-model"), + model_id: ModelId::new(fabro_model::Provider::Anthropic, "target-model"), }; let tool = make_web_fetch_tool(Some(summarizer)); diff --git a/lib/crates/fabro-agent/tests/guardrails.rs b/lib/crates/fabro-agent/tests/guardrails.rs index 8da505683..e60f3e601 100644 --- a/lib/crates/fabro-agent/tests/guardrails.rs +++ b/lib/crates/fabro-agent/tests/guardrails.rs @@ -1,6 +1,6 @@ use fabro_agent::{AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile}; -use fabro_llm::catalog; -use fabro_llm::provider::Provider; +use fabro_model as catalog; +use fabro_model::Provider; #[test] fn profile_context_window_matches_catalog_for_default_models() { diff --git a/lib/crates/fabro-api/Cargo.toml b/lib/crates/fabro-api/Cargo.toml index 620d73a31..c83d51587 100644 --- a/lib/crates/fabro-api/Cargo.toml +++ b/lib/crates/fabro-api/Cargo.toml @@ -18,6 +18,7 @@ fabro-sandbox = { path = "../fabro-sandbox", features = ["daytona"] } fabro-github = { path = "../fabro-github" } fabro-agent = { path = "../fabro-agent" } fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } fabro-retro = { path = "../fabro-retro" } fabro-util = { path = "../fabro-util" } fabro-db = { path = "../fabro-db" } @@ -62,4 +63,4 @@ http-body-util = "0.1" tempfile = "3" openapiv3 = "2" serde_yaml = "0.9" -fabro-sandbox = { path = "../fabro-sandbox", features = ["exe"] } +fabro-sandbox = { path = "../fabro-sandbox", features = ["exe"] } \ No newline at end of file diff --git a/lib/crates/fabro-api/src/demo/mod.rs b/lib/crates/fabro-api/src/demo/mod.rs index 09df97656..f26536403 100644 --- a/lib/crates/fabro-api/src/demo/mod.rs +++ b/lib/crates/fabro-api/src/demo/mod.rs @@ -576,7 +576,7 @@ pub async fn list_models( State(_state): State>, Query(pagination): Query, ) -> Response { - paginated_response(fabro_llm::catalog::list_models(None), &pagination) + paginated_response(fabro_model::list_models(None), &pagination) } // ── Settings ─────────────────────────────────────────────────────────── diff --git a/lib/crates/fabro-api/src/serve.rs b/lib/crates/fabro-api/src/serve.rs index d263dbdcd..0758a84d8 100644 --- a/lib/crates/fabro-api/src/serve.rs +++ b/lib/crates/fabro-api/src/serve.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock}; use std::time::Duration; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use fabro_util::terminal::Styles; use tokio::net::TcpListener; use tracing::{error, info, warn}; @@ -291,13 +291,13 @@ fn resolve_model_provider( // Look up default model from catalog for the given provider, // falling back to the best provider with an API key configured. provider_str - .and_then(fabro_llm::catalog::default_model_for_provider) - .unwrap_or_else(fabro_llm::catalog::default_model_from_env) + .and_then(fabro_model::default_model_for_provider) + .unwrap_or_else(fabro_model::default_model_from_env) .id }); // Resolve model alias through catalog - let (model, provider_str) = match fabro_llm::catalog::get_model_info(&model) { + let (model, provider_str) = match fabro_model::get_model_info(&model) { Some(info) => ( info.id, provider_str.map(|s| s.to_string()).or(Some(info.provider)), diff --git a/lib/crates/fabro-api/src/server.rs b/lib/crates/fabro-api/src/server.rs index 3fd2efee8..8da054ac8 100644 --- a/lib/crates/fabro-api/src/server.rs +++ b/lib/crates/fabro-api/src/server.rs @@ -1053,7 +1053,7 @@ async fn test_model( State(state): State>, Path(id): Path, ) -> Response { - let Some(info) = fabro_llm::catalog::get_model_info(&id) else { + let Some(info) = fabro_model::get_model_info(&id) else { return ApiError::not_found(format!("Model not found: {id}")).into_response(); }; @@ -1163,12 +1163,12 @@ async fn create_completion( ) -> Response { // Resolve model let model_id = req.model.unwrap_or_else(|| { - fabro_llm::catalog::list_models(None) + fabro_model::list_models(None) .first() .map_or_else(|| "claude-sonnet-4-5".to_string(), |m| m.id.clone()) }); - let catalog_info = fabro_llm::catalog::get_model_info(&model_id); + let catalog_info = fabro_model::get_model_info(&model_id); // Resolve provider: explicit request > catalog > None let provider_name = req diff --git a/lib/crates/fabro-api/src/sessions.rs b/lib/crates/fabro-api/src/sessions.rs index 2655cc3c4..fcff3296a 100644 --- a/lib/crates/fabro-api/src/sessions.rs +++ b/lib/crates/fabro-api/src/sessions.rs @@ -61,11 +61,11 @@ fn generate_title(content: &str) -> String { fn resolve_model(model_arg: Option) -> (String, Option) { let raw = model_arg.unwrap_or_else(|| { - fabro_llm::catalog::list_models(None) + fabro_model::list_models(None) .first() .map_or_else(|| "claude-sonnet-4-5".to_string(), |m| m.id.clone()) }); - match fabro_llm::catalog::get_model_info(&raw) { + match fabro_model::get_model_info(&raw) { Some(info) => (info.id, Some(info.provider)), None => (raw, None), } diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml index aa273a8ed..0ac564507 100644 --- a/lib/crates/fabro-cli/Cargo.toml +++ b/lib/crates/fabro-cli/Cargo.toml @@ -18,6 +18,7 @@ sleep_inhibitor = ["dep:fabro-beastie"] [dependencies] fabro-config = { path = "../fabro-config" } fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } fabro-openai-oauth = { path = "../fabro-openai-oauth" } fabro-github = { path = "../fabro-github" } fabro-agent = { path = "../fabro-agent" } @@ -87,4 +88,4 @@ predicates = "3" serde_json.workspace = true tempfile = "3" httpmock = "0.8" -trycmd = "0.15" +trycmd = "0.15" \ No newline at end of file diff --git a/lib/crates/fabro-cli/src/commands/pr.rs b/lib/crates/fabro-cli/src/commands/pr.rs index ea58023db..c66ff5e5f 100644 --- a/lib/crates/fabro-cli/src/commands/pr.rs +++ b/lib/crates/fabro-cli/src/commands/pr.rs @@ -385,7 +385,7 @@ async fn create_from( let model = args .model - .unwrap_or_else(|| fabro_llm::catalog::default_model_from_env().id); + .unwrap_or_else(|| fabro_model::default_model_from_env().id); let record = fabro_workflows::pull_request::maybe_open_pull_request( &creds, diff --git a/lib/crates/fabro-cli/src/commands/provider.rs b/lib/crates/fabro-cli/src/commands/provider.rs index 3145c7019..3d89fc23e 100644 --- a/lib/crates/fabro-cli/src/commands/provider.rs +++ b/lib/crates/fabro-cli/src/commands/provider.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use clap::Args; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use fabro_util::terminal::Styles; use crate::provider_auth; diff --git a/lib/crates/fabro-cli/src/commands/run.rs b/lib/crates/fabro-cli/src/commands/run.rs index da58742f5..8bfa9d51a 100644 --- a/lib/crates/fabro-cli/src/commands/run.rs +++ b/lib/crates/fabro-cli/src/commands/run.rs @@ -11,7 +11,7 @@ use fabro_agent::{DockerSandbox, DockerSandboxConfig, LocalSandbox, Sandbox}; use fabro_config::run::{RunDefaults, WorkflowRunConfig}; use fabro_config::{project as project_config, run as run_config, sandbox as sandbox_config}; use fabro_interview::{AutoApproveInterviewer, ConsoleInterviewer, Interviewer}; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use fabro_util::terminal::Styles; use fabro_validate::Severity; use fabro_workflows::backend::{AgentApiBackend, AgentCliBackend, BackendRouter}; @@ -208,13 +208,13 @@ fn resolve_model_provider( .unwrap_or_else(|| { provider .as_deref() - .and_then(fabro_llm::catalog::default_model_for_provider) - .unwrap_or_else(fabro_llm::catalog::default_model_from_env) + .and_then(fabro_model::default_model_for_provider) + .unwrap_or_else(fabro_model::default_model_from_env) .id }); // Resolve model alias through catalog - match fabro_llm::catalog::get_model_info(&model) { + match fabro_model::get_model_info(&model) { Some(info) => (info.id, provider.or(Some(info.provider))), None => (model, provider), } @@ -357,13 +357,13 @@ fn resolve_fallback_chain( provider: Provider, model: &str, run_cfg: Option<&WorkflowRunConfig>, -) -> Vec { +) -> Vec { let fallbacks = run_cfg .and_then(|c| c.llm.as_ref()) .and_then(|l| l.fallbacks.as_ref()); match fallbacks { - Some(map) => fabro_llm::catalog::build_fallback_chain(provider.as_str(), model, map), + Some(map) => fabro_model::build_fallback_chain(provider.as_str(), model, map), None => Vec::new(), } } @@ -1893,14 +1893,14 @@ async fn run_from_branch( let model = args .model - .unwrap_or_else(|| fabro_llm::catalog::default_model_from_env().id); + .unwrap_or_else(|| fabro_model::default_model_from_env().id); let provider_enum = args .provider .as_deref() - .map(|s| s.parse::()) + .map(|s| s.parse::()) .transpose() .map_err(|e| anyhow::anyhow!("{e}"))? - .unwrap_or_else(fabro_llm::provider::Provider::default_from_env); + .unwrap_or_else(fabro_model::Provider::default_from_env); // No fallback config available for branch resume; use empty chain. let fallback_chain = Vec::new(); @@ -2281,7 +2281,7 @@ async fn run_preflight( // Resolve through catalog to get canonical model ID and provider let (resolved_model, resolved_provider) = - if let Some(info) = fabro_llm::catalog::get_model_info(node_model) { + if let Some(info) = fabro_model::get_model_info(node_model) { (info.id, info.provider) } else { (node_model.to_string(), node_provider.to_string()) @@ -2300,7 +2300,7 @@ async fn run_preflight( // If no LLM nodes found, fall back to the default model/provider if model_providers.is_empty() { let (resolved_model, resolved_provider) = - if let Some(info) = fabro_llm::catalog::get_model_info(&model) { + if let Some(info) = fabro_model::get_model_info(&model) { (info.id, info.provider) } else { (model.clone(), default_provider.to_string()) diff --git a/lib/crates/fabro-cli/src/doctor.rs b/lib/crates/fabro-cli/src/doctor.rs index 3deb71d0d..1747f40d5 100644 --- a/lib/crates/fabro-cli/src/doctor.rs +++ b/lib/crates/fabro-cli/src/doctor.rs @@ -6,7 +6,7 @@ use std::sync::LazyLock; #[cfg(feature = "server")] use fabro_config::server::{ApiAuthStrategy, AuthProvider}; -use fabro_llm::provider::Provider; +use fabro_model::Provider; pub use fabro_util::check_report::{ CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus, }; @@ -856,7 +856,7 @@ async fn probe_daytona() -> Option> { } pub(crate) fn probe_model(provider: Provider) -> String { - fabro_llm::catalog::probe_model_for_provider(provider.as_str()) + fabro_model::probe_model_for_provider(provider.as_str()) .map(|m| m.id) .unwrap_or_else(|| format!("unknown-{}", provider.as_str())) } diff --git a/lib/crates/fabro-cli/src/install.rs b/lib/crates/fabro-cli/src/install.rs index a401a0aa3..55b21b020 100644 --- a/lib/crates/fabro-cli/src/install.rs +++ b/lib/crates/fabro-cli/src/install.rs @@ -9,7 +9,7 @@ use axum::extract::Query; use axum::response::Html; use axum::routing::get; use dialoguer::{MultiSelect, Select}; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use fabro_util::terminal::Styles; use rand::Rng; use tokio::net::TcpListener; diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs index 6a4a9ea5a..91659c72b 100644 --- a/lib/crates/fabro-cli/src/main.rs +++ b/lib/crates/fabro-cli/src/main.rs @@ -952,7 +952,7 @@ mod tests { Command::Provider { command: ProviderCommand::Login(args), } => { - assert_eq!(args.provider, fabro_llm::provider::Provider::OpenAi); + assert_eq!(args.provider, fabro_model::Provider::OpenAi); } _ => panic!("unexpected command variant"), } @@ -966,7 +966,7 @@ mod tests { Command::Provider { command: ProviderCommand::Login(args), } => { - assert_eq!(args.provider, fabro_llm::provider::Provider::Anthropic); + assert_eq!(args.provider, fabro_model::Provider::Anthropic); } _ => panic!("unexpected command variant"), } diff --git a/lib/crates/fabro-cli/src/provider_auth.rs b/lib/crates/fabro-cli/src/provider_auth.rs index ba8b89f8a..83b691f5e 100644 --- a/lib/crates/fabro-cli/src/provider_auth.rs +++ b/lib/crates/fabro-cli/src/provider_auth.rs @@ -2,7 +2,7 @@ use std::path::Path; use anyhow::Result; use dialoguer::{Confirm, Password}; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use fabro_util::terminal::Styles; use crate::doctor; diff --git a/lib/crates/fabro-hooks/Cargo.toml b/lib/crates/fabro-hooks/Cargo.toml index 377f4238f..9f88953fa 100644 --- a/lib/crates/fabro-hooks/Cargo.toml +++ b/lib/crates/fabro-hooks/Cargo.toml @@ -12,6 +12,7 @@ doctest = false fabro-agent = { path = "../fabro-agent" } fabro-config = { path = "../fabro-config" } fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } fabro-util = { path = "../fabro-util" } serde.workspace = true serde_json.workspace = true @@ -25,4 +26,4 @@ tokio-util.workspace = true [dev-dependencies] mockito = "1" tokio = { workspace = true, features = ["test-util", "macros"] } -toml.workspace = true +toml.workspace = true \ No newline at end of file diff --git a/lib/crates/fabro-hooks/src/executor.rs b/lib/crates/fabro-hooks/src/executor.rs index 12b0b9a32..4d63e6da8 100644 --- a/lib/crates/fabro-hooks/src/executor.rs +++ b/lib/crates/fabro-hooks/src/executor.rs @@ -218,7 +218,7 @@ impl HookExecutorImpl { /// Resolve a model alias (e.g. "haiku") to a concrete model ID. fn resolve_model(model: &Option) -> String { let model_id = model.as_deref().unwrap_or("haiku"); - let model_info = fabro_llm::catalog::get_model_info(model_id); + let model_info = fabro_model::get_model_info(model_id); model_info .as_ref() .map_or(model_id, |m| m.id.as_str()) diff --git a/lib/crates/fabro-llm/Cargo.toml b/lib/crates/fabro-llm/Cargo.toml index cd8e70bc3..2d87759d2 100644 --- a/lib/crates/fabro-llm/Cargo.toml +++ b/lib/crates/fabro-llm/Cargo.toml @@ -31,6 +31,7 @@ cli-table.workspace = true clap.workspace = true dialoguer.workspace = true tracing.workspace = true +fabro-model = { path = "../fabro-model" } fabro-util = { path = "../fabro-util" } [dev-dependencies] @@ -39,4 +40,4 @@ insta = { workspace = true } tokio = { workspace = true, features = ["test-util", "macros"] } dotenvy.workspace = true httpmock = "0.8" -serde_json.workspace = true +serde_json.workspace = true \ No newline at end of file diff --git a/lib/crates/fabro-llm/src/cli.rs b/lib/crates/fabro-llm/src/cli.rs index d9afae73a..1f74e897e 100644 --- a/lib/crates/fabro-llm/src/cli.rs +++ b/lib/crates/fabro-llm/src/cli.rs @@ -13,9 +13,11 @@ use serde::Deserialize; use fabro_util::terminal::Styles; -use crate::catalog; +use fabro_model as catalog; + use crate::generate::{self, GenerateParams}; -use crate::types::{Message, ModelInfo}; +use crate::types::Message; +use fabro_model::ModelInfo; pub struct ServerConnection { pub client: reqwest::Client, @@ -117,7 +119,7 @@ fn color_if(use_color: bool, color: Color) -> Option { } } -fn model_row(model: &crate::types::ModelInfo, use_color: bool) -> Vec { +fn model_row(model: &ModelInfo, use_color: bool) -> Vec { let aliases = model.aliases.join(", "); let cost = format!( "{} / {}", diff --git a/lib/crates/fabro-llm/src/client.rs b/lib/crates/fabro-llm/src/client.rs index 50e6774f0..5fb603375 100644 --- a/lib/crates/fabro-llm/src/client.rs +++ b/lib/crates/fabro-llm/src/client.rs @@ -144,7 +144,7 @@ impl Client { /// Resolve the provider for a request. fn resolve_provider(&self, request: &Request) -> Result, SdkError> { let catalog_provider = - crate::catalog::get_model_info(&request.model).map(|info| info.provider); + fabro_model::get_model_info(&request.model).map(|info| info.provider); let provider_name = request .provider diff --git a/lib/crates/fabro-llm/src/lib.rs b/lib/crates/fabro-llm/src/lib.rs index a519031e9..ecc6dfbde 100644 --- a/lib/crates/fabro-llm/src/lib.rs +++ b/lib/crates/fabro-llm/src/lib.rs @@ -1,4 +1,3 @@ -pub mod catalog; pub mod cli; pub mod client; pub mod error; @@ -11,5 +10,5 @@ pub mod tools; pub mod types; // Re-export module-level default client helpers (Section 2.5). +pub use fabro_model::{ModelId, Provider}; pub use generate::set_default_client; -pub use provider::{ModelId, Provider}; diff --git a/lib/crates/fabro-llm/src/provider.rs b/lib/crates/fabro-llm/src/provider.rs index 5f19e6af1..c891f1468 100644 --- a/lib/crates/fabro-llm/src/provider.rs +++ b/lib/crates/fabro-llm/src/provider.rs @@ -1,149 +1,9 @@ use crate::error::SdkError; use crate::types::{Request, Response, StreamEvent, ToolChoice}; use futures::Stream; -use serde::{Deserialize, Serialize}; -use std::fmt; use std::pin::Pin; -use std::str::FromStr; -// --------------------------------------------------------------------------- -// Provider enum — compile-time safe provider identity -// --------------------------------------------------------------------------- - -/// Known LLM provider variants. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Provider { - Anthropic, - OpenAi, - Gemini, - Kimi, - Zai, - Minimax, - Inception, -} - -impl Provider { - /// All known provider variants, for use in guardrail tests and iteration. - pub const ALL: &[Provider] = &[ - Provider::Anthropic, - Provider::OpenAi, - Provider::Gemini, - Provider::Kimi, - Provider::Zai, - Provider::Minimax, - Provider::Inception, - ]; - - /// Environment variable names that can provide the API key for this provider. - /// Gemini accepts either `GEMINI_API_KEY` or `GOOGLE_API_KEY`. - #[must_use] - pub fn api_key_env_vars(self) -> &'static [&'static str] { - match self { - Self::Anthropic => &["ANTHROPIC_API_KEY"], - Self::OpenAi => &["OPENAI_API_KEY"], - Self::Gemini => &["GEMINI_API_KEY", "GOOGLE_API_KEY"], - Self::Kimi => &["KIMI_API_KEY"], - Self::Zai => &["ZAI_API_KEY"], - Self::Minimax => &["MINIMAX_API_KEY"], - Self::Inception => &["INCEPTION_API_KEY"], - } - } - - /// Returns `true` if at least one of the provider's API key env vars is set. - #[must_use] - pub fn has_api_key(self) -> bool { - self.api_key_env_vars() - .iter() - .any(|var| std::env::var(var).is_ok()) - } - - /// Pick the best default provider based on which API keys are available. - /// - /// Checks Anthropic → OpenAI → Gemini; falls back to Anthropic if none - /// have a key configured. - #[must_use] - pub fn default_from_env() -> Self { - Self::default_with(Self::has_api_key) - } - - /// Testable core of [`default_from_env`]: walks the precedence list and - /// returns the first provider for which `is_configured` returns `true`. - fn default_with(is_configured: impl Fn(Self) -> bool) -> Self { - const PRECEDENCE: [Provider; 3] = [Provider::Anthropic, Provider::OpenAi, Provider::Gemini]; - PRECEDENCE - .iter() - .copied() - .find(|&p| is_configured(p)) - .unwrap_or(Provider::Anthropic) - } - - /// Stable lowercase string representation used in `Request.provider`, - /// adapter names, and other serialization boundaries. - #[must_use] - pub fn as_str(self) -> &'static str { - match self { - Self::Anthropic => "anthropic", - Self::OpenAi => "openai", - Self::Gemini => "gemini", - Self::Kimi => "kimi", - Self::Zai => "zai", - Self::Minimax => "minimax", - Self::Inception => "inception", - } - } -} - -impl fmt::Display for Provider { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(self.as_str()) - } -} - -impl FromStr for Provider { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "anthropic" => Ok(Self::Anthropic), - "openai" | "open_ai" => Ok(Self::OpenAi), - "gemini" => Ok(Self::Gemini), - "kimi" => Ok(Self::Kimi), - "zai" => Ok(Self::Zai), - "minimax" => Ok(Self::Minimax), - "inception" | "inception_labs" => Ok(Self::Inception), - other => Err(format!("unknown provider: {other}")), - } - } -} - -// --------------------------------------------------------------------------- -// ModelId — bundles a provider with a model name -// --------------------------------------------------------------------------- - -/// A model identifier that pairs a [`Provider`] with the provider-specific -/// model name (e.g. `"claude-opus-4-6"` or `"gpt-4o-mini"`). -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ModelId { - pub provider: Provider, - pub model: String, -} - -impl ModelId { - #[must_use] - pub fn new(provider: Provider, model: impl Into) -> Self { - Self { - provider, - model: model.into(), - } - } -} - -impl fmt::Display for ModelId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}:{}", self.provider, self.model) - } -} +pub use fabro_model::{ModelId, Provider}; // --------------------------------------------------------------------------- // ProviderAdapter trait @@ -209,145 +69,6 @@ pub fn validate_tool_choice( mod tests { use super::*; - #[test] - fn parse_kimi() { - assert_eq!("kimi".parse::().unwrap(), Provider::Kimi); - } - - #[test] - fn parse_zai() { - assert_eq!("zai".parse::().unwrap(), Provider::Zai); - } - - #[test] - fn parse_minimax() { - assert_eq!("minimax".parse::().unwrap(), Provider::Minimax); - } - - #[test] - fn kimi_as_str() { - assert_eq!(Provider::Kimi.as_str(), "kimi"); - } - - #[test] - fn zai_as_str() { - assert_eq!(Provider::Zai.as_str(), "zai"); - } - - #[test] - fn minimax_as_str() { - assert_eq!(Provider::Minimax.as_str(), "minimax"); - } - - #[test] - fn parse_inception() { - assert_eq!( - "inception".parse::().unwrap(), - Provider::Inception - ); - assert_eq!( - "inception_labs".parse::().unwrap(), - Provider::Inception - ); - } - - #[test] - fn inception_as_str() { - assert_eq!(Provider::Inception.as_str(), "inception"); - } - - #[test] - fn default_with_all_configured_prefers_anthropic() { - assert_eq!(Provider::default_with(|_| true), Provider::Anthropic); - } - - #[test] - fn default_with_only_openai() { - assert_eq!( - Provider::default_with(|p| p == Provider::OpenAi), - Provider::OpenAi - ); - } - - #[test] - fn default_with_only_gemini() { - assert_eq!( - Provider::default_with(|p| p == Provider::Gemini), - Provider::Gemini - ); - } - - #[test] - fn default_with_openai_and_gemini_prefers_openai() { - assert_eq!( - Provider::default_with(|p| p == Provider::OpenAi || p == Provider::Gemini), - Provider::OpenAi, - ); - } - - #[test] - fn default_with_none_configured_falls_back_to_anthropic() { - assert_eq!(Provider::default_with(|_| false), Provider::Anthropic); - } - - #[test] - fn default_with_only_kimi_falls_back_to_anthropic() { - assert_eq!( - Provider::default_with(|p| p == Provider::Kimi), - Provider::Anthropic - ); - } - - #[test] - fn api_key_env_vars_anthropic() { - assert_eq!( - Provider::Anthropic.api_key_env_vars(), - &["ANTHROPIC_API_KEY"] - ); - } - - #[test] - fn api_key_env_vars_openai() { - assert_eq!(Provider::OpenAi.api_key_env_vars(), &["OPENAI_API_KEY"]); - } - - #[test] - fn api_key_env_vars_gemini_has_two() { - let vars = Provider::Gemini.api_key_env_vars(); - assert_eq!(vars.len(), 2); - assert_eq!(vars, &["GEMINI_API_KEY", "GOOGLE_API_KEY"]); - } - - #[test] - fn api_key_env_vars_kimi() { - assert_eq!(Provider::Kimi.api_key_env_vars(), &["KIMI_API_KEY"]); - } - - #[test] - fn api_key_env_vars_zai() { - assert_eq!(Provider::Zai.api_key_env_vars(), &["ZAI_API_KEY"]); - } - - #[test] - fn api_key_env_vars_minimax() { - assert_eq!(Provider::Minimax.api_key_env_vars(), &["MINIMAX_API_KEY"]); - } - - #[test] - fn api_key_env_vars_inception() { - assert_eq!( - Provider::Inception.api_key_env_vars(), - &["INCEPTION_API_KEY"] - ); - } - - #[test] - fn every_provider_has_at_least_one_env_var() { - assert!(Provider::ALL - .iter() - .all(|p| !p.api_key_env_vars().is_empty())); - } - // Mock adapter that supports all tool choices struct MockAdapter; diff --git a/lib/crates/fabro-llm/src/providers/anthropic.rs b/lib/crates/fabro-llm/src/providers/anthropic.rs index 38dd97120..638b81e95 100644 --- a/lib/crates/fabro-llm/src/providers/anthropic.rs +++ b/lib/crates/fabro-llm/src/providers/anthropic.rs @@ -1078,7 +1078,7 @@ fn build_api_request( max_tokens: request .max_tokens .or_else(|| { - crate::catalog::get_model_info(&request.model).and_then(|m| m.limits.max_output) + fabro_model::get_model_info(&request.model).and_then(|m| m.limits.max_output) }) .unwrap_or(65536), system: system_value, diff --git a/lib/crates/fabro-llm/src/types.rs b/lib/crates/fabro-llm/src/types.rs index 4706f5f88..05f782459 100644 --- a/lib/crates/fabro-llm/src/types.rs +++ b/lib/crates/fabro-llm/src/types.rs @@ -636,43 +636,9 @@ impl StreamEvent { } } -// --- 2.9 ModelInfo --- +// --- 2.9 ModelInfo (re-exported from fabro-model) --- -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ModelLimits { - pub context_window: i64, - pub max_output: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ModelFeatures { - pub tools: bool, - pub vision: bool, - pub reasoning: bool, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ModelCosts { - pub input_cost_per_mtok: Option, - pub output_cost_per_mtok: Option, - pub cache_input_cost_per_mtok: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ModelInfo { - pub id: String, - pub provider: String, - pub family: String, - pub display_name: String, - pub limits: ModelLimits, - pub training: Option, - pub features: ModelFeatures, - pub costs: ModelCosts, - pub estimated_output_tps: Option, - pub aliases: Vec, - #[serde(default)] - pub default: bool, -} +pub use fabro_model::{ModelCosts, ModelFeatures, ModelInfo, ModelLimits}; // --- 4.7 Timeouts --- diff --git a/lib/crates/fabro-model/Cargo.toml b/lib/crates/fabro-model/Cargo.toml new file mode 100644 index 000000000..6c80932c3 --- /dev/null +++ b/lib/crates/fabro-model/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "fabro-model" +edition.workspace = true +version.workspace = true +license.workspace = true +description = "LLM model catalog: provider identity, model metadata, and resolution" + +[lib] +doctest = false + +[dependencies] +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +insta.workspace = true diff --git a/lib/crates/fabro-llm/src/catalog.json b/lib/crates/fabro-model/src/catalog.json similarity index 100% rename from lib/crates/fabro-llm/src/catalog.json rename to lib/crates/fabro-model/src/catalog.json diff --git a/lib/crates/fabro-llm/src/catalog.rs b/lib/crates/fabro-model/src/catalog.rs similarity index 100% rename from lib/crates/fabro-llm/src/catalog.rs rename to lib/crates/fabro-model/src/catalog.rs diff --git a/lib/crates/fabro-model/src/lib.rs b/lib/crates/fabro-model/src/lib.rs new file mode 100644 index 000000000..8913772e3 --- /dev/null +++ b/lib/crates/fabro-model/src/lib.rs @@ -0,0 +1,10 @@ +pub mod catalog; +pub mod provider; +pub mod types; + +pub use catalog::{ + build_fallback_chain, closest_model, default_model, default_model_for_provider, + default_model_from_env, get_model_info, list_models, probe_model_for_provider, FallbackTarget, +}; +pub use provider::{ModelId, Provider}; +pub use types::{ModelCosts, ModelFeatures, ModelInfo, ModelLimits}; diff --git a/lib/crates/fabro-model/src/provider.rs b/lib/crates/fabro-model/src/provider.rs new file mode 100644 index 000000000..e7f330933 --- /dev/null +++ b/lib/crates/fabro-model/src/provider.rs @@ -0,0 +1,286 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +// --------------------------------------------------------------------------- +// Provider enum — compile-time safe provider identity +// --------------------------------------------------------------------------- + +/// Known LLM provider variants. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Provider { + Anthropic, + OpenAi, + Gemini, + Kimi, + Zai, + Minimax, + Inception, +} + +impl Provider { + /// All known provider variants, for use in guardrail tests and iteration. + pub const ALL: &[Provider] = &[ + Provider::Anthropic, + Provider::OpenAi, + Provider::Gemini, + Provider::Kimi, + Provider::Zai, + Provider::Minimax, + Provider::Inception, + ]; + + /// Environment variable names that can provide the API key for this provider. + /// Gemini accepts either `GEMINI_API_KEY` or `GOOGLE_API_KEY`. + #[must_use] + pub fn api_key_env_vars(self) -> &'static [&'static str] { + match self { + Self::Anthropic => &["ANTHROPIC_API_KEY"], + Self::OpenAi => &["OPENAI_API_KEY"], + Self::Gemini => &["GEMINI_API_KEY", "GOOGLE_API_KEY"], + Self::Kimi => &["KIMI_API_KEY"], + Self::Zai => &["ZAI_API_KEY"], + Self::Minimax => &["MINIMAX_API_KEY"], + Self::Inception => &["INCEPTION_API_KEY"], + } + } + + /// Returns `true` if at least one of the provider's API key env vars is set. + #[must_use] + pub fn has_api_key(self) -> bool { + self.api_key_env_vars() + .iter() + .any(|var| std::env::var(var).is_ok()) + } + + /// Pick the best default provider based on which API keys are available. + /// + /// Checks Anthropic → OpenAI → Gemini; falls back to Anthropic if none + /// have a key configured. + #[must_use] + pub fn default_from_env() -> Self { + Self::default_with(Self::has_api_key) + } + + /// Testable core of [`default_from_env`]: walks the precedence list and + /// returns the first provider for which `is_configured` returns `true`. + fn default_with(is_configured: impl Fn(Self) -> bool) -> Self { + const PRECEDENCE: [Provider; 3] = [Provider::Anthropic, Provider::OpenAi, Provider::Gemini]; + PRECEDENCE + .iter() + .copied() + .find(|&p| is_configured(p)) + .unwrap_or(Provider::Anthropic) + } + + /// Stable lowercase string representation used in `Request.provider`, + /// adapter names, and other serialization boundaries. + #[must_use] + pub fn as_str(self) -> &'static str { + match self { + Self::Anthropic => "anthropic", + Self::OpenAi => "openai", + Self::Gemini => "gemini", + Self::Kimi => "kimi", + Self::Zai => "zai", + Self::Minimax => "minimax", + Self::Inception => "inception", + } + } +} + +impl fmt::Display for Provider { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Provider { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "anthropic" => Ok(Self::Anthropic), + "openai" | "open_ai" => Ok(Self::OpenAi), + "gemini" => Ok(Self::Gemini), + "kimi" => Ok(Self::Kimi), + "zai" => Ok(Self::Zai), + "minimax" => Ok(Self::Minimax), + "inception" | "inception_labs" => Ok(Self::Inception), + other => Err(format!("unknown provider: {other}")), + } + } +} + +// --------------------------------------------------------------------------- +// ModelId — bundles a provider with a model name +// --------------------------------------------------------------------------- + +/// A model identifier that pairs a [`Provider`] with the provider-specific +/// model name (e.g. `"claude-opus-4-6"` or `"gpt-4o-mini"`). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ModelId { + pub provider: Provider, + pub model: String, +} + +impl ModelId { + #[must_use] + pub fn new(provider: Provider, model: impl Into) -> Self { + Self { + provider, + model: model.into(), + } + } +} + +impl fmt::Display for ModelId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.provider, self.model) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_kimi() { + assert_eq!("kimi".parse::().unwrap(), Provider::Kimi); + } + + #[test] + fn parse_zai() { + assert_eq!("zai".parse::().unwrap(), Provider::Zai); + } + + #[test] + fn parse_minimax() { + assert_eq!("minimax".parse::().unwrap(), Provider::Minimax); + } + + #[test] + fn kimi_as_str() { + assert_eq!(Provider::Kimi.as_str(), "kimi"); + } + + #[test] + fn zai_as_str() { + assert_eq!(Provider::Zai.as_str(), "zai"); + } + + #[test] + fn minimax_as_str() { + assert_eq!(Provider::Minimax.as_str(), "minimax"); + } + + #[test] + fn parse_inception() { + assert_eq!( + "inception".parse::().unwrap(), + Provider::Inception + ); + assert_eq!( + "inception_labs".parse::().unwrap(), + Provider::Inception + ); + } + + #[test] + fn inception_as_str() { + assert_eq!(Provider::Inception.as_str(), "inception"); + } + + #[test] + fn default_with_all_configured_prefers_anthropic() { + assert_eq!(Provider::default_with(|_| true), Provider::Anthropic); + } + + #[test] + fn default_with_only_openai() { + assert_eq!( + Provider::default_with(|p| p == Provider::OpenAi), + Provider::OpenAi + ); + } + + #[test] + fn default_with_only_gemini() { + assert_eq!( + Provider::default_with(|p| p == Provider::Gemini), + Provider::Gemini + ); + } + + #[test] + fn default_with_openai_and_gemini_prefers_openai() { + assert_eq!( + Provider::default_with(|p| p == Provider::OpenAi || p == Provider::Gemini), + Provider::OpenAi, + ); + } + + #[test] + fn default_with_none_configured_falls_back_to_anthropic() { + assert_eq!(Provider::default_with(|_| false), Provider::Anthropic); + } + + #[test] + fn default_with_only_kimi_falls_back_to_anthropic() { + assert_eq!( + Provider::default_with(|p| p == Provider::Kimi), + Provider::Anthropic + ); + } + + #[test] + fn api_key_env_vars_anthropic() { + assert_eq!( + Provider::Anthropic.api_key_env_vars(), + &["ANTHROPIC_API_KEY"] + ); + } + + #[test] + fn api_key_env_vars_openai() { + assert_eq!(Provider::OpenAi.api_key_env_vars(), &["OPENAI_API_KEY"]); + } + + #[test] + fn api_key_env_vars_gemini_has_two() { + let vars = Provider::Gemini.api_key_env_vars(); + assert_eq!(vars.len(), 2); + assert_eq!(vars, &["GEMINI_API_KEY", "GOOGLE_API_KEY"]); + } + + #[test] + fn api_key_env_vars_kimi() { + assert_eq!(Provider::Kimi.api_key_env_vars(), &["KIMI_API_KEY"]); + } + + #[test] + fn api_key_env_vars_zai() { + assert_eq!(Provider::Zai.api_key_env_vars(), &["ZAI_API_KEY"]); + } + + #[test] + fn api_key_env_vars_minimax() { + assert_eq!(Provider::Minimax.api_key_env_vars(), &["MINIMAX_API_KEY"]); + } + + #[test] + fn api_key_env_vars_inception() { + assert_eq!( + Provider::Inception.api_key_env_vars(), + &["INCEPTION_API_KEY"] + ); + } + + #[test] + fn every_provider_has_at_least_one_env_var() { + assert!(Provider::ALL + .iter() + .all(|p| !p.api_key_env_vars().is_empty())); + } +} diff --git a/lib/crates/fabro-model/src/types.rs b/lib/crates/fabro-model/src/types.rs new file mode 100644 index 000000000..e5e8ef979 --- /dev/null +++ b/lib/crates/fabro-model/src/types.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +// --- 2.9 ModelInfo --- + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ModelLimits { + pub context_window: i64, + pub max_output: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ModelFeatures { + pub tools: bool, + pub vision: bool, + pub reasoning: bool, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ModelCosts { + pub input_cost_per_mtok: Option, + pub output_cost_per_mtok: Option, + pub cache_input_cost_per_mtok: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ModelInfo { + pub id: String, + pub provider: String, + pub family: String, + pub display_name: String, + pub limits: ModelLimits, + pub training: Option, + pub features: ModelFeatures, + pub costs: ModelCosts, + pub estimated_output_tps: Option, + pub aliases: Vec, + #[serde(default)] + pub default: bool, +} diff --git a/lib/crates/fabro-validate/Cargo.toml b/lib/crates/fabro-validate/Cargo.toml index e9841c3a1..cd7db2a61 100644 --- a/lib/crates/fabro-validate/Cargo.toml +++ b/lib/crates/fabro-validate/Cargo.toml @@ -10,6 +10,6 @@ doctest = false [dependencies] fabro-graphviz = { path = "../fabro-graphviz" } -fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } serde = { workspace = true } -thiserror = { workspace = true } +thiserror = { workspace = true } \ No newline at end of file diff --git a/lib/crates/fabro-validate/src/rules.rs b/lib/crates/fabro-validate/src/rules.rs index b62b59e3d..82cd09c9b 100644 --- a/lib/crates/fabro-validate/src/rules.rs +++ b/lib/crates/fabro-validate/src/rules.rs @@ -934,7 +934,7 @@ impl LintRule for StylesheetModelKnownRule { for decl in &rule.declarations { match decl.property.as_str() { "model" => { - if fabro_llm::catalog::get_model_info(&decl.value).is_none() { + if fabro_model::get_model_info(&decl.value).is_none() { diagnostics.push(Diagnostic { rule: self.name().to_string(), severity: Severity::Warning, @@ -949,8 +949,8 @@ impl LintRule for StylesheetModelKnownRule { } } "provider" => { - if fabro_llm::Provider::from_str(&decl.value).is_err() { - let valid: Vec<&str> = fabro_llm::Provider::ALL + if fabro_model::Provider::from_str(&decl.value).is_err() { + let valid: Vec<&str> = fabro_model::Provider::ALL .iter() .map(|p| p.as_str()) .collect(); diff --git a/lib/crates/fabro-workflows/Cargo.toml b/lib/crates/fabro-workflows/Cargo.toml index 994175572..731bfc59d 100644 --- a/lib/crates/fabro-workflows/Cargo.toml +++ b/lib/crates/fabro-workflows/Cargo.toml @@ -32,6 +32,7 @@ fabro-interview = { path = "../fabro-interview" } fabro-util = { path = "../fabro-util" } fabro-git-storage = { path = "../fabro-git-storage" } fabro-llm = { path = "../fabro-llm" } +fabro-model = { path = "../fabro-model" } fabro-retro = { path = "../fabro-retro" } thiserror.workspace = true serde.workspace = true diff --git a/lib/crates/fabro-workflows/src/backend/api.rs b/lib/crates/fabro-workflows/src/backend/api.rs index 1844c59c7..cc751bab3 100644 --- a/lib/crates/fabro-workflows/src/backend/api.rs +++ b/lib/crates/fabro-workflows/src/backend/api.rs @@ -8,9 +8,9 @@ use fabro_agent::{ AgentEvent, AnthropicProfile, GeminiProfile, OpenAiProfile, ProviderProfile, Sandbox, Session, SessionConfig, Turn, }; -use fabro_llm::catalog::FallbackTarget; use fabro_llm::client::Client; -use fabro_llm::provider::Provider; +use fabro_model::FallbackTarget; +use fabro_model::Provider; use crate::context::Context; use crate::cost::compute_stage_cost; @@ -268,9 +268,9 @@ impl CodergenBackend for AgentApiBackend { .map(String::from) .or_else(|| Some(self.provider.as_str().to_string())); - let max_tokens = node.max_tokens().or_else(|| { - fabro_llm::catalog::get_model_info(model).and_then(|m| m.limits.max_output) - }); + let max_tokens = node + .max_tokens() + .or_else(|| fabro_model::get_model_info(model).and_then(|m| m.limits.max_output)); let mut messages = Vec::new(); if let Some(sys) = system_prompt { @@ -343,8 +343,7 @@ impl CodergenBackend for AgentApiBackend { ); let max_tokens = node.max_tokens().or_else(|| { - fabro_llm::catalog::get_model_info(&target.model) - .and_then(|m| m.limits.max_output) + fabro_model::get_model_info(&target.model).and_then(|m| m.limits.max_output) }); let fallback_request = fabro_llm::types::Request { diff --git a/lib/crates/fabro-workflows/src/backend/cli.rs b/lib/crates/fabro-workflows/src/backend/cli.rs index 2855b12ce..16af8e6dd 100644 --- a/lib/crates/fabro-workflows/src/backend/cli.rs +++ b/lib/crates/fabro-workflows/src/backend/cli.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use async_trait::async_trait; use fabro_agent::sandbox::ExecResult; use fabro_agent::Sandbox; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use crate::context::Context; use crate::cost::compute_stage_cost; diff --git a/lib/crates/fabro-workflows/src/cost.rs b/lib/crates/fabro-workflows/src/cost.rs index c5b7d4c63..09200322b 100644 --- a/lib/crates/fabro-workflows/src/cost.rs +++ b/lib/crates/fabro-workflows/src/cost.rs @@ -3,7 +3,7 @@ use crate::outcome::StageUsage; /// Compute the dollar cost for a stage's token usage, if pricing is available. #[must_use] pub fn compute_stage_cost(usage: &StageUsage) -> Option { - let info = fabro_llm::catalog::get_model_info(&usage.model)?; + let info = fabro_model::get_model_info(&usage.model)?; let input_rate = info.costs.input_cost_per_mtok?; let output_rate = info.costs.output_cost_per_mtok?; Some( diff --git a/lib/crates/fabro-workflows/src/handler/prompt.rs b/lib/crates/fabro-workflows/src/handler/prompt.rs index fb4caf8be..31bb5cb5b 100644 --- a/lib/crates/fabro-workflows/src/handler/prompt.rs +++ b/lib/crates/fabro-workflows/src/handler/prompt.rs @@ -2,7 +2,7 @@ use std::path::Path; use async_trait::async_trait; -use fabro_llm::provider::Provider; +use fabro_model::Provider; use crate::context::keys; use crate::context::Context; diff --git a/lib/crates/fabro-workflows/src/transform.rs b/lib/crates/fabro-workflows/src/transform.rs index fcb7158f6..80ea396ab 100644 --- a/lib/crates/fabro-workflows/src/transform.rs +++ b/lib/crates/fabro-workflows/src/transform.rs @@ -125,7 +125,7 @@ impl Transform for ProviderInferenceTransform { .map(String::from); if let Some(model) = model { if !node.attrs.contains_key("provider") { - if let Some(info) = fabro_llm::catalog::get_model_info(&model) { + if let Some(info) = fabro_model::get_model_info(&model) { node.attrs .insert("provider".to_string(), AttrValue::String(info.provider)); }