Extract fabro-model crate from fabro-llm (#109)

This PR extracts a new `fabro-model` crate from `fabro-llm`, isolating
the model catalog, provider identity types, and model metadata into a
focused, lightweight package. The new crate contains `Provider`,
`ModelId`, `ModelInfo` (and related structs), and the full catalog API
(`get_model_info`, `list_models`, `default_model_from_env`,
`build_fallback_chain`, etc.) — with only `serde` and `serde_json` as
runtime dependencies, no async runtime required.

The extraction follows a clean boundary: `fabro-llm` now depends on
`fabro-model` and re-exports `Provider` and `ModelId` at its root so
existing `fabro_llm::Provider` usage remains valid without a migration.
The `ProviderAdapter` trait, `StreamEventStream`, and
`validate_tool_choice` stay in `fabro-llm` since they depend on async
streaming types. All seven consumer crates (`fabro-agent`, `fabro-api`,
`fabro-cli`, `fabro-hooks`, `fabro-workflows`, `fabro-validate`, and
`fabro-llm` itself) are updated to import catalog functions and provider
types directly from `fabro_model` rather than `fabro_llm::catalog` or
`fabro_llm::provider`.

The most impactful dependency reduction is in `fabro-validate`, which
previously pulled in all of `fabro-llm` (tokio, reqwest, the full async
runtime) just to perform catalog lookups and validate provider names. It
now depends solely on `fabro-model`, eliminating that heavyweight
transitive closure. All other consumers retain their `fabro-llm`
dependency for client and streaming functionality, but gain a precise
import path for catalog and identity concerns.

### Fabro Details

<details>
<summary>Ran 9 stages in 21m 16s for $5.47</summary>

| Stage | Duration | Cost | Retries |
|---|---|---|---|
| start | 0s | – | 0 |
| toolchain | 0s | – | 0 |
| preflight_compile | 1m 10s | – | 0 |
| preflight_lint | 12s | – | 0 |
| implement | 13m 29s | $3.81 | 1 |
| simplify_opus | 5m 41s | $1.66 | 0 |
| simplify_gpt | 0s | – | 0 |
| verify | 15s | – | 0 |
| fmt | 1s | – | 0 |
| **Total** | **21m 16s** | **$5.47** | **1** |

</details>

<details>
<summary>Ran <code>ImplementAndSimplify.fabro</code> (12 nodes and 15
edges)</summary>

```dot
digraph ImplementAndSimplify {
    graph [
        goal="Implement and simplify",
        model_stylesheet="
            * { backend: api; model: claude-opus-4-6;}
        "
    ]
    rankdir=LR

    start [shape=Mdiamond, label="Start"]
    exit  [shape=Msquare, label="Exit"]

    toolchain         [label="Toolchain", shape=parallelogram, script="command -v cargo >/dev/null || { curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && sudo ln -sf $HOME/.cargo/bin/* /usr/local/bin/; }; cargo --version 2>&1", max_retries=0]
    preflight_compile [label="Preflight Compile", shape=parallelogram, script="cargo check -q --workspace 2>&1", max_retries=0]
    preflight_lint    [label="Preflight Lint", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1", max_retries=0]
    fix_lints         [label="Fix Lints", prompt="The preflight lint step failed. Read the build output from context and fix all clippy lint warnings.", max_visits=3]
    implement         [label="Implement", prompt="Read the plan file referenced in the goal and implement every step. Make all the code changes described in the plan. Use red/green TDD."]
    simplify_opus     [label="Simplify (Opus)", prompt="@prompts/simplify.md"]
    simplify_gpt      [label="Simplify (GPT-54)", prompt="@prompts/simplify.md", model="gpt-54"]
    verify            [label="Verify", shape=parallelogram, script="cargo clippy -q --workspace -- -D warnings 2>&1 && cargo nextest run --cargo-quiet --workspace --status-level fail 2>&1", goal_gate=true, retry_target="fixup"]
    fixup             [label="Fixup", prompt="The verify step failed. Read the build output from context and fix all clippy lint warnings and test failures.", max_visits=3]
    fmt               [label="Format", shape=parallelogram, script="cargo fmt --all 2>&1", max_retries=0]

    start -> toolchain
    toolchain -> preflight_compile [condition="outcome=success"]
    toolchain -> exit
    preflight_compile -> preflight_lint [condition="outcome=success"]
    preflight_compile -> exit
    preflight_lint -> implement [condition="outcome=success"]
    preflight_lint -> fix_lints
    fix_lints -> preflight_lint
    implement -> simplify_opus -> simplify_gpt -> verify
    verify -> fmt   [condition="outcome=success"]
    verify -> fixup
    fixup -> verify
    fmt -> exit
}

```

</details>

⚒️ Generated with [Fabro](https://fabro.sh)

---------

Co-authored-by: Fabro <noreply@fabro.sh>
This commit is contained in:
brynary-fabro[bot] 2026-03-19 22:26:47 -04:00 committed by GitHub
parent 35df8583db
commit 62c310bcec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 455 additions and 395 deletions

17
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View file

@ -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<Styles> = std::sync::LazyLock::new(|| Styles::new(false));

View file

@ -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};

View file

@ -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") {

View file

@ -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 {

View file

@ -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.
///

View file

@ -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 {

View file

@ -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() {

View file

@ -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,

View file

@ -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};

View file

@ -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));

View file

@ -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() {

View file

@ -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"] }

View file

@ -576,7 +576,7 @@ pub async fn list_models(
State(_state): State<Arc<AppState>>,
Query(pagination): Query<PaginationParams>,
) -> Response {
paginated_response(fabro_llm::catalog::list_models(None), &pagination)
paginated_response(fabro_model::list_models(None), &pagination)
}
// ── Settings ───────────────────────────────────────────────────────────

View file

@ -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)),

View file

@ -1053,7 +1053,7 @@ async fn test_model(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> 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

View file

@ -61,11 +61,11 @@ fn generate_title(content: &str) -> String {
fn resolve_model(model_arg: Option<String>) -> (String, Option<String>) {
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),
}

View file

@ -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"

View file

@ -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,

View file

@ -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;

View file

@ -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<fabro_llm::catalog::FallbackTarget> {
) -> Vec<fabro_model::FallbackTarget> {
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::<fabro_llm::provider::Provider>())
.map(|s| s.parse::<fabro_model::Provider>())
.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())

View file

@ -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<Result<(), String>> {
}
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()))
}

View file

@ -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;

View file

@ -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"),
}

View file

@ -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;

View file

@ -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

View file

@ -218,7 +218,7 @@ impl HookExecutorImpl {
/// Resolve a model alias (e.g. "haiku") to a concrete model ID.
fn resolve_model(model: &Option<String>) -> 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())

View file

@ -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

View file

@ -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<Color> {
}
}
fn model_row(model: &crate::types::ModelInfo, use_color: bool) -> Vec<CellStruct> {
fn model_row(model: &ModelInfo, use_color: bool) -> Vec<CellStruct> {
let aliases = model.aliases.join(", ");
let cost = format!(
"{} / {}",

View file

@ -144,7 +144,7 @@ impl Client {
/// Resolve the provider for a request.
fn resolve_provider(&self, request: &Request) -> Result<Arc<dyn ProviderAdapter>, 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

View file

@ -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};

View file

@ -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<Self, Self::Err> {
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<String>) -> 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::<Provider>().unwrap(), Provider::Kimi);
}
#[test]
fn parse_zai() {
assert_eq!("zai".parse::<Provider>().unwrap(), Provider::Zai);
}
#[test]
fn parse_minimax() {
assert_eq!("minimax".parse::<Provider>().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::<Provider>().unwrap(),
Provider::Inception
);
assert_eq!(
"inception_labs".parse::<Provider>().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;

View file

@ -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,

View file

@ -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<i64>,
}
#[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<f64>,
pub output_cost_per_mtok: Option<f64>,
pub cache_input_cost_per_mtok: Option<f64>,
}
#[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<String>,
pub features: ModelFeatures,
pub costs: ModelCosts,
pub estimated_output_tps: Option<f64>,
pub aliases: Vec<String>,
#[serde(default)]
pub default: bool,
}
pub use fabro_model::{ModelCosts, ModelFeatures, ModelInfo, ModelLimits};
// --- 4.7 Timeouts ---

View file

@ -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

View file

@ -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};

View file

@ -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<Self, Self::Err> {
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<String>) -> 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::<Provider>().unwrap(), Provider::Kimi);
}
#[test]
fn parse_zai() {
assert_eq!("zai".parse::<Provider>().unwrap(), Provider::Zai);
}
#[test]
fn parse_minimax() {
assert_eq!("minimax".parse::<Provider>().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::<Provider>().unwrap(),
Provider::Inception
);
assert_eq!(
"inception_labs".parse::<Provider>().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()));
}
}

View file

@ -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<i64>,
}
#[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<f64>,
pub output_cost_per_mtok: Option<f64>,
pub cache_input_cost_per_mtok: Option<f64>,
}
#[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<String>,
pub features: ModelFeatures,
pub costs: ModelCosts,
pub estimated_output_tps: Option<f64>,
pub aliases: Vec<String>,
#[serde(default)]
pub default: bool,
}

View file

@ -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 }

View file

@ -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();

View file

@ -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

View file

@ -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 {

View file

@ -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;

View file

@ -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<f64> {
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(

View file

@ -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;

View file

@ -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));
}