Read catalog policy from lithos core fields and metadata.agent

Fabro's policy layer restated the lithos built-ins under `metadata.fabro`:
enabled flags, credentials, display facts, probe and small-default roles,
and agent profiles. lithos-llm now carries every one of those as a core
field or under the shared `metadata.agent` namespace, so the layer and its
typed view go:

- Delete `fabro-policy.toml` and `FABRO_POLICY_TOML`. The catalog is the
  lithos built-ins plus the operator's `[llm]` overlay, nothing between.
- Delete `fabro_types::catalog_policy`. `enabled`, `stands_in_for`,
  `api_key_url`, `family`, the cutoffs, `estimated_output_tps`,
  `small_default`, and `probe` are read from lithos accessors; the agent
  profile and `reasoning_by_default` come from `metadata.agent`, which
  Pebble reads too.
- `catalog::provider`, `enabled_providers`, and `listed_providers` return
  the lithos `CatalogProvider` directly; `ModelEntry` loses its policy
  field and gains `agent_profile()`.
- Test fixtures move `[providers.x.metadata.fabro] enabled = true` onto
  the provider table, drop `credentials` lists in favor of the secret name
  lithos derives from the provider id, and spell `agent_profile` as
  `metadata.agent.profile`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-09 23:20:12 -06:00
parent 6dfc96d3fd
commit 2a2fc41807
No known key found for this signature in database
36 changed files with 314 additions and 1445 deletions

View file

@ -83,22 +83,22 @@ fn supports_install_api_key(provider: &CatalogProvider) -> bool {
fn install_llm_provider_ids(catalog: &Catalog) -> Vec<ProviderId> {
catalog::listed_providers(catalog)
.iter()
.filter(|entry| supports_install_api_key(entry.provider))
.map(|entry| entry.provider.id().clone())
.into_iter()
.filter(|provider| supports_install_api_key(provider))
.map(|provider| provider.id().clone())
.collect()
}
fn provider_env_var_label(provider: &ProviderId, catalog: &Catalog) -> String {
catalog::provider(catalog, provider.as_str())
.map(|entry| fabro_auth::secret_names(entry.provider).join(" / "))
.map(|provider| fabro_auth::secret_names(provider).join(" / "))
.filter(|label| !label.is_empty())
.unwrap_or_else(|| "API_KEY".to_string())
}
fn provider_vault_secret_name(provider: &ProviderId, catalog: &Catalog) -> String {
catalog::provider(catalog, provider.as_str())
.and_then(|entry| fabro_auth::expected_secret_name(entry.provider))
.and_then(fabro_auth::expected_secret_name)
.unwrap_or_else(|| format!("{}_API_KEY", provider.to_string().to_uppercase()))
}

View file

@ -61,7 +61,7 @@ fn default_catalog_for_provider_auth() -> Arc<Catalog> {
pub(crate) fn provider_display_name(provider: &ProviderId, catalog: &Catalog) -> String {
catalog::provider(catalog, provider.as_str()).map_or_else(
|| provider.to_string(),
|entry| entry.provider.display_name().to_string(),
|provider| provider.display_name().to_string(),
)
}
@ -69,14 +69,14 @@ fn api_key_catalog_provider<'a>(
provider: &ProviderId,
catalog: &'a Catalog,
) -> Result<&'a CatalogProvider> {
let entry = catalog::provider(catalog, provider.as_str())
let provider = catalog::provider(catalog, provider.as_str())
.with_context(|| format!("provider '{provider}' is not configured in the model catalog"))?;
anyhow::ensure!(
fabro_auth::accepts_api_key(entry.provider),
fabro_auth::accepts_api_key(provider),
"provider '{}' does not define an API-key credential path",
entry.provider.id()
provider.id()
);
Ok(entry.provider)
Ok(provider)
}
pub(crate) async fn validate_api_key(
@ -368,8 +368,6 @@ async fn await_user_response_from_source(
#[cfg(test)]
mod tests {
use fabro_types::catalog_policy;
use super::*;
#[test]
@ -385,8 +383,7 @@ mod tests {
ProviderId::new("inception"),
] {
let provider = api_key_catalog_provider(&provider, &catalog).unwrap();
let policy = catalog_policy::provider_policy(provider);
let url = policy.api_key_url.as_deref().unwrap_or_default();
let url = provider.api_key_url().unwrap_or_default();
assert!(!url.is_empty(), "{} has empty URL", provider.id());
assert!(url.starts_with("https://"), "{} URL: {url}", provider.id());
}

View file

@ -333,7 +333,7 @@ fn exec_accepts_configured_custom_provider_from_settings() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.fabro]\nagent_profile = \"openai\"\ncredentials = [\"env:ACME_AWS_API_KEY\"]\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n",
"_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n",
);
let mut cmd = context.exec_cmd();
@ -398,7 +398,7 @@ fn exec_server_target_accepts_configured_custom_provider_from_settings() {
let context = test_context!();
context.write_home(
".fabro/settings.toml",
"_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.fabro]\nagent_profile = \"openai\"\ncredentials = [\"env:ACME_AWS_API_KEY\"]\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n",
"_version = 1\n\n[llm.providers.acme-aws]\ndisplay_name = \"Acme AWS\"\nadapter = \"openai-compatible\"\ncodec = \"openai-chat\"\nbase_url = \"https://bedrock.example.invalid/v1\"\nauth = { type = \"bearer\" }\nallow_passthrough = true\n\n[llm.providers.acme-aws.metadata.agent]\nprofile = \"openai\"\n\n[cli.exec.model]\nprovider = \"acme-aws\"\nname = \"acme-claude-sonnet-4-6\"\n",
);
let server = MockServer::start();
server.mock(|when, then| {

View file

@ -846,14 +846,14 @@ async fn put_install_llm(
}
fn install_catalog_provider(provider: &ProviderId) -> Result<&'static CatalogProvider, String> {
let entry = llm_catalog::provider(&INSTALL_CATALOG, provider.as_str())
let catalog_provider = llm_catalog::provider(&INSTALL_CATALOG, provider.as_str())
.ok_or_else(|| format!("provider '{provider}' is not configured in the model catalog"))?;
if fabro_auth::accepts_api_key(entry.provider) {
Ok(entry.provider)
if fabro_auth::accepts_api_key(catalog_provider) {
Ok(catalog_provider)
} else {
Err(format!(
"provider '{}' does not define an API-key credential path",
entry.provider.id()
catalog_provider.id()
))
}
}

View file

@ -1763,7 +1763,7 @@ mod tests {
fn openrouter_catalog() -> Catalog {
fabro_llm::test_support::test_catalog_with_overlay(
"[providers.openrouter.metadata.fabro]\nenabled = true\n",
"[providers.openrouter]\nenabled = true\n",
)
}
@ -1883,9 +1883,8 @@ base_url = "{moonshot_url}"
[providers.openrouter]
base_url = "{openrouter_url}"
[providers.openrouter.metadata.fabro]
enabled = true
"#
))
.vault_entries([
@ -3029,9 +3028,8 @@ base_url = "https://api.acme.test/v1"
auth = { type = "bearer" }
default_model = "acme-large"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models."acme-large"]
display_name = "Acme Large"

View file

@ -909,13 +909,13 @@ fn api_model_on_eligible(
eligible: &std::collections::HashSet<ProviderId>,
) -> Option<(ProviderId, String)> {
catalog::enabled_providers(catalog)
.iter()
.filter(|entry| eligible.contains(entry.provider.id()))
.find_map(|entry| {
catalog::provider_models(entry.provider)
.into_iter()
.filter(|provider| eligible.contains(provider.id()))
.find_map(|provider| {
catalog::provider_models(provider)
.into_iter()
.find(|model| model.model.api_model() == api_model)
.map(|model| (entry.provider.id().clone(), model.model.id().to_string()))
.map(|model| (provider.id().clone(), model.model.id().to_string()))
})
}
@ -1554,9 +1554,8 @@ default_model = "gpt-5.6-sol"
[providers.openrouter]
default_model = "gpt-5.6-sol"
[providers.openrouter.metadata.fabro]
enabled = true
"#,
)
}

View file

@ -323,7 +323,10 @@ fn openai_responses_payload(text: &str) -> serde_json::Value {
/// An operator-defined OpenAI-compatible provider `acme` offering one model,
/// `acme-large`, with `credential` (`env:NAME` or `vault:NAME`).
fn acme_overlay(base_url: &str, credential: &str) -> String {
/// An operator-defined provider. Its API key is `ACME_API_KEY`, the name
/// lithos derives from the provider id, whether it lives in the vault or the
/// environment.
fn acme_overlay(base_url: &str) -> String {
format!(
r#"
[providers.acme]
@ -335,21 +338,18 @@ auth = {{ type = "bearer" }}
priority = 120
default_model = "acme-large"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = [{credential}]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models."acme-large"]
display_name = "Acme Large"
api_model = "acme-large"
limits = {{ context_tokens = 128000, max_output_tokens = 8192 }}
capabilities = {{ text = true, tools = true }}
[providers.acme.models."acme-large".metadata.fabro]
probe = true
"#,
base_url = toml::Value::String(base_url.to_string()),
credential = toml::Value::String(credential.to_string()),
)
}
@ -5591,10 +5591,7 @@ async fn validate_endpoint_returns_workflow_summary_without_preflight_checks() {
#[tokio::test]
async fn validate_endpoint_uses_app_state_catalog_for_model_diagnostics() {
let state = TestAppStateBuilder::new()
.llm_overlay_toml(&acme_overlay(
"https://api.acme.test/v1",
"env:ACME_API_KEY",
))
.llm_overlay_toml(&acme_overlay("https://api.acme.test/v1"))
.build();
let app = crate::test_support::build_test_router(state);
let dot = r#"digraph Test {
@ -8826,9 +8823,8 @@ auth = {{ type = "bearer" }}
priority = 120
default_model = "portable-model"
[providers.direct.metadata.fabro]
agent_profile = "openai"
credentials = ["vault:DIRECT_API_KEY"]
[providers.direct.metadata.agent]
profile = "openai"
[providers.direct.models.portable-model]
display_name = "Portable (direct)"
@ -8846,9 +8842,8 @@ auth = {{ type = "bearer" }}
priority = 110
default_model = "portable-model"
[providers.aggregator.metadata.fabro]
agent_profile = "openai"
credentials = ["vault:AGGREGATOR_API_KEY"]
[providers.aggregator.metadata.agent]
profile = "openai"
[providers.aggregator.models.portable-model]
display_name = "Portable (aggregator)"
@ -9011,9 +9006,8 @@ auth = {{ type = "bearer" }}
priority = 120
default_model = "acme-reasoner"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["vault:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-reasoner]
display_name = "Acme Reasoner"
@ -9080,7 +9074,7 @@ async fn test_provider_credentials_uses_app_state_catalog() {
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}
}));
});
let overlay = acme_overlay(&upstream.base_url(), "vault:ACME_API_KEY");
let overlay = acme_overlay(&upstream.base_url());
let state = TestAppStateBuilder::new()
.runtime_settings(default_test_server_settings(), RunLayer::default())
.max_concurrent_runs(5)
@ -9200,7 +9194,7 @@ async fn list_models_marks_configured_true_when_provider_has_credential_material
#[tokio::test]
async fn list_models_marks_configured_false_when_provider_cannot_register() {
let overlay = acme_overlay("https://api.acme.test/v1", "env:ACME_API_KEY");
let overlay = acme_overlay("https://api.acme.test/v1");
let state = TestAppStateBuilder::new()
.runtime_settings(default_test_server_settings(), RunLayer::default())
.max_concurrent_runs(5)
@ -9270,7 +9264,7 @@ async fn list_models_unknown_provider_returns_empty_page() {
#[tokio::test]
async fn list_models_uses_app_state_catalog_overrides() {
let overlay = acme_overlay("https://api.acme.test/v1", "env:ACME_API_KEY");
let overlay = acme_overlay("https://api.acme.test/v1");
let state = TestAppStateBuilder::new()
.llm_overlay_toml(&overlay)
.build();
@ -9338,9 +9332,7 @@ async fn list_providers_marks_configured_per_provider_and_omits_secrets() {
// this exact provider, not merely be populated.
let catalog = state_test_catalog();
let expected_model_count = fabro_llm::catalog::provider_models(
fabro_llm::catalog::provider(&catalog, "anthropic")
.expect("anthropic should be listed")
.provider,
fabro_llm::catalog::provider(&catalog, "anthropic").expect("anthropic should be listed"),
)
.len();
assert_eq!(
@ -9557,7 +9549,7 @@ async fn test_providers_auth_issue_returns_error_without_upstream_call() {
async fn test_providers_registration_issue_returns_error_without_probe() {
// An adapter lithos does not ship cannot be built, so the provider is
// configured (it has a vault key) yet unavailable.
let overlay = acme_overlay("https://api.acme.test/v1", "vault:ACME_API_KEY").replace(
let overlay = acme_overlay("https://api.acme.test/v1").replace(
"adapter = \"openai-compatible\"",
"adapter = \"not-an-adapter\"",
);
@ -9639,16 +9631,11 @@ auth = {{ type = "bearer" }}
priority = 50
default_model = "zeta-probe"
[providers.zeta.metadata.fabro]
credentials = ["vault:ZETA_API_KEY"]
[providers.zeta.models.zeta-probe]
display_name = "Zeta Probe"
api_model = "zeta-probe"
limits = {{ context_tokens = 128000, max_output_tokens = 8192 }}
capabilities = {{ text = true, tools = true }}
[providers.zeta.models.zeta-probe.metadata.fabro]
probe = true
[providers.alpha]
@ -9660,17 +9647,13 @@ auth = {{ type = "bearer" }}
priority = 40
default_model = "alpha-probe"
[providers.alpha.metadata.fabro]
credentials = ["vault:ALPHA_API_KEY"]
[providers.alpha.models.alpha-probe]
display_name = "Alpha Probe"
api_model = "alpha-probe"
limits = {{ context_tokens = 128000, max_output_tokens = 8192 }}
capabilities = {{ text = true, tools = true }}
[providers.alpha.models.alpha-probe.metadata.fabro]
probe = true
"#,
base_url = toml::Value::String(server.base_url()),
);
@ -18678,7 +18661,7 @@ async fn create_completion_default_model_uses_app_state_catalog() {
.header("content-type", "application/json")
.json_body(json!({"error": {"message": "expected test failure"}}));
});
let overlay = acme_overlay(&upstream.base_url(), "vault:ACME_API_KEY");
let overlay = acme_overlay(&upstream.base_url());
let state = TestAppStateBuilder::new()
.llm_overlay_toml(&overlay)
.vault_entries([("ACME_API_KEY", "acme-test-key")])

View file

@ -40,7 +40,7 @@ pub trait AgentProfile: Send + Sync {
fn knowledge_cutoff(&self) -> Option<String> {
self.catalog_model()
.and_then(|entry| entry.policy.knowledge_cutoff)
.and_then(|entry| entry.model.knowledge_cutoff().map(str::to_string))
}
/// The catalog row for this profile's route, when the catalog knows it.

View file

@ -425,8 +425,8 @@ fn cli_client_options(args: &AgentArgs, styles: &'static Styles) -> ClientOption
}
}
/// The catalog the standalone agent runs against: lithos built-ins, Fabro
/// policy, and the operator's `[llm]` overlay from the active settings file.
/// The catalog the standalone agent runs against: the lithos built-ins and
/// the operator's `[llm]` overlay from the active settings file.
#[expect(
clippy::disallowed_methods,
reason = "Standalone agent honors OPENAI_BASE_URL from the process environment."
@ -958,19 +958,18 @@ base_url = "https://example.invalid/v1"
auth = { type = "bearer" }
default_model = "acme-aws-claude"
[providers.acme-aws.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme-aws.metadata.agent]
profile = "openai"
[providers.acme-aws.models.acme-aws-claude]
display_name = "Acme AWS Claude"
api_model = "acme-aws-claude"
limits = { context_tokens = 1000, max_output_tokens = 500 }
capabilities = { text = true, tools = true }
[providers.acme-aws.models.acme-aws-claude.metadata.fabro]
family = "claude"
agent_profile = "anthropic"
[providers.acme-aws.models.acme-aws-claude.metadata.agent]
profile = "anthropic"
"#;
/// The same provider with no models, so its default comes from the
@ -984,9 +983,8 @@ base_url = "https://example.invalid/v1"
auth = { type = "bearer" }
allow_passthrough = true
[providers.acme-aws.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme-aws.metadata.agent]
profile = "openai"
"#;
fn acme_catalog() -> Catalog {

View file

@ -237,7 +237,7 @@ mod tests {
/// OpenRouter ships disabled in the built-in catalog.
fn catalog_with_openrouter() -> Arc<Catalog> {
Arc::new(test_catalog_with_overlay(
"[providers.openrouter.metadata.fabro]
"[providers.openrouter]
enabled = true
",
))

View file

@ -172,7 +172,7 @@ mod tests {
/// selectable. Enable it the way they would, to observe gateway routing.
fn catalog_with_openrouter() -> Arc<Catalog> {
Arc::new(test_catalog_with_overlay(
"[providers.openrouter.metadata.fabro]
"[providers.openrouter]
enabled = true
",
))

View file

@ -467,7 +467,7 @@ mod tests {
/// OpenRouter ships disabled, so an operator opts in before its models are
/// selectable.
const OPENROUTER_ENABLED: &str = "[providers.openrouter.metadata.fabro]\nenabled = true\n";
const OPENROUTER_ENABLED: &str = "[providers.openrouter]\nenabled = true\n";
fn native_tool_options(
profile_kind: AgentProfileKind,

View file

@ -8,7 +8,7 @@ use fabro_llm::test_support::test_catalog;
fn profile_context_window_matches_catalog_for_default_models() {
let catalog = Arc::new(test_catalog());
for provider in catalog::listed_providers(&catalog) {
let provider_id = provider.provider.id().clone();
let provider_id = provider.id().clone();
let Some(default) = catalog::default_model(&catalog, provider_id.as_str()) else {
// Deployment-defined providers (LiteLLM, Modal, Ollama) carry no
// built-in default model.

View file

@ -168,7 +168,7 @@ async fn make_twin_client(twin: &OpenAiTwinOptions) -> Client {
/// resolves the OpenAI-compatible codec the twin speaks.
fn litellm_twin_overlay(base_url: &str) -> String {
format!(
"[providers.litellm]\nbase_url = {}\n\n[providers.litellm.metadata.fabro]\nenabled = true\n",
"[providers.litellm]\nbase_url = {}\nenabled = true\n",
toml::Value::String(base_url.to_string())
)
}

View file

@ -1,883 +0,0 @@
# Fabro's policy layer over the lithos-llm built-in catalog.
#
# Applied after the built-ins and before operator `[llm]` overlays. Everything
# under `metadata.fabro` is Fabro policy that lithos carries verbatim:
#
# - `enabled`: whether Fabro offers the provider or model.
# - `credentials`: ordered credential references (`env:NAME`, `vault:NAME`,
# `aws_sigv4`); the first that resolves wins.
# - `extra_headers`: request headers, literal or `{{ secrets.NAME }}`.
# - `agent_profile`, `family`, `small_default`, `probe`: agent and selection
# policy read by the Fabro model resolver.
#
# Modal's default model comes from a deployment template, not this layer.
# GPT-OSS is excluded at user request. This layer cannot restore that family.
schema_version = 1
[providers."anthropic"]
priority = 100
default_model = "claude-sonnet-5"
[providers."anthropic".metadata.fabro]
api_key_url = "https://console.anthropic.com/settings/keys"
credentials = ["env:ANTHROPIC_API_KEY", "vault:ANTHROPIC_API_KEY"]
enabled = true
[providers."anthropic".models."claude-fable-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."anthropic".models."claude-opus-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."anthropic".models."claude-sonnet-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."anthropic".models."claude-opus-4.8".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."anthropic".models."claude-opus-4.7".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."anthropic".models."claude-opus-4.6".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."anthropic".models."claude-sonnet-4.5".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."anthropic".models."claude-sonnet-4.6".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."anthropic".models."claude-haiku-4.5".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = true
probe = true
[providers."bedrock-openai"]
priority = 19
default_model = "gpt-5.5"
[providers."bedrock-openai".metadata.fabro]
api_key_url = "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html"
credentials = ["env:AWS_BEARER_TOKEN_BEDROCK", "env:BEDROCK_API_KEY", "vault:AWS_BEARER_TOKEN_BEDROCK", "vault:BEDROCK_API_KEY"]
enabled = false
[providers."bedrock-openai".models."gpt-5.5".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock-openai".models."gpt-5.4".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock"]
priority = 20
default_model = "claude-sonnet-5"
[providers."bedrock".metadata.fabro]
api_key_url = "https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys.html"
credentials = ["env:AWS_BEARER_TOKEN_BEDROCK", "env:BEDROCK_API_KEY", "vault:AWS_BEARER_TOKEN_BEDROCK", "vault:BEDROCK_API_KEY", "aws_sigv4"]
enabled = false
[providers."bedrock".models."anthropic.claude-sonnet-4-6".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."bedrock".models."claude-opus-4-8".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = false
probe = false
[providers."bedrock".models."claude-haiku-4-5".metadata.fabro]
family = "claude-4"
agent_profile = "anthropic"
small_default = true
probe = false
[providers."bedrock".models."nova-2-lite".metadata.fabro]
family = "nova-2"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."llama-4-maverick".metadata.fabro]
family = "llama-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."mistral-large-3".metadata.fabro]
family = "mistral-large"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."devstral-2".metadata.fabro]
family = "devstral"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."deepseek-v3.2".metadata.fabro]
family = "deepseek-v3"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."kimi-k2.5".metadata.fabro]
family = "kimi-k2"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."glm-5".metadata.fabro]
family = "glm"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."minimax-m2.5".metadata.fabro]
family = "minimax-m2"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."nemotron-3-super".metadata.fabro]
family = "nemotron-3"
agent_profile = "openai"
small_default = false
probe = false
[providers."bedrock".models."claude-fable-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."bedrock".models."claude-sonnet-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."deepseek"]
priority = 75
default_model = "deepseek-v4-flash"
[providers."deepseek".metadata.fabro]
api_key_url = "https://platform.deepseek.com/api_keys"
credentials = ["env:DEEPSEEK_API_KEY", "vault:DEEPSEEK_API_KEY"]
enabled = true
[providers."deepseek".models."deepseek-v4-flash".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = true
probe = true
[providers."deepseek".models."deepseek-v4-pro".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks"]
priority = 30
default_model = "kimi-k2.7-code"
[providers."fireworks".metadata.fabro]
api_key_url = "https://app.fireworks.ai/settings/users/api-keys"
credentials = ["env:FIREWORKS_API_KEY", "vault:FIREWORKS_API_KEY"]
enabled = false
# Moonshot documents K3 as always reasoning with low, high, and max effort;
# lithos leaves the levels unverified, so Fabro records them here.
[providers."fireworks".models."kimi-k3"]
capabilities = { reasoning_effort = { minimal = false, low = true, medium = false, high = true, xhigh = false, max = true } }
[providers."fireworks".models."kimi-k3".metadata.fabro]
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."fireworks".models."kimi-k3-fast".metadata.fabro]
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."fireworks".models."kimi-k2.7-code".metadata.fabro]
family = "kimi-k2"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."kimi-k2.6".metadata.fabro]
family = "kimi-k2"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."deepseek-v4-pro".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."deepseek-v4-flash".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."glm-5.2".metadata.fabro]
family = "glm-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."minimax-m2.7".metadata.fabro]
family = "minimax-m2"
agent_profile = "openai"
small_default = false
probe = false
[providers."fireworks".models."qwen3.7-plus".metadata.fabro]
family = "qwen3"
agent_profile = "openai"
small_default = false
probe = false
[providers."gemini"]
priority = 80
default_model = "gemini-3.5-flash"
[providers."gemini".metadata.fabro]
api_key_url = "https://aistudio.google.com/apikey"
credentials = ["env:GEMINI_API_KEY", "env:GOOGLE_API_KEY", "vault:GEMINI_API_KEY"]
enabled = true
[providers."gemini".models."gemini-3.1-pro-preview".metadata.fabro]
family = "gemini-3"
agent_profile = "gemini"
small_default = false
probe = false
[providers."gemini".models."gemini-3.1-pro-preview-customtools".metadata.fabro]
family = "gemini-3"
agent_profile = "gemini"
small_default = false
probe = false
[providers."gemini".models."gemini-3.5-flash".metadata.fabro]
family = "gemini-3"
agent_profile = "gemini"
small_default = false
probe = false
[providers."gemini".models."gemini-3-flash-preview".metadata.fabro]
family = "gemini-3"
agent_profile = "gemini"
small_default = false
probe = false
[providers."gemini".models."gemini-3.1-flash-lite".metadata.fabro]
family = "gemini-3"
agent_profile = "gemini"
small_default = true
probe = false
[providers."inception"]
priority = 40
default_model = "mercury-2"
[providers."inception".metadata.fabro]
api_key_url = "https://console.inceptionlabs.ai/api-keys"
credentials = ["env:INCEPTION_API_KEY", "vault:INCEPTION_API_KEY"]
enabled = true
[providers."inception".models."mercury-2".metadata.fabro]
family = "mercury"
agent_profile = "openai"
small_default = false
probe = false
[providers."litellm"]
priority = 50
[providers."litellm".metadata.fabro]
credentials = ["env:LITELLM_API_KEY", "vault:LITELLM_API_KEY"]
enabled = false
[providers."minimax"]
priority = 50
default_model = "minimax-m2.5"
[providers."minimax".metadata.fabro]
api_key_url = "https://platform.minimaxi.com/user-center/basic-information/interface-key"
credentials = ["env:MINIMAX_API_KEY", "vault:MINIMAX_API_KEY"]
enabled = true
[providers."minimax".models."minimax-m2.5".metadata.fabro]
family = "minimax-m2"
agent_profile = "openai"
small_default = false
probe = false
[providers."modal"]
priority = 75
[providers."modal".metadata.fabro]
api_key_url = "https://modal.com/docs/guide/endpoints#proxy-tokens"
enabled = false
[providers."modal".metadata.fabro.extra_headers]
"Modal-Key" = "{{ secrets.MODAL_TOKEN_ID }}"
"Modal-Secret" = "{{ secrets.MODAL_TOKEN_SECRET }}"
[providers."moonshot"]
priority = 70
default_model = "kimi-k3"
[providers."moonshot".metadata.fabro]
api_key_url = "https://platform.kimi.ai/console/api-keys"
credentials = ["env:MOONSHOT_API_KEY", "env:KIMI_API_KEY", "vault:MOONSHOT_API_KEY", "vault:KIMI_API_KEY"]
enabled = true
[providers."moonshot".models."kimi-k2.5".metadata.fabro]
family = "kimi-k2"
agent_profile = "kimi"
small_default = false
probe = false
# Moonshot documents K3 as always reasoning with low, high, and max effort;
# lithos leaves the levels unverified, so Fabro records them here.
[providers."moonshot".models."kimi-k3"]
capabilities = { reasoning_effort = { minimal = false, low = true, medium = false, high = true, xhigh = false, max = true } }
[providers."moonshot".models."kimi-k3".metadata.fabro]
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."ollama"]
priority = 30
[providers."ollama".metadata.fabro]
enabled = false
[providers."openai"]
priority = 90
default_model = "gpt-5.6-sol"
[providers."openai".metadata.fabro]
api_key_url = "https://platform.openai.com/api-keys"
credentials = ["env:OPENAI_API_KEY", "vault:OPENAI_API_KEY"]
enabled = true
[providers."openai".models."gpt-5.6-sol".metadata.fabro]
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openai".models."gpt-5.6-terra".metadata.fabro]
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openai".models."gpt-5.6-luna".metadata.fabro]
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openai".models."gpt-5.4".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openai".models."gpt-5.5".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openai".models."gpt-5.5-pro".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openai".models."gpt-5.4-pro".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openai".models."gpt-5.4-mini".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = true
probe = true
[providers."openrouter"]
priority = 25
default_model = "claude-sonnet-5"
[providers."openrouter".metadata.fabro]
api_key_url = "https://openrouter.ai/keys"
credentials = ["env:OPENROUTER_API_KEY", "vault:OPENROUTER_API_KEY"]
enabled = false
[providers."openrouter".models."claude-fable-5".metadata.fabro]
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."openrouter".models."claude-opus-5".metadata.fabro]
training = "2026-05-01"
knowledge_cutoff = "May 2026"
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."openrouter".models."claude-sonnet-5".metadata.fabro]
training = "2026-01-01"
knowledge_cutoff = "Jan 2026"
family = "claude-5"
agent_profile = "claude-5"
small_default = false
probe = false
[providers."openrouter".models."claude-opus-4.8".metadata.fabro]
training = "2026-01-01"
knowledge_cutoff = "Jan 2026"
family = "claude-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."claude-opus-4.7".metadata.fabro]
family = "claude-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."claude-sonnet-4.6".metadata.fabro]
family = "claude-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."claude-haiku-4.5".metadata.fabro]
family = "claude-4"
agent_profile = "openai"
small_default = true
probe = false
[providers."openrouter".models."gpt-5.6-sol".metadata.fabro]
training = "2026-02-16"
knowledge_cutoff = "February 16, 2026"
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openrouter".models."gpt-5.6-terra".metadata.fabro]
training = "2026-02-16"
knowledge_cutoff = "February 16, 2026"
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openrouter".models."gpt-5.6-luna".metadata.fabro]
training = "2026-02-16"
knowledge_cutoff = "February 16, 2026"
family = "gpt-5"
agent_profile = "gpt56"
small_default = false
probe = false
[providers."openrouter".models."gpt-5.4".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."gpt-5.5".metadata.fabro]
family = "gpt-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."gemini-3.1-pro-preview".metadata.fabro]
family = "gemini-3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."gemini-3.5-flash".metadata.fabro]
family = "gemini-3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."mimo-v2.5-pro".metadata.fabro]
family = "mimo-v2"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."minimax-m2.7".metadata.fabro]
family = "minimax-m2"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."deepseek-v4-pro".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."deepseek-v4-flash".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."kimi-k2.6".metadata.fabro]
family = "kimi-k2"
agent_profile = "kimi"
small_default = false
probe = false
# Moonshot documents K3 as always reasoning with low, high, and max effort;
# lithos leaves the levels unverified, so Fabro records them here.
[providers."openrouter".models."kimi-k3"]
capabilities = { reasoning_effort = { minimal = false, low = true, medium = false, high = true, xhigh = false, max = true } }
[providers."openrouter".models."kimi-k3".metadata.fabro]
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."openrouter".models."laguna-s-2.1".metadata.fabro]
family = "laguna-2"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."laguna-xs-2.1".metadata.fabro]
family = "laguna-2"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."qwen3-coder".metadata.fabro]
family = "qwen3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."qwen3.6-flash".metadata.fabro]
family = "qwen3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."qwen3.8-max".metadata.fabro]
family = "qwen3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."glm-5.2".metadata.fabro]
family = "glm-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."glm-4.6".metadata.fabro]
family = "glm-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."nemotron-3-super-120b-a12b".metadata.fabro]
family = "nemotron-3"
agent_profile = "openai"
small_default = false
probe = false
[providers."openrouter".models."devstral-2512".metadata.fabro]
family = "devstral"
agent_profile = "openai"
small_default = false
probe = false
[providers."poolside"]
priority = 65
default_model = "laguna-s-2.1"
[providers."poolside".metadata.fabro]
api_key_url = "https://platform.poolside.ai"
credentials = ["env:POOLSIDE_API_KEY", "vault:POOLSIDE_API_KEY"]
enabled = true
[providers."poolside".models."laguna-s-2.1".metadata.fabro]
family = "laguna-2"
agent_profile = "openai"
small_default = false
probe = false
[providers."poolside".models."laguna-xs-2.1".metadata.fabro]
family = "laguna-2"
agent_profile = "openai"
small_default = true
probe = true
[providers."venice"]
priority = 35
default_model = "deepseek-v4-flash"
[providers."venice".metadata.fabro]
credentials = ["env:VENICE_API_KEY", "vault:VENICE_API_KEY"]
enabled = true
[providers."venice".models."kimi-k3".metadata.fabro]
reasoning_by_default = true
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."venice".models."kimi-k3-fast".metadata.fabro]
reasoning_by_default = true
family = "kimi-k3"
agent_profile = "kimi"
small_default = false
probe = false
[providers."venice".models."grok-4.6".metadata.fabro]
reasoning_by_default = true
family = "grok-4"
agent_profile = "openai"
small_default = false
probe = false
[providers."venice".models."glm-5.3".metadata.fabro]
reasoning_by_default = true
family = "glm-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."venice".models."deepseek-v4-flash".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."venice".models."deepseek-v4-pro".metadata.fabro]
reasoning_by_default = true
family = "deepseek-v4"
agent_profile = "openai"
small_default = false
probe = false
[providers."venice".models."qwen3.8-max".metadata.fabro]
reasoning_by_default = true
family = "qwen3"
agent_profile = "openai"
small_default = false
probe = false
[providers."venice".models."qwen3.8-27b".metadata.fabro]
reasoning_by_default = true
family = "qwen3.8"
agent_profile = "openai"
small_default = false
probe = false
[providers."zai"]
priority = 60
default_model = "glm-5.2"
[providers."zai".metadata.fabro]
api_key_url = "https://open.bigmodel.cn/usercenter/apikeys"
credentials = ["env:ZAI_API_KEY", "vault:ZAI_API_KEY"]
enabled = true
[providers."zai".models."glm-5.2".metadata.fabro]
family = "glm-5"
agent_profile = "openai"
small_default = false
probe = false
[providers."zai".models."glm-4.7".metadata.fabro]
family = "glm-4"
agent_profile = "openai"
small_default = false
probe = false
# ChatGPT OAuth (Codex) access to the OpenAI roster. lithos speaks the Codex
# deployment through the `openai` adapter in codex mode; this provider exists
# so a `fabro provider login --provider openai` device-flow credential routes
# here while an API key keeps routing to `openai`. It is hidden from listings
# and stands in for `openai` when `openai` itself has no credentials.
[providers."openai-codex"]
display_name = "OpenAI (ChatGPT)"
adapter = "openai"
codec = "openai-responses"
base_url = "https://chatgpt.com/backend-api/codex"
priority = 89
allow_passthrough = true
default_model = "gpt-5.6-sol"
adapter_options = { mode = "codex" }
default_headers = { originator = "fabro" }
[providers."openai-codex".auth]
type = "bearer"
[providers."openai-codex".metadata.fabro]
enabled = true
stands_in_for = "openai"
credentials = ["vault:OPENAI_CODEX"]
[providers."openai-codex".models."gpt-5.6-sol"]
display_name = "GPT-5.6 Sol"
aliases = ["sol", "gpt-sol", "gpt56-sol", "gpt-56-sol", "gpt-5.6", "gpt56", "gpt-56"]
api_model = "gpt-5.6-sol"
limits = { context_tokens = 1050000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 4000000, output_usd_micros_per_million = 20000000, cached_input_usd_micros_per_million = 400000, cache_write_usd_micros_per_million = 5000000, long_context = { above_input_tokens = 272000, input_usd_micros_per_million = 8000000, output_usd_micros_per_million = 30000000, cached_input_usd_micros_per_million = 800000, cache_write_usd_micros_per_million = 10000000 }, speed = { fast = { input_usd_micros_per_million = 8000000, output_usd_micros_per_million = 40000000, cached_input_usd_micros_per_million = 800000, cache_write_usd_micros_per_million = 10000000 }, economical = { input_usd_micros_per_million = 2000000, output_usd_micros_per_million = 10000000, cached_input_usd_micros_per_million = 200000, cache_write_usd_micros_per_million = 2500000 } } }
[providers."openai-codex".models."gpt-5.6-sol".metadata.fabro]
agent_profile = "gpt56"
family = "gpt-5"
small_default = false
probe = false
[providers."openai-codex".models."gpt-5.6-terra"]
display_name = "GPT-5.6 Terra"
aliases = ["terra", "gpt-terra", "gpt56-terra", "gpt-56-terra"]
api_model = "gpt-5.6-terra"
limits = { context_tokens = 1050000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 2000000, output_usd_micros_per_million = 12000000, cached_input_usd_micros_per_million = 200000, cache_write_usd_micros_per_million = 2500000, long_context = { above_input_tokens = 272000, input_usd_micros_per_million = 4000000, output_usd_micros_per_million = 18000000, cached_input_usd_micros_per_million = 400000, cache_write_usd_micros_per_million = 5000000 }, speed = { fast = { input_usd_micros_per_million = 4000000, output_usd_micros_per_million = 24000000, cached_input_usd_micros_per_million = 400000, cache_write_usd_micros_per_million = 5000000 }, economical = { input_usd_micros_per_million = 1000000, output_usd_micros_per_million = 6000000, cached_input_usd_micros_per_million = 100000, cache_write_usd_micros_per_million = 1250000 } } }
[providers."openai-codex".models."gpt-5.6-terra".metadata.fabro]
agent_profile = "gpt56"
family = "gpt-5"
small_default = false
probe = false
[providers."openai-codex".models."gpt-5.6-luna"]
display_name = "GPT-5.6 Luna"
aliases = ["luna", "gpt-luna", "gpt56-luna", "gpt-56-luna"]
api_model = "gpt-5.6-luna"
limits = { context_tokens = 1050000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 200000, output_usd_micros_per_million = 1200000, cached_input_usd_micros_per_million = 20000, cache_write_usd_micros_per_million = 250000, long_context = { above_input_tokens = 272000, input_usd_micros_per_million = 400000, output_usd_micros_per_million = 1800000, cached_input_usd_micros_per_million = 40000, cache_write_usd_micros_per_million = 500000 }, speed = { fast = { input_usd_micros_per_million = 400000, output_usd_micros_per_million = 2400000, cached_input_usd_micros_per_million = 40000, cache_write_usd_micros_per_million = 500000 }, economical = { input_usd_micros_per_million = 100000, output_usd_micros_per_million = 600000, cached_input_usd_micros_per_million = 10000, cache_write_usd_micros_per_million = 125000 } } }
[providers."openai-codex".models."gpt-5.6-luna".metadata.fabro]
agent_profile = "gpt56"
family = "gpt-5"
small_default = false
probe = false
[providers."openai-codex".models."gpt-5.4"]
display_name = "GPT-5.4"
aliases = ["gpt54", "gpt-54", "gpt-5.2", "gpt5", "gpt-5.3-codex", "codex"]
api_model = "gpt-5.4"
limits = { context_tokens = 1050000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, sampling = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 2500000, output_usd_micros_per_million = 15000000, cached_input_usd_micros_per_million = 250000, long_context = { above_input_tokens = 272000, input_usd_micros_per_million = 5000000, output_usd_micros_per_million = 22500000, cached_input_usd_micros_per_million = 500000 }, speed = { fast = { input_usd_micros_per_million = 5000000, output_usd_micros_per_million = 30000000, cached_input_usd_micros_per_million = 500000 }, economical = { input_usd_micros_per_million = 1250000, output_usd_micros_per_million = 7500000, cached_input_usd_micros_per_million = 125000 } } }
[providers."openai-codex".models."gpt-5.4".metadata.fabro]
agent_profile = "openai"
family = "gpt-5"
small_default = false
probe = false
[providers."openai-codex".models."gpt-5.5"]
display_name = "GPT-5.5"
aliases = ["gpt55", "gpt-55"]
api_model = "gpt-5.5"
limits = { context_tokens = 1050000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 5000000, output_usd_micros_per_million = 30000000, cached_input_usd_micros_per_million = 500000, long_context = { above_input_tokens = 272000, input_usd_micros_per_million = 10000000, output_usd_micros_per_million = 45000000, cached_input_usd_micros_per_million = 1000000 }, speed = { fast = { input_usd_micros_per_million = 12500000, output_usd_micros_per_million = 75000000, cached_input_usd_micros_per_million = 1250000 }, economical = { input_usd_micros_per_million = 2500000, output_usd_micros_per_million = 15000000, cached_input_usd_micros_per_million = 250000 } } }
[providers."openai-codex".models."gpt-5.5".metadata.fabro]
agent_profile = "openai"
family = "gpt-5"
small_default = false
probe = false
[providers."openai-codex".models."gpt-5.4-mini"]
display_name = "GPT-5.4 Mini"
aliases = ["gpt54-mini", "gpt-54-mini", "gpt-5.3-codex-spark", "codex-spark"]
api_model = "gpt-5.4-mini"
limits = { context_tokens = 400000, max_output_tokens = 128000 }
capabilities = { text = true, images = true, documents = true, tools = true, response_format = { json_object = true, json_schema = true }, reasoning = true, caching = true, cache_routing = true, sampling = true, tool_choice = { required = true, named = true }, speed = { fast = true, economical = true } }
protocol_options = { reasoning_effort_levels = true }
pricing = { input_usd_micros_per_million = 750000, output_usd_micros_per_million = 4500000, cached_input_usd_micros_per_million = 75000, speed = { fast = { input_usd_micros_per_million = 1500000, output_usd_micros_per_million = 9000000, cached_input_usd_micros_per_million = 150000 }, economical = { input_usd_micros_per_million = 375000, output_usd_micros_per_million = 2250000, cached_input_usd_micros_per_million = 37500 } } }
[providers."openai-codex".models."gpt-5.4-mini".metadata.fabro]
agent_profile = "openai"
family = "gpt-5"
small_default = true
probe = true

View file

@ -1,7 +1,7 @@
//! API projections of the catalog for `GET /models` and `GET /providers`.
//!
//! Every row is a lithos catalog entry plus its Fabro policy, stamped with
//! whether the caller holds credential material for the provider.
//! Every row is a lithos catalog entry stamped with whether the caller holds
//! credential material for the provider.
use std::collections::HashSet;
@ -9,9 +9,9 @@ use fabro_types::controls::REASONING_EFFORTS;
use fabro_types::{
Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider, ProviderId,
};
use lithos_llm::catalog::Catalog;
use lithos_llm::catalog::{Catalog, CatalogProvider};
use crate::catalog::{self, ModelEntry, ProviderEntry};
use crate::catalog::{self, ModelEntry};
const USD_MICROS_PER_USD: f64 = 1_000_000.0;
@ -29,7 +29,7 @@ pub fn models(catalog: &Catalog, configured: &HashSet<ProviderId>) -> Vec<Model>
pub fn providers(catalog: &Catalog, configured: &HashSet<ProviderId>) -> Vec<Provider> {
catalog::listed_providers(catalog)
.iter()
.map(|entry| provider_view(entry, configured.contains(entry.provider.id())))
.map(|provider| provider_view(provider, configured.contains(provider.id())))
.collect()
}
@ -41,11 +41,9 @@ fn model_view(entry: &ModelEntry<'_>, configured: bool) -> Model {
Model {
id: model.id().clone(),
provider: entry.provider.id().clone(),
family: entry
.policy
.family
.clone()
.unwrap_or_else(|| model.id().to_string()),
family: model
.family()
.map_or_else(|| model.id().to_string(), str::to_string),
display_name: model.display_name().to_string(),
limits: ModelLimits {
context_window: limits.map_or(0, |limits| saturating_i64(limits.context_tokens)),
@ -54,8 +52,8 @@ fn model_view(entry: &ModelEntry<'_>, configured: bool) -> Model {
.filter(|tokens| *tokens > 0)
.map(saturating_i64),
},
training: entry.policy.training.clone(),
knowledge_cutoff: entry.policy.knowledge_cutoff.clone(),
training: model.training_cutoff().map(str::to_string),
knowledge_cutoff: model.knowledge_cutoff().map(str::to_string),
features: ModelFeatures {
tools: capabilities.tools().is_supported(),
vision: capabilities.images().is_supported(),
@ -81,22 +79,21 @@ fn model_view(entry: &ModelEntry<'_>, configured: bool) -> Model {
.and_then(|pricing| pricing.cached_input_usd_micros_per_million)
.map(usd_per_million),
},
estimated_output_tps: entry.policy.estimated_output_tps,
estimated_output_tps: model.estimated_output_tps(),
aliases: model.aliases().to_vec(),
default: entry.provider.default_model() == Some(model.id().as_str()),
small_default: entry.policy.small_default,
small_default: model.is_small_default(),
configured,
}
}
fn provider_view(entry: &ProviderEntry<'_>, configured: bool) -> Provider {
let provider = entry.provider;
fn provider_view(provider: &CatalogProvider, configured: bool) -> Provider {
Provider {
id: provider.id().clone(),
display_name: provider.display_name().to_string(),
adapter: provider.adapter().as_str().to_string(),
base_url: provider.base_url().to_string(),
api_key_url: entry.policy.api_key_url.clone(),
api_key_url: provider.api_key_url().map(str::to_string),
priority: provider.priority(),
aliases: provider.aliases().to_vec(),
model_count: u32::try_from(catalog::provider_models(provider).len()).unwrap_or(u32::MAX),

View file

@ -1,20 +1,22 @@
//! Catalog construction and Fabro-policy queries.
//! Catalog construction and the queries Fabro's dispatch boundaries share.
//!
//! Layer order is fixed: lithos built-ins, then Fabro's policy layer, then the
//! operator's `[llm]` overlay. Every query here reads Fabro policy from the
//! `metadata.fabro` namespace and never bypasses `enabled`.
//! Layer order is fixed: lithos built-ins, then the operator's `[llm]`
//! overlay. Provider and model facts, `enabled`, `stands_in_for`,
//! `small_default`, and `probe` are lithos core fields. The agent harness a
//! model expects lives in the shared `metadata.agent` namespace, which Pebble
//! reads too. Every query here skips disabled providers.
use std::collections::{BTreeMap, HashSet};
use fabro_config::LlmLayer;
use fabro_static::EnvVars;
use fabro_types::catalog_policy::{self, ModelPolicy, ProviderPolicy};
use fabro_types::{AgentProfileKind, Cost, ModelId, ModelRef, ProviderId, TokenCounts};
use lithos_llm::catalog::{Catalog, CatalogError, CatalogModel, CatalogProvider};
use lithos_llm::catalog::{Catalog, CatalogError, CatalogModel, CatalogProvider, Metadata};
use lithos_llm::resolver::ResolvedRoute;
use serde::Deserialize;
/// Fabro's policy layer, applied above the lithos built-ins.
pub const FABRO_POLICY_TOML: &str = include_str!("../catalog/fabro-policy.toml");
/// The metadata namespace agent harnesses read.
const AGENT_METADATA_NAMESPACE: &str = "agent";
/// Builds the effective catalog.
///
@ -25,9 +27,7 @@ pub fn build_catalog(
overlay: &LlmLayer,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<Catalog, CatalogError> {
let mut builder = Catalog::builder()
.with_builtin()
.toml_layer("fabro-policy.toml", FABRO_POLICY_TOML)?;
let mut builder = Catalog::builder().with_builtin();
if !overlay.is_empty() {
let mut document = overlay.to_overlay_toml();
document.insert_str(0, "schema_version = 1\n");
@ -43,50 +43,78 @@ pub fn build_catalog(
builder.build()
}
/// A provider with its Fabro policy attached.
#[derive(Debug, Clone)]
pub struct ProviderEntry<'a> {
pub provider: &'a CatalogProvider,
pub policy: ProviderPolicy,
}
/// A model with its Fabro policy attached.
#[derive(Debug, Clone)]
pub struct ModelEntry<'a> {
pub provider: &'a CatalogProvider,
pub model: &'a CatalogModel,
pub policy: ModelPolicy,
}
impl ModelEntry<'_> {
/// Whether requests to this model reason when no effort is requested.
///
/// Fabro policy can state it outright. Otherwise a model that supports
/// reasoning and takes named effort levels reasons by default, while one
/// that needs an explicit thinking budget does not.
#[must_use]
pub fn reasons_by_default(&self) -> bool {
self.policy.reasoning_by_default.unwrap_or_else(|| {
self.model.capabilities().reasoning().is_supported()
&& self.model.protocol_options().reasoning_effort_levels
})
}
#[must_use]
pub fn agent_profile(&self) -> AgentProfileKind {
catalog_policy::effective_agent_profile(self.provider, self.model)
}
}
/// The catalog with no operator overlay: lithos built-ins plus Fabro policy.
/// The catalog with no operator overlay: the lithos built-ins.
///
/// Used where no settings file is in play, such as the standalone hook
/// runner. Servers and the CLI build from the operator's `[llm]` overlay
/// with [`build_catalog`] instead.
#[must_use]
pub fn default_catalog() -> Catalog {
build_catalog(&LlmLayer::default(), &|_| None)
.expect("the built-in catalog and Fabro policy layer always build")
build_catalog(&LlmLayer::default(), &|_| None).expect("the built-in catalog always builds")
}
/// A model on the provider that offers it.
#[derive(Debug, Clone)]
pub struct ModelEntry<'a> {
pub provider: &'a CatalogProvider,
pub model: &'a CatalogModel,
}
impl ModelEntry<'_> {
/// Whether requests to this model reason when no effort is requested.
///
/// The catalog can state it outright under `metadata.agent`. Otherwise a
/// model that supports reasoning and takes named effort levels reasons by
/// default, while one that needs an explicit thinking budget does not.
#[must_use]
pub fn reasons_by_default(&self) -> bool {
agent_metadata(self.model.metadata())
.reasoning_by_default
.or(agent_metadata(self.provider.metadata()).reasoning_by_default)
.unwrap_or_else(|| {
self.model.capabilities().reasoning().is_supported()
&& self.model.protocol_options().reasoning_effort_levels
})
}
/// The agent harness this model runs under: the model's own answer, then
/// the provider's, then the profile implied by the provider's adapter.
#[must_use]
pub fn agent_profile(&self) -> AgentProfileKind {
agent_metadata(self.model.metadata())
.profile
.unwrap_or_else(|| provider_agent_profile(self.provider))
}
}
/// The `metadata.agent` namespace on a catalog entry. Malformed metadata
/// falls back to the defaults; the lithos built-ins are validated in lithos.
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct AgentMetadata {
profile: Option<AgentProfileKind>,
reasoning_by_default: Option<bool>,
}
fn agent_metadata(metadata: &Metadata) -> AgentMetadata {
metadata
.namespace::<AgentMetadata>(AGENT_METADATA_NAMESPACE)
.ok()
.flatten()
.unwrap_or_default()
}
/// The agent profile a provider's models run under unless a model row says
/// otherwise: the provider's `metadata.agent.profile`, else the profile
/// implied by its wire protocol.
fn provider_agent_profile(provider: &CatalogProvider) -> AgentProfileKind {
agent_metadata(provider.metadata())
.profile
.unwrap_or_else(|| match provider.adapter().as_str() {
"anthropic" | "bedrock" => AgentProfileKind::Anthropic,
"gemini" => AgentProfileKind::Gemini,
_ => AgentProfileKind::OpenAi,
})
}
/// Estimates the catalog cost of `usage` on `model`, when the catalog prices
@ -101,21 +129,16 @@ pub fn estimate_cost(catalog: &Catalog, model: &ModelRef, usage: TokenCounts) ->
/// Enabled providers, highest priority first, ties broken by id.
#[must_use]
pub fn enabled_providers(catalog: &Catalog) -> Vec<ProviderEntry<'_>> {
pub fn enabled_providers(catalog: &Catalog) -> Vec<&CatalogProvider> {
let mut providers: Vec<_> = catalog
.providers()
.map(|provider| ProviderEntry {
provider,
policy: catalog_policy::provider_policy(provider),
})
.filter(|entry| entry.policy.is_enabled())
.filter(|provider| provider.is_enabled())
.collect();
providers.sort_by(|left, right| {
right
.provider
.priority()
.cmp(&left.provider.priority())
.then_with(|| left.provider.id().cmp(right.provider.id()))
.cmp(&left.priority())
.then_with(|| left.id().cmp(right.id()))
});
providers
}
@ -123,10 +146,10 @@ pub fn enabled_providers(catalog: &Catalog) -> Vec<ProviderEntry<'_>> {
/// Enabled providers that Fabro lists to operators. Stand-in providers such
/// as `openai-codex` route requests but are not offerings of their own.
#[must_use]
pub fn listed_providers(catalog: &Catalog) -> Vec<ProviderEntry<'_>> {
pub fn listed_providers(catalog: &Catalog) -> Vec<&CatalogProvider> {
enabled_providers(catalog)
.into_iter()
.filter(|entry| entry.policy.stands_in_for.is_none())
.filter(|provider| provider.stands_in_for().is_none())
.collect()
}
@ -135,82 +158,70 @@ pub fn listed_providers(catalog: &Catalog) -> Vec<ProviderEntry<'_>> {
pub fn enabled_provider_ids(catalog: &Catalog) -> HashSet<ProviderId> {
enabled_providers(catalog)
.into_iter()
.map(|entry| entry.provider.id().clone())
.map(|provider| provider.id().clone())
.collect()
}
/// Looks up an enabled provider by id or alias.
#[must_use]
pub fn provider<'a>(catalog: &'a Catalog, selector: &str) -> Option<ProviderEntry<'a>> {
let provider = catalog.provider(selector).ok()?;
let policy = catalog_policy::provider_policy(provider);
policy
.is_enabled()
.then_some(ProviderEntry { provider, policy })
pub fn provider<'a>(catalog: &'a Catalog, selector: &str) -> Option<&'a CatalogProvider> {
catalog
.provider(selector)
.ok()
.filter(|provider| provider.is_enabled())
}
/// Canonicalizes a provider id or alias to its catalog id, when enabled.
#[must_use]
pub fn canonical_provider_id(catalog: &Catalog, selector: &str) -> Option<ProviderId> {
provider(catalog, selector).map(|entry| entry.provider.id().clone())
provider(catalog, selector).map(|provider| provider.id().clone())
}
/// Enabled models of an enabled provider, in catalog order.
/// The models of a provider, in catalog order.
#[must_use]
pub fn provider_models(provider: &CatalogProvider) -> Vec<ModelEntry<'_>> {
provider
.models()
.map(|model| ModelEntry {
provider,
model,
policy: catalog_policy::model_policy(model),
})
.filter(|entry| entry.policy.is_enabled())
.map(|model| ModelEntry { provider, model })
.collect()
}
/// Every enabled model across listed providers, provider priority order.
/// Every model across listed providers, provider priority order.
#[must_use]
pub fn models(catalog: &Catalog) -> Vec<ModelEntry<'_>> {
listed_providers(catalog)
.into_iter()
.flat_map(|entry| provider_models(entry.provider))
.flat_map(provider_models)
.collect()
}
/// Finds an enabled model on an enabled provider by id, alias, or wire id.
/// Finds a model on an enabled provider by id, alias, or wire id.
#[must_use]
pub fn model_on_provider<'a>(
catalog: &'a Catalog,
provider_selector: &str,
model_selector: &str,
) -> Option<ModelEntry<'a>> {
let entry = provider(catalog, provider_selector)?;
let provider = provider(catalog, provider_selector)?;
// lithos matches ids and aliases. The provider's wire id (an aggregator's
// `vendor/model`) is accepted too, so a selector copied from the
// provider's own listing lands on the catalog row instead of passing
// through unknown.
let model = entry.provider.model(model_selector).or_else(|| {
entry
.provider
let model = provider.model(model_selector).or_else(|| {
provider
.models()
.find(|model| model.api_model() == model_selector)
})?;
let policy = catalog_policy::model_policy(model);
policy.is_enabled().then_some(ModelEntry {
provider: entry.provider,
model,
policy,
})
Some(ModelEntry { provider, model })
}
/// Enabled models matching `selector` by id or alias, ordered like lithos
/// selection: exact ids before aliases, then provider priority.
/// Models matching `selector` by id or alias, ordered like lithos selection:
/// exact ids before aliases, then provider priority.
#[must_use]
pub fn models_matching<'a>(catalog: &'a Catalog, selector: &str) -> Vec<ModelEntry<'a>> {
let mut matches: Vec<_> = enabled_providers(catalog)
.into_iter()
.flat_map(|entry| provider_models(entry.provider))
.flat_map(provider_models)
.filter(|entry| {
entry.model.id().as_str() == selector
|| entry.model.aliases().iter().any(|alias| alias == selector)
@ -220,7 +231,7 @@ pub fn models_matching<'a>(catalog: &'a Catalog, selector: &str) -> Vec<ModelEnt
matches
}
/// Whether `selector` names an enabled model on any enabled provider.
/// Whether `selector` names a model on any enabled provider.
#[must_use]
pub fn is_model_selector(catalog: &Catalog, selector: &str) -> bool {
!models_matching(catalog, selector).is_empty()
@ -232,22 +243,22 @@ pub fn is_provider_selector(catalog: &Catalog, selector: &str) -> bool {
provider(catalog, selector).is_some()
}
/// The enabled default model of an enabled provider.
/// The default model of an enabled provider.
#[must_use]
pub fn default_model<'a>(catalog: &'a Catalog, provider_selector: &str) -> Option<ModelEntry<'a>> {
let entry = provider(catalog, provider_selector)?;
let default = entry.provider.default_model()?;
model_on_provider(catalog, entry.provider.id().as_str(), default)
let provider = provider(catalog, provider_selector)?;
let default = provider.default_model()?;
model_on_provider(catalog, provider.id().as_str(), default)
}
/// The model Fabro probes a provider with: the `probe` model, else the
/// provider default.
#[must_use]
pub fn probe_model<'a>(catalog: &'a Catalog, provider_selector: &str) -> Option<ModelEntry<'a>> {
let entry = provider(catalog, provider_selector)?;
provider_models(entry.provider)
let provider = provider(catalog, provider_selector)?;
provider_models(provider)
.into_iter()
.find(|model| model.policy.probe)
.find(|entry| entry.model.is_probe())
.or_else(|| default_model(catalog, provider_selector))
}
@ -262,9 +273,9 @@ pub fn default_for_ready<'a>(
let providers = enabled_providers(catalog);
providers
.iter()
.filter(|entry| ready.contains(entry.provider.id()))
.filter(|provider| ready.contains(provider.id()))
.chain(providers.iter())
.find_map(|entry| default_model(catalog, entry.provider.id().as_str()))
.find_map(|provider| default_model(catalog, provider.id().as_str()))
}
/// The small utility model across `ready` providers: the first
@ -274,12 +285,11 @@ pub fn small_default_for_ready<'a>(
catalog: &'a Catalog,
ready: &HashSet<ProviderId>,
) -> Option<ModelEntry<'a>> {
let providers = enabled_providers(catalog);
providers
.iter()
.filter(|entry| ready.contains(entry.provider.id()))
.flat_map(|entry| provider_models(entry.provider))
.find(|model| model.policy.small_default)
enabled_providers(catalog)
.into_iter()
.filter(|provider| ready.contains(provider.id()))
.flat_map(provider_models)
.find(|entry| entry.model.is_small_default())
.or_else(|| default_for_ready(catalog, ready))
}
@ -306,14 +316,11 @@ pub fn agent_profile(
provider_selector: &str,
model_selector: Option<&str>,
) -> Option<AgentProfileKind> {
let entry = provider(catalog, provider_selector)?;
let model = model_selector.and_then(|selector| entry.provider.model(selector));
let provider = provider(catalog, provider_selector)?;
let model = model_selector.and_then(|selector| provider.model(selector));
Some(match model {
Some(model) => catalog_policy::effective_agent_profile(entry.provider, model),
None => entry
.policy
.agent_profile
.unwrap_or_else(|| catalog_policy::default_agent_profile(entry.provider)),
Some(model) => ModelEntry { provider, model }.agent_profile(),
None => provider_agent_profile(provider),
})
}
@ -331,7 +338,7 @@ pub fn closest_model<'a>(
.pricing()
.and_then(|pricing| pricing.input_usd_micros_per_million)
.unwrap_or(0);
provider_models(target.provider)
provider_models(target)
.into_iter()
.filter(|entry| {
let caps = entry.model.capabilities();
@ -354,12 +361,12 @@ pub fn closest_model<'a>(
pub fn model_ids_by_provider(catalog: &Catalog) -> BTreeMap<ProviderId, Vec<ModelId>> {
listed_providers(catalog)
.into_iter()
.map(|entry| {
.map(|provider| {
(
entry.provider.id().clone(),
provider_models(entry.provider)
provider.id().clone(),
provider_models(provider)
.into_iter()
.map(|model| model.model.id().clone())
.map(|entry| entry.model.id().clone())
.collect(),
)
})
@ -372,11 +379,11 @@ mod tests {
use crate::test_support::test_catalog;
#[test]
fn policy_layer_builds_over_the_builtins() {
fn builtins_ship_fabro_defaults() {
let catalog = test_catalog();
let ids: Vec<_> = enabled_providers(&catalog)
.iter()
.map(|entry| entry.provider.id().to_string())
.map(|provider| provider.id().to_string())
.collect();
assert_eq!(ids[0], "anthropic");
assert!(ids.contains(&"openai".to_string()));
@ -387,7 +394,7 @@ mod tests {
assert!(
!listed_providers(&catalog)
.iter()
.any(|entry| entry.provider.id().as_str() == "openai-codex"),
.any(|provider| provider.id().as_str() == "openai-codex"),
"stand-in providers are not listed"
);
}
@ -399,7 +406,6 @@ mod tests {
r"
[providers.openai]
priority = 500
[providers.openai.metadata.fabro]
enabled = false
",
)
@ -410,7 +416,7 @@ enabled = false
assert_eq!(
catalog.provider("openai").unwrap().priority(),
500,
"overlay values win over the policy layer"
"overlay values win over the built-ins"
);
}
@ -427,7 +433,7 @@ enabled = false
}
#[test]
fn probe_and_small_default_follow_policy() {
fn probe_and_small_default_follow_the_catalog() {
let catalog = test_catalog();
assert_eq!(
probe_model(&catalog, "openai").unwrap().model.id().as_str(),
@ -488,11 +494,41 @@ enabled = false
);
assert_eq!(
agent_profile(&catalog, "moonshot", None),
Some(AgentProfileKind::Kimi),
"a passthrough model on Moonshot takes the provider's Kimi profile"
);
assert_eq!(
agent_profile(&catalog, "deepseek", None),
Some(AgentProfileKind::OpenAi)
);
assert_eq!(
agent_profile(&catalog, "openrouter", None),
None,
"disabled providers have no profile to offer"
);
assert_eq!(
agent_profile(&catalog, "moonshot", Some("kimi-k3")),
Some(AgentProfileKind::Kimi)
);
assert_eq!(
agent_profile(&catalog, "openai", Some("gpt-6-astra")),
Some(AgentProfileKind::Gpt6)
);
assert_eq!(
agent_profile(&catalog, "anthropic", Some("claude-sonnet-4.5")),
Some(AgentProfileKind::Anthropic)
);
}
#[test]
fn reasoning_by_default_reads_agent_metadata_then_capabilities() {
let catalog = test_catalog();
let kimi = model_on_provider(&catalog, "moonshot", "kimi-k2.5").unwrap();
assert!(kimi.reasons_by_default(), "the catalog row says so");
let sonnet = model_on_provider(&catalog, "anthropic", "claude-sonnet-4.5").unwrap();
assert!(
!sonnet.reasons_by_default(),
"a thinking-budget model reasons only when asked"
);
}
}

View file

@ -3,8 +3,9 @@
//! lithos owns the LLM vocabulary, the provider catalog, the wire codecs, and
//! the client. This crate adds what is specific to Fabro:
//!
//! - building the catalog from lithos built-ins, Fabro's policy layer, and the
//! operator `[llm]` overlay ([`catalog`]);
//! - building the catalog from the lithos built-ins and the operator `[llm]`
//! overlay, and the catalog queries Fabro's dispatch boundaries share
//! ([`catalog`]);
//! - Fabro's passthrough policy for selections made before a request exists
//! ([`selection`]); at request time the lithos resolver enforces `enabled`
//! and `stands_in_for` itself;
@ -33,7 +34,7 @@ pub mod structured;
#[cfg(any(test, feature = "test-support"))]
pub mod test_support;
pub use catalog::{FABRO_POLICY_TOML, build_catalog, default_catalog};
pub use catalog::{build_catalog, default_catalog};
pub use client::{
ClientOptions, FabroClient, LlmSetupError, RetryListener, RetryNotice, build_client,
build_offline_client,

View file

@ -104,10 +104,10 @@ pub async fn probe_provider_with_api_key(
api_key: String,
timeout: Duration,
) -> Result<ModelTestOutcome, ApiKeyProbeError> {
let entry = catalog::provider(&catalog, provider.as_str())
let catalog_provider = catalog::provider(&catalog, provider.as_str())
.ok_or_else(|| ApiKeyProbeError::UnknownProvider(provider.to_string()))?;
let provider_id = entry.provider.id().clone();
if !fabro_auth::accepts_api_key(entry.provider) {
let provider_id = catalog_provider.id().clone();
if !fabro_auth::accepts_api_key(catalog_provider) {
return Err(ApiKeyProbeError::NoApiKeyPath(provider_id));
}
let model = catalog::probe_model(&catalog, provider_id.as_str())

View file

@ -13,8 +13,8 @@
//! - No selector picks the default offering (of the pinned provider, when one
//! is given).
//!
//! Only enabled providers and models take part. Disabled ones are invisible
//! here, exactly as they are to the client's resolver.
//! Only enabled providers take part. Disabled ones are invisible here,
//! exactly as they are to the lithos resolver at request time.
use std::collections::HashSet;
use std::fmt;
@ -115,7 +115,7 @@ pub fn ready_provider(
}
}
/// Finds `selector` as an enabled model on an enabled provider.
/// Finds `selector` as a model on an enabled provider.
pub fn resolve_on_provider<'a>(
catalog: &'a Catalog,
provider: &ProviderId,
@ -182,9 +182,9 @@ pub fn select_default<'a>(
let eligible = canonical_eligible(catalog, eligible);
let providers_with_defaults: Vec<_> = catalog::enabled_providers(catalog)
.into_iter()
.filter_map(|entry| {
catalog::default_model(catalog, entry.provider.id().as_str())
.map(|model| (entry.provider.id().clone(), model))
.filter_map(|provider| {
catalog::default_model(catalog, provider.id().as_str())
.map(|model| (provider.id().clone(), model))
})
.collect();
providers_with_defaults
@ -350,8 +350,7 @@ mod tests {
#[test]
fn slash_selector_with_a_non_provider_prefix_matches_api_ids_on_a_pinned_provider() {
let catalog =
test_catalog_with_overlay("[providers.openrouter.metadata.fabro]\nenabled = true\n");
let catalog = test_catalog_with_overlay("[providers.openrouter]\nenabled = true\n");
let selected = resolve_selection(
&catalog,
Some("openai/gpt-5.6-sol"),
@ -409,8 +408,7 @@ mod tests {
select(&catalog, "gpt-5.4", None, &eligible(&["openrouter"])),
Err(ModelSelectionError::NoEligibleOffering { .. })
));
let enabled =
test_catalog_with_overlay("[providers.openrouter.metadata.fabro]\nenabled = true\n");
let enabled = test_catalog_with_overlay("[providers.openrouter]\nenabled = true\n");
let entry = select(&enabled, "gpt-5.4", None, &eligible(&["openrouter"])).unwrap();
assert_eq!(entry.provider.id(), &ProviderId::new("openrouter"));
}

View file

@ -20,7 +20,7 @@ use lithos_llm::types::{
use crate::client::{ClientOptions, build_client, build_offline_client};
/// The lithos built-in catalog with Fabro's policy layer applied.
/// The lithos built-in catalog, as Fabro ships it.
#[must_use]
pub fn test_catalog() -> Catalog {
crate::build_catalog(&LlmLayer::default(), &|_| None).expect("test catalog should build")

View file

@ -221,9 +221,8 @@ base_url = "https://api.venice.ai/api/v1"
auth = { type = "bearer" }
default_model = "venice-large"
[providers.acme-venice.metadata.fabro]
agent_profile = "openai"
credentials = ["env:VENICE_API_KEY"]
[providers.acme-venice.metadata.agent]
profile = "openai"
[providers.acme-venice.models.venice-large]
display_name = "Venice Large"

View file

@ -39,7 +39,7 @@ pub(super) fn check_provider_known(
}
let valid: Vec<String> = catalog::listed_providers(catalog)
.iter()
.map(|entry| entry.provider.id().to_string())
.map(|provider| provider.id().to_string())
.collect();
let valid_str = valid.join(", ");
Some(Diagnostic {

View file

@ -2065,7 +2065,9 @@ mod tests {
}
/// An OpenAI-compatible mock provider served by `server`, with one model.
fn mock_provider_overlay(provider: &str, model: &str, base_url: &str, env_var: &str) -> String {
/// Its API key is the name lithos derives from the provider id, such as
/// `MOCK_API_KEY`.
fn mock_provider_overlay(provider: &str, model: &str, base_url: &str) -> String {
format!(
r#"
[providers.{provider}]
@ -2076,9 +2078,8 @@ base_url = {base_url}
auth = {{ type = "bearer" }}
default_model = "{model}"
[providers.{provider}.metadata.fabro]
agent_profile = "openai"
credentials = ["env:{env_var}"]
[providers.{provider}.metadata.agent]
profile = "openai"
[providers.{provider}.models.{model}]
display_name = "{model}"
@ -2095,7 +2096,6 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object =
"mock",
"mock-model",
&server.base_url(),
"MOCK_API_KEY",
)))
}
@ -2103,7 +2103,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object =
/// would so their models become fallback targets.
fn enabled_fallback_catalog() -> Arc<Catalog> {
Arc::new(test_catalog_with_overlay(
"[providers.modal.metadata.fabro]\nenabled = true\n\n[providers.openrouter.metadata.fabro]\nenabled = true\n",
"[providers.modal]\nenabled = true\n\n[providers.openrouter]\nenabled = true\n",
))
}
@ -2132,13 +2132,11 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object =
"primary",
"test-model",
&format!("{}/primary", server.base_url()),
"PRIMARY_API_KEY",
),
mock_provider_overlay(
"fallback",
"test-model",
&format!("{}/fallback", server.base_url()),
"FALLBACK_API_KEY",
),
);
let catalog = Arc::new(test_catalog_with_overlay(&overlay));
@ -4073,7 +4071,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object =
}))
}
const OPENROUTER_ENABLED: &str = "[providers.openrouter.metadata.fabro]\nenabled = true\n";
const OPENROUTER_ENABLED: &str = "[providers.openrouter]\nenabled = true\n";
/// An operator-defined OpenAI-compatible provider whose models take the
/// provider's `openai` agent profile.
@ -4086,19 +4084,17 @@ base_url = "https://api.acme.test/v1"
auth = { type = "bearer" }
default_model = "acme-llama"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-llama]
display_name = "Acme Llama"
api_model = "acme-llama"
limits = { context_tokens = 131072, max_output_tokens = 8192 }
capabilities = { text = true, tools = true }
[providers.acme.models.acme-llama.metadata.fabro]
family = "llama"
training = "2026-01"
training_cutoff = "2026-01"
"#;
/// The same provider serving a Claude model that overrides the profile.
@ -4111,9 +4107,8 @@ base_url = "https://api.acme.test/v1"
auth = { type = "bearer" }
default_model = "acme-claude"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-claude]
display_name = "Acme Claude"
@ -4121,11 +4116,11 @@ aliases = ["ac"]
api_model = "acme-claude"
limits = { context_tokens = 131072, max_output_tokens = 8192 }
capabilities = { text = true, tools = true }
[providers.acme.models.acme-claude.metadata.fabro]
family = "claude"
training = "2026-01"
agent_profile = "anthropic"
training_cutoff = "2026-01"
[providers.acme.models.acme-claude.metadata.agent]
profile = "anthropic"
"#;
#[tokio::test]

View file

@ -699,9 +699,8 @@ mod tests {
auth = { type = "bearer" }
default_model = "acme-claude"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-claude]
display_name = "Acme Claude"
@ -709,10 +708,10 @@ mod tests {
api_model = "acme-claude"
limits = { context_tokens = 1000, max_output_tokens = 500 }
capabilities = { text = true, tools = true }
[providers.acme.models.acme-claude.metadata.fabro]
family = "claude"
agent_profile = "anthropic"
[providers.acme.models.acme-claude.metadata.agent]
profile = "anthropic"
"#,
));
let mut services = make_services();
@ -774,9 +773,8 @@ mod tests {
auth = { type = "bearer" }
default_model = "acme-claude"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-claude]
display_name = "Acme Claude"
@ -784,10 +782,10 @@ mod tests {
api_model = "acme-claude"
limits = { context_tokens = 1000, max_output_tokens = 500 }
capabilities = { text = true, tools = true }
[providers.acme.models.acme-claude.metadata.fabro]
family = "claude"
agent_profile = "anthropic"
[providers.acme.models.acme-claude.metadata.agent]
profile = "anthropic"
"#,
));
let mut services = make_services();

View file

@ -405,7 +405,7 @@ mod tests {
}
fn openrouter_catalog() -> Catalog {
test_catalog_with_overlay("[providers.openrouter.metadata.fabro]\nenabled = true\n")
test_catalog_with_overlay("[providers.openrouter]\nenabled = true\n")
}
#[test]
@ -508,7 +508,7 @@ mod tests {
#[test]
fn resolves_the_requested_production_policy_as_independent_chains() {
let catalog = test_catalog_with_overlay(
"[providers.modal.metadata.fabro]\nenabled = true\n\n[providers.openrouter.metadata.fabro]\nenabled = true\n",
"[providers.modal]\nenabled = true\n\n[providers.openrouter]\nenabled = true\n",
);
let eligible = [
ProviderId::new("modal"),

View file

@ -746,9 +746,8 @@ mod tests {
[providers.openrouter]
priority = 25
default_model = "gpt-5.6-sol"
[providers.openrouter.metadata.fabro]
enabled = true
"#,
))
}

View file

@ -1454,9 +1454,8 @@ mod tests {
[providers.openrouter]
priority = 25
default_model = "gpt-5.6-sol"
[providers.openrouter.metadata.fabro]
enabled = true
"#,
)
}
@ -1493,9 +1492,8 @@ mod tests {
auth = { type = "bearer" }
default_model = "acme-claude"
[providers.acme.metadata.fabro]
agent_profile = "openai"
credentials = ["env:ACME_API_KEY"]
[providers.acme.metadata.agent]
profile = "openai"
[providers.acme.models.acme-claude]
display_name = "Acme Claude"
@ -1503,10 +1501,10 @@ mod tests {
api_model = "acme-claude"
limits = { context_tokens = 1000, max_output_tokens = 500 }
capabilities = { text = true, tools = true }
[providers.acme.models.acme-claude.metadata.fabro]
family = "claude"
agent_profile = "anthropic"
[providers.acme.models.acme-claude.metadata.agent]
profile = "anthropic"
"#,
);
let mut settings = ResolvedRunSettings::default();

View file

@ -782,9 +782,8 @@ base_url = "http://mock.invalid/v1"
auth = { type = "bearer" }
allow_passthrough = true
[providers.mock.metadata.fabro]
agent_profile = "openai"
credentials = ["env:MOCK_API_KEY"]
[providers.mock.metadata.agent]
profile = "openai"
[providers.mock.models.mock-model]
display_name = "Mock Model"

View file

@ -174,9 +174,8 @@ auth = { type = "bearer" }
priority = 200
default_model = "venice-large"
[providers.acme-venice.metadata.fabro]
agent_profile = "openai"
credentials = ["env:VENICE_API_KEY"]
[providers.acme-venice.metadata.agent]
profile = "openai"
[providers.acme-venice.models.venice-large]
display_name = "Venice Large"
@ -355,7 +354,7 @@ capabilities = { text = true, tools = true }
#[test]
fn fallback_resolution_keeps_ready_preference_for_unpinned_nodes() {
let catalog = Arc::new(test_catalog_with_overlay(
"[providers.openrouter.metadata.fabro]\nenabled = true\n",
"[providers.openrouter]\nenabled = true\n",
));
let mut graph = Graph::new("test");
let mut portable = Node::new("portable");

View file

@ -2672,9 +2672,8 @@ base_url = {base_url}
auth = {{ type = "bearer" }}
default_model = "compact-model"
[providers.compact.metadata.fabro]
agent_profile = "openai"
credentials = ["env:COMPACT_API_KEY"]
[providers.compact.metadata.agent]
profile = "openai"
[providers.compact.models.compact-model]
display_name = "Compact Model"
@ -2834,8 +2833,6 @@ async fn workflow_persists_authoritative_openrouter_cost_for_agent_stage() {
&format!(
"[providers.openrouter]
base_url = {}
[providers.openrouter.metadata.fabro]
enabled = true
",
toml::Value::String(server.base_url()),

View file

@ -693,10 +693,7 @@ adapter = "openai-compatible"
codec = "openai-chat"
base_url = "https://api.acme.test/v1"
auth = { type = "bearer" }
[llm.providers.acme.metadata.fabro]
enabled = true
credentials = ["env:ACME_API_KEY"]
[llm.providers.acme.models."acme-large"]
display_name = "Acme Large"
@ -710,7 +707,7 @@ api_model = "acme-large"
let overlay = settings.llm_overlay.0;
let acme = &overlay["providers"]["acme"];
assert_eq!(acme["display_name"].as_str(), Some("Acme"));
assert_eq!(acme["metadata"]["fabro"]["enabled"].as_bool(), Some(true));
assert_eq!(acme["enabled"].as_bool(), Some(true));
assert_eq!(
acme["models"]["acme-large"]["api_model"].as_str(),
Some("acme-large")

View file

@ -2,17 +2,14 @@
//!
//! Operator model catalog overrides. The table uses the lithos-llm catalog
//! schema verbatim, minus `schema_version`, and is applied as an overlay layer
//! on top of the lithos built-in catalog and Fabro's policy layer:
//! on top of the lithos built-in catalog:
//!
//! ```toml
//! [llm.providers.moonshot]
//! [llm.providers.openrouter]
//! priority = 60
//!
//! [llm.providers.moonshot.metadata.fabro]
//! enabled = true
//! credentials = ["env:MOONSHOT_API_KEY", "vault:MOONSHOT_API_KEY"]
//!
//! [llm.providers.moonshot.models."kimi-k2.5".metadata.fabro]
//! [llm.providers.openrouter.models."kimi-k2.5"]
//! small_default = true
//! ```
//!
@ -83,7 +80,6 @@ mod tests {
r"
[providers.acme]
priority = 10
[providers.acme.metadata.fabro]
enabled = false
",
);
@ -92,19 +88,19 @@ enabled = false
[providers.acme]
priority = 5
base_url = "https://acme.test"
[providers.acme.metadata.fabro]
enabled = true
small_default = true
[providers.acme.metadata.agent]
profile = "openai"
"#,
);
let merged = higher.combine(lower).0;
let acme = &merged["providers"]["acme"];
assert_eq!(acme["priority"].as_integer(), Some(10));
assert_eq!(acme["base_url"].as_str(), Some("https://acme.test"));
assert_eq!(acme["metadata"]["fabro"]["enabled"].as_bool(), Some(false));
assert_eq!(acme["enabled"].as_bool(), Some(false));
assert_eq!(
acme["metadata"]["fabro"]["small_default"].as_bool(),
Some(true)
acme["metadata"]["agent"]["profile"].as_str(),
Some("openai")
);
}

View file

@ -295,22 +295,21 @@ mod tests {
#[test]
fn llm_overlay_merges_across_layers() {
let higher = r#"
[llm.providers.openai.models."gpt-5.4".metadata.fabro]
[llm.providers.openai.models."gpt-5.4"]
small_default = true
"#
.parse::<SettingsLayer>()
.unwrap();
let lower = r#"
[llm.providers.openai.models."gpt-5.4".metadata.fabro]
[llm.providers.openai.models."gpt-5.4"]
probe = true
"#
.parse::<SettingsLayer>()
.unwrap();
let merged = crate::Combine::combine(higher, lower);
let fabro =
&merged.llm.unwrap().0["providers"]["openai"]["models"]["gpt-5.4"]["metadata"]["fabro"];
assert_eq!(fabro["small_default"].as_bool(), Some(true));
assert_eq!(fabro["probe"].as_bool(), Some(true));
let model = &merged.llm.unwrap().0["providers"]["openai"]["models"]["gpt-5.4"];
assert_eq!(model["small_default"].as_bool(), Some(true));
assert_eq!(model["probe"].as_bool(), Some(true));
}
#[test]

View file

@ -1,229 +0,0 @@
//! Fabro's `metadata.fabro` catalog namespace.
//!
//! lithos-llm owns provider and model facts. Fabro attaches its own policy to
//! each entry under `metadata.fabro`, which lithos carries verbatim and never
//! interprets. These types are the typed view of that namespace. Every field
//! is optional in the TOML; the accessors here apply Fabro's defaults.
use lithos_llm::catalog::{CatalogModel, CatalogProvider};
use serde::{Deserialize, Serialize};
use crate::AgentProfileKind;
/// Name of the metadata namespace Fabro owns on catalog entries.
pub const FABRO_METADATA_NAMESPACE: &str = "fabro";
/// Provider-level Fabro policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ProviderPolicy {
/// Whether Fabro offers this provider at all. Missing means enabled.
pub enabled: Option<bool>,
/// Default agent profile for models on this provider.
pub agent_profile: Option<AgentProfileKind>,
/// Where an operator obtains an API key.
pub api_key_url: Option<String>,
/// Ordered credential references (`env:NAME`, `vault:NAME`, `aws_sigv4`).
/// The first that resolves wins.
pub credentials: Vec<String>,
/// Extra request headers. Values are literal text or `{{ secrets.NAME }}`
/// interpolation strings resolved against the vault.
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub extra_headers: std::collections::BTreeMap<String, String>,
/// Another provider this one serves requests for when that provider has
/// no credentials of its own. Used by `openai-codex`, which answers
/// `openai` requests with a ChatGPT OAuth credential.
pub stands_in_for: Option<String>,
}
impl ProviderPolicy {
#[must_use]
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
/// Model-level Fabro policy.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct ModelPolicy {
/// Whether Fabro offers this model. Missing means enabled.
pub enabled: Option<bool>,
/// Agent profile override for this model.
pub agent_profile: Option<AgentProfileKind>,
/// Model family label for display and grouping.
pub family: Option<String>,
/// Training data cutoff label.
pub training: Option<String>,
/// Public knowledge cutoff label.
pub knowledge_cutoff: Option<String>,
/// Estimated output tokens per second.
pub estimated_output_tps: Option<f64>,
/// Preferred for small utility calls such as title generation.
pub small_default: bool,
/// Preferred for provider connectivity probes.
pub probe: bool,
/// Whether requests reason when no effort is requested. Missing means
/// "reasons when the model supports reasoning".
pub reasoning_by_default: Option<bool>,
}
impl ModelPolicy {
#[must_use]
pub fn is_enabled(&self) -> bool {
self.enabled.unwrap_or(true)
}
}
/// Reads a provider's Fabro policy. Malformed metadata falls back to the
/// defaults; the catalog build is the place to validate shape, and Fabro's
/// own policy file is checked in tests.
#[must_use]
pub fn provider_policy(provider: &CatalogProvider) -> ProviderPolicy {
provider
.metadata()
.namespace::<ProviderPolicy>(FABRO_METADATA_NAMESPACE)
.ok()
.flatten()
.unwrap_or_default()
}
/// Reads a model's Fabro policy.
#[must_use]
pub fn model_policy(model: &CatalogModel) -> ModelPolicy {
model
.metadata()
.namespace::<ModelPolicy>(FABRO_METADATA_NAMESPACE)
.ok()
.flatten()
.unwrap_or_default()
}
/// The agent profile a model runs under: the model override, then the
/// provider default, then the profile implied by the provider's adapter.
#[must_use]
pub fn effective_agent_profile(
provider: &CatalogProvider,
model: &CatalogModel,
) -> AgentProfileKind {
model_policy(model)
.agent_profile
.or(provider_policy(provider).agent_profile)
.unwrap_or_else(|| default_agent_profile(provider))
}
/// The agent profile implied by a provider's wire protocol.
#[must_use]
pub fn default_agent_profile(provider: &CatalogProvider) -> AgentProfileKind {
match provider.adapter().as_str() {
"anthropic" | "bedrock" => AgentProfileKind::Anthropic,
"gemini" => AgentProfileKind::Gemini,
_ => AgentProfileKind::OpenAi,
}
}
#[cfg(test)]
mod tests {
use lithos_llm::catalog::Catalog;
use super::*;
fn catalog() -> Catalog {
Catalog::builder()
.toml_layer(
"test",
r#"
schema_version = 1
[providers.acme]
display_name = "Acme"
adapter = "openai-compatible"
codec = "openai-chat"
base_url = "https://acme.test/v1"
auth = { type = "bearer" }
default_model = "large"
[providers.acme.metadata.fabro]
enabled = false
credentials = ["env:ACME_API_KEY"]
agent_profile = "kimi"
[providers.acme.models.large]
display_name = "Large"
api_model = "large"
[providers.acme.models.large.metadata.fabro]
small_default = true
probe = true
family = "acme"
[providers.acme.models.small]
display_name = "Small"
api_model = "small"
[providers.acme.models.small.metadata.fabro]
agent_profile = "openai"
enabled = false
"#,
)
.unwrap()
.build()
.unwrap()
}
#[test]
fn reads_provider_and_model_policy() {
let catalog = catalog();
let provider = catalog.provider("acme").unwrap();
let policy = provider_policy(provider);
assert!(!policy.is_enabled());
assert_eq!(policy.credentials, vec!["env:ACME_API_KEY"]);
assert_eq!(policy.agent_profile, Some(AgentProfileKind::Kimi));
let large = provider.model("large").unwrap();
let policy = model_policy(large);
assert!(policy.small_default && policy.probe && policy.is_enabled());
assert_eq!(policy.family.as_deref(), Some("acme"));
assert_eq!(
effective_agent_profile(provider, large),
AgentProfileKind::Kimi
);
let small = provider.model("small").unwrap();
assert!(!model_policy(small).is_enabled());
assert_eq!(
effective_agent_profile(provider, small),
AgentProfileKind::OpenAi
);
}
#[test]
fn missing_namespace_yields_defaults() {
let catalog = Catalog::builder()
.toml_layer(
"test",
r#"
schema_version = 1
[providers.bare]
display_name = "Bare"
adapter = "anthropic"
codec = "anthropic-messages"
base_url = "https://bare.test"
auth = { type = "none" }
[providers.bare.models.m]
display_name = "M"
api_model = "m"
"#,
)
.unwrap()
.build()
.unwrap();
let provider = catalog.provider("bare").unwrap();
assert!(provider_policy(provider).is_enabled());
let model = provider.model("m").unwrap();
assert!(model_policy(model).is_enabled());
assert_eq!(
effective_agent_profile(provider, model),
AgentProfileKind::Anthropic
);
}
}

View file

@ -8,7 +8,6 @@ pub mod billing_rollup;
pub mod blob_hash;
pub mod blob_ref;
pub mod catalog_api;
pub mod catalog_policy;
pub mod checkpoint;
pub mod command_output;
pub mod conclusion;
@ -75,7 +74,6 @@ pub use billing::{
pub use blob_hash::BlobHash;
pub use blob_ref::{format_blob_ref, parse_blob_ref, parse_managed_blob_file_ref};
pub use catalog_api::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider};
pub use catalog_policy::{ModelPolicy, ProviderPolicy};
pub use checkpoint::Checkpoint;
pub use command_output::{CommandOutputStream, CommandTermination};
pub use conclusion::{Conclusion, StageSummary};