From 6eb9ee8cd98c65154c3961a4be8c064ee29f3533 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:09:21 -0600 Subject: [PATCH 1/7] Pin lithos-llm at the Rust boundary revision lithos-llm ba2f418 adds provider readiness, catalog queries, error policy predicates, one-call structured output, local-file inlining, the reasoning normalizer, the nearest supported effort, and built-in provider ids. Every addition is additive, so this pin changes nothing yet; the commits that follow adopt each one and delete Fabro's copy. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 22 +++++++++++----------- Cargo.toml | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 336485026..3459e9ed6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2190,7 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4952,7 +4952,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lithos-llm" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/lithos-llm?rev=38ccb14f08c56382e4af3888704de2f0951c910a#38ccb14f08c56382e4af3888704de2f0951c910a" +source = "git+https://github.com/lithoscomputer/lithos-llm?rev=ba2f4184e3db9a37f36aa650ce4b7c5f3376b109#ba2f4184e3db9a37f36aa650ce4b7c5f3376b109" dependencies = [ "async-trait", "aws-config", @@ -5413,7 +5413,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6410,7 +6410,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -6887,7 +6887,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6946,7 +6946,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7470,7 +7470,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.2.8", + "errno 0.3.14", "libc", ] @@ -8060,7 +8060,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -8106,7 +8106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9184,7 +9184,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 194d53a90..09e61ddeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ insta = "1" fabro-test = { path = "lib/foundation/fabro-test" } # Provider-neutral LLM catalog and client. Pinned to a revision until 0.x is # published to crates.io. -lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "38ccb14f08c56382e4af3888704de2f0951c910a", default-features = false } +lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "ba2f4184e3db9a37f36aa650ce4b7c5f3376b109", default-features = false } # Deterministic OpenAI twin used by twin-mode E2E tests; the same revision # lithos-llm verifies its codecs against. twin-openai = { git = "https://github.com/lithoscomputer/twins", rev = "ca45f0e50a6716d716aa2f638ca3cf767e88f613" } From 3510d5081d911bfc3a0e18d272b1727323e2b33b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:16:39 -0600 Subject: [PATCH 2/7] Implement the lithos CredentialProvider trait directly fabro-auth defined its own `CredentialSource` trait beside the lithos `CredentialProvider`, with a parallel `ResolveError` and an adapter between them, because lithos had no way to ask which providers a store can serve right now. It does now: `credentials::readiness`, `ClientBuilder::build_ready`, and `CredentialError::Unusable`. - The vault, SQL vault, API-key, and extra-headers stores implement `CredentialProvider` directly. Material that is present but unusable (an expired token with no refresh, a wrong-typed vault entry, a header secret that did not resolve, a store read failure) is `CredentialError::Unusable` with the operator-facing reason; its `Display` replaces `auth_issue_message`. `is_configured` is the cheap presence check. - `fabro_llm::build_client` calls `build_ready`; `FabroClient::auth_issues` carries `CredentialError`. `fabro_llm::configured_providers` replaces the per-store `configured_providers` method. - `CredentialSource`, `ResolvedCredentials`, `lithos_credentials`, `ResolveError`, and `auth_issue_message` are deleted. Twenty files that held `Arc` hold `Arc`. Co-Authored-By: Claude Fable 5.1 --- docs/internal/llm-client-resolution.md | 6 +- docs/public/reference/sdk.mdx | 2 +- lib/apps/fabro-cli/src/command_context.rs | 11 +- lib/apps/fabro-server/src/diagnostics.rs | 5 +- lib/apps/fabro-server/src/run_manifest.rs | 3 +- lib/apps/fabro-server/src/server.rs | 11 +- .../fabro-server/src/server/handler/models.rs | 5 +- lib/apps/fabro-server/src/server/tests.rs | 29 +-- .../tests/it/scenario/run_completion.rs | 2 +- lib/components/fabro-agent/src/cli.rs | 7 +- lib/components/fabro-hooks/src/bridge.rs | 3 +- lib/components/fabro-hooks/src/executor.rs | 17 +- lib/components/fabro-hooks/src/runner.rs | 10 +- .../fabro-hooks/tests/host_command_hooks.rs | 5 +- lib/components/fabro-llm/src/client.rs | 47 +++-- lib/components/fabro-llm/src/lib.rs | 2 +- lib/components/fabro-llm/src/probe.rs | 5 +- .../fabro-workflow/src/handler/llm/api.rs | 12 +- .../fabro-workflow/src/operations/start.rs | 10 +- .../fabro-workflow/src/pipeline/initialize.rs | 36 ++-- .../src/pipeline/pull_request.rs | 23 ++- lib/components/fabro-workflow/src/services.rs | 37 ++-- .../fabro-workflow/src/test_support.rs | 9 +- .../fabro-workflow/tests/it/integration.rs | 7 +- .../fabro-auth/src/api_key_source.rs | 37 ++-- .../fabro-auth/src/credential_source.rs | 98 --------- lib/foundation/fabro-auth/src/error.rs | 83 -------- .../fabro-auth/src/extra_headers_source.rs | 56 ++--- lib/foundation/fabro-auth/src/lib.rs | 14 +- .../fabro-auth/src/sql_vault_source.rs | 43 ++-- lib/foundation/fabro-auth/src/test_support.rs | 6 +- lib/foundation/fabro-auth/src/vault_source.rs | 192 +++++++++--------- 32 files changed, 342 insertions(+), 491 deletions(-) delete mode 100644 lib/foundation/fabro-auth/src/credential_source.rs delete mode 100644 lib/foundation/fabro-auth/src/error.rs diff --git a/docs/internal/llm-client-resolution.md b/docs/internal/llm-client-resolution.md index 3187b22de..891528c85 100644 --- a/docs/internal/llm-client-resolution.md +++ b/docs/internal/llm-client-resolution.md @@ -4,12 +4,12 @@ This document defines how Fabro resolves LLM credentials and constructs `fabro-l ## Core Rules -- `fabro_auth::CredentialSource` is the credential authority. -- Long-lived runtime contexts store `Arc` and `Arc`, not `Client`. +- The lithos `CredentialProvider` trait is the credential authority; Fabro's vault, SQL secret store, and API-key stores implement it directly. +- Long-lived runtime contexts store `Arc` and `Arc`, not `Client`. - Call `fabro_llm::client::Client::from_source(&source, catalog).await?` at the point of use. - Standalone setup and tests that use default settings build a default `Arc` locally, then pass it explicitly. - `GenerateParams::new(model, client)` always receives an explicit `Arc`. -- When a caller needs diagnostics in runtime request-serving paths, call `source.resolve(catalog)` directly and consume both `credentials` and `auth_issues`. +- When a caller needs diagnostics in runtime request-serving paths, read `FabroClient::ready` and `auth_issues` (from `ClientBuilder::build_ready`), or call `lithos_llm::credentials::readiness` directly. - `VaultCredentialSource` is the normal source for vault-backed runtime contexts; `VaultCredentialSource::environment_only()` serves env-only or no-vault contexts. ## Why diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index 05668b258..1b34463dc 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -374,7 +374,7 @@ The `fabro_llm::catalog` module reads Fabro policy from the catalog: `enabled_pr ### Client -`fabro_llm::build_client(catalog, source, options)` returns a `FabroClient`: the lithos `Client`, the providers that are ready, the providers whose credentials could not be used, and the providers lithos could not build an adapter for. Credentials are read from the `CredentialSource` on every provider attempt, so a refreshed OAuth token is picked up without rebuilding the client. +`fabro_llm::build_client(catalog, credentials, options)` takes any lithos `CredentialProvider` and returns a `FabroClient`: the lithos `Client`, the providers that are ready, the providers whose credentials could not be used, and the providers lithos could not build an adapter for. Credentials are read from the provider on every attempt, so a refreshed OAuth token is picked up without rebuilding the client. `ClientOptions::standard()` turns on the lithos retry middleware (three attempts with short exponential backoff) and local attachment inlining. Add middleware with `with_middleware`, replace a provider's adapter with `with_adapter`, or set `http` to inject a configured HTTP client. `fabro_llm::build_offline_client(catalog, options)` builds a client whose only providers are custom adapters, which is how `fabro exec --server` routes every call through a Fabro server. diff --git a/lib/apps/fabro-cli/src/command_context.rs b/lib/apps/fabro-cli/src/command_context.rs index fea71ff73..0d78fa996 100644 --- a/lib/apps/fabro-cli/src/command_context.rs +++ b/lib/apps/fabro-cli/src/command_context.rs @@ -2,8 +2,9 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use anyhow::{Context as _, Result, bail}; -use fabro_auth::{CredentialSource, SqlVaultCredentialSource}; +use fabro_auth::SqlVaultCredentialSource; use fabro_config::{CliLayer, Storage, load_llm_overlay}; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_types::UserSettings; use fabro_types::settings::RunNamespace; @@ -44,7 +45,7 @@ pub(crate) struct CommandContext { run_settings_key_presence: RunSettingsKeyPresence, server_mode: ServerMode, server: OnceCell>, - llm_source: OnceCell>, + llm_source: OnceCell>, catalog: OnceLock>, } @@ -163,7 +164,7 @@ impl CommandContext { Ok(Arc::clone(client)) } - pub(crate) async fn llm_source(&self) -> Result> { + pub(crate) async fn llm_source(&self) -> Result> { let storage_dir = self.storage_dir.clone(); let source = self @@ -173,9 +174,9 @@ impl CommandContext { let store = SecretStore::open(storage.sqlite_path(), storage.secrets_path()) .await .context("opening the Fabro secret store")?; - let source: Arc = + let source: Arc = Arc::new(SqlVaultCredentialSource::new(Arc::new(store))); - Ok::, anyhow::Error>(source) + Ok::, anyhow::Error>(source) }) .await?; diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index c5d325175..0397c6c11 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -4,7 +4,6 @@ use std::time::Duration; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use fabro_auth::auth_issue_message; use fabro_http::Response; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::probe::{self, ModelTestStatus}; @@ -219,7 +218,7 @@ pub(crate) async fn test_llm_providers(state: &AppState) -> anyhow::Result, ) -> ProviderProbeResult { if let Some(message) = auth_issue { - // `auth_issue_message` already embeds the provider's display name, so the + // The credential error already names the provider, so the // diagnostics detail uses the message as-is rather than re-prefixing. return provider_probe_error(provider, None, message.clone(), Some(message)); } diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index 6b94bfc89..dcef70c8f 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -6,7 +6,6 @@ use std::time::Duration; use anyhow::{Context as _, Result, anyhow, bail}; use fabro_api::types; -use fabro_auth::auth_issue_message; use fabro_config::parse::SettingsSource; use fabro_config::{ CliLayer, CliOutputLayer, EnvironmentLayer, MergeMap, RunLayer, SettingsLayer, @@ -1112,7 +1111,7 @@ async fn run_llm_check( status: CheckStatus::Warning, summary: model_id.clone(), details: vec![CheckDetail::new(format!("Provider: {provider_name}"))], - remediation: Some(auth_issue_message(&provider_id, issue)), + remediation: Some(issue.to_string()), })); } else if let Some(issue) = registration_issues .iter() diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index 70e22b006..6911f9c88 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -48,7 +48,7 @@ pub use fabro_api::types::{ SystemRepairRunsResponse, SystemResourcesResponse, SystemRunCounts, TimelineEntryResponse, UpdateVariableRequest, VariableListResponse, VncPreviewResponse, WriteBlobResponse, }; -use fabro_auth::{CredentialSource, SqlVaultCredentialSource, auth_issue_message}; +use fabro_auth::SqlVaultCredentialSource; use fabro_automation::{self, AutomationStore}; use fabro_config::daemon::ServerDaemon; use fabro_config::{LlmLayer, RunLayer, Storage, WorkflowSettingsBuilder}; @@ -57,6 +57,7 @@ use fabro_environment::EnvironmentStore; use fabro_interview::{ Answer, AnswerSubmission, ControlInterviewer, Interviewer, Question, WorkerControlEnvelope, }; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{ClientOptions, FabroClient, catalog}; use fabro_mcp_store::McpServerStore; @@ -1126,7 +1127,7 @@ pub struct AppState { parent_link_lock: AsyncMutex<()>, pub(super) server_secrets: ServerSecrets, - pub(crate) llm_source: Arc, + pub(crate) llm_source: Arc, manifest_run_defaults: RwLock>, manifest_run_settings: RwLock>, pub(crate) server_settings: RwLock>, @@ -1401,7 +1402,7 @@ impl AppState { pub(crate) async fn configured_llm_provider_ids(&self) -> Vec { let catalog = self.catalog(); - self.llm_source.configured_providers(catalog.as_ref()).await + fabro_llm::configured_providers(catalog.as_ref(), self.llm_source.as_ref()).await } /// Resolve the LLM client once and derive the ready provider IDs from it, @@ -1679,7 +1680,7 @@ impl AppState { /// Builds the server's LLM client: retries and attachment inlining on, the /// server's HTTP client for provider requests when one is configured. async fn resolve_llm_client_from_source( - source: Arc, + source: Arc, catalog: Arc, http_client: Option, ) -> anyhow::Result { @@ -2455,7 +2456,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result = Arc::new(SqlVaultCredentialSource::vault_only( + let llm_source: Arc = Arc::new(SqlVaultCredentialSource::vault_only( Arc::clone(&secret_store), )); let (global_event_tx, _) = broadcast::channel(4096); diff --git a/lib/apps/fabro-server/src/server/handler/models.rs b/lib/apps/fabro-server/src/server/handler/models.rs index cb47cea3f..544721f6d 100644 --- a/lib/apps/fabro-server/src/server/handler/models.rs +++ b/lib/apps/fabro-server/src/server/handler/models.rs @@ -11,8 +11,7 @@ use fabro_types::ReasoningEffort; use super::super::{ ApiError, AppState, FromStr, IntoResponse, Json, MAX_PAGE_OFFSET, ModelTestMode, Path, ProviderCredentialTestRequest, ProviderCredentialTestResponse, ProviderId, ProviderList, Query, - RequiredUser, Response, Router, State, StatusCode, auth_issue_message, default_page_limit, - error, get, post, + RequiredUser, Response, Router, State, StatusCode, default_page_limit, error, get, post, }; use crate::diagnostics; @@ -251,7 +250,7 @@ async fn test_model( .iter() .find(|(provider, _)| provider == &provider_id) { - return ApiError::bad_request(auth_issue_message(&provider_id, issue)).into_response(); + return ApiError::bad_request(issue.to_string()).into_response(); } if !llm_result.has_provider(&provider_id) { return Json(serde_json::json!({ diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 68b2503a7..e753cfe28 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -1813,22 +1813,18 @@ async fn resolve_llm_client_ignores_env_lookup_provider_tokens() { struct FailingCredentialSource; #[async_trait::async_trait] -impl CredentialSource for FailingCredentialSource { +impl CredentialProvider for FailingCredentialSource { async fn credentials( &self, provider: &fabro_llm::lithos_catalog::CatalogProvider, - ) -> Result { - Err(fabro_auth::ResolveError::NotConfigured( - provider.id().clone(), - )) + ) -> Result { + Err(fabro_llm::credentials::CredentialError::NotConfigured { + provider: provider.id().clone(), + }) } - async fn configured_providers( - &self, - catalog: &fabro_llm::lithos_catalog::Catalog, - ) -> Vec { - let _ = catalog; - Vec::new() + async fn is_configured(&self, _provider: &fabro_llm::lithos_catalog::CatalogProvider) -> bool { + false } } @@ -1864,14 +1860,9 @@ async fn llm_source_configured_providers_reads_openai_token_from_vault() { .await .unwrap(); - let catalog = state.catalog(); - assert_eq!( - state - .llm_source - .configured_providers(catalog.as_ref()) - .await, - vec![fabro_types::provider_ids::openai()] - ); + assert_eq!(state.configured_llm_provider_ids().await, vec![ + fabro_types::provider_ids::openai() + ]); } #[tokio::test] diff --git a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs index 3f2073886..f32702a30 100644 --- a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs +++ b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs @@ -38,7 +38,7 @@ fn test_app_with_openai_agent_backend(openai_base_url: String, api_key: String) )); let source_api_key = api_key.clone(); let env_api_key = api_key.clone(); - let llm_source: Arc = + let llm_source: Arc = test_support::env_credential_source(move |name| match name { "OPENAI_API_KEY" => Some(source_api_key.clone()), _ => None, diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index 84f257831..a677ceacf 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -9,9 +9,10 @@ use std::sync::{Arc, Mutex}; use anyhow::Context as _; use clap::{Args, Parser}; -use fabro_auth::{CredentialSource, SqlVaultCredentialSource}; +use fabro_auth::SqlVaultCredentialSource; use fabro_config::Storage; use fabro_config::user::default_storage_dir; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::middleware::{Call, Middleware, Next, Output}; use fabro_llm::{Client, ClientOptions, Error as LlmError, catalog}; @@ -244,7 +245,7 @@ fn resolve_provider_id( catalog::canonical_provider_id(catalog, requested.as_str()).unwrap_or(requested) } -async fn standalone_llm_source() -> anyhow::Result> { +async fn standalone_llm_source() -> anyhow::Result> { let storage = Storage::new(default_storage_dir()); let store = SecretStore::open(storage.sqlite_path(), storage.secrets_path()) .await @@ -455,7 +456,7 @@ pub async fn run_with_args( )] pub async fn run_with_args_and_source_and_catalog( args: AgentArgs, - llm_source: Arc, + llm_source: Arc, mcp_servers: Vec, catalog: Arc, ) -> anyhow::Result<()> { diff --git a/lib/components/fabro-hooks/src/bridge.rs b/lib/components/fabro-hooks/src/bridge.rs index 6f49a6a00..b064e6171 100644 --- a/lib/components/fabro-hooks/src/bridge.rs +++ b/lib/components/fabro-hooks/src/bridge.rs @@ -78,6 +78,7 @@ mod tests { use std::path::PathBuf; use std::sync::Mutex; + use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_types::fixtures; @@ -100,7 +101,7 @@ mod tests { context: &HookContext, _sandbox: Arc, execution_context: &HookExecutionContext, - _llm_source: Arc, + _llm_source: Arc, _catalog: Arc, ) -> HookResult { self.captured_contexts.lock().unwrap().push(context.clone()); diff --git a/lib/components/fabro-hooks/src/executor.rs b/lib/components/fabro-hooks/src/executor.rs index 3d208ed7e..418a77270 100644 --- a/lib/components/fabro-hooks/src/executor.rs +++ b/lib/components/fabro-hooks/src/executor.rs @@ -6,7 +6,7 @@ use std::time::Instant; use async_trait::async_trait; use fabro_agent::Sandbox; use fabro_agent::tool_registry::ToolContext; -use fabro_auth::CredentialSource; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{Client, ClientOptions, Request, structured}; use fabro_redact::redacted_url_for_log; @@ -48,7 +48,7 @@ pub trait HookExecutor: Send + Sync { context: &HookContext, sandbox: Arc, execution_context: &HookExecutionContext, - llm_source: Arc, + llm_source: Arc, catalog: Arc, ) -> HookResult; } @@ -281,7 +281,7 @@ impl HookExecutorImpl { prompt: &InterpString, model: Option<&InterpString>, context: &HookContext, - llm_source: Arc, + llm_source: Arc, catalog: Arc, ) -> HookDecision { let (prompt, model) = match Self::resolve_prompt_and_model(prompt, model) { @@ -361,7 +361,7 @@ impl HookExecutorImpl { max_tool_rounds: Option, context: &HookContext, sandbox: Arc, - llm_source: Arc, + llm_source: Arc, catalog: Arc, ) -> HookDecision { let (prompt, model) = match Self::resolve_prompt_and_model(prompt, model) { @@ -476,7 +476,7 @@ impl HookExecutorImpl { /// serve, with standard retries. async fn build_client( catalog: Arc, - llm_source: Arc, + llm_source: Arc, ) -> Result { fabro_llm::build_client( Catalog::clone(&catalog), @@ -662,7 +662,7 @@ impl HookExecutor for HookExecutorImpl { context: &HookContext, sandbox: Arc, execution_context: &HookExecutionContext, - llm_source: Arc, + llm_source: Arc, catalog: Arc, ) -> HookResult { use std::sync::OnceLock; @@ -761,7 +761,8 @@ impl HookExecutor for HookExecutorImpl { #[cfg(test)] mod tests { - use fabro_auth::{CredentialSource, test_support}; + use fabro_auth::test_support; + use fabro_llm::credentials::CredentialProvider; use fabro_types::fixtures; use fabro_types::settings::ResolveErrorKind; @@ -779,7 +780,7 @@ mod tests { )) } - fn test_llm_source() -> Arc { + fn test_llm_source() -> Arc { test_support::vault_only_credential_source() } diff --git a/lib/components/fabro-hooks/src/runner.rs b/lib/components/fabro-hooks/src/runner.rs index 489edd0dd..7a9f8fd57 100644 --- a/lib/components/fabro-hooks/src/runner.rs +++ b/lib/components/fabro-hooks/src/runner.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::sync::Arc; use fabro_agent::Sandbox; -use fabro_auth::CredentialSource; #[cfg(test)] use fabro_auth::test_support; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use crate::config::{HookDefinition, HookSettings}; @@ -16,7 +16,7 @@ use crate::types::{HookContext, HookDecision, HookExecutionContext}; pub struct HookRunner { config: HookSettings, executor: Arc, - llm_source: Arc, + llm_source: Arc, catalog: Arc, /// Pre-compiled regexes keyed by matcher pattern string. compiled_matchers: HashMap, @@ -26,7 +26,7 @@ impl HookRunner { #[must_use] pub fn new( config: HookSettings, - llm_source: Arc, + llm_source: Arc, catalog: Arc, ) -> Self { let compiled_matchers = Self::compile_matchers(&config); @@ -256,7 +256,7 @@ mod tests { _context: &HookContext, _sandbox: Arc, _execution_context: &HookExecutionContext, - _llm_source: Arc, + _llm_source: Arc, _catalog: Arc, ) -> HookResult { HookResult { @@ -277,7 +277,7 @@ mod tests { HookContext::new(event, fixtures::RUN_1, "test-wf".into()) } - fn test_llm_source() -> Arc { + fn test_llm_source() -> Arc { test_support::vault_only_credential_source() } diff --git a/lib/components/fabro-hooks/tests/host_command_hooks.rs b/lib/components/fabro-hooks/tests/host_command_hooks.rs index 74cacd33c..8cac52b80 100644 --- a/lib/components/fabro-hooks/tests/host_command_hooks.rs +++ b/lib/components/fabro-hooks/tests/host_command_hooks.rs @@ -2,16 +2,17 @@ use std::path::Path; use std::sync::Arc; use fabro_agent::{LocalSandbox, Sandbox}; -use fabro_auth::{CredentialSource, test_support}; +use fabro_auth::test_support; use fabro_hooks::{ HookContext, HookDecision, HookDefinition, HookEvent, HookExecutionContext, HookRunner, HookSettings, InterpString, }; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_types::RunId; use tokio::fs; -fn test_llm_source() -> Arc { +fn test_llm_source() -> Arc { test_support::vault_only_credential_source() } diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index 412456ffb..708819e7f 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -3,11 +3,11 @@ use std::sync::Arc; use std::time::Duration; -use fabro_auth::{CredentialSource, ResolveError, lithos_credentials}; use fabro_types::ProviderId; use lithos_llm::adapter::ProviderAdapter; use lithos_llm::catalog::Catalog; use lithos_llm::client::{Client, ClientBuildError, ClientBuilder, ProviderBuildIssue}; +use lithos_llm::credentials::{CredentialError, CredentialProvider}; use lithos_llm::middleware::{ Call, Middleware, Observer, RetryMiddleware, RetryPolicy, RetryStage, }; @@ -167,8 +167,9 @@ pub struct FabroClient { pub client: Client, /// Enabled providers with working credentials, in catalog order. pub ready: Vec, - /// Enabled providers whose credential material could not be used. - pub auth_issues: Vec<(ProviderId, ResolveError)>, + /// Enabled providers whose credential material could not be used. The + /// error's `Display` is the operator-facing line. + pub auth_issues: Vec<(ProviderId, CredentialError)>, /// Ready providers lithos could not build an adapter for. pub build_issues: Vec, } @@ -193,34 +194,42 @@ pub enum LlmSetupError { Build(#[from] ClientBuildError), } -/// Builds a client whose ready providers are those the credential source can -/// serve. Credentials are re-read from `source` on every provider attempt. +/// Builds a client whose ready providers are those `credentials` can serve. +/// Credentials are re-read on every provider attempt, so a refreshed OAuth +/// token is picked up by the next retry. pub async fn build_client( catalog: Catalog, - source: Arc, + credentials: Arc, options: ClientOptions, ) -> Result { - let resolved = source.resolve_all(&catalog).await; - let mut ready = resolved.ready; - for provider in options.adapter_providers() { - if !ready.contains(provider) { - ready.push(provider.clone()); - } - } let builder = Client::builder() .catalog(catalog) .application(APPLICATION_NAME) - .credentials_arc(lithos_credentials(source)) - .enabled_providers(ready.iter().cloned()); - let build = options.apply(builder).build()?; + .credentials_arc(credentials); + let build = options.apply(builder).build_ready().await?; Ok(FabroClient { - client: build.client, - ready, - auth_issues: resolved.auth_issues, + client: build.client, + ready: build.ready, + auth_issues: build.credential_issues, build_issues: build.issues, }) } +/// The enabled providers `credentials` holds material for, in catalog order, +/// without refreshing anything. Cheap enough for listings. +pub async fn configured_providers( + catalog: &Catalog, + credentials: &dyn CredentialProvider, +) -> Vec { + let mut configured = Vec::new(); + for provider in catalog.providers().filter(|provider| provider.is_enabled()) { + if credentials.is_configured(provider).await { + configured.push(provider.id().clone()); + } + } + configured +} + /// Builds a client that needs no credentials: every available provider is /// served by a custom adapter from `options.adapters`, such as the /// `fabro exec` gateway or a test double. diff --git a/lib/components/fabro-llm/src/lib.rs b/lib/components/fabro-llm/src/lib.rs index 31cd9d74b..00e9b8c7d 100644 --- a/lib/components/fabro-llm/src/lib.rs +++ b/lib/components/fabro-llm/src/lib.rs @@ -37,7 +37,7 @@ pub mod test_support; pub use catalog::{build_catalog, default_catalog}; pub use client::{ ClientOptions, FabroClient, LlmSetupError, RetryListener, RetryNotice, build_client, - build_offline_client, + build_offline_client, configured_providers, }; pub use error::{ErrorFacts, LlmError}; pub use lithos_llm::client::{Client, ClientBuild}; diff --git a/lib/components/fabro-llm/src/probe.rs b/lib/components/fabro-llm/src/probe.rs index 1e2cbcb25..58c5a1e51 100644 --- a/lib/components/fabro-llm/src/probe.rs +++ b/lib/components/fabro-llm/src/probe.rs @@ -120,10 +120,7 @@ pub async fn probe_provider_with_api_key( .iter() .find(|(candidate, _)| candidate == &provider_id) { - return Ok(ModelTestOutcome::error(fabro_auth::auth_issue_message( - &provider_id, - issue, - ))); + return Ok(ModelTestOutcome::error(issue.to_string())); } Ok(run_basic_probe(&built.client, &selector, timeout).await) } diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index ab140a10c..87cffd0fa 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -10,8 +10,8 @@ use fabro_agent::{ Sandbox, Session, SessionOptions, SessionShutdownReason, StaticEnvProvider, ToolEnvProvider, ToolSecrets, WebFetchSummarizer, canonical_tool_name, register_question_tools, }; -use fabro_auth::CredentialSource; use fabro_graphviz::graph::{AttrValue, Node}; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::error::failover_eligible; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::types::ResponseFormat; @@ -638,7 +638,7 @@ pub struct AgentApiBackend { mcp_servers: Vec, tool_secrets: ToolSecrets, run_model_controls: RunModelControls, - source: Arc, + source: Arc, steering_hub: Arc, catalog: Arc, fabro_run_tools: Option, @@ -784,7 +784,7 @@ impl AgentApiBackend { model: String, provider_id: impl Into, fallbacks: ModelFallbackPolicy, - source: Arc, + source: Arc, steering_hub: Arc, ) -> Self { let catalog = Arc::new(fabro_llm::default_catalog()); @@ -803,7 +803,7 @@ impl AgentApiBackend { model: String, provider_id: ProviderId, fallbacks: ModelFallbackPolicy, - source: Arc, + source: Arc, steering_hub: Arc, catalog: Arc, ) -> Self { @@ -1051,7 +1051,7 @@ impl AgentApiBackend { controls: EffectiveRequestControls, node: &Node, sandbox: &Arc, - source: Arc, + source: Arc, catalog: Arc, tool_env: Option<&Arc>, tool_hooks: Option>, @@ -1453,7 +1453,7 @@ impl AgentApiBackend { /// Build the LLM client a stage session dispatches through. async fn build_llm_client( catalog: &Arc, - source: Arc, + source: Arc, ) -> Result { fabro_llm::build_client(Catalog::clone(catalog), source, ClientOptions::standard()) .await diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index f5a5c8ad0..83e56fbcf 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -4,8 +4,9 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; -use fabro_auth::{CredentialSource, VaultCredentialSource}; +use fabro_auth::VaultCredentialSource; use fabro_interview::{AutoApproveInterviewer, Interviewer}; +use fabro_llm::credentials::readiness; use fabro_llm::lithos_catalog::Catalog; use fabro_mcp::config::McpServerSettings; use fabro_sandbox::daytona::DaytonaConfig; @@ -746,11 +747,8 @@ async fn configured_providers_for_start( vault: &Arc>, catalog: Arc, ) -> Vec { - let source: Arc = Arc::new(VaultCredentialSource::with_env_lookup( - Arc::clone(vault), - process_env_var, - )); - source.resolve_all(catalog.as_ref()).await.ready + let source = VaultCredentialSource::with_env_lookup(Arc::clone(vault), process_env_var); + readiness(catalog.enabled_providers(), &source).await.ready } fn git_checkpoint_options_from_start( diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 38fc31e51..d4effcf2e 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -4,10 +4,11 @@ use std::sync::Arc; use std::time::Instant; use fabro_agent::{Sandbox, ToolSecrets}; -use fabro_auth::{CredentialSource, ExtraHeadersCredentialSource, VaultCredentialSource}; +use fabro_auth::{ExtraHeadersCredentialSource, VaultCredentialSource}; use fabro_github::token_source::InstallationTokenSource; use fabro_graphviz::graph; use fabro_hooks::{HookContext, HookDecision, HookEvent, HookExecutionContext, HookRunner}; +use fabro_llm::credentials::{CredentialProvider, readiness}; use fabro_llm::lithos_catalog::Catalog; use fabro_sandbox::{ GitSetupIntent, SandboxEventCallback, SandboxSpec, reconnect_for_run_with_callback, shell_quote, @@ -210,7 +211,7 @@ async fn build_registry( tool_env_provider: Arc, github_token_refresh_managed: bool, graph: &graph::Graph, - llm_source: Arc, + llm_source: Arc, catalog: Arc, tool_secrets: ToolSecrets, fabro_run_tools: Option, @@ -276,11 +277,17 @@ async fn build_registry( return Ok((build_llm_registry(), false)); } - let result = llm_source.resolve_all(catalog.as_ref()).await; + let result = readiness(catalog.enabled_providers(), llm_source.as_ref()).await; if result.ready.is_empty() { if graph_needs_llm { - let detail = - (!result.auth_issues.is_empty()).then(|| result.issue_messages().join("; ")); + let detail = (!result.issues.is_empty()).then(|| { + result + .issues + .iter() + .map(|(_, issue)| issue.to_string()) + .collect::>() + .join("; ") + }); let prefix = detail.map_or_else( || "No LLM providers configured".to_string(), |detail| format!("No usable LLM providers configured: {detail}"), @@ -314,7 +321,7 @@ const SESSION_ID_HEADER: &str = "x-session-id"; fn build_llm_source( vault: Arc>, run_id: fabro_types::RunId, -) -> Arc { +) -> Arc { Arc::new(ExtraHeadersCredentialSource::new( Arc::new(VaultCredentialSource::new(vault)), HashMap::from([(SESSION_ID_HEADER.to_string(), run_id.to_string())]), @@ -1087,14 +1094,13 @@ mod tests { fabro_types::provider_ids::anthropic() ); assert!( - initialized - .engine - .run - .llm_source - .resolve_all(&initialized.engine.run.catalog) - .await - .ready - .is_empty() + readiness( + initialized.engine.run.catalog.enabled_providers(), + initialized.engine.run.llm_source.as_ref(), + ) + .await + .ready + .is_empty() ); } @@ -1244,7 +1250,7 @@ mod tests { let source = build_llm_source(vault, run_id); let catalog = test_catalog(); - let resolved = source.resolve_all(catalog.as_ref()).await; + let resolved = readiness(catalog.enabled_providers(), source.as_ref()).await; assert!(!resolved.ready.is_empty()); for provider in &resolved.ready { diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index f6fb74de5..d7990d5fe 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use std::sync::{Arc, LazyLock}; use std::time::Duration; -use fabro_auth::CredentialSource; use fabro_github::{self as github_app, ssh_url_to_https}; use fabro_graphviz::parser; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{Client, ClientOptions, Request, selection, structured}; use fabro_store::RunProjection; @@ -333,7 +333,7 @@ pub async fn build_pr_content( goal: &str, model: &str, run_store: &RunStoreHandle, - llm_source: Arc, + llm_source: Arc, catalog: Arc, conclusion: Option<&Conclusion>, run_state: Option<&RunProjection>, @@ -462,7 +462,7 @@ pub struct OpenPullRequestRequest<'a> { pub draft: bool, pub auto_merge: Option, pub run_store: &'a RunStoreHandle, - pub llm_source: Arc, + pub llm_source: Arc, pub catalog: Arc, pub conclusion: Option<&'a Conclusion>, pub run_state: Option<&'a RunProjection>, @@ -686,9 +686,10 @@ mod tests { use std::time::Duration; use chrono::Utc; - use fabro_auth::{CredentialSource, VaultCredentialSource}; + use fabro_auth::VaultCredentialSource; use fabro_graphviz::graph::Graph; use fabro_llm::adapter::{ProviderAdapter, ResolvedCall}; + use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::AdapterId; use fabro_llm::{Response, ResponseStream}; use fabro_store::Database; @@ -1308,9 +1309,9 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr None, ) .unwrap(); - let llm_source: Arc = Arc::new(VaultCredentialSource::new(Arc::new( - AsyncRwLock::new(vault), - ))); + let llm_source: Arc = Arc::new(VaultCredentialSource::new( + Arc::new(AsyncRwLock::new(vault)), + )); // Use catalog settings to override base_url instead of env var let catalog = test_catalog_with_provider_base_url("openai", &server.url("/v1")); @@ -1696,7 +1697,7 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr branch_mock_id: usize, reconcile_mock_id: usize, github_mock_id: usize, - llm_source: Arc, + llm_source: Arc, catalog: Arc, creds: fabro_github::GitHubCredentials, run_store: RunStoreHandle, @@ -1804,9 +1805,9 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr None, ) .unwrap(); - let llm_source: Arc = Arc::new(VaultCredentialSource::new(Arc::new( - AsyncRwLock::new(vault), - ))); + let llm_source: Arc = Arc::new(VaultCredentialSource::new( + Arc::new(AsyncRwLock::new(vault)), + )); // Use catalog settings to override base_url instead of env var let catalog = test_catalog_with_provider_base_url("openai", &openai_server.url("/v1")); diff --git a/lib/components/fabro-workflow/src/services.rs b/lib/components/fabro-workflow/src/services.rs index b1662de01..2456b56fb 100644 --- a/lib/components/fabro-workflow/src/services.rs +++ b/lib/components/fabro-workflow/src/services.rs @@ -5,10 +5,10 @@ use std::sync::Arc; use std::time::Duration; use fabro_agent::{Sandbox, ToolEnvProvider}; -use fabro_auth::CredentialSource; use fabro_github::token_source::InstallationTokenSource; use fabro_hooks::{HookContext, HookDecision, HookExecutionContext, HookRunner}; use fabro_interview::Interviewer; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_types::{ManifestPath, ProviderId, RunId}; use tokio_util::sync::CancellationToken; @@ -99,7 +99,7 @@ pub struct RunServices { pub(crate) cancel_token: CancellationToken, pub provider_id: ProviderId, pub model: String, - pub llm_source: Arc, + pub llm_source: Arc, pub catalog: Arc, pub(crate) sandbox_git: Arc, pub(crate) metadata_runtime: Arc, @@ -121,7 +121,7 @@ impl RunServices { cancel_token: CancellationToken, provider_id: ProviderId, model: String, - llm_source: Arc, + llm_source: Arc, catalog: Arc, sandbox_git: Arc, metadata_runtime: Arc, @@ -268,19 +268,22 @@ impl EngineServices { struct StubCredentialSource; #[async_trait::async_trait] - impl CredentialSource for StubCredentialSource { + impl CredentialProvider for StubCredentialSource { async fn credentials( &self, provider: &fabro_llm::lithos_catalog::CatalogProvider, - ) -> Result { - Err(fabro_auth::ResolveError::NotConfigured( - provider.id().clone(), - )) + ) -> Result + { + Err(fabro_llm::credentials::CredentialError::NotConfigured { + provider: provider.id().clone(), + }) } - async fn configured_providers(&self, catalog: &Catalog) -> Vec { - let _ = catalog; - Vec::new() + async fn is_configured( + &self, + _provider: &fabro_llm::lithos_catalog::CatalogProvider, + ) -> bool { + false } } @@ -383,12 +386,12 @@ mod tests { let services = EngineServices::test_default(); assert!( - services - .run - .llm_source - .configured_providers(&services.run.catalog) - .await - .is_empty() + fabro_llm::configured_providers( + &services.run.catalog, + services.run.llm_source.as_ref() + ) + .await + .is_empty() ); } diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 9efbeae84..57a39592f 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -5,10 +5,11 @@ use std::sync::Arc; use std::time::Duration; use fabro_agent::Sandbox; -use fabro_auth::{CredentialSource, test_support as auth_test_support}; +use fabro_auth::test_support as auth_test_support; use fabro_graphviz::graph::Graph as GvGraph; use fabro_interview::AutoApproveInterviewer; use fabro_llm::catalog; +use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::test_catalog; use fabro_store::{ArtifactStore, RunProjection, test_support as store_test_support}; @@ -145,7 +146,7 @@ struct InitializedOptions { hook_runner: Option>, env: HashMap, checkpoint: Option, - llm_source: Option>, + llm_source: Option>, } struct InitializedState { @@ -481,7 +482,7 @@ pub async fn run_graph_with_state_and_llm_source( sandbox: Arc, graph: &GvGraph, run_options: &RunOptions, - llm_source: Arc, + llm_source: Arc, ) -> Result<(Outcome, RunProjection)> { let initialized = initialized( registry, @@ -576,7 +577,7 @@ impl WorkflowRunner { &self, graph: &GvGraph, run_options: &RunOptions, - llm_source: Arc, + llm_source: Arc, ) -> Result<(Outcome, RunProjection)> { let registry = self .registry diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index ee407786e..1be24c4cb 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -7395,7 +7395,7 @@ mod real_llm { } fabro_test::require_env("ANTHROPIC_API_KEY")?; - let source: Arc = + let source: Arc = Arc::new(VaultCredentialSource::environment_only()); Some(Arc::new( fabro_llm::build_client( @@ -8081,7 +8081,8 @@ fn openai_responses_payload(text: &str) -> serde_json::Value { #[tokio::test] async fn workflow_run_with_vault_only_openai_codex_builds_pr_body() { use chrono::Utc; - use fabro_auth::{CredentialSource, VaultCredentialSource}; + use fabro_auth::VaultCredentialSource; + use fabro_llm::credentials::CredentialProvider; use fabro_types::Conclusion; use fabro_vault::{SecretType, Vault}; use httpmock::Method::POST; @@ -8138,7 +8139,7 @@ async fn workflow_run_with_vault_only_openai_codex_builds_pr_body() { None, ) .unwrap(); - let llm_source: Arc = Arc::new(VaultCredentialSource::new(Arc::new( + let llm_source: Arc = Arc::new(VaultCredentialSource::new(Arc::new( AsyncRwLock::new(vault), ))); // Use catalog settings to override base_url instead of env var diff --git a/lib/foundation/fabro-auth/src/api_key_source.rs b/lib/foundation/fabro-auth/src/api_key_source.rs index 17b706498..621b696f6 100644 --- a/lib/foundation/fabro-auth/src/api_key_source.rs +++ b/lib/foundation/fabro-auth/src/api_key_source.rs @@ -9,14 +9,14 @@ use std::sync::Arc; use async_trait::async_trait; use fabro_vault::Vault; -use lithos_llm::catalog::{Catalog, CatalogProvider, ProviderId}; -use lithos_llm::credentials::{ConventionalCredentials, CredentialProvider, Credentials}; +use lithos_llm::catalog::{CatalogProvider, ProviderId}; +use lithos_llm::credentials::{ + ConventionalCredentials, CredentialError, CredentialProvider, Credentials, +}; use tokio::sync::RwLock as AsyncRwLock; -use crate::credential_source::CredentialSource; -use crate::error::ResolveError; use crate::secrets::expected_secret_name; -use crate::vault_source::{auth_scheme_name, interpolated_headers, resolve_error}; +use crate::vault_source::interpolated_headers; pub struct ApiKeyCredentialSource { provider: ProviderId, @@ -61,19 +61,17 @@ pub(crate) async fn credentials_for_api_key( provider: &CatalogProvider, key: String, vault: &Vault, -) -> Result { +) -> Result { let Some(name) = expected_secret_name(provider) else { - return Err(ResolveError::SchemeMismatch { + return Err(CredentialError::SchemeMismatch { provider: provider.id().clone(), - scheme: auth_scheme_name(provider.auth()).to_string(), }); }; let interpolated = interpolated_headers(vault, provider)?; let mut credentials = ConventionalCredentials::new() .with_lookup(move |candidate| (candidate == name).then(|| key.clone())) .credentials(provider) - .await - .map_err(|err| resolve_error(provider, &err))?; + .await?; if let Credentials::Http(http) = &mut credentials { http.extra_headers.extend(interpolated); } @@ -81,21 +79,22 @@ pub(crate) async fn credentials_for_api_key( } #[async_trait] -impl CredentialSource for ApiKeyCredentialSource { - async fn credentials(&self, provider: &CatalogProvider) -> Result { +impl CredentialProvider for ApiKeyCredentialSource { + async fn credentials( + &self, + provider: &CatalogProvider, + ) -> Result { if provider.id() != &self.provider { - return Err(ResolveError::NotConfigured(provider.id().clone())); + return Err(CredentialError::NotConfigured { + provider: provider.id().clone(), + }); } let vault = self.vault.read().await.clone(); credentials_for_api_key(provider, self.key.clone(), &vault).await } - async fn configured_providers(&self, catalog: &Catalog) -> Vec { - catalog - .provider(self.provider.as_str()) - .ok() - .map(|provider| vec![provider.id().clone()]) - .unwrap_or_default() + async fn is_configured(&self, provider: &CatalogProvider) -> bool { + provider.id() == &self.provider } } diff --git a/lib/foundation/fabro-auth/src/credential_source.rs b/lib/foundation/fabro-auth/src/credential_source.rs deleted file mode 100644 index 69b677385..000000000 --- a/lib/foundation/fabro-auth/src/credential_source.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! Per-attempt credential lookup for LLM providers. -//! -//! [`CredentialSource`] is Fabro's storage-aware credential seam: the vault, -//! the SQL secret store, and the process environment each implement it. -//! [`lithos_credentials`] adapts a source into the lithos -//! [`CredentialProvider`] the client calls before every provider attempt, so a -//! refreshed OAuth token is picked up by the next retry. - -use std::sync::Arc; - -use async_trait::async_trait; -use lithos_llm::catalog::{Catalog, CatalogProvider, ProviderId}; -use lithos_llm::credentials::{CredentialError, CredentialProvider, Credentials}; - -use crate::{ResolveError, auth_issue_message}; - -/// Which providers a source can serve right now, and why the rest cannot. -#[derive(Debug, Default)] -pub struct ResolvedCredentials { - /// Enabled providers whose credentials resolved. - pub ready: Vec, - /// Enabled providers with credential material that failed to resolve, - /// such as an expired OAuth token that could not be refreshed. Providers - /// with no material at all are not issues; they are simply absent. - pub auth_issues: Vec<(ProviderId, ResolveError)>, -} - -impl ResolvedCredentials { - /// A human-readable line per auth issue. - #[must_use] - pub fn issue_messages(&self) -> Vec { - self.auth_issues - .iter() - .map(|(provider, issue)| auth_issue_message(provider, issue)) - .collect() - } -} - -#[async_trait] -pub trait CredentialSource: Send + Sync { - /// Resolves `provider`'s credentials for one request attempt. - async fn credentials(&self, provider: &CatalogProvider) -> Result; - - /// Providers with credential material present. Does not refresh or - /// validate anything, so it is cheap enough for listings. - async fn configured_providers(&self, catalog: &Catalog) -> Vec; - - /// Resolves every enabled provider once, separating the ready set from - /// the providers that have material but cannot use it. - async fn resolve_all(&self, catalog: &Catalog) -> ResolvedCredentials { - let mut resolved = ResolvedCredentials::default(); - for provider in catalog.providers().filter(|provider| provider.is_enabled()) { - match self.credentials(provider).await { - Ok(_) => resolved.ready.push(provider.id().clone()), - Err(ResolveError::NotConfigured(_)) => {} - Err(err) => resolved.auth_issues.push((provider.id().clone(), err)), - } - } - resolved - } -} - -/// Adapts a [`CredentialSource`] into the lithos credential provider. -#[must_use] -pub fn lithos_credentials(source: Arc) -> Arc { - Arc::new(SourceCredentialProvider { source }) -} - -struct SourceCredentialProvider { - source: Arc, -} - -#[async_trait] -impl CredentialProvider for SourceCredentialProvider { - async fn credentials( - &self, - provider: &CatalogProvider, - ) -> Result { - self.source.credentials(provider).await.map_err(|err| { - tracing::warn!( - provider = %provider.id(), - error = %err, - "LLM credentials could not be resolved for this attempt" - ); - match err { - ResolveError::NotConfigured(provider) => { - CredentialError::NotConfigured { provider } - } - ResolveError::SchemeMismatch { provider, .. } => { - CredentialError::SchemeMismatch { provider } - } - other => CredentialError::NotConfigured { - provider: other.provider().clone(), - }, - } - }) - } -} diff --git a/lib/foundation/fabro-auth/src/error.rs b/lib/foundation/fabro-auth/src/error.rs deleted file mode 100644 index 7986637da..000000000 --- a/lib/foundation/fabro-auth/src/error.rs +++ /dev/null @@ -1,83 +0,0 @@ -//! Why a provider's credentials could not be resolved. - -use fabro_types::settings::ResolveError as InterpResolveError; -use fabro_vault::SecretType; -use lithos_llm::catalog::ProviderId; - -#[derive(Debug, thiserror::Error)] -pub enum ResolveError { - #[error("{0} is not configured")] - NotConfigured(ProviderId), - #[error("{provider} header interpolation failed: {source}")] - Interpolation { - provider: ProviderId, - #[source] - source: InterpResolveError, - }, - #[error("{provider} vault credential '{name}' is not valid Oauth JSON: {source}")] - VaultDecodeFailed { - provider: ProviderId, - name: String, - #[source] - source: serde_json::Error, - }, - #[error("{provider} vault credential '{name}' has schema {actual:?}, expected Token or Oauth")] - VaultSchemaMismatch { - provider: ProviderId, - name: String, - actual: SecretType, - }, - #[error("{provider} requires re-authentication: {source}")] - RefreshFailed { - provider: ProviderId, - #[source] - source: anyhow::Error, - }, - #[error("{0} requires re-authentication: missing refresh token")] - RefreshTokenMissing(ProviderId), - #[error("{provider} resolved a secret its `{scheme}` auth scheme cannot use")] - SchemeMismatch { - provider: ProviderId, - scheme: String, - }, -} - -impl ResolveError { - #[must_use] - pub fn provider(&self) -> &ProviderId { - match self { - Self::NotConfigured(provider) - | Self::RefreshTokenMissing(provider) - | Self::Interpolation { provider, .. } - | Self::VaultDecodeFailed { provider, .. } - | Self::VaultSchemaMismatch { provider, .. } - | Self::RefreshFailed { provider, .. } - | Self::SchemeMismatch { provider, .. } => provider, - } - } -} - -#[must_use] -pub fn auth_issue_message(provider: &ProviderId, err: &ResolveError) -> String { - match err { - ResolveError::NotConfigured(_) => format!("{provider} is not configured"), - ResolveError::Interpolation { source, .. } => { - format!("{provider} header interpolation failed: {source}") - } - ResolveError::VaultDecodeFailed { name, source, .. } => { - format!("{provider} vault credential '{name}' is not valid OAuth JSON: {source}") - } - ResolveError::VaultSchemaMismatch { name, actual, .. } => format!( - "{provider} vault credential '{name}' has schema {actual:?}, expected Token or Oauth" - ), - ResolveError::RefreshFailed { source, .. } => { - format!("{provider} requires re-authentication: {source}") - } - ResolveError::RefreshTokenMissing(_) => { - format!("{provider} requires re-authentication: refresh token missing") - } - ResolveError::SchemeMismatch { scheme, .. } => { - format!("{provider} resolved a secret its `{scheme}` auth scheme cannot use") - } - } -} diff --git a/lib/foundation/fabro-auth/src/extra_headers_source.rs b/lib/foundation/fabro-auth/src/extra_headers_source.rs index 0226e1fee..8b358bdde 100644 --- a/lib/foundation/fabro-auth/src/extra_headers_source.rs +++ b/lib/foundation/fabro-auth/src/extra_headers_source.rs @@ -2,33 +2,35 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; -use lithos_llm::catalog::{Catalog, CatalogProvider, ProviderId}; -use lithos_llm::credentials::{CredentialHeader, Credentials, SecretValue}; +use lithos_llm::catalog::CatalogProvider; +use lithos_llm::credentials::{ + CredentialError, CredentialHeader, CredentialProvider, Credentials, SecretValue, +}; -use crate::ResolveError; -use crate::credential_source::CredentialSource; - -/// Decorates another [`CredentialSource`] by appending fixed extra headers to -/// every HTTP credential it resolves. +/// Decorates another [`CredentialProvider`] by appending fixed extra headers +/// to every HTTP credential it resolves. /// /// Headers already present on a credential (for example from explicit /// provider configuration) are left untouched. AWS-signed credentials carry /// no header list and pass through unchanged. pub struct ExtraHeadersCredentialSource { - inner: Arc, + inner: Arc, headers: HashMap, } impl ExtraHeadersCredentialSource { #[must_use] - pub fn new(inner: Arc, headers: HashMap) -> Self { + pub fn new(inner: Arc, headers: HashMap) -> Self { Self { inner, headers } } } #[async_trait] -impl CredentialSource for ExtraHeadersCredentialSource { - async fn credentials(&self, provider: &CatalogProvider) -> Result { +impl CredentialProvider for ExtraHeadersCredentialSource { + async fn credentials( + &self, + provider: &CatalogProvider, + ) -> Result { let mut credentials = self.inner.credentials(provider).await?; if let Credentials::Http(http) = &mut credentials { for (name, value) in &self.headers { @@ -48,8 +50,8 @@ impl CredentialSource for ExtraHeadersCredentialSource { Ok(credentials) } - async fn configured_providers(&self, catalog: &Catalog) -> Vec { - self.inner.configured_providers(catalog).await + async fn is_configured(&self, provider: &CatalogProvider) -> bool { + self.inner.is_configured(provider).await } } @@ -61,16 +63,16 @@ mod tests { use crate::test_support::test_catalog; struct StubSource { - configured_providers: Vec, - existing_header: Option<(String, String)>, + configured: bool, + existing_header: Option<(String, String)>, } #[async_trait] - impl CredentialSource for StubSource { + impl CredentialProvider for StubSource { async fn credentials( &self, provider: &CatalogProvider, - ) -> Result { + ) -> Result { if provider.id().as_str() == "bedrock" { return Ok(Credentials::AwsDefaultChain { region: None }); } @@ -86,8 +88,8 @@ mod tests { Ok(credentials) } - async fn configured_providers(&self, _catalog: &Catalog) -> Vec { - self.configured_providers.clone() + async fn is_configured(&self, _provider: &CatalogProvider) -> bool { + self.configured } } @@ -107,8 +109,8 @@ mod tests { let catalog = test_catalog(); let source = ExtraHeadersCredentialSource::new( Arc::new(StubSource { - configured_providers: Vec::new(), - existing_header: None, + configured: false, + existing_header: None, }), HashMap::from([("x-session-id".to_string(), "run-123".to_string())]), ); @@ -133,8 +135,8 @@ mod tests { let catalog = test_catalog(); let source = ExtraHeadersCredentialSource::new( Arc::new(StubSource { - configured_providers: vec![ProviderId::new("openai")], - existing_header: Some(("X-Session-Id".to_string(), "configured".to_string())), + configured: true, + existing_header: Some(("X-Session-Id".to_string(), "configured".to_string())), }), HashMap::from([("x-session-id".to_string(), "run-123".to_string())]), ); @@ -143,8 +145,10 @@ mod tests { .await .unwrap(); assert_eq!(header(&credentials, "x-session-id"), Some("configured")); - assert_eq!(source.configured_providers(&catalog).await, vec![ - ProviderId::new("openai") - ]); + assert!( + source + .is_configured(catalog.provider("openai").unwrap()) + .await + ); } } diff --git a/lib/foundation/fabro-auth/src/lib.rs b/lib/foundation/fabro-auth/src/lib.rs index 34cd2590e..878863027 100644 --- a/lib/foundation/fabro-auth/src/lib.rs +++ b/lib/foundation/fabro-auth/src/lib.rs @@ -1,8 +1,16 @@ +//! Fabro's credential stores and login flows over lithos-llm. +//! +//! Every store implements the lithos [`CredentialProvider`] trait, so the +//! client, `credentials::readiness`, and `ClientBuilder::build_ready` read +//! them directly. What is Fabro's own: the vault and the SQL secret store as +//! the place secrets live, the Codex OAuth login and refresh, and the +//! `{{ secrets.NAME }}` interpolation of a provider's `default_headers`. +//! +//! [`CredentialProvider`]: lithos_llm::credentials::CredentialProvider + mod api_key_source; mod context; mod credential; -mod credential_source; -mod error; mod extra_headers_source; mod refresh; mod secrets; @@ -18,8 +26,6 @@ pub mod strategies; pub use api_key_source::ApiKeyCredentialSource; pub use context::{AuthContextRequest, AuthContextResponse}; pub use credential::{OAuthConfig, OAuthCredential, OAuthTokens}; -pub use credential_source::{CredentialSource, ResolvedCredentials, lithos_credentials}; -pub use error::{ResolveError, auth_issue_message}; pub use extra_headers_source::ExtraHeadersCredentialSource; pub use refresh::refresh_oauth_credential; pub use secrets::{accepts_api_key, expected_secret_name, secret_names}; diff --git a/lib/foundation/fabro-auth/src/sql_vault_source.rs b/lib/foundation/fabro-auth/src/sql_vault_source.rs index 6f0c0803b..330eede74 100644 --- a/lib/foundation/fabro-auth/src/sql_vault_source.rs +++ b/lib/foundation/fabro-auth/src/sql_vault_source.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use async_trait::async_trait; use fabro_types::SecretType; use fabro_vault::{SecretSnapshot, SecretStore, SecretStoreError, Vault}; -use lithos_llm::catalog::{Catalog, CatalogProvider, ProviderId}; -use lithos_llm::credentials::Credentials; +use lithos_llm::catalog::{CatalogProvider, ProviderId}; +use lithos_llm::credentials::{CredentialError, CredentialProvider, Credentials}; use tokio::sync::RwLock; use tracing::error; -use crate::credential_source::CredentialSource; -use crate::{EnvLookup, ResolveError, VaultCredentialSource}; +use crate::vault_source::unusable; +use crate::{EnvLookup, VaultCredentialSource}; /// Credentials backed by the SQL secret store. /// @@ -90,11 +90,12 @@ impl SqlVaultCredentialSource { Ok(true) } - fn store_error(provider: &ProviderId, err: SecretStoreError) -> ResolveError { - ResolveError::RefreshFailed { - provider: provider.clone(), - source: anyhow::Error::new(err), - } + fn store_error(provider: &ProviderId, err: SecretStoreError) -> CredentialError { + unusable( + provider, + format!("the secret store could not be read: {err}"), + Some(Box::new(err)), + ) } } @@ -106,8 +107,11 @@ impl std::fmt::Debug for SqlVaultCredentialSource { } #[async_trait] -impl CredentialSource for SqlVaultCredentialSource { - async fn credentials(&self, provider: &CatalogProvider) -> Result { +impl CredentialProvider for SqlVaultCredentialSource { + async fn credentials( + &self, + provider: &CatalogProvider, + ) -> Result { for _ in 0..2 { let before = self .store @@ -134,22 +138,23 @@ impl CredentialSource for SqlVaultCredentialSource { return Ok(credentials); } } - Err(ResolveError::RefreshFailed { - provider: provider.id().clone(), - source: anyhow::anyhow!("OAuth credential changed concurrently during refresh"), - }) + Err(unusable( + provider.id(), + "the OAuth credential changed concurrently during refresh", + None, + )) } - async fn configured_providers(&self, catalog: &Catalog) -> Vec { + async fn is_configured(&self, provider: &CatalogProvider) -> bool { let snapshot = match self.store.snapshot().await { Ok(snapshot) => snapshot, Err(err) => { - error!(error = ?err, "Failed to load configured providers from secret store"); - return Vec::new(); + error!(error = ?err, "Failed to read the secret store while checking a provider"); + return false; } }; self.source_for_snapshot(snapshot) - .configured_providers(catalog) + .is_configured(provider) .await } } diff --git a/lib/foundation/fabro-auth/src/test_support.rs b/lib/foundation/fabro-auth/src/test_support.rs index 0b12e04d7..4030d87c0 100644 --- a/lib/foundation/fabro-auth/src/test_support.rs +++ b/lib/foundation/fabro-auth/src/test_support.rs @@ -9,9 +9,9 @@ use std::sync::Arc; use fabro_vault::Vault; use lithos_llm::catalog::Catalog; +use lithos_llm::credentials::CredentialProvider; use tokio::sync::RwLock as AsyncRwLock; -use crate::credential_source::CredentialSource; use crate::vault_source::VaultCredentialSource; /// The lithos built-in catalog. @@ -45,7 +45,7 @@ pub fn empty_vault() -> Arc> { /// Tests that inject fake provider keys use this instead of reading the real /// process environment, which would make them order-dependent. #[must_use] -pub fn env_credential_source(env_lookup: F) -> Arc +pub fn env_credential_source(env_lookup: F) -> Arc where F: Fn(&str) -> Option + Send + Sync + 'static, { @@ -57,6 +57,6 @@ where /// A vault-backed source over an empty vault with no process-env fallback. #[must_use] -pub fn vault_only_credential_source() -> Arc { +pub fn vault_only_credential_source() -> Arc { Arc::new(VaultCredentialSource::vault_only(empty_vault())) } diff --git a/lib/foundation/fabro-auth/src/vault_source.rs b/lib/foundation/fabro-auth/src/vault_source.rs index f99654d91..c1bef4db0 100644 --- a/lib/foundation/fabro-auth/src/vault_source.rs +++ b/lib/foundation/fabro-auth/src/vault_source.rs @@ -14,6 +14,7 @@ //! never reaches the wire; //! - OpenAI organization and project headers from the environment. +use std::error::Error as StdError; use std::sync::Arc; use async_trait::async_trait; @@ -21,7 +22,7 @@ use fabro_static::EnvVars; use fabro_types::provider_ids; use fabro_types::settings::{InterpString, ResolveCtx}; use fabro_vault::{SecretType, Vault}; -use lithos_llm::catalog::{AuthScheme, Catalog, CatalogProvider, ProviderId}; +use lithos_llm::catalog::{CatalogProvider, ProviderId}; use lithos_llm::credentials::{ ConventionalCredentials, CredentialError, CredentialHeader, CredentialProvider, Credentials, HttpAuthentication, HttpCredentials, SecretValue, @@ -30,8 +31,6 @@ use tokio::sync::RwLock as AsyncRwLock; use tokio::task::spawn_blocking; use crate::credential::OAuthCredential; -use crate::credential_source::CredentialSource; -use crate::error::ResolveError; use crate::refresh::refresh_oauth_credential; use crate::secrets::oauth_secret_name; use crate::vault_ext::{VaultLookupError, vault_get_oauth, vault_set_oauth, vault_token_lookup}; @@ -106,7 +105,7 @@ impl VaultCredentialSource { &self, provider: &CatalogProvider, vault: &Vault, - ) -> Result, ResolveError> { + ) -> Result, CredentialError> { let Some(name) = oauth_secret_name(provider.id()) else { return Ok(None); }; @@ -124,13 +123,20 @@ impl VaultCredentialSource { .expect("entry is present"); let credential = if credential.needs_refresh() { if credential.tokens.refresh_token.is_none() { - return Err(ResolveError::RefreshTokenMissing(provider.id().clone())); + return Err(unusable( + provider.id(), + "requires re-authentication: refresh token missing", + None, + )); } let refreshed = refresh_oauth_credential(&credential) .await - .map_err(|source| ResolveError::RefreshFailed { - provider: provider.id().clone(), - source, + .map_err(|source| { + unusable( + provider.id(), + format!("requires re-authentication: {source}"), + Some(source.into()), + ) })?; self.persist_oauth(provider.id(), name, &refreshed).await?; refreshed @@ -145,13 +151,16 @@ impl VaultCredentialSource { provider: &ProviderId, name: &str, refreshed: &OAuthCredential, - ) -> Result<(), ResolveError> { + ) -> Result<(), CredentialError> { let refreshed = refreshed.clone(); let name = name.to_string(); let vault = Arc::clone(&self.vault); - let failed = |source| ResolveError::RefreshFailed { - provider: provider.clone(), - source, + let failed = |source: anyhow::Error| { + unusable( + provider, + format!("the refreshed token could not be stored: {source}"), + Some(source.into()), + ) }; spawn_blocking(move || { let mut vault = vault.blocking_write(); @@ -204,30 +213,25 @@ impl std::fmt::Debug for VaultCredentialSource { } #[async_trait] -impl CredentialSource for VaultCredentialSource { - async fn credentials(&self, provider: &CatalogProvider) -> Result { +impl CredentialProvider for VaultCredentialSource { + async fn credentials( + &self, + provider: &CatalogProvider, + ) -> Result { let vault = self.snapshot().await; let interpolated = interpolated_headers(&vault, provider)?; if let Some(oauth) = self.oauth_credentials(provider, &vault).await? { return Ok(self.decorate(provider, oauth, interpolated)); } - let credentials = self - .conventional(&vault) - .credentials(provider) - .await - .map_err(|err| resolve_error(provider, &err))?; + let credentials = self.conventional(&vault).credentials(provider).await?; Ok(self.decorate(provider, credentials, interpolated)) } - async fn configured_providers(&self, catalog: &Catalog) -> Vec { + /// Presence without refresh: an OAuth entry under the provider's name, or + /// a conventional secret that resolves. + async fn is_configured(&self, provider: &CatalogProvider) -> bool { let vault = self.snapshot().await; - let mut configured = Vec::new(); - for provider in catalog.providers().filter(|provider| provider.is_enabled()) { - if self.has_credential_material(&vault, provider).await { - configured.push(provider.id().clone()); - } - } - configured + self.has_credential_material(&vault, provider).await } } @@ -254,7 +258,7 @@ fn oauth_bearer(credential: &OAuthCredential) -> Credentials { pub(crate) fn interpolated_headers( vault: &Vault, provider: &CatalogProvider, -) -> Result, ResolveError> { +) -> Result, CredentialError> { let mut ctx = ResolveCtx::new().with_secrets(|secret_name| vault_token_lookup(vault, secret_name)); provider @@ -263,55 +267,42 @@ pub(crate) fn interpolated_headers( .map(|(name, source)| (name, InterpString::parse(source))) .filter(|(_, template)| !template.is_literal()) .map(|(name, template)| { - let value = - template - .resolve_with(&mut ctx) - .map_err(|source| ResolveError::Interpolation { - provider: provider.id().clone(), - source, - })?; + let value = template.resolve_with(&mut ctx).map_err(|source| { + unusable( + provider.id(), + format!("header interpolation failed: {source}"), + Some(Box::new(source)), + ) + })?; Ok(CredentialHeader::new(name.clone(), SecretValue::new(value))) }) .collect() } -pub(crate) fn auth_scheme_name(scheme: &AuthScheme) -> &'static str { - match scheme { - AuthScheme::None => "none", - AuthScheme::Bearer { .. } => "bearer", - AuthScheme::Header { .. } => "header", - AuthScheme::Headers => "headers", - AuthScheme::Aws { .. } => "aws", - AuthScheme::BedrockBearer => "bedrock_bearer", - _ => "unknown", +/// Material is present but cannot be used. `reason` is the operator-facing +/// line and must not carry secret content. +pub(crate) fn unusable( + provider: &ProviderId, + reason: impl Into, + source: Option>, +) -> CredentialError { + CredentialError::Unusable { + provider: provider.clone(), + reason: reason.into(), + source, } } -/// Maps a lithos lookup failure onto Fabro's vocabulary. A missing secret is -/// not an issue to report; the provider is simply not configured. -pub(crate) fn resolve_error(provider: &CatalogProvider, err: &CredentialError) -> ResolveError { - match err { - CredentialError::SchemeMismatch { .. } => ResolveError::SchemeMismatch { - provider: provider.id().clone(), - scheme: auth_scheme_name(provider.auth()).to_string(), - }, - _ => ResolveError::NotConfigured(provider.id().clone()), - } -} - -fn vault_lookup_error(provider: &ProviderId, name: &str, err: VaultLookupError) -> ResolveError { - match err { - VaultLookupError::SchemaMismatch { actual, .. } => ResolveError::VaultSchemaMismatch { - provider: provider.clone(), - name: name.to_string(), - actual, - }, - VaultLookupError::DecodeFailed { source, .. } => ResolveError::VaultDecodeFailed { - provider: provider.clone(), - name: name.to_string(), - source, - }, - } +fn vault_lookup_error(provider: &ProviderId, name: &str, err: VaultLookupError) -> CredentialError { + let reason = match &err { + VaultLookupError::SchemaMismatch { actual, .. } => { + format!("vault credential '{name}' has schema {actual:?}, expected Token or Oauth") + } + VaultLookupError::DecodeFailed { .. } => { + format!("vault credential '{name}' is not valid OAuth JSON") + } + }; + unusable(provider, reason, Some(Box::new(err))) } #[cfg(test)] @@ -322,12 +313,13 @@ mod tests { use httpmock::Method::POST; use httpmock::MockServer; use lithos_llm::catalog::Catalog; + use lithos_llm::credentials::readiness; use super::*; + use crate::OPENAI_CODEX_VAULT_SECRET_NAME; use crate::credential::{OAuthConfig, OAuthTokens}; use crate::test_support::{test_catalog, test_catalog_with_overlay}; use crate::vault_ext::vault_set_token; - use crate::{OPENAI_CODEX_VAULT_SECRET_NAME, auth_issue_message}; fn oauth_credential(token_url: String, expires_at: chrono::DateTime) -> OAuthCredential { OAuthCredential { @@ -488,7 +480,10 @@ api_model = "large" .credentials(catalog.provider("gateway").unwrap()) .await .unwrap_err(); - assert!(matches!(err, ResolveError::Interpolation { .. }), "{err}"); + assert!( + matches!(&err, CredentialError::Unusable { reason, .. } if reason.contains("header interpolation failed")), + "{err}" + ); assert!(!err.to_string().contains("gw-key")); } @@ -565,10 +560,8 @@ api_model = "large" .credentials(catalog.provider("anthropic").unwrap()) .await .unwrap_err(); - assert!(matches!( - err, - ResolveError::NotConfigured(provider) if provider.as_str() == "anthropic" - )); + assert!(err.is_not_configured(), "{err}"); + assert_eq!(err.provider().as_str(), "anthropic"); } #[tokio::test] @@ -578,10 +571,13 @@ api_model = "large" let source = source_with(vault, |_| None); let catalog = test_catalog(); let modal = catalog.provider("modal").unwrap(); - assert!(matches!( - source.credentials(modal).await.unwrap_err(), - ResolveError::NotConfigured(_) - )); + assert!( + source + .credentials(modal) + .await + .unwrap_err() + .is_not_configured() + ); let mut vault = empty_vault(); vault_set_token(&mut vault, "MODAL_TOKEN_ID", "wk-test").unwrap(); @@ -592,6 +588,16 @@ api_model = "large" assert_eq!(header_value(&credentials, "Modal-Secret"), Some("ws-test")); } + async fn configured(source: &VaultCredentialSource, catalog: &Catalog) -> Vec { + let mut ids = Vec::new(); + for provider in catalog.enabled_providers() { + if source.is_configured(provider).await { + ids.push(provider.id().clone()); + } + } + ids + } + #[tokio::test] async fn configured_providers_reads_vault_and_env_without_refreshing() { let mut vault = empty_vault(); @@ -609,7 +615,7 @@ api_model = "large" (name == "ANTHROPIC_API_KEY").then(|| "env".to_string()) }); let catalog = test_catalog(); - let configured = source.configured_providers(&catalog).await; + let configured = configured(&source, &catalog).await; assert!(configured.contains(&ProviderId::new("openai"))); assert!(configured.contains(&ProviderId::new("anthropic"))); assert!(configured.contains(&ProviderId::new("openai-codex"))); @@ -632,13 +638,16 @@ api_model = "large" .unwrap(); vault_set_token(&mut vault, "ANTHROPIC_API_KEY", "anthropic-key").unwrap(); let source = source_with(vault, |_| None); - let resolved = source.resolve_all(&test_catalog()).await; + let catalog = test_catalog(); + let resolved = readiness(catalog.enabled_providers(), &source).await; assert_eq!(resolved.ready, vec![ProviderId::new("anthropic")]); - assert_eq!(resolved.auth_issues.len(), 1); - assert!(matches!( - &resolved.auth_issues[0].1, - ResolveError::RefreshFailed { provider, .. } if provider.as_str() == "openai-codex" - )); + assert_eq!(resolved.issues.len(), 1); + let (provider, issue) = &resolved.issues[0]; + assert_eq!(provider.as_str(), "openai-codex"); + assert!( + issue.to_string().contains("requires re-authentication"), + "{issue}" + ); } #[tokio::test] @@ -646,10 +655,10 @@ api_model = "large" let catalog = test_catalog(); let vault_only = VaultCredentialSource::vault_only(Arc::new(AsyncRwLock::new(empty_vault()))); - assert!(vault_only.configured_providers(&catalog).await.is_empty()); - let resolved = vault_only.resolve_all(&catalog).await; + assert!(configured(&vault_only, &catalog).await.is_empty()); + let resolved = readiness(catalog.enabled_providers(), &vault_only).await; assert!(resolved.ready.is_empty()); - assert!(resolved.auth_issues.is_empty()); + assert!(resolved.issues.is_empty()); } #[tokio::test] @@ -722,10 +731,9 @@ api_model = "large" .credentials(catalog.provider("openai-codex").unwrap()) .await .unwrap_err(); - assert!(matches!(err, ResolveError::RefreshTokenMissing(_))); assert_eq!( - auth_issue_message(&ProviderId::new("openai-codex"), &err), - "openai-codex requires re-authentication: refresh token missing" + err.to_string(), + "credentials for provider openai-codex cannot be used: requires re-authentication: refresh token missing" ); } } From 3a998196ff768d1480ba151a5b1feb80aefcf63a Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:26:40 -0600 Subject: [PATCH 3/7] Answer catalog questions with the lithos catalog queries fabro-llm's catalog module held some 250 lines of listing and picking helpers over lithos data: enabled and listed providers, model lookup by id, alias, or wire id, matches ranked as the resolver ranks, default and probe models, the small utility model across ready providers, the nearest model on another provider, and cost by handle. lithos-llm now answers all of those on `Catalog` and `CatalogProvider` through `Offering`, so the helpers and the `ModelEntry` wrapper go. What stays in Fabro's catalog module is its own: building the catalog from the operator overlay, and reading the agent harness and `reasoning_by_default` from the shared `metadata.agent` namespace. The passthrough selection policy in `selection.rs` keeps its rules and calls lithos for the lookups. Co-Authored-By: Claude Fable 5.1 --- lib/apps/fabro-cli/src/commands/exec.rs | 8 +- lib/apps/fabro-cli/src/commands/install.rs | 10 +- .../fabro-cli/src/shared/provider_auth.rs | 6 +- lib/apps/fabro-server/src/diagnostics.rs | 9 +- lib/apps/fabro-server/src/install.rs | 4 +- lib/apps/fabro-server/src/run_compiler.rs | 3 +- lib/apps/fabro-server/src/run_manifest.rs | 8 +- lib/apps/fabro-server/src/server.rs | 6 +- .../fabro-server/src/server/handler/models.rs | 8 +- .../src/server/handler/pull_requests.rs | 9 +- .../fabro-server/src/server/handler/runs.rs | 8 +- .../src/server/handler/sessions.rs | 31 +- lib/apps/fabro-server/src/server/tests.rs | 24 +- lib/apps/fabro-server/src/test_support.rs | 3 +- lib/apps/fabro-server/tests/it/api/runs.rs | 5 +- .../fabro-agent/src/agent_profile.rs | 13 +- lib/components/fabro-agent/src/cli.rs | 48 +- lib/components/fabro-agent/src/compaction.rs | 14 +- .../fabro-agent/tests/it/guardrails.rs | 6 +- lib/components/fabro-llm/src/api.rs | 21 +- lib/components/fabro-llm/src/catalog.rs | 428 +++--------------- lib/components/fabro-llm/src/lib.rs | 3 +- lib/components/fabro-llm/src/probe.rs | 7 +- lib/components/fabro-llm/src/selection.rs | 49 +- .../fabro-validate/src/rules/model_support.rs | 8 +- .../fabro-workflow/src/handler/llm/api.rs | 28 +- .../fabro-workflow/src/handler/llm/routing.rs | 23 +- .../fabro-workflow/src/model_fallback.rs | 31 +- .../fabro-workflow/src/operations/create.rs | 3 +- .../fabro-workflow/src/operations/start.rs | 3 +- lib/components/fabro-workflow/src/outcome.rs | 5 +- .../src/pipeline/pull_request.rs | 2 +- .../fabro-workflow/src/test_support.rs | 3 +- .../src/transforms/model_resolution.rs | 4 +- 34 files changed, 299 insertions(+), 542 deletions(-) diff --git a/lib/apps/fabro-cli/src/commands/exec.rs b/lib/apps/fabro-cli/src/commands/exec.rs index 1363f3185..535e94332 100644 --- a/lib/apps/fabro-cli/src/commands/exec.rs +++ b/lib/apps/fabro-cli/src/commands/exec.rs @@ -9,7 +9,7 @@ use fabro_agent::cli::{ }; use fabro_llm::gateway::{GatewayAdapter, GatewayError, GatewayTransport}; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{ErrorFacts, ErrorKind, catalog}; +use fabro_llm::{ErrorFacts, ErrorKind}; use fabro_mcp::config::McpServerSettings; use fabro_types::ProviderId; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; @@ -143,8 +143,10 @@ pub(crate) async fn execute(mut args: ExecArgs, ctx: &CommandContext) -> AnyResu .clone() .unwrap_or_else(|| "anthropic".to_string()); let catalog = ctx.catalog()?; - let provider_id = catalog::canonical_provider_id(&catalog, &provider_name) - .unwrap_or_else(|| ProviderId::new(provider_name.as_str())); + let provider_id = catalog.enabled_provider(&provider_name).map_or_else( + || ProviderId::new(provider_name.as_str()), + |provider| provider.id().clone(), + ); let server_client = server_client::connect_server_target(&target).await?; let adapter = Arc::new(GatewayAdapter::new(Box::new( ServerCompletionTransport::new(server_client), diff --git a/lib/apps/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs index f7817bf2f..11d88f15d 100644 --- a/lib/apps/fabro-cli/src/commands/install.rs +++ b/lib/apps/fabro-cli/src/commands/install.rs @@ -34,7 +34,6 @@ use fabro_install::{ restore_optional_file, rollback_dev_token_write, seed_environments_in_storage, write_github_app_settings, write_token_settings, }; -use fabro_llm::catalog; use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_server::serve; use fabro_store::ArtifactStore; @@ -82,7 +81,8 @@ fn supports_install_api_key(provider: &CatalogProvider) -> bool { } fn install_llm_provider_ids(catalog: &Catalog) -> Vec { - catalog::listed_providers(catalog) + catalog + .listed_providers() .into_iter() .filter(|provider| supports_install_api_key(provider)) .map(|provider| provider.id().clone()) @@ -90,14 +90,16 @@ fn install_llm_provider_ids(catalog: &Catalog) -> Vec { } fn provider_env_var_label(provider: &ProviderId, catalog: &Catalog) -> String { - catalog::provider(catalog, provider.as_str()) + catalog + .enabled_provider(provider.as_str()) .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()) + catalog + .enabled_provider(provider.as_str()) .and_then(fabro_auth::expected_secret_name) .unwrap_or_else(|| format!("{}_API_KEY", provider.to_string().to_uppercase())) } diff --git a/lib/apps/fabro-cli/src/shared/provider_auth.rs b/lib/apps/fabro-cli/src/shared/provider_auth.rs index 6de3d0652..f9a2872b3 100644 --- a/lib/apps/fabro-cli/src/shared/provider_auth.rs +++ b/lib/apps/fabro-cli/src/shared/provider_auth.rs @@ -18,7 +18,6 @@ use fabro_auth::{ AuthContextRequest, AuthContextResponse, AuthMethod, LoginResult, codex_oauth_config, strategy_for, }; -use fabro_llm::catalog; use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; use fabro_types::{ProviderId, provider_ids}; @@ -59,7 +58,7 @@ fn default_catalog_for_provider_auth() -> Arc { } pub(crate) fn provider_display_name(provider: &ProviderId, catalog: &Catalog) -> String { - catalog::provider(catalog, provider.as_str()).map_or_else( + catalog.enabled_provider(provider.as_str()).map_or_else( || provider.to_string(), |provider| provider.display_name().to_string(), ) @@ -69,7 +68,8 @@ fn api_key_catalog_provider<'a>( provider: &ProviderId, catalog: &'a Catalog, ) -> Result<&'a CatalogProvider> { - let provider = catalog::provider(catalog, provider.as_str()) + let provider = catalog + .enabled_provider(provider.as_str()) .with_context(|| format!("provider '{provider}' is not configured in the model catalog"))?; anyhow::ensure!( fabro_auth::accepts_api_key(provider), diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index 0397c6c11..ff6edc66f 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -5,9 +5,9 @@ use std::time::Duration; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_http::Response; -use fabro_llm::lithos_catalog::Catalog; +use fabro_llm::Client; +use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::probe::{self, ModelTestStatus}; -use fabro_llm::{Client, catalog}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; use fabro_static::EnvVars; @@ -249,7 +249,10 @@ async fn probe_single_provider( return provider_probe_error(provider, None, message, None); } - let Some(model) = catalog::probe_model(catalog, provider.as_str()) else { + let Some(model) = catalog + .enabled_provider(provider.as_str()) + .and_then(CatalogProvider::probe_offering) + else { return provider_probe_error( provider, None, diff --git a/lib/apps/fabro-server/src/install.rs b/lib/apps/fabro-server/src/install.rs index 63cc592b1..f91a28463 100644 --- a/lib/apps/fabro-server/src/install.rs +++ b/lib/apps/fabro-server/src/install.rs @@ -24,7 +24,6 @@ use fabro_install::{ write_github_app_settings, write_object_store_settings, write_sandbox_settings, write_token_settings, }; -use fabro_llm::catalog as llm_catalog; use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; use fabro_sandbox::daytona; @@ -846,7 +845,8 @@ async fn put_install_llm( } fn install_catalog_provider(provider: &ProviderId) -> Result<&'static CatalogProvider, String> { - let catalog_provider = llm_catalog::provider(&INSTALL_CATALOG, provider.as_str()) + let catalog_provider = INSTALL_CATALOG + .enabled_provider(provider.as_str()) .ok_or_else(|| format!("provider '{provider}' is not configured in the model catalog"))?; if fabro_auth::accepts_api_key(catalog_provider) { Ok(catalog_provider) diff --git a/lib/apps/fabro-server/src/run_compiler.rs b/lib/apps/fabro-server/src/run_compiler.rs index cfcbe14d5..5e85741f3 100644 --- a/lib/apps/fabro-server/src/run_compiler.rs +++ b/lib/apps/fabro-server/src/run_compiler.rs @@ -765,7 +765,8 @@ mod tests { } fn test_provider_ids() -> Vec { - fabro_llm::catalog::enabled_provider_ids(&fabro_llm::test_support::test_catalog()) + fabro_llm::test_support::test_catalog() + .enabled_provider_ids() .into_iter() .collect() } diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index dcef70c8f..fcc9c42cc 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -14,9 +14,9 @@ use fabro_config::{ use fabro_github::token_source::{InstallationTokenSource, ResolvedToken, TokenSnapshot}; use fabro_graphviz::graph::{Graph, is_llm_handler_type}; use fabro_graphviz::render::apply_direction; +use fabro_llm::FabroClient; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::probe::{self, ModelTestStatus}; -use fabro_llm::{FabroClient, catalog}; use fabro_sandbox::daytona::DaytonaConfig; use fabro_sandbox::from_environment::{ daytona_config_from_environment, docker_config_from_environment, @@ -1209,8 +1209,10 @@ async fn run_llm_check( } fn canonical_provider_id(catalog: &Catalog, provider_name: &str) -> ProviderId { - catalog::canonical_provider_id(catalog, provider_name) - .unwrap_or_else(|| ProviderId::new(provider_name)) + catalog.enabled_provider(provider_name).map_or_else( + || ProviderId::new(provider_name), + |provider| provider.id().clone(), + ) } async fn run_github_token_check( diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index 6911f9c88..da28e8865 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -59,7 +59,7 @@ use fabro_interview::{ }; use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{ClientOptions, FabroClient, catalog}; +use fabro_llm::{ClientOptions, FabroClient}; use fabro_mcp_store::McpServerStore; use fabro_redact::redact_jsonl_line; use fabro_sandbox::daytona::{self, DaytonaSandbox}; @@ -1446,8 +1446,8 @@ impl AppState { let default_model = if provider_ids.is_empty() { None } else { - let ready = provider_ids.iter().cloned().collect::>(); - catalog::default_for_ready(&self.catalog(), &ready) + self.catalog() + .default_offering_for(&provider_ids) .map(|entry| entry.model.id().to_string()) }; AskFabroReadiness { default_model } diff --git a/lib/apps/fabro-server/src/server/handler/models.rs b/lib/apps/fabro-server/src/server/handler/models.rs index 544721f6d..10f3ae110 100644 --- a/lib/apps/fabro-server/src/server/handler/models.rs +++ b/lib/apps/fabro-server/src/server/handler/models.rs @@ -4,7 +4,7 @@ use std::time::Duration; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; -use fabro_llm::{ModelSelectionError, api, catalog, selection}; +use fabro_llm::{ModelSelectionError, api, selection}; use fabro_redact::redact_string; use fabro_types::ReasoningEffort; @@ -59,8 +59,10 @@ async fn list_models( let catalog = state.catalog(); // An unknown provider filter matches nothing rather than erroring. let provider_id = params.provider.as_deref().map(|selector| { - catalog::canonical_provider_id(&catalog, selector) - .unwrap_or_else(|| ProviderId::new(selector)) + catalog.enabled_provider(selector).map_or_else( + || ProviderId::new(selector), + |provider| provider.id().clone(), + ) }); let query = params.query.as_ref().map(|value| value.to_lowercase()); diff --git a/lib/apps/fabro-server/src/server/handler/pull_requests.rs b/lib/apps/fabro-server/src/server/handler/pull_requests.rs index 46b088d76..50b473c6b 100644 --- a/lib/apps/fabro-server/src/server/handler/pull_requests.rs +++ b/lib/apps/fabro-server/src/server/handler/pull_requests.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use std::time::Duration; use axum::http::{HeaderValue, header}; -use fabro_llm::catalog; use super::super::{ ApiError, AppState, CloseRunPullRequestResponse, CreateRunPullRequestRequest, IntoResponse, @@ -344,12 +343,8 @@ async fn create_run_pull_request( model } else { let catalog = state.catalog(); - let configured = state - .ready_llm_provider_ids() - .await - .into_iter() - .collect::>(); - match catalog::default_for_ready(&catalog, &configured) { + let configured = state.ready_llm_provider_ids().await; + match catalog.default_offering_for(&configured) { Some(entry) => entry.model.id().to_string(), None => { return ApiError::bad_request("no LLM model is available for PR generation") diff --git a/lib/apps/fabro-server/src/server/handler/runs.rs b/lib/apps/fabro-server/src/server/handler/runs.rs index 4b4f28612..1e52514ee 100644 --- a/lib/apps/fabro-server/src/server/handler/runs.rs +++ b/lib/apps/fabro-server/src/server/handler/runs.rs @@ -21,7 +21,7 @@ use fabro_api::types::{ use fabro_config::{CliLayer, RunLayer, Storage, project}; use fabro_environment::{DEFAULT_ENVIRONMENT_ID, EnvironmentId}; use fabro_interview::AnswerSubmission; -use fabro_llm::{Client as LlmClient, catalog}; +use fabro_llm::Client as LlmClient; use fabro_manifest::RunOverrideInput; use fabro_static::EnvVars; use fabro_store::{ @@ -953,11 +953,7 @@ async fn finalize_created_run( let workflow = run_title_generation::workflow_summary(&run_spec.graph); let run_inputs = run_spec.settings.run.inputs.clone(); let title_catalog = state.catalog(); - let ready = ready_provider_ids - .iter() - .cloned() - .collect::>(); - if let Some(title_model) = catalog::small_default_for_ready(&title_catalog, &ready) { + if let Some(title_model) = title_catalog.small_default_for(&ready_provider_ids) { spawn_generated_title_task(GeneratedTitleTask { state: Arc::clone(&state), run_id: created.run_id, diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index 35203846b..bce2e83fe 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -829,7 +829,7 @@ fn canonical_session_model( ) -> Result<(ProviderId, String), ApiError> { let explicit_provider = explicit_provider .map(|provider| { - catalog::canonical_provider_id(catalog, provider.as_str()).ok_or_else(|| { + enabled_provider_id(catalog, provider.as_str()).ok_or_else(|| { session_selection_error(&ModelSelectionError::UnknownProvider { provider: provider.to_string(), }) @@ -849,7 +849,10 @@ fn canonical_session_model( // An aggregator's wire id (`openai/gpt-5.6-sol` on OpenRouter) is matched // whole on a pinned provider before its prefix is read as a provider. if let Some(explicit) = explicit_provider.as_ref().filter(|p| eligible.contains(*p)) { - if let Some(entry) = catalog::model_on_provider(catalog, explicit.as_str(), requested) { + if let Some(entry) = catalog + .enabled_provider(explicit.as_str()) + .and_then(|provider| provider.offering(requested)) + { return Ok((explicit.clone(), entry.model.id().to_string())); } } @@ -859,7 +862,7 @@ fn canonical_session_model( .qualify(catalog); let (qualified_provider, selector) = match model_ref { SettingsModelRef::Qualified { provider, selector } => { - let provider = catalog::canonical_provider_id(catalog, &provider).ok_or_else(|| { + let provider = enabled_provider_id(catalog, &provider).ok_or_else(|| { session_selection_error(&ModelSelectionError::UnknownProvider { provider }) })?; // When the prefixed provider is not ready, the whole string may @@ -880,8 +883,8 @@ fn canonical_session_model( (Some(provider), selector) } SettingsModelRef::Bare(selector) => { - if explicit_provider.is_none() && catalog::is_provider_selector(catalog, &selector) { - let detail = if catalog::is_model_selector(catalog, &selector) { + if explicit_provider.is_none() && catalog.enabled_provider(&selector).is_some() { + let detail = if catalog.is_model_selector(&selector) { format!( "Session model reference '{selector}' is ambiguous between a provider and \ a model selector; supply `provider` or use `provider:model`." @@ -908,17 +911,25 @@ fn api_model_on_eligible( api_model: &str, eligible: &std::collections::HashSet, ) -> Option<(ProviderId, String)> { - catalog::enabled_providers(catalog) + catalog + .enabled_providers() .into_iter() .filter(|provider| eligible.contains(provider.id())) .find_map(|provider| { - catalog::provider_models(provider) - .into_iter() + provider + .offerings() .find(|model| model.model.api_model() == api_model) .map(|model| (provider.id().clone(), model.model.id().to_string())) }) } +/// The catalog id of an enabled provider named by id or alias. +fn enabled_provider_id(catalog: &Catalog, selector: &str) -> Option { + catalog + .enabled_provider(selector) + .map(|provider| provider.id().clone()) +} + fn session_selection_error(error: &ModelSelectionError) -> ApiError { ApiError::bad_request(error.to_string()) } @@ -1705,7 +1716,7 @@ enabled = true assert_eq!( canonical_session_model( &catalog, - &fabro_llm::catalog::enabled_provider_ids(&catalog), + &catalog.enabled_provider_ids().into_iter().collect(), Some("openrouter:gpt-56-sol"), None, ) @@ -1719,7 +1730,7 @@ enabled = true let catalog = portable_session_catalog(); let error = canonical_session_model( &catalog, - &fabro_llm::catalog::enabled_provider_ids(&catalog), + &catalog.enabled_provider_ids().into_iter().collect(), Some("openrouter:gpt-56-sol"), Some(&fabro_types::provider_ids::openai()), ) diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index e753cfe28..317451efa 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -9322,16 +9322,17 @@ async fn list_providers_marks_configured_per_provider_and_omits_secrets() { // `model_count` and `default_model` must reflect the catalog truth for // 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"), - ) - .len(); + let anthropic_provider = catalog + .enabled_provider("anthropic") + .expect("anthropic should be listed"); + let expected_model_count = anthropic_provider.offerings().len(); assert_eq!( anthropic["model_count"].as_u64(), Some(expected_model_count as u64), "anthropic model_count should match the catalog" ); - let expected_default = fabro_llm::catalog::default_model(&catalog, "anthropic") + let expected_default = anthropic_provider + .default_offering() .expect("anthropic should have a catalog default model"); assert_eq!( anthropic["default_model"].as_str(), @@ -11387,12 +11388,13 @@ async fn pull_request_creation_returns_the_active_durable_request() { .await .into_iter() .collect::>(); - let expected_default_model = - fabro_llm::catalog::default_for_ready(&state.catalog(), &configured_provider_ids) - .expect("a ready provider should have a default model") - .model - .id() - .to_string(); + let expected_default_model = state + .catalog() + .default_offering_for(&configured_provider_ids) + .expect("a ready provider should have a default model") + .model + .id() + .to_string(); let request_body = json!({ "force": false, "model": null diff --git a/lib/apps/fabro-server/src/test_support.rs b/lib/apps/fabro-server/src/test_support.rs index 3b78dc2ef..b3743aaeb 100644 --- a/lib/apps/fabro-server/src/test_support.rs +++ b/lib/apps/fabro-server/src/test_support.rs @@ -18,7 +18,6 @@ use fabro_config::user::default_storage_dir; use fabro_config::{LlmLayer, RunLayer, ServerSettingsBuilder, Storage, envfile}; use fabro_db::DbPool; use fabro_interview::Interviewer; -use fabro_llm::catalog; use fabro_llm::lithos_catalog::Catalog; use fabro_sandbox::SandboxProviderRegistry; use fabro_static::EnvVars; @@ -67,7 +66,7 @@ pub(crate) fn test_run_materialization_provider_ids( let assume_ready = process_env_var(FABRO_TEST_ASSUME_LLM_READY) .is_some_and(|value| !matches!(value.as_str(), "" | "0" | "false" | "no")); if assume_ready { - catalog::enabled_provider_ids(catalog).into_iter().collect() + catalog.enabled_provider_ids().into_iter().collect() } else { ready_provider_ids.to_vec() } diff --git a/lib/apps/fabro-server/tests/it/api/runs.rs b/lib/apps/fabro-server/tests/it/api/runs.rs index c3167a7a7..05f887387 100644 --- a/lib/apps/fabro-server/tests/it/api/runs.rs +++ b/lib/apps/fabro-server/tests/it/api/runs.rs @@ -1,5 +1,6 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; +use fabro_llm::lithos_catalog::CatalogProvider; use fabro_types::settings::run::EnvironmentProvider; use tower::ServiceExt; @@ -161,7 +162,9 @@ _version = 1 "sandbox_not_ready" ); let catalog = fabro_llm::test_support::test_catalog(); - let default_openai_model = fabro_llm::catalog::default_model(&catalog, "openai") + let default_openai_model = catalog + .enabled_provider("openai") + .and_then(CatalogProvider::default_offering) .expect("the built-in OpenAI provider should have a default model"); assert_eq!( created["ask_fabro"]["default_model"].as_str(), diff --git a/lib/components/fabro-agent/src/agent_profile.rs b/lib/components/fabro-agent/src/agent_profile.rs index a8021f886..52a9ae2e2 100644 --- a/lib/components/fabro-agent/src/agent_profile.rs +++ b/lib/components/fabro-agent/src/agent_profile.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use fabro_llm::catalog::{self, ModelEntry}; -use fabro_llm::lithos_catalog::Catalog; +use fabro_llm::catalog; +use fabro_llm::lithos_catalog::{Catalog, Offering}; use fabro_types::{AgentProfileKind, ProviderId, ToolDefinition}; use crate::profiles::EnvContext; @@ -44,9 +44,10 @@ pub trait AgentProfile: Send + Sync { } /// The catalog row for this profile's route, when the catalog knows it. - fn catalog_model(&self) -> Option> { - let catalog = self.catalog()?; - catalog::model_on_provider(catalog, self.provider_id().as_str(), self.model()) + fn catalog_model(&self) -> Option> { + self.catalog()? + .enabled_provider(self.provider_id().as_str())? + .offering(self.model()) } fn context_window_size(&self) -> usize { @@ -65,7 +66,7 @@ pub trait AgentProfile: Send + Sync { fn reasons_by_default(&self) -> bool { self.catalog_model() - .is_some_and(|entry| entry.reasons_by_default()) + .is_some_and(|entry| catalog::reasons_by_default(&entry)) } fn register_subagent_tools( diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index a677ceacf..51aa41db7 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -13,7 +13,7 @@ use fabro_auth::SqlVaultCredentialSource; use fabro_config::Storage; use fabro_config::user::default_storage_dir; use fabro_llm::credentials::CredentialProvider; -use fabro_llm::lithos_catalog::Catalog; +use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::middleware::{Call, Middleware, Next, Output}; use fabro_llm::{Client, ClientOptions, Error as LlmError, catalog}; use fabro_mcp::config::McpServerSettings; @@ -193,14 +193,18 @@ fn summarizer_model_id( catalog: &Catalog, selected_model: &str, ) -> ModelHandle { - let model = - catalog::small_default_for_ready(catalog, &std::iter::once(provider_id.clone()).collect()) - .filter(|entry| entry.provider.id() == provider_id) - .or_else(|| catalog::default_model(catalog, provider_id.as_str())) - .map_or_else( - || selected_model.to_string(), - |entry| entry.model.id().to_string(), - ); + let model = catalog + .small_default_for([provider_id]) + .filter(|entry| entry.provider.id() == provider_id) + .or_else(|| { + catalog + .enabled_provider(provider_id.as_str())? + .default_offering() + }) + .map_or_else( + || selected_model.to_string(), + |entry| entry.model.id().to_string(), + ); ModelHandle::new(provider_id.clone(), ModelId::new(model)) } @@ -227,12 +231,12 @@ fn resolve_provider_id( ) -> ProviderId { if args.provider.is_some() { let requested = parse_provider(args); - return catalog::canonical_provider_id(catalog, requested.as_str()).unwrap_or(requested); + return canonical_provider_id(catalog, &requested); } if let Some(model_id) = args.model.as_deref() { // A bare model selector picks the highest-priority eligible provider // offering it, matching how the client resolves the request. - let matches = catalog::models_matching(catalog, model_id); + let matches = catalog.offerings_matching(model_id); if let Some(entry) = matches .iter() .find(|entry| eligible_providers.contains(entry.provider.id())) @@ -242,7 +246,15 @@ fn resolve_provider_id( } } let requested = parse_provider(args); - catalog::canonical_provider_id(catalog, requested.as_str()).unwrap_or(requested) + canonical_provider_id(catalog, &requested) +} + +/// The catalog id for `requested`, resolving aliases; the request itself when +/// the catalog does not know it, so the error names what the caller typed. +fn canonical_provider_id(catalog: &Catalog, requested: &ProviderId) -> ProviderId { + catalog + .enabled_provider(requested.as_str()) + .map_or_else(|| requested.clone(), |provider| provider.id().clone()) } async fn standalone_llm_source() -> anyhow::Result> { @@ -528,7 +540,9 @@ async fn run_with_args_and_client_and_catalog_styled( let model = if let Some(model) = args.model.clone() { model } else { - catalog::default_model(&catalog, provider_id.as_str()) + catalog + .enabled_provider(provider_id.as_str()) + .and_then(CatalogProvider::default_offering) .map(|entry| entry.model.id().to_string()) .ok_or_else(|| { anyhow::anyhow!( @@ -943,6 +957,10 @@ mod tests { assert!(approval_fn("shell", &json!({})).is_ok()); } + fn enabled_ids(catalog: &Catalog) -> std::collections::HashSet { + catalog.enabled_provider_ids().into_iter().collect() + } + fn test_catalog() -> Arc { Arc::new(fabro_test_catalog()) } @@ -1035,7 +1053,7 @@ profile = "openai" let args = args_with(None, Some("acme-aws-claude")); assert_eq!( - resolve_provider_id(&catalog, &args, &catalog::enabled_provider_ids(&catalog)), + resolve_provider_id(&catalog, &args, &enabled_ids(&catalog)), ProviderId::new("acme-aws") ); } @@ -1046,7 +1064,7 @@ profile = "openai" let args = args_with(Some("br"), None); assert_eq!( - resolve_provider_id(&catalog, &args, &catalog::enabled_provider_ids(&catalog)), + resolve_provider_id(&catalog, &args, &enabled_ids(&catalog)), ProviderId::new("acme-aws") ); } diff --git a/lib/components/fabro-agent/src/compaction.rs b/lib/components/fabro-agent/src/compaction.rs index 168c908fe..5c36f2e2d 100644 --- a/lib/components/fabro-agent/src/compaction.rs +++ b/lib/components/fabro-agent/src/compaction.rs @@ -365,8 +365,8 @@ mod tests { use std::sync::Arc; use std::time::SystemTime; - use fabro_llm::catalog::model_on_provider; - use fabro_llm::lithos_catalog::Catalog; + use fabro_llm::catalog; + use fabro_llm::lithos_catalog::{Catalog, Offering}; use fabro_llm::test_support::test_catalog; use fabro_types::{TokenCounts, ToolCall, tool_result_from_json}; @@ -381,6 +381,14 @@ mod tests { test_catalog() } + fn model_on_provider<'a>( + catalog: &'a Catalog, + provider: &str, + id: &str, + ) -> Option> { + catalog.enabled_provider(provider)?.offering(id) + } + fn builtin_summary_max_tokens(catalog: &Catalog, provider: &str, id: &str) -> u32 { let entry = model_on_provider(catalog, provider, id) .unwrap_or_else(|| panic!("{provider}/{id} missing from the catalog")); @@ -388,7 +396,7 @@ mod tests { .model .limits() .map(|limits| u32::try_from(limits.max_output_tokens).unwrap_or(u32::MAX)); - summary_max_tokens(entry.reasons_by_default(), max_output) + summary_max_tokens(catalog::reasons_by_default(&entry), max_output) } #[test] diff --git a/lib/components/fabro-agent/tests/it/guardrails.rs b/lib/components/fabro-agent/tests/it/guardrails.rs index e84599131..1b43401ee 100644 --- a/lib/components/fabro-agent/tests/it/guardrails.rs +++ b/lib/components/fabro-agent/tests/it/guardrails.rs @@ -7,9 +7,9 @@ use fabro_llm::test_support::test_catalog; #[test] fn profile_context_window_matches_catalog_for_default_models() { let catalog = Arc::new(test_catalog()); - for provider in catalog::listed_providers(&catalog) { + for provider in catalog.listed_providers() { let provider_id = provider.id().clone(); - let Some(default) = catalog::default_model(&catalog, provider_id.as_str()) else { + let Some(default) = provider.default_offering() else { // Deployment-defined providers (LiteLLM, Modal, Ollama) carry no // built-in default model. continue; @@ -21,7 +21,7 @@ fn profile_context_window_matches_catalog_for_default_models() { ); let profile: Box = AgentProfileBuilder::new( - default.agent_profile(), + catalog::offering_agent_profile(&default), provider_id.clone(), model.as_str(), Arc::clone(&catalog), diff --git a/lib/components/fabro-llm/src/api.rs b/lib/components/fabro-llm/src/api.rs index e08f6ec7b..965b3046f 100644 --- a/lib/components/fabro-llm/src/api.rs +++ b/lib/components/fabro-llm/src/api.rs @@ -9,31 +9,32 @@ use fabro_types::{ Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider, ProviderId, ReasoningEffort, }; -use lithos_llm::catalog::{Catalog, CatalogProvider}; - -use crate::catalog::{self, ModelEntry}; +use lithos_llm::catalog::{Catalog, CatalogProvider, Offering}; const USD_MICROS_PER_USD: f64 = 1_000_000.0; /// Every enabled model on every listed provider, provider priority order. #[must_use] pub fn models(catalog: &Catalog, configured: &HashSet) -> Vec { - catalog::models(catalog) - .iter() - .map(|entry| model_view(entry, configured.contains(entry.provider.id()))) + catalog + .listed_providers() + .into_iter() + .flat_map(CatalogProvider::offerings) + .map(|offering| model_view(&offering, configured.contains(offering.provider.id()))) .collect() } /// Every listed provider, priority order. #[must_use] pub fn providers(catalog: &Catalog, configured: &HashSet) -> Vec { - catalog::listed_providers(catalog) - .iter() + catalog + .listed_providers() + .into_iter() .map(|provider| provider_view(provider, configured.contains(provider.id()))) .collect() } -fn model_view(entry: &ModelEntry<'_>, configured: bool) -> Model { +fn model_view(entry: &Offering<'_>, configured: bool) -> Model { let model = entry.model; let capabilities = model.capabilities(); let pricing = model.pricing(); @@ -95,7 +96,7 @@ fn provider_view(provider: &CatalogProvider, configured: bool) -> Provider { 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), + model_count: u32::try_from(provider.offerings().len()).unwrap_or(u32::MAX), default_model: provider.default_model().map(str::to_string), configured, expected_secret_name: fabro_auth::expected_secret_name(provider), diff --git a/lib/components/fabro-llm/src/catalog.rs b/lib/components/fabro-llm/src/catalog.rs index a12b27c8a..5314ba346 100644 --- a/lib/components/fabro-llm/src/catalog.rs +++ b/lib/components/fabro-llm/src/catalog.rs @@ -1,18 +1,18 @@ -//! Catalog construction and the queries Fabro's dispatch boundaries share. +//! Catalog construction and the agent-profile reading that is Fabro's own. //! //! 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}; +//! overlay. Which providers are on, which model a selector names, and which +//! model to pick for a job are lithos questions, answered by +//! [`Catalog`] and [`CatalogProvider`] (`enabled_providers`, +//! `offerings_matching`, `default_offering_for`, and the rest). What stays +//! here is the coding harness a model expects, read from the shared +//! `metadata.agent` namespace that Pebble reads too. use fabro_config::LlmLayer; use fabro_static::EnvVars; -use fabro_types::{AgentProfileKind, Cost, ModelId, ModelRef, ProviderId, TokenCounts}; +use fabro_types::AgentProfileKind; +pub use lithos_llm::catalog::Offering; use lithos_llm::catalog::{Catalog, CatalogError, CatalogModel, CatalogProvider, Metadata}; -use lithos_llm::resolver::ResolvedRoute; use serde::Deserialize; /// The metadata namespace agent harnesses read. @@ -53,40 +53,6 @@ pub fn default_catalog() -> Catalog { 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)] @@ -104,6 +70,35 @@ fn agent_metadata(metadata: &Metadata) -> AgentMetadata { .unwrap_or_default() } +/// Whether requests to `offering` 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(offering: &Offering<'_>) -> bool { + agent_metadata(offering.model.metadata()) + .reasoning_by_default + .or(agent_metadata(offering.provider.metadata()).reasoning_by_default) + .unwrap_or_else(|| { + offering.model.capabilities().reasoning().is_supported() + && offering.model.protocol_options().reasoning_effort_levels + }) +} + +/// The agent harness `offering` runs under: the model's own answer, then +/// the provider's, then the profile implied by the provider's adapter. +#[must_use] +pub fn offering_agent_profile(offering: &Offering<'_>) -> AgentProfileKind { + model_agent_profile(offering.provider, offering.model) +} + +fn model_agent_profile(provider: &CatalogProvider, model: &CatalogModel) -> AgentProfileKind { + agent_metadata(model.metadata()) + .profile + .unwrap_or_else(|| provider_agent_profile(provider)) +} + /// 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. @@ -117,260 +112,22 @@ fn provider_agent_profile(provider: &CatalogProvider) -> AgentProfileKind { }) } -/// Estimates the catalog cost of `usage` on `model`, when the catalog prices -/// that route. Passthrough models and unknown providers have no price. -#[must_use] -pub fn estimate_cost(catalog: &Catalog, model: &ModelRef, usage: TokenCounts) -> Option { - let entry = model_on_provider(catalog, model.provider.as_str(), model.model_id.as_str())?; - ResolvedRoute::try_new(entry.provider.clone(), entry.model.clone()) - .ok()? - .estimate_cost(usage, model.speed) -} - -/// Enabled providers, highest priority first, ties broken by id. -#[must_use] -pub fn enabled_providers(catalog: &Catalog) -> Vec<&CatalogProvider> { - let mut providers: Vec<_> = catalog - .providers() - .filter(|provider| provider.is_enabled()) - .collect(); - providers.sort_by(|left, right| { - right - .priority() - .cmp(&left.priority()) - .then_with(|| left.id().cmp(right.id())) - }); - providers -} - -/// 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<&CatalogProvider> { - enabled_providers(catalog) - .into_iter() - .filter(|provider| provider.stands_in_for().is_none()) - .collect() -} - -/// The ids of every enabled provider. -#[must_use] -pub fn enabled_provider_ids(catalog: &Catalog) -> HashSet { - enabled_providers(catalog) - .into_iter() - .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<&'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 { - provider(catalog, selector).map(|provider| provider.id().clone()) -} - -/// The models of a provider, in catalog order. -#[must_use] -pub fn provider_models(provider: &CatalogProvider) -> Vec> { - provider - .models() - .map(|model| ModelEntry { provider, model }) - .collect() -} - -/// Every model across listed providers, provider priority order. -#[must_use] -pub fn models(catalog: &Catalog) -> Vec> { - listed_providers(catalog) - .into_iter() - .flat_map(provider_models) - .collect() -} - -/// 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> { - 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 = provider.model(model_selector).or_else(|| { - provider - .models() - .find(|model| model.api_model() == model_selector) - })?; - Some(ModelEntry { provider, model }) -} - -/// 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> { - let mut matches: Vec<_> = enabled_providers(catalog) - .into_iter() - .flat_map(provider_models) - .filter(|entry| { - entry.model.id().as_str() == selector - || entry.model.aliases().iter().any(|alias| alias == selector) - }) - .collect(); - matches.sort_by_key(|entry| entry.model.id().as_str() != selector); - matches -} - -/// 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() -} - -/// Whether `selector` names an enabled provider. -#[must_use] -pub fn is_provider_selector(catalog: &Catalog, selector: &str) -> bool { - provider(catalog, selector).is_some() -} - -/// The default model of an enabled provider. -#[must_use] -pub fn default_model<'a>(catalog: &'a Catalog, provider_selector: &str) -> Option> { - 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> { - let provider = provider(catalog, provider_selector)?; - provider_models(provider) - .into_iter() - .find(|entry| entry.model.is_probe()) - .or_else(|| default_model(catalog, provider_selector)) -} - -/// The default model across `ready` providers: the highest-priority ready -/// provider's default. Falls back to any enabled provider's default when no -/// provider is ready, so callers always have a model to name. -#[must_use] -pub fn default_for_ready<'a>( - catalog: &'a Catalog, - ready: &HashSet, -) -> Option> { - let providers = enabled_providers(catalog); - providers - .iter() - .filter(|provider| ready.contains(provider.id())) - .chain(providers.iter()) - .find_map(|provider| default_model(catalog, provider.id().as_str())) -} - -/// The small utility model across `ready` providers: the first -/// `small_default` model in provider priority order, else the ready default. -#[must_use] -pub fn small_default_for_ready<'a>( - catalog: &'a Catalog, - ready: &HashSet, -) -> Option> { - 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)) -} - -/// Canonicalizes a model selector to a catalog model id, preferring -/// `provider`'s offering. Unknown selectors pass through verbatim so -/// passthrough models keep their names. -#[must_use] -pub fn canonical_model_id(catalog: &Catalog, provider: &ProviderId, selector: &str) -> String { - model_on_provider(catalog, provider.as_str(), selector) - .map(|entry| entry.model.id().to_string()) - .or_else(|| { - models_matching(catalog, selector) - .first() - .map(|entry| entry.model.id().to_string()) - }) - .unwrap_or_else(|| selector.to_string()) -} - -/// The agent profile for a route. Unknown (passthrough) models take the -/// provider default. +/// The agent profile for a route on an enabled provider. Unknown +/// (passthrough) models take the provider default; a disabled or unknown +/// provider has none. #[must_use] pub fn agent_profile( catalog: &Catalog, provider_selector: &str, model_selector: Option<&str>, ) -> Option { - let provider = provider(catalog, provider_selector)?; - let model = model_selector.and_then(|selector| provider.model(selector)); - Some(match model { - Some(model) => ModelEntry { provider, model }.agent_profile(), - None => provider_agent_profile(provider), - }) -} - -/// The `target` provider's model closest to `reference` in capability and -/// input price, for provider-level fallbacks. -#[must_use] -pub fn closest_model<'a>( - catalog: &'a Catalog, - target: &str, - reference: &CatalogModel, -) -> Option> { - let target = provider(catalog, target)?; - let reference_caps = reference.capabilities(); - let reference_price = reference - .pricing() - .and_then(|pricing| pricing.input_usd_micros_per_million) - .unwrap_or(0); - provider_models(target) - .into_iter() - .filter(|entry| { - let caps = entry.model.capabilities(); - caps.tools().is_supported() == reference_caps.tools().is_supported() - && caps.images().is_supported() == reference_caps.images().is_supported() - && caps.reasoning().is_supported() == reference_caps.reasoning().is_supported() - }) - .min_by_key(|entry| { - let price = entry - .model - .pricing() - .and_then(|pricing| pricing.input_usd_micros_per_million) - .unwrap_or(0); - price.abs_diff(reference_price) - }) -} - -/// Model ids grouped by provider, for diagnostics and documentation. -#[must_use] -pub fn model_ids_by_provider(catalog: &Catalog) -> BTreeMap> { - listed_providers(catalog) - .into_iter() - .map(|provider| { - ( - provider.id().clone(), - provider_models(provider) - .into_iter() - .map(|entry| entry.model.id().clone()) - .collect(), - ) - }) - .collect() + let provider = catalog.enabled_provider(provider_selector)?; + Some( + match model_selector.and_then(|selector| provider.model(selector)) { + Some(model) => model_agent_profile(provider, model), + None => provider_agent_profile(provider), + }, + ) } #[cfg(test)] @@ -378,27 +135,6 @@ mod tests { use super::*; use crate::test_support::test_catalog; - #[test] - fn builtins_ship_fabro_defaults() { - let catalog = test_catalog(); - let ids: Vec<_> = enabled_providers(&catalog) - .iter() - .map(|provider| provider.id().to_string()) - .collect(); - assert_eq!(ids[0], "anthropic"); - assert!(ids.contains(&"openai".to_string())); - assert!( - !ids.contains(&"bedrock".to_string()), - "bedrock ships disabled" - ); - assert!( - !listed_providers(&catalog) - .iter() - .any(|provider| provider.id().as_str() == "openai-codex"), - "stand-in providers are not listed" - ); - } - #[test] fn operator_overlay_applies_last() { let overlay = LlmLayer( @@ -412,7 +148,7 @@ enabled = false .unwrap(), ); let catalog = build_catalog(&overlay, &|_| None).unwrap(); - assert!(provider(&catalog, "openai").is_none()); + assert!(catalog.enabled_provider("openai").is_none()); assert_eq!( catalog.provider("openai").unwrap().priority(), 500, @@ -433,61 +169,8 @@ enabled = false } #[test] - fn probe_and_small_default_follow_the_catalog() { + fn agent_profiles_follow_the_model_then_the_provider() { let catalog = test_catalog(); - assert_eq!( - probe_model(&catalog, "openai").unwrap().model.id().as_str(), - "gpt-5.4-mini" - ); - assert_eq!( - probe_model(&catalog, "anthropic") - .unwrap() - .model - .id() - .as_str(), - "claude-haiku-4.5" - ); - let ready = HashSet::from([ProviderId::new("openai")]); - assert_eq!( - small_default_for_ready(&catalog, &ready) - .unwrap() - .model - .id() - .as_str(), - "gpt-5.4-mini" - ); - assert_eq!( - default_for_ready(&catalog, &ready) - .unwrap() - .model - .id() - .as_str(), - "gpt-5.6-sol" - ); - assert_eq!( - default_for_ready(&catalog, &HashSet::new()) - .unwrap() - .provider - .id() - .as_str(), - "anthropic" - ); - } - - #[test] - fn selectors_resolve_aliases_and_canonical_ids() { - let catalog = test_catalog(); - assert!(is_model_selector(&catalog, "sonnet")); - assert!(is_model_selector(&catalog, "gpt-5.4-mini")); - assert!(!is_model_selector(&catalog, "nope")); - assert_eq!( - canonical_model_id(&catalog, &ProviderId::new("openai"), "codex"), - "gpt-5.4" - ); - assert_eq!( - canonical_model_id(&catalog, &ProviderId::new("openai"), "unknown-model"), - "unknown-model" - ); assert_eq!( agent_profile(&catalog, "openai", Some("gpt-5.6-sol")), Some(AgentProfileKind::Gpt56) @@ -523,12 +206,15 @@ enabled = false #[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(); + let moonshot = catalog.enabled_provider("moonshot").unwrap(); + let kimi = moonshot.offering("kimi-k2.5").unwrap(); + assert!(reasons_by_default(&kimi), "the catalog row says so"); + let anthropic = catalog.enabled_provider("anthropic").unwrap(); + let sonnet = anthropic.offering("claude-sonnet-4.5").unwrap(); assert!( - !sonnet.reasons_by_default(), + !reasons_by_default(&sonnet), "a thinking-budget model reasons only when asked" ); + assert_eq!(offering_agent_profile(&kimi), AgentProfileKind::Kimi); } } diff --git a/lib/components/fabro-llm/src/lib.rs b/lib/components/fabro-llm/src/lib.rs index 00e9b8c7d..4e325b1c1 100644 --- a/lib/components/fabro-llm/src/lib.rs +++ b/lib/components/fabro-llm/src/lib.rs @@ -4,8 +4,7 @@ //! the client. This crate adds what is specific to Fabro: //! //! - building the catalog from the lithos built-ins and the operator `[llm]` -//! overlay, and the catalog queries Fabro's dispatch boundaries share -//! ([`catalog`]); +//! overlay, and reading the agent harness a model expects ([`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; diff --git a/lib/components/fabro-llm/src/probe.rs b/lib/components/fabro-llm/src/probe.rs index 58c5a1e51..510733c22 100644 --- a/lib/components/fabro-llm/src/probe.rs +++ b/lib/components/fabro-llm/src/probe.rs @@ -9,7 +9,6 @@ use lithos_llm::catalog::Catalog; use lithos_llm::client::{Client, ProbeOptions, ProbeOutcome}; use strum::IntoStaticStr; -use crate::catalog; use crate::client::{ClientOptions, LlmSetupError, build_client}; #[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)] @@ -104,13 +103,15 @@ pub async fn probe_provider_with_api_key( api_key: String, timeout: Duration, ) -> Result { - let catalog_provider = catalog::provider(&catalog, provider.as_str()) + let catalog_provider = catalog + .enabled_provider(provider.as_str()) .ok_or_else(|| ApiKeyProbeError::UnknownProvider(provider.to_string()))?; 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()) + let model = catalog_provider + .probe_offering() .ok_or_else(|| ApiKeyProbeError::NoProbeModel(provider_id.clone()))?; let selector = format!("{provider_id}/{}", model.model.id()); let source = Arc::new(ApiKeyCredentialSource::new(provider_id.clone(), api_key)); diff --git a/lib/components/fabro-llm/src/selection.rs b/lib/components/fabro-llm/src/selection.rs index 80c6d6fbd..65f26e9a0 100644 --- a/lib/components/fabro-llm/src/selection.rs +++ b/lib/components/fabro-llm/src/selection.rs @@ -20,11 +20,9 @@ use std::collections::HashSet; use std::fmt; use fabro_types::{ModelId, ProviderId}; -use lithos_llm::catalog::Catalog; +use lithos_llm::catalog::{Catalog, Offering}; use thiserror::Error; -use crate::catalog::{self, ModelEntry}; - /// A provider/model pair one of the selection functions chose. /// /// `model` is the canonical catalog id when the selector matched an offering, @@ -94,11 +92,12 @@ pub fn require_provider( catalog: &Catalog, selector: &str, ) -> Result { - catalog::canonical_provider_id(catalog, selector).ok_or_else(|| { - ModelSelectionError::UnknownProvider { + catalog + .enabled_provider(selector) + .map(|provider| provider.id().clone()) + .ok_or_else(|| ModelSelectionError::UnknownProvider { provider: selector.to_string(), - } - }) + }) } /// Canonicalizes a provider and requires it to be in the eligible set. @@ -120,14 +119,15 @@ pub fn resolve_on_provider<'a>( catalog: &'a Catalog, provider: &ProviderId, selector: &str, -) -> Result, ModelSelectionError> { +) -> Result, ModelSelectionError> { let provider = require_provider(catalog, provider.as_str())?; - catalog::model_on_provider(catalog, provider.as_str(), selector).ok_or( - ModelSelectionError::UnknownSelectorOnProvider { + catalog + .enabled_provider(provider.as_str()) + .and_then(|provider| provider.offering(selector)) + .ok_or(ModelSelectionError::UnknownSelectorOnProvider { selector: selector.to_string(), provider, - }, - ) + }) } /// Selects a catalog model for `selector`, requiring a real offering. @@ -140,7 +140,7 @@ pub fn select<'a>( selector: &str, explicit_provider: Option<&ProviderId>, eligible: &HashSet, -) -> Result, ModelSelectionError> { +) -> Result, ModelSelectionError> { if let Some(explicit) = explicit_provider { let provider = ready_provider(catalog, explicit, eligible)?; return resolve_on_provider(catalog, &provider, selector); @@ -149,12 +149,12 @@ pub fn select<'a>( // it at request time. A slash whose prefix is not a provider (an // aggregator's `vendor/model` api id) falls through to plain matching. if let Some((prefix, rest)) = selector.split_once('/') { - if let Some(provider) = catalog::canonical_provider_id(catalog, prefix) { - let provider = ready_provider(catalog, &provider, eligible)?; + if let Some(provider) = catalog.enabled_provider(prefix) { + let provider = ready_provider(catalog, provider.id(), eligible)?; return resolve_on_provider(catalog, &provider, rest); } } - let matches = catalog::models_matching(catalog, selector); + let matches = catalog.offerings_matching(selector); if matches.is_empty() { return Err(ModelSelectionError::UnknownSelector { selector: selector.to_string(), @@ -178,19 +178,21 @@ pub fn select<'a>( pub fn select_default<'a>( catalog: &'a Catalog, eligible: &HashSet, -) -> Result, ModelSelectionError> { +) -> Result, ModelSelectionError> { let eligible = canonical_eligible(catalog, eligible); - let providers_with_defaults: Vec<_> = catalog::enabled_providers(catalog) + let providers_with_defaults: Vec<_> = catalog + .enabled_providers() .into_iter() .filter_map(|provider| { - catalog::default_model(catalog, provider.id().as_str()) - .map(|model| (provider.id().clone(), model)) + provider + .default_offering() + .map(|offering| (provider.id().clone(), offering)) }) .collect(); providers_with_defaults .iter() .find(|(provider, _)| eligible.contains(provider)) - .map(|(_, model)| model.clone()) + .map(|(_, offering)| *offering) .ok_or_else(|| ModelSelectionError::NoDefaultModel { providers: providers_with_defaults .into_iter() @@ -258,7 +260,7 @@ pub fn resolve_selection_with_catalog_fallback( catalog, selector, explicit_provider, - &catalog::enabled_provider_ids(catalog), + &catalog.enabled_provider_ids().into_iter().collect(), ), result => result, } @@ -267,7 +269,8 @@ pub fn resolve_selection_with_catalog_fallback( fn canonical_eligible(catalog: &Catalog, eligible: &HashSet) -> HashSet { eligible .iter() - .filter_map(|id| catalog::canonical_provider_id(catalog, id.as_str())) + .filter_map(|id| catalog.enabled_provider(id.as_str())) + .map(|provider| provider.id().clone()) .collect() } diff --git a/lib/components/fabro-validate/src/rules/model_support.rs b/lib/components/fabro-validate/src/rules/model_support.rs index 21735ef5a..50c4e20d6 100644 --- a/lib/components/fabro-validate/src/rules/model_support.rs +++ b/lib/components/fabro-validate/src/rules/model_support.rs @@ -1,4 +1,3 @@ -use fabro_llm::catalog; use fabro_llm::lithos_catalog::Catalog; use crate::{Diagnostic, Severity}; @@ -10,7 +9,7 @@ pub(super) fn check_model_known( context: &str, node_id: Option, ) -> Option { - if catalog::is_model_selector(catalog, model) { + if catalog.is_model_selector(model) { return None; } Some(Diagnostic { @@ -34,10 +33,11 @@ pub(super) fn check_provider_known( context: &str, node_id: Option, ) -> Option { - if catalog::is_provider_selector(catalog, provider) { + if catalog.enabled_provider(provider).is_some() { return None; } - let valid: Vec = catalog::listed_providers(catalog) + let valid: Vec = catalog + .listed_providers() .iter() .map(|provider| provider.id().to_string()) .collect(); diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index 87cffd0fa..d254ca5f1 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -15,7 +15,7 @@ use fabro_llm::credentials::CredentialProvider; use fabro_llm::error::failover_eligible; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::types::ResponseFormat; -use fabro_llm::{Client, ClientOptions, FallbackTarget, LlmError, Request, Response, catalog}; +use fabro_llm::{Client, ClientOptions, FallbackTarget, LlmError, Request, Response}; use fabro_mcp::config::McpServerSettings; use fabro_types::settings::run::RunModelControls; use fabro_types::{ @@ -40,7 +40,7 @@ use crate::context::WorkflowContext; use crate::context::keys::Fidelity; use crate::error::Error; use crate::event::{Emitter, Event, StageScope}; -use crate::model_fallback::{ModelFallbackNotice, ModelFallbackPolicy}; +use crate::model_fallback::{ModelFallbackNotice, ModelFallbackPolicy, canonical_model_id}; use crate::outcome::billed_model_usage_from_llm; use crate::services::FabroRunToolServices; use crate::steering_hub::{ActiveControlHandle, SteeringHub}; @@ -888,11 +888,11 @@ impl AgentApiBackend { let Some(requested_effort) = requested.reasoning_effort else { return FallbackControls::Usable(requested); }; - let Some(offering) = catalog::model_on_provider( - &self.catalog, - target.provider.as_str(), - target.model.as_str(), - ) else { + let Some(offering) = self + .catalog + .enabled_provider(target.provider.as_str()) + .and_then(|provider| provider.offering(target.model.as_str())) + else { // A catalog-unknown passthrough target has no advertised controls. // Preserve the request and let the provider validate it. return FallbackControls::Usable(requested); @@ -925,7 +925,7 @@ impl AgentApiBackend { provider: &ProviderId, requested_controls: EffectiveRequestControls, ) -> (FallbackPlan, Vec) { - let primary_model = catalog::canonical_model_id(&self.catalog, provider, model); + let primary_model = canonical_model_id(&self.catalog, provider, model); let original = LlmRoute { target: FallbackTarget::new(provider, &primary_model), controls: requested_controls, @@ -1371,13 +1371,11 @@ impl AgentApiBackend { fn route_max_tokens(&self, node: &Node, route: &LlmRoute) -> Option { node_max_output_tokens(node).or_else(|| { - catalog::model_on_provider( - &self.catalog, - route.target.provider.as_str(), - route.target.model.as_str(), - ) - .and_then(|entry| entry.model.limits()) - .map(|limits| u32::try_from(limits.max_output_tokens).unwrap_or(u32::MAX)) + self.catalog + .enabled_provider(route.target.provider.as_str()) + .and_then(|provider| provider.offering(route.target.model.as_str())) + .and_then(|entry| entry.model.limits()) + .map(|limits| u32::try_from(limits.max_output_tokens).unwrap_or(u32::MAX)) }) } diff --git a/lib/components/fabro-workflow/src/handler/llm/routing.rs b/lib/components/fabro-workflow/src/handler/llm/routing.rs index c3db8ae5d..8ce674008 100644 --- a/lib/components/fabro-workflow/src/handler/llm/routing.rs +++ b/lib/components/fabro-workflow/src/handler/llm/routing.rs @@ -51,10 +51,17 @@ pub(crate) fn resolve_provider_context( provider_attr: Option<&str>, ) -> Result { let provider_id = if let Some(provider) = provider_attr { - catalog::canonical_provider_id(catalog, provider).ok_or_else(|| { - Error::Precondition(format!("Provider \"{provider}\" is not configured")) - })? - } else if catalog::model_on_provider(catalog, default_provider_id.as_str(), model).is_some() { + catalog + .enabled_provider(provider) + .map(|found| found.id().clone()) + .ok_or_else(|| { + Error::Precondition(format!("Provider \"{provider}\" is not configured")) + })? + } else if catalog + .enabled_provider(default_provider_id.as_str()) + .and_then(|provider| provider.offering(model)) + .is_some() + { // The run's selected provider is a pin whenever it offers the model. default_provider_id.clone() } else { @@ -62,7 +69,7 @@ pub(crate) fn resolve_provider_context( catalog, model, None, - &catalog::enabled_provider_ids(catalog), + &catalog.enabled_provider_ids().into_iter().collect(), ) { Ok(entry) => entry.provider.id().clone(), Err(ModelSelectionError::UnknownSelector { .. }) => default_provider_id.clone(), @@ -70,8 +77,10 @@ pub(crate) fn resolve_provider_context( } }; - let provider_id = - catalog::canonical_provider_id(catalog, provider_id.as_str()).ok_or_else(|| { + let provider_id = catalog + .enabled_provider(provider_id.as_str()) + .map(|provider| provider.id().clone()) + .ok_or_else(|| { Error::Precondition(format!("Provider \"{provider_id}\" is not configured")) })?; let profile_kind = catalog::agent_profile(catalog, provider_id.as_str(), Some(model)) diff --git a/lib/components/fabro-workflow/src/model_fallback.rs b/lib/components/fabro-workflow/src/model_fallback.rs index 4a9d3c73f..b78e9711c 100644 --- a/lib/components/fabro-workflow/src/model_fallback.rs +++ b/lib/components/fabro-workflow/src/model_fallback.rs @@ -1,8 +1,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use fabro_llm::catalog::ModelEntry; -use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{FallbackTarget, ModelSelectionError, catalog, selection}; +use fabro_llm::lithos_catalog::{Catalog, Offering}; +use fabro_llm::{FallbackTarget, ModelSelectionError, selection}; use fabro_types::settings::{ModelRef, ResolvedModelRef}; use fabro_types::{ProviderId, ReasoningEffort, RunNoticeCode, RunNoticeLevel}; @@ -31,7 +30,7 @@ impl ModelFallbackPolicy { provider: &ProviderId, model: &str, ) -> Option<&'a [FallbackTarget]> { - self.chain_for_canonical(&catalog::canonical_model_id(catalog, provider, model)) + self.chain_for_canonical(&canonical_model_id(catalog, provider, model)) } /// Look up a chain by an already-canonicalized requested model ID. @@ -224,8 +223,9 @@ pub fn resolve_model_fallbacks( } let primary = FallbackTarget::new(&selected.provider, &requested_model); - let primary_model = - catalog::model_on_provider(catalog, selected.provider.as_str(), &requested_model); + let primary_model = catalog + .enabled_provider(selected.provider.as_str()) + .and_then(|provider| provider.offering(&requested_model)); let mut targets = Vec::new(); for model_ref in references { @@ -295,11 +295,23 @@ enum FallbackCandidate { Skipped(ModelFallbackNotice), } +/// The catalog id for `selector` on `provider`, else anywhere; the selector +/// itself for a passthrough model the catalog does not know. +pub(crate) fn canonical_model_id( + catalog: &Catalog, + provider: &ProviderId, + selector: &str, +) -> String { + catalog + .canonical_model_id(Some(provider), selector) + .map_or_else(|| selector.to_string(), ToString::to_string) +} + fn resolve_fallback_candidate( catalog: &Catalog, requested_model: &str, primary: &FallbackTarget, - primary_model: Option<&ModelEntry<'_>>, + primary_model: Option<&Offering<'_>>, eligible: &HashSet, model_ref: &ModelRef, ) -> Result { @@ -326,7 +338,10 @@ fn resolve_fallback_candidate( }, )); }; - match catalog::closest_model(catalog, provider.as_str(), primary_model.model) { + match catalog + .enabled_provider(provider.as_str()) + .and_then(|target| target.closest_offering(primary_model.model)) + { Some(entry) => { FallbackCandidate::Target(FallbackTarget::new(provider, entry.model.id())) } diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index c0cbe93bf..196100033 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -753,7 +753,8 @@ mod tests { } fn test_provider_ids() -> Vec { - fabro_llm::catalog::enabled_provider_ids(&fabro_llm::test_support::test_catalog()) + fabro_llm::test_support::test_catalog() + .enabled_provider_ids() .into_iter() .collect() } diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 83e56fbcf..78f7691b6 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -1435,7 +1435,8 @@ mod tests { } fn test_provider_ids() -> Vec { - fabro_llm::catalog::enabled_provider_ids(&fabro_llm::test_support::test_catalog()) + fabro_llm::test_support::test_catalog() + .enabled_provider_ids() .into_iter() .collect() } diff --git a/lib/components/fabro-workflow/src/outcome.rs b/lib/components/fabro-workflow/src/outcome.rs index e41717ca5..2025bab23 100644 --- a/lib/components/fabro-workflow/src/outcome.rs +++ b/lib/components/fabro-workflow/src/outcome.rs @@ -1,7 +1,6 @@ pub use fabro_core::outcome::{ FailureCategory, FailureDetail, OutcomeMeta, StageOutcome, StageState, }; -use fabro_llm::catalog; use fabro_llm::lithos_catalog::Catalog; pub use fabro_types::BilledModelUsage; use fabro_types::{BilledTokenCounts, ModelRef, TokenCounts}; @@ -19,13 +18,13 @@ pub fn billed_model_usage_from_llm( model: &ModelRef, usage: TokenCounts, ) -> Result { - if catalog::provider(catalog, model.provider.as_str()).is_none() { + if catalog.enabled_provider(model.provider.as_str()).is_none() { return Err(Error::Precondition(format!( "Provider \"{}\" is not configured", model.provider ))); } - let cost = catalog::estimate_cost(catalog, model, usage); + let cost = catalog.estimate_cost(&model.handle(), usage, model.speed); Ok(BilledModelUsage::new(model.clone(), usage, cost)) } diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index d7990d5fe..1b60a3882 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -1485,7 +1485,7 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr assert_eq!( truncation_caps( "unknown-model", - &fabro_llm::catalog::enabled_provider_ids(&mock_catalog()), + &mock_catalog().enabled_provider_ids().into_iter().collect(), &mock_catalog(), ), TruncationCaps { diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 57a39592f..47410b8db 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -8,7 +8,6 @@ use fabro_agent::Sandbox; use fabro_auth::test_support as auth_test_support; use fabro_graphviz::graph::Graph as GvGraph; use fabro_interview::AutoApproveInterviewer; -use fabro_llm::catalog; use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::test_catalog; @@ -40,7 +39,7 @@ pub(crate) fn test_configured_provider_ids( assume_ready: bool, ) -> Vec { if assume_ready { - catalog::enabled_provider_ids(catalog).into_iter().collect() + catalog.enabled_provider_ids().into_iter().collect() } else { configured_provider_ids } diff --git a/lib/components/fabro-workflow/src/transforms/model_resolution.rs b/lib/components/fabro-workflow/src/transforms/model_resolution.rs index 22f1cf33c..c60d38aa7 100644 --- a/lib/components/fabro-workflow/src/transforms/model_resolution.rs +++ b/lib/components/fabro-workflow/src/transforms/model_resolution.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use fabro_graphviz::graph::{AttrValue, Graph}; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{catalog, selection}; +use fabro_llm::selection; use fabro_types::ProviderId; use super::Transform; @@ -21,7 +21,7 @@ pub struct ModelResolutionTransform { impl ModelResolutionTransform { #[must_use] pub fn new(catalog: Arc) -> Self { - let eligible_providers = catalog::enabled_provider_ids(&catalog); + let eligible_providers = catalog.enabled_provider_ids().into_iter().collect(); Self { catalog, default_provider: None, From 87e1e3a00f46a9d44413709ca3be264e4c709314 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:41:20 -0600 Subject: [PATCH 4/7] Replace fabro-llm helper modules with lithos-llm equivalents Delete fabro-llm's attachments, reasoning, and structured modules and the LlmError newtype and ErrorFacts trait. lithos-llm now provides all of them: InlineLocalFiles under the local-files feature, ReasoningOutput with Response::reasoning(), Client::complete_object, and the retry, auth, cancel, and failover predicates directly on Error and ErrorData. fabro-llm keeps only failure_signature_hint, which is Fabro's own loop detection policy. Store ErrorData directly in the agent and workflow error enums, boxed where the variant would otherwise dominate the enum size. Repin lithos-llm to a1e3fd3 for these additions. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 26 +- Cargo.toml | 2 +- docs/public/reference/sdk.mdx | 26 +- lib/apps/fabro-cli/src/commands/exec.rs | 2 +- .../src/commands/run/run_progress/mod.rs | 6 +- .../fabro-server/src/run_title_generation.rs | 13 +- .../src/server/handler/completions.rs | 7 +- lib/components/fabro-agent/src/error.rs | 41 ++- lib/components/fabro-agent/src/history.rs | 5 +- lib/components/fabro-agent/src/session.rs | 23 +- lib/components/fabro-agent/src/types.rs | 14 +- lib/components/fabro-hooks/src/executor.rs | 10 +- lib/components/fabro-llm/Cargo.toml | 5 +- lib/components/fabro-llm/src/attachments.rs | 268 --------------- lib/components/fabro-llm/src/client.rs | 13 +- lib/components/fabro-llm/src/error.rs | 320 ++---------------- lib/components/fabro-llm/src/lib.rs | 17 +- lib/components/fabro-llm/src/reasoning.rs | 292 ---------------- lib/components/fabro-llm/src/structured.rs | 103 ------ lib/components/fabro-workflow/src/error.rs | 54 +-- .../fabro-workflow/src/handler/llm/api.rs | 39 ++- .../src/pipeline/pull_request.rs | 10 +- lib/foundation/fabro-types/src/lib.rs | 5 +- lib/foundation/fabro-types/src/reasoning.rs | 142 -------- 24 files changed, 184 insertions(+), 1259 deletions(-) delete mode 100644 lib/components/fabro-llm/src/attachments.rs delete mode 100644 lib/components/fabro-llm/src/reasoning.rs delete mode 100644 lib/components/fabro-llm/src/structured.rs delete mode 100644 lib/foundation/fabro-types/src/reasoning.rs diff --git a/Cargo.lock b/Cargo.lock index 3459e9ed6..9b9bada2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2063,7 +2063,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2190,7 +2190,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2751,7 +2751,6 @@ version = "0.348.0-nightly.0" dependencies = [ "anyhow", "async-trait", - "base64", "bytes", "fabro-auth", "fabro-config", @@ -2765,11 +2764,9 @@ dependencies = [ "futures", "httpmock", "lithos-llm", - "mime_guess", "serde", "serde_json", "strum 0.28.0", - "tempfile", "thiserror 2.0.18", "tokio", "tokio-util", @@ -4952,7 +4949,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lithos-llm" version = "0.1.0" -source = "git+https://github.com/lithoscomputer/lithos-llm?rev=ba2f4184e3db9a37f36aa650ce4b7c5f3376b109#ba2f4184e3db9a37f36aa650ce4b7c5f3376b109" +source = "git+https://github.com/lithoscomputer/lithos-llm?rev=a1e3fd37b7153870411701327ac117606753fe90#a1e3fd37b7153870411701327ac117606753fe90" dependencies = [ "async-trait", "aws-config", @@ -4964,6 +4961,7 @@ dependencies = [ "crc32fast", "futures-core", "futures-util", + "mime_guess", "reqwest 0.13.4", "serde", "serde_json", @@ -5413,7 +5411,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6410,7 +6408,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6887,7 +6885,7 @@ dependencies = [ "errno 0.3.14", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6946,7 +6944,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7470,7 +7468,7 @@ version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ - "errno 0.3.14", + "errno 0.2.8", "libc", ] @@ -8060,7 +8058,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -8106,7 +8104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9184,7 +9182,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 09e61ddeb..854cef45e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,7 @@ insta = "1" fabro-test = { path = "lib/foundation/fabro-test" } # Provider-neutral LLM catalog and client. Pinned to a revision until 0.x is # published to crates.io. -lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "ba2f4184e3db9a37f36aa650ce4b7c5f3376b109", default-features = false } +lithos-llm = { git = "https://github.com/lithoscomputer/lithos-llm", rev = "a1e3fd37b7153870411701327ac117606753fe90", default-features = false } # Deterministic OpenAI twin used by twin-mode E2E tests; the same revision # lithos-llm verifies its codecs against. twin-openai = { git = "https://github.com/lithoscomputer/twins", rev = "ca45f0e50a6716d716aa2f638ca3cf767e88f613" } diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index 1b34463dc..63be41cec 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -300,7 +300,7 @@ All fallible `Session` methods return `Result`: | Variant | Description | |---|---| -| `Llm(LlmError)` | An error from the LLM provider (the stored form of a lithos `Error`). | +| `Llm(Box)` | An error from the LLM provider: the lithos `ErrorData`, the stored form of a lithos `Error`. | | `SessionClosed` | `process_input` was called on a closed session. | | `InvalidState(String)` | The session is in an unexpected state. | | `ToolExecution(String)` | A tool execution failed. | @@ -425,10 +425,10 @@ A turn that ends with `FinishReason::Length` or `FinishReason::Incomplete` is no ### Structured output -`fabro_llm::structured::complete_object` attaches a JSON Schema as the request's response format and parses the reply: +`Client::complete_object` (a lithos method) attaches a JSON Schema as the request's response format and parses the reply into a `StructuredCompletion` with the response and the parsed document: ```rust -use fabro_llm::{Request, structured}; +use fabro_llm::Request; use serde_json::json; let schema = json!({ @@ -444,31 +444,31 @@ let request = Request::builder() .model("claude-sonnet-4.5") .user("Generate a profile for a fictional character") .build()?; -let completion = structured::complete_object(&client, request, "profile", schema).await?; +let completion = client.complete_object(request, "profile", schema).await?; println!("Name: {}", completion.object["name"]); ``` ### Reasoning -`fabro_llm::reasoning::normalize(&response.content)` folds a response's readable reasoning parts into a `fabro_types::ReasoningOutput` with a summary and a trace. Provider replay data such as signatures and encrypted reasoning never appears in it. +`response.reasoning()` (a lithos method) folds a response's readable reasoning parts into a `ReasoningOutput` with a summary and a trace, whichever channel the provider used. Provider replay data such as signatures and encrypted reasoning never appears in it; `ContentPart::is_replay_material()` marks the parts a conversation keeps for the next request instead. ### Middleware -Middleware is the lithos `Middleware` trait: `handle(&self, call: Call, next: Next)` sees the resolved route and request and returns an `Output` that is either a complete response or a stream. `fabro_llm::attachments::InlineLocalAttachments` is Fabro's own middleware; it rewrites local file references in messages into inline media before dispatch. +Middleware is the lithos `Middleware` trait: `handle(&self, call: Call, next: Next)` sees the resolved route and request and returns an `Output` that is either a complete response or a stream. `ClientOptions::standard()` installs lithos's `InlineLocalFiles`, which rewrites local file paths in messages into inline media before dispatch. ### Error handling -Every fallible operation returns `Result`, the lithos error. `error.kind()` is an `ErrorKind` such as `Authentication`, `RateLimit`, `Server`, `ContextLength`, `ContentFilter`, `Timeout`, `StreamDecode`, or `Cancelled`. `error.data()` is the `ErrorData` snapshot Fabro stores in run events; `fabro_llm::LlmError` wraps it. +Every fallible operation returns `Result`, the lithos error. `error.kind()` is an `ErrorKind` such as `Authentication`, `RateLimit`, `Server`, `ContextLength`, `ContentFilter`, `Timeout`, `StreamDecode`, or `Cancelled`. `error.data()` is the `ErrorData` snapshot Fabro stores in run events; it reads like `Error`, prints its message, and implements `std::error::Error`. -The `fabro_llm::ErrorFacts` trait is implemented for `Error`, `ErrorData`, and `LlmError`, and the classification helpers take any of them: +Both `Error` and `ErrorData` answer the policy questions directly; only the loop-detection signature is Fabro's: | Function | Description | |---|---| -| `is_retryable(&error)` | Safe to retry with the same provider, from lithos's retry classification | -| `failover_eligible(&error)` | Safe to try a different provider | -| `is_auth_error(&error)` | The credential was missing or rejected | -| `is_cancelled(&error)` | The caller cancelled the call | -| `failure_signature_hint(&error)` | A stable string for loop and restart detection | +| `error.is_retryable()` | Safe to retry with the same provider, from lithos's retry classification | +| `error.failover_eligible()` | Safe to try a different provider | +| `error.is_auth_error()` | The credential was missing or rejected | +| `error.is_cancelled()` | The caller cancelled the call | +| `fabro_llm::failure_signature_hint(&data)` | A stable string for loop and restart detection | ### Retries diff --git a/lib/apps/fabro-cli/src/commands/exec.rs b/lib/apps/fabro-cli/src/commands/exec.rs index 535e94332..46340bdde 100644 --- a/lib/apps/fabro-cli/src/commands/exec.rs +++ b/lib/apps/fabro-cli/src/commands/exec.rs @@ -7,9 +7,9 @@ use fabro_agent::cli::{ OutputFormat, diagnostic_client_options, run_with_args_and_client_and_catalog, run_with_args_and_source_and_catalog, }; +use fabro_llm::ErrorKind; use fabro_llm::gateway::{GatewayAdapter, GatewayError, GatewayTransport}; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{ErrorFacts, ErrorKind}; use fabro_mcp::config::McpServerSettings; use fabro_types::ProviderId; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs index 6aaebce34..a04f7b943 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs @@ -836,7 +836,7 @@ mod tests { attempt: 1, delay_secs: 0.1, phase: fabro_types::LlmRetryPhase::Consume, - error: fabro_llm::LlmError::from(fabro_llm::Error::new( + error: fabro_llm::ErrorData::from(fabro_llm::Error::new( fabro_llm::ErrorKind::Configuration, "retry", )), @@ -955,7 +955,7 @@ mod tests { attempt: 2, delay_secs: 1.5, phase: fabro_types::LlmRetryPhase::Open, - error: fabro_llm::LlmError::from(fabro_llm::Error::new( + error: fabro_llm::ErrorData::from(fabro_llm::Error::new( fabro_llm::ErrorKind::Configuration, "busy", )), @@ -1319,7 +1319,7 @@ mod tests { attempt: 2, delay_secs: 1.5, phase: fabro_types::LlmRetryPhase::Open, - error: fabro_llm::LlmError::from(fabro_llm::Error::new( + error: fabro_llm::ErrorData::from(fabro_llm::Error::new( fabro_llm::ErrorKind::Configuration, "busy", )), diff --git a/lib/apps/fabro-server/src/run_title_generation.rs b/lib/apps/fabro-server/src/run_title_generation.rs index 070f18d09..f54be4e17 100644 --- a/lib/apps/fabro-server/src/run_title_generation.rs +++ b/lib/apps/fabro-server/src/run_title_generation.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use fabro_llm::{Client, Request, structured}; +use fabro_llm::{Client, Request}; use fabro_template::{TemplateContext, TemplateError}; use fabro_types::{Graph, MAX_RUN_TITLE_CHARS, ProviderId, RunId}; use fabro_util::error; @@ -56,13 +56,10 @@ pub(crate) async fn generate_title_or_current(input: GenerateTitleInput<'_>) -> } }; - let completion = match structured::complete_object( - &input.client, - request, - "run_title", - title_response_schema(), - ) - .await + let completion = match input + .client + .complete_object(request, "run_title", title_response_schema()) + .await { Ok(completion) => completion, Err(err) => { diff --git a/lib/apps/fabro-server/src/server/handler/completions.rs b/lib/apps/fabro-server/src/server/handler/completions.rs index b043e54fc..febd0c939 100644 --- a/lib/apps/fabro-server/src/server/handler/completions.rs +++ b/lib/apps/fabro-server/src/server/handler/completions.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{ModelSelectionError, Request, selection, structured}; +use fabro_llm::{ModelSelectionError, Request, selection}; use fabro_types::{Message, Role}; use super::super::{ @@ -138,7 +138,10 @@ async fn create_completion( } if let Some(schema) = req.schema { - return match structured::complete_object(&client, request, "output_schema", schema).await { + return match client + .complete_object(request, "output_schema", schema) + .await + { Ok(completion) => { let mut body = match serde_json::to_value(&completion.response) { Ok(body) => body, diff --git a/lib/components/fabro-agent/src/error.rs b/lib/components/fabro-agent/src/error.rs index b3fb48592..21dca27f2 100644 --- a/lib/components/fabro-agent/src/error.rs +++ b/lib/components/fabro-agent/src/error.rs @@ -1,4 +1,4 @@ -use fabro_llm::LlmError; +use fabro_llm::ErrorData; /// Why a session was interrupted. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -21,7 +21,7 @@ impl std::fmt::Display for InterruptReason { #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum CompactionError { #[error("summary request failed: {0}")] - Llm(#[source] LlmError), + Llm(#[source] Box), #[error( "generated summary was empty after trimming; refused to replace \ @@ -34,9 +34,10 @@ pub enum CompactionError { #[serde(tag = "type", content = "data", rename_all = "snake_case")] pub enum Error { /// A provider call failed. Carries lithos's stored error projection so - /// the failure stays cloneable and serializable. + /// the failure stays cloneable and serializable. Boxed because the + /// projection is large and every other variant is small. #[error("LLM error: {0}")] - Llm(#[from] LlmError), + Llm(Box), #[error("Context compaction failed: {0}")] Compaction(#[from] CompactionError), @@ -54,15 +55,27 @@ pub enum Error { Interrupted(InterruptReason), } +impl From for Error { + fn from(error: ErrorData) -> Self { + Self::Llm(Box::new(error)) + } +} + impl From for Error { fn from(error: fabro_llm::Error) -> Self { - Self::Llm(LlmError::from(error)) + Self::from(ErrorData::from(error)) + } +} + +impl From for CompactionError { + fn from(error: ErrorData) -> Self { + Self::Llm(Box::new(error)) } } impl From for CompactionError { fn from(error: fabro_llm::Error) -> Self { - Self::Llm(LlmError::from(error)) + Self::from(ErrorData::from(error)) } } @@ -72,14 +85,14 @@ pub type Result = std::result::Result; mod tests { use std::time::Duration; - use fabro_llm::{ErrorFacts, ErrorKind, RetryClassification}; + use fabro_llm::{ErrorKind, RetryClassification}; use fabro_types::provider_ids; use fabro_util::error; use super::*; - fn network_error(message: &str) -> LlmError { - LlmError::from( + fn network_error(message: &str) -> ErrorData { + ErrorData::from( fabro_llm::Error::new(ErrorKind::Network, message) .with_retry(RetryClassification::Safe), ) @@ -95,7 +108,7 @@ mod tests { #[test] fn compaction_error_preserves_llm_source_chain() { - let err = Error::Compaction(CompactionError::Llm(network_error("connection refused"))); + let err = Error::Compaction(CompactionError::from(network_error("connection refused"))); let chain = error::collect_chain(&err); @@ -157,7 +170,7 @@ mod tests { #[test] fn serde_roundtrip_llm_network() { - let err = Error::Llm(network_error("connection refused")); + let err = Error::from(network_error("connection refused")); let json = serde_json::to_string(&err).unwrap(); let deserialized: Error = serde_json::from_str(&json).unwrap(); assert_eq!(err.to_string(), deserialized.to_string()); @@ -165,7 +178,7 @@ mod tests { #[test] fn serde_roundtrip_llm_provider() { - let err = Error::Llm(LlmError::from( + let err = Error::from(ErrorData::from( fabro_llm::Error::new(ErrorKind::RateLimit, "too fast") .with_provider(provider_ids::openai()) .with_status(429) @@ -229,7 +242,7 @@ mod tests { #[test] fn clone_all_variants() { let errors: Vec = vec![ - Error::Llm(network_error("refused")), + Error::from(network_error("refused")), Error::Compaction(CompactionError::EmptySummary { summarized_turn_count: 3, }), @@ -247,7 +260,7 @@ mod tests { #[test] fn serde_tag_format_llm() { - let err = Error::Llm(network_error("refused")); + let err = Error::from(network_error("refused")); let json = serde_json::to_string(&err).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(v["type"], "llm"); diff --git a/lib/components/fabro-agent/src/history.rs b/lib/components/fabro-agent/src/history.rs index c898d0dce..addd9325c 100644 --- a/lib/components/fabro-agent/src/history.rs +++ b/lib/components/fabro-agent/src/history.rs @@ -1,6 +1,5 @@ use std::collections::HashSet; -use fabro_llm::reasoning; use fabro_types::{Message as LlmMessage, SessionMessage, TokenCounts}; use crate::types::Message; @@ -87,7 +86,7 @@ impl History { fn strip_opaque_provider_items(&mut self) { for turn in &mut self.turns { if let Message::Assistant { provider_parts, .. } = turn { - provider_parts.retain(|p| !reasoning::is_opaque_openai(p)); + provider_parts.retain(|p| !p.is_opaque_openai()); } } } @@ -164,7 +163,7 @@ fn add_tool_result_call_ids<'a>(turns: &'a [Message], call_ids: &mut HashSet<&'a mod tests { use std::time::SystemTime; - use fabro_llm::reasoning::OPENAI_REASONING_KIND; + use fabro_llm::types::OPENAI_REASONING_KIND; use fabro_types::{ ContentPart, ReasoningContent, Role, TokenCounts, ToolCall, text_of, tool_result_from_json, }; diff --git a/lib/components/fabro-agent/src/session.rs b/lib/components/fabro-agent/src/session.rs index cc562da08..15500c1fd 100644 --- a/lib/components/fabro-agent/src/session.rs +++ b/lib/components/fabro-agent/src/session.rs @@ -4,8 +4,8 @@ use std::time::{Duration, Instant, SystemTime}; use fabro_llm::types::ContentBlockKind; use fabro_llm::{ - CallContext, Client, FinishReason, LlmError, Request, Response, RetryClassification, - RetryListener, RetryStage, StreamEvent, reasoning, + CallContext, Client, ErrorData, FinishReason, Request, Response, RetryClassification, + RetryListener, RetryStage, StreamEvent, }; use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_mcp::connection_manager::McpConnectionManager; @@ -1138,14 +1138,14 @@ impl Session { } fn emit_llm_error(&mut self, err: fabro_llm::Error) -> Error { - let err = LlmError::from(err); + let err = ErrorData::from(err); self.event_emitter.emit(self.id.clone(), AgentEvent::Error { - error: Error::Llm(err.clone()), + error: Error::from(err.clone()), }); if err.is_auth_error() { self.transition(SessionState::Closed); } - Error::Llm(err) + Error::from(err) } #[must_use] @@ -1585,11 +1585,11 @@ impl Session { let text = response.text(); let tool_calls: Vec = response.tool_calls().cloned().collect(); // Normalize before the response's content moves into history. - let reasoning = reasoning::normalize(&response.content); + let reasoning = response.reasoning(); let provider_parts: Vec<_> = response .content .iter() - .filter(|part| reasoning::is_provider_part(part)) + .filter(|part| part.is_replay_material()) .cloned() .collect(); let usage = response.usage; @@ -1814,7 +1814,7 @@ impl Session { model: requested_model.model_id.to_string(), attempt: usize::try_from(replay_attempt).unwrap_or(usize::MAX), delay_secs: delay.as_secs_f64(), - error: LlmError::from(&error), + error: ErrorData::from(&error), phase: LlmRetryPhase::Consume, }); @@ -2243,10 +2243,11 @@ mod tests { use anyhow::Context as _; use fabro_llm::adapter::{ProviderAdapter, ResolvedCall}; use fabro_llm::lithos_catalog::AdapterId; - use fabro_llm::reasoning::OPENAI_COMPAT_REASONING_DETAILS_KIND; use fabro_llm::test_support::response_to_stream; - use fabro_llm::types::{ContentBlockId, ContentBlockKind, ToolCallKind}; - use fabro_llm::{ErrorFacts, ErrorKind, ResponseStream, RetryPolicy}; + use fabro_llm::types::{ + ContentBlockId, ContentBlockKind, OPENAI_COMPAT_REASONING_DETAILS_KIND, ToolCallKind, + }; + use fabro_llm::{ErrorKind, ResponseStream, RetryPolicy}; use fabro_types::{ ContentPart, Cost, CostSource, ReasoningOutput, StageContextWindowCountMethod, ToolDefinition, provider_ids, text_of, tool_result_to_json, diff --git a/lib/components/fabro-agent/src/types.rs b/lib/components/fabro-agent/src/types.rs index cf1627e03..b31458592 100644 --- a/lib/components/fabro-agent/src/types.rs +++ b/lib/components/fabro-agent/src/types.rs @@ -1,7 +1,7 @@ use std::time::SystemTime; use chrono::{DateTime, Utc}; -use fabro_llm::LlmError; +use fabro_llm::ErrorData; use fabro_types::{ CommandTermination, ContentPart, Cost, ExecOutputTail, LlmOutputKind, LlmRetryPhase, Message as LlmMessage, ModelRef, ReasoningOutput, Role, SessionMessage, Speed, @@ -401,7 +401,7 @@ pub enum AgentEvent { model: String, attempt: usize, delay_secs: f64, - error: LlmError, + error: ErrorData, phase: LlmRetryPhase, }, SubAgentSpawned { @@ -816,13 +816,13 @@ pub struct SessionEvent { #[cfg(test)] mod tests { - use fabro_llm::{ErrorFacts, ErrorKind, RetryClassification}; + use fabro_llm::{ErrorKind, RetryClassification}; use fabro_types::{CostSource, ModelId, ProviderId, provider_ids}; use super::*; - fn network_error(message: &str) -> LlmError { - LlmError::from( + fn network_error(message: &str) -> ErrorData { + ErrorData::from( fabro_llm::Error::new(ErrorKind::Network, message) .with_retry(RetryClassification::Safe), ) @@ -1152,7 +1152,7 @@ mod tests { #[test] fn error_event_serde_roundtrip_with_agent_error() { let event = AgentEvent::Error { - error: Error::Llm(network_error("refused")), + error: Error::from(network_error("refused")), }; let json = serde_json::to_string(&event).unwrap(); let deserialized: AgentEvent = serde_json::from_str(&json).unwrap(); @@ -1172,7 +1172,7 @@ mod tests { attempt: 1, delay_secs: 2.0, phase: LlmRetryPhase::Open, - error: LlmError::from( + error: ErrorData::from( fabro_llm::Error::new(ErrorKind::RateLimit, "too fast") .with_provider(ProviderId::new("openai")) .with_status(429) diff --git a/lib/components/fabro-hooks/src/executor.rs b/lib/components/fabro-hooks/src/executor.rs index 418a77270..28e86b08c 100644 --- a/lib/components/fabro-hooks/src/executor.rs +++ b/lib/components/fabro-hooks/src/executor.rs @@ -8,7 +8,7 @@ use fabro_agent::Sandbox; use fabro_agent::tool_registry::ToolContext; use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{Client, ClientOptions, Request, structured}; +use fabro_llm::{Client, ClientOptions, Request}; use fabro_redact::redacted_url_for_log; use fabro_types::settings::{InterpString, ResolveCtx, ResolveError}; use fabro_types::{Message, Role, ToolCall, tool_call_arguments, tool_result_from_json}; @@ -320,12 +320,8 @@ impl HookExecutorImpl { } }; - match structured::complete_object( - &client, - request, - "hook_response", - HOOK_RESPONSE_SCHEMA.clone(), - ) + match client + .complete_object(request, "hook_response", HOOK_RESPONSE_SCHEMA.clone()) .await { Ok(completion) => { diff --git a/lib/components/fabro-llm/Cargo.toml b/lib/components/fabro-llm/Cargo.toml index 546fd1f1e..cccc5fee2 100644 --- a/lib/components/fabro-llm/Cargo.toml +++ b/lib/components/fabro-llm/Cargo.toml @@ -19,7 +19,6 @@ workspace = true [dependencies] anyhow.workspace = true async-trait.workspace = true -base64.workspace = true bytes.workspace = true fabro-auth = { path = "../../foundation/fabro-auth" } fabro-config = { path = "../../foundation/fabro-config" } @@ -28,8 +27,7 @@ fabro-redact.workspace = true fabro-static.workspace = true fabro-types = { path = "../../foundation/fabro-types" } futures.workspace = true -lithos-llm = { workspace = true, features = ["builtin-catalog", "openai", "anthropic", "gemini", "openai-compatible", "bedrock", "bedrock-aws"] } -mime_guess = "2" +lithos-llm = { workspace = true, features = ["builtin-catalog", "openai", "anthropic", "gemini", "openai-compatible", "bedrock", "bedrock-aws", "local-files"] } serde.workspace = true serde_json.workspace = true strum.workspace = true @@ -45,5 +43,4 @@ fabro-llm = { path = ".", features = ["test-support"] } fabro-macros = { path = "../../foundation/fabro-macros" } fabro-test = { workspace = true } httpmock = "0.8" -tempfile = "3" tokio = { workspace = true, features = ["test-util", "macros"] } diff --git a/lib/components/fabro-llm/src/attachments.rs b/lib/components/fabro-llm/src/attachments.rs deleted file mode 100644 index 37c9c8432..000000000 --- a/lib/components/fabro-llm/src/attachments.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Inlines local file attachments before a request reaches a codec. -//! -//! lithos accepts media as a URL or as base64. Fabro lets a caller point an -//! image, document, or audio part at a local path; this middleware reads the -//! file and rewrites the part to inline base64 with an inferred media type. -//! A part whose file cannot be read is dropped, so the model sees the rest of -//! the message rather than a request that fails outright. - -use std::sync::Arc; - -use async_trait::async_trait; -use base64::Engine as _; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; -use fabro_static::EnvVars; -use lithos_llm::middleware::{Call, Middleware, Next, Output}; -use lithos_llm::types::{ - AudioContent, ContentPart, DocumentContent, Error, ImageContent, MediaSource, Message, Request, - ToolResult, -}; -use tokio::fs; - -/// Resolves an environment variable name to its value. -type EnvLookup = Arc Option + Send + Sync>; - -/// Middleware that inlines local-path media parts. -#[derive(Clone, Default)] -pub struct InlineLocalAttachments { - env_lookup: Option, -} - -impl std::fmt::Debug for InlineLocalAttachments { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("InlineLocalAttachments") - .finish_non_exhaustive() - } -} - -impl InlineLocalAttachments { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// Resolves `~/` against this lookup instead of the process environment. - #[must_use] - pub fn with_env_lookup(env_lookup: EnvLookup) -> Self { - Self { - env_lookup: Some(env_lookup), - } - } - - #[expect( - clippy::disallowed_methods, - reason = "Attachment path expansion supports the conventional HOME env var." - )] - fn home(&self) -> Option { - match &self.env_lookup { - Some(lookup) => lookup(EnvVars::HOME), - None => std::env::var(EnvVars::HOME).ok(), - } - } - - fn expand(&self, path: &str) -> String { - path.strip_prefix("~/").map_or_else( - || path.to_string(), - |rest| format!("{}/{rest}", self.home().unwrap_or_else(|| "/".to_string())), - ) - } - - async fn load(&self, path: &str) -> Option { - let expanded = self.expand(path); - match fs::read(&expanded).await { - Ok(bytes) => Some(MediaSource::base64( - BASE64_STANDARD.encode(bytes), - media_type_for_path(&expanded), - )), - Err(err) => { - tracing::warn!(path = %expanded, error = %err, "dropping unreadable attachment"); - None - } - } - } - - async fn inline_part(&self, part: ContentPart) -> Option { - match part { - ContentPart::Image(ImageContent { source, detail }) if is_local_file(&source) => { - let source = self.load(url_of(&source)).await?; - Some(ContentPart::Image(ImageContent { source, detail })) - } - ContentPart::Document(DocumentContent { source, name }) if is_local_file(&source) => { - let source = self.load(url_of(&source)).await?; - Some(ContentPart::Document(DocumentContent { source, name })) - } - ContentPart::Audio(AudioContent { source }) if is_local_file(&source) => { - let source = self.load(url_of(&source)).await?; - Some(ContentPart::Audio(AudioContent { source })) - } - ContentPart::ToolResult(result) if result.content.iter().any(part_is_local_file) => { - let mut content = Vec::with_capacity(result.content.len()); - for part in result.content { - if let Some(part) = Box::pin(self.inline_part(part)).await { - content.push(part); - } - } - Some(ContentPart::ToolResult(ToolResult { content, ..result })) - } - other => Some(other), - } - } - - async fn inline_request(&self, request: Request) -> Request { - let mut messages = Vec::with_capacity(request.messages().len()); - for message in request.messages() { - let mut content = Vec::with_capacity(message.content().len()); - for part in message.content() { - if let Some(part) = self.inline_part(part.clone()).await { - content.push(part); - } - } - let mut rebuilt = Message::new(message.role(), content); - if let Some(name) = message.name() { - rebuilt = rebuilt.with_name(name); - } - if let Some(id) = message.tool_call_id() { - rebuilt = rebuilt.with_tool_call_id(id); - } - messages.push(rebuilt); - } - replace_messages(&request, messages).unwrap_or(request) - } -} - -/// Rebuilds `request` with `messages` in place of its own. -/// -/// The request builder appends messages and has no way to clear them, so the -/// swap goes through the request's serde form. -fn replace_messages(request: &Request, messages: Vec) -> Option { - let mut value = serde_json::to_value(request).ok()?; - value["messages"] = serde_json::to_value(messages).ok()?; - serde_json::from_value(value).ok() -} - -fn part_is_local_file(part: &ContentPart) -> bool { - match part { - ContentPart::Image(ImageContent { source, .. }) - | ContentPart::Document(DocumentContent { source, .. }) - | ContentPart::Audio(AudioContent { source }) => is_local_file(source), - _ => false, - } -} - -fn url_of(source: &MediaSource) -> &str { - match source { - MediaSource::Url { url, .. } => url, - _ => "", - } -} - -fn is_local_file(source: &MediaSource) -> bool { - matches!( - source, - MediaSource::Url { url, .. } - if url.starts_with('/') || url.starts_with("./") || url.starts_with("~/") - ) -} - -fn needs_inlining(request: &Request) -> bool { - request.messages().iter().any(|message| { - message.content().iter().any(|part| match part { - ContentPart::ToolResult(result) => result.content.iter().any(part_is_local_file), - part => part_is_local_file(part), - }) - }) -} - -/// Media type for a local path, from its extension. -#[must_use] -pub fn media_type_for_path(path: &str) -> String { - mime_guess::from_path(path) - .first_raw() - .unwrap_or("application/octet-stream") - .to_string() -} - -#[async_trait] -impl Middleware for InlineLocalAttachments { - async fn handle(&self, call: Call, next: Next) -> Result { - if !needs_inlining(call.request()) { - return next.run(call).await; - } - let inlined = self.inline_request(call.request().clone()).await; - let call = call.map_request(|_| Ok(inlined))?; - next.run(call).await - } -} - -#[cfg(test)] -mod tests { - use lithos_llm::types::Role; - - use super::*; - - fn request_with(part: ContentPart) -> Request { - Request::builder() - .model("openai/gpt-5.4") - .message(Message::new(Role::User, [ - ContentPart::Text { - text: "look".to_string(), - }, - part, - ])) - .build() - .unwrap() - } - - #[tokio::test] - async fn inlines_local_images_and_drops_missing_files() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("pixel.png"); - fs::write(&path, b"\x89PNG").await.unwrap(); - let middleware = InlineLocalAttachments::new(); - - let request = request_with(ContentPart::Image(ImageContent::new(MediaSource::url( - path.to_string_lossy().to_string(), - )))); - let inlined = middleware.inline_request(request).await; - match &inlined.messages()[0].content()[1] { - ContentPart::Image(image) => { - assert_eq!(image.source.media_type(), Some("image/png")); - assert_eq!( - image.source.base64_data(), - Some(BASE64_STANDARD.encode(b"\x89PNG").as_str()) - ); - } - other => panic!("expected inlined image, got {other:?}"), - } - - let missing = request_with(ContentPart::Document(DocumentContent::new( - MediaSource::url("/definitely/missing.pdf"), - ))); - let inlined = middleware.inline_request(missing).await; - assert_eq!(inlined.messages()[0].content().len(), 1); - } - - #[test] - fn remote_urls_and_inline_data_pass_through() { - let request = request_with(ContentPart::Image(ImageContent::new(MediaSource::url( - "https://example.com/a.png", - )))); - assert!(!needs_inlining(&request)); - let request = request_with(ContentPart::Image(ImageContent::new(MediaSource::base64( - "AAAA", - "image/png", - )))); - assert!(!needs_inlining(&request)); - let request = request_with(ContentPart::Image(ImageContent::new(MediaSource::url( - "~/shot.png", - )))); - assert!(needs_inlining(&request)); - } - - #[test] - fn media_types_follow_extensions() { - assert_eq!(media_type_for_path("a.jpg"), "image/jpeg"); - assert_eq!(media_type_for_path("a.pdf"), "application/pdf"); - assert_eq!(media_type_for_path("a.bin"), "application/octet-stream"); - } -} diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index 708819e7f..9a7ff7674 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -9,12 +9,9 @@ use lithos_llm::catalog::Catalog; use lithos_llm::client::{Client, ClientBuildError, ClientBuilder, ProviderBuildIssue}; use lithos_llm::credentials::{CredentialError, CredentialProvider}; use lithos_llm::middleware::{ - Call, Middleware, Observer, RetryMiddleware, RetryPolicy, RetryStage, + Call, InlineLocalFiles, Middleware, Observer, RetryMiddleware, RetryPolicy, RetryStage, }; -use lithos_llm::types::Error; - -use crate::attachments::InlineLocalAttachments; -use crate::error::LlmError; +use lithos_llm::types::{Error, ErrorData}; /// The application name lithos reports to providers that ask, such as the /// `originator` header on the OpenAI Codex deployment. @@ -36,7 +33,7 @@ pub fn default_retry_policy() -> RetryPolicy { #[derive(Clone, Debug)] pub struct RetryNotice { /// The failure that ended the attempt. - pub error: LlmError, + pub error: ErrorData, /// The attempt that failed, counted from 1. pub attempt: u32, /// How long the middleware waits before the next attempt. @@ -78,7 +75,7 @@ impl Observer for RetryNotifier { ) { if let Some(listener) = call.context().extensions().get::() { listener.notify(RetryNotice { - error: LlmError::from(error), + error: ErrorData::from(error), attempt, delay, stage, @@ -150,7 +147,7 @@ impl ClientOptions { builder = builder.middleware(retry_middleware(policy)); } if self.inline_attachments { - builder = builder.middleware(InlineLocalAttachments::new()); + builder = builder.middleware(InlineLocalFiles::new()); } for middleware in self.middleware { builder = builder.middleware_arc(middleware); diff --git a/lib/components/fabro-llm/src/error.rs b/lib/components/fabro-llm/src/error.rs index 05eec0e1c..6444b5597 100644 --- a/lib/components/fabro-llm/src/error.rs +++ b/lib/components/fabro-llm/src/error.rs @@ -1,270 +1,41 @@ -//! Classification of lithos errors for Fabro's retry, failover, and failure -//! signature policies, plus the stored form of a failure. +//! The one failure-classification rule that is Fabro's own. //! -//! lithos's live [`Error`] carries a source chain and is therefore neither -//! `Clone` nor serializable. Fabro records failures in events and agent -//! errors, so it works with [`LlmError`], a thin wrapper over lithos's own -//! [`ErrorData`] projection. Every policy here reads through [`ErrorFacts`] -//! and so applies to both forms. - -use std::fmt; -use std::time::Duration; +//! Retry, auth, cancellation, and failover questions are answered by the +//! lithos `Error` and `ErrorData` themselves. What stays here is the loop and +//! restart detector's signature format, which names Fabro's own categories. use fabro_types::ProviderId; -use lithos_llm::types::{Error, ErrorData, ErrorKind, RetryClassification}; -use serde::{Deserialize, Serialize}; - -/// The facts Fabro's policies read from an LLM failure. -pub trait ErrorFacts { - fn kind(&self) -> ErrorKind; - fn message(&self) -> &str; - fn provider(&self) -> Option<&ProviderId>; - fn provider_code(&self) -> Option<&str>; - fn status(&self) -> Option; - fn retry_classification(&self) -> RetryClassification; - - /// The delay the classification advises, when repeating is safe after - /// a wait. - fn retry_after(&self) -> Option { - self.retry_classification().delay() - } -} - -impl ErrorFacts for Error { - fn kind(&self) -> ErrorKind { - Self::kind(self) - } - - fn message(&self) -> &str { - Self::message(self) - } - - fn provider(&self) -> Option<&ProviderId> { - Self::provider(self) - } - - fn provider_code(&self) -> Option<&str> { - Self::provider_code(self) - } - - fn status(&self) -> Option { - Self::status(self) - } - - fn retry_classification(&self) -> RetryClassification { - Self::retry_classification(self) - } -} - -impl ErrorFacts for ErrorData { - fn kind(&self) -> ErrorKind { - self.kind.clone() - } - - fn message(&self) -> &str { - &self.message - } - - fn provider(&self) -> Option<&ProviderId> { - self.provider.as_ref() - } - - fn provider_code(&self) -> Option<&str> { - self.provider_code.as_deref() - } - - fn status(&self) -> Option { - self.status - } - - fn retry_classification(&self) -> RetryClassification { - self.retry - } -} - -/// A cloneable, serializable LLM failure. -/// -/// This is lithos's [`ErrorData`] projection with Fabro's policy helpers -/// attached. It is what agent errors, run events, and API responses carry; -/// the live [`Error`] converts into it at the boundary where a failure stops -/// being handled and starts being recorded. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(transparent)] -pub struct LlmError(Box); - -impl LlmError { - /// A failure Fabro itself raises, never retried. - #[must_use] - pub fn new(kind: ErrorKind, message: impl Into) -> Self { - Self::from(Error::new(kind, message)) - } - - #[must_use] - pub fn data(&self) -> &ErrorData { - &self.0 - } - - #[must_use] - pub fn into_data(self) -> ErrorData { - *self.0 - } - - /// The immediate source of the failure, rendered as text. - #[must_use] - pub fn source_message(&self) -> Option<&str> { - self.0.source_message.as_deref() - } - - /// The provider's advised wait, whatever the error kind. - #[must_use] - pub fn provider_retry_after(&self) -> Option { - self.0 - .provider_retry_after_millis - .map(Duration::from_millis) - } - - #[must_use] - pub fn is_retryable(&self) -> bool { - is_retryable(self) - } - - #[must_use] - pub fn is_auth_error(&self) -> bool { - is_auth_error(self) - } - - #[must_use] - pub fn is_cancelled(&self) -> bool { - is_cancelled(self) - } - - #[must_use] - pub fn failover_eligible(&self) -> bool { - failover_eligible(self) - } - - #[must_use] - pub fn failure_signature_hint(&self) -> String { - failure_signature_hint(self) - } -} - -impl ErrorFacts for LlmError { - fn kind(&self) -> ErrorKind { - self.0.kind.clone() - } - - fn message(&self) -> &str { - &self.0.message - } - - fn provider(&self) -> Option<&ProviderId> { - self.0.provider.as_ref() - } - - fn provider_code(&self) -> Option<&str> { - self.0.provider_code.as_deref() - } - - fn status(&self) -> Option { - self.0.status - } - - fn retry_classification(&self) -> RetryClassification { - self.0.retry - } -} - -impl fmt::Display for LlmError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str(&self.0.message) - } -} - -impl std::error::Error for LlmError {} - -impl From for LlmError { - fn from(error: Error) -> Self { - Self(Box::new(error.data())) - } -} - -impl From<&Error> for LlmError { - fn from(error: &Error) -> Self { - Self(Box::new(error.data())) - } -} - -impl From for LlmError { - fn from(data: ErrorData) -> Self { - Self(Box::new(data)) - } -} - -/// Whether repeating the same call on the same provider may succeed. -#[must_use] -pub fn is_retryable(error: &E) -> bool { - !matches!(error.retry_classification(), RetryClassification::Never) -} - -/// Whether the failure came from a credential problem. -#[must_use] -pub fn is_auth_error(error: &E) -> bool { - matches!( - error.kind(), - ErrorKind::Authentication | ErrorKind::AccessDenied - ) -} - -/// Whether the call was cancelled by Fabro rather than failed by the provider. -#[must_use] -pub fn is_cancelled(error: &E) -> bool { - error.kind() == ErrorKind::Cancelled -} - -/// Whether another provider is worth trying. -/// -/// Everything retryable qualifies, plus failures that are local to this -/// provider: credentials, access policy, model inventory, quota, and a -/// provider that ran out of time. A different provider has its own. -#[must_use] -pub fn failover_eligible(error: &E) -> bool { - if is_retryable(error) { - return true; - } - matches!( - error.kind(), - ErrorKind::Authentication - | ErrorKind::AccessDenied - | ErrorKind::NotFound - | ErrorKind::QuotaExceeded - | ErrorKind::RateLimit - | ErrorKind::Server - | ErrorKind::Network - | ErrorKind::Timeout - | ErrorKind::StreamDecode - ) || (error.kind() == ErrorKind::ContentFilter && error.provider_code() == Some("refusal")) -} +use lithos_llm::types::{ErrorData, ErrorKind}; /// A stable `category|provider|detail` string for loop and restart detection. +/// +/// The category is `api_canceled` for a cancelled call, `api_transient` for a +/// failure the provider may be asked to repeat, and `api_deterministic` for +/// everything else; the detail is the error kind's stored spelling. #[must_use] -pub fn failure_signature_hint(error: &E) -> String { +pub fn failure_signature_hint(error: &ErrorData) -> String { let provider = error.provider().map_or("unknown", ProviderId::as_str); - let category = match error.kind() { - ErrorKind::Cancelled => "api_canceled", - _ if is_retryable(error) => "api_transient", - _ => "api_deterministic", + let category = if error.is_cancelled() { + "api_canceled" + } else if error.is_retryable() { + "api_transient" + } else { + "api_deterministic" }; - let detail = error.kind().as_str().to_string(); - format!("{category}|{provider}|{detail}") + let kind: ErrorKind = error.kind(); + format!("{category}|{provider}|{}", kind.as_str()) } #[cfg(test)] mod tests { + use lithos_llm::types::{Error, RetryClassification}; + use super::*; - fn error(kind: ErrorKind) -> Error { - Error::new(kind, "boom").with_provider(ProviderId::new("openai")) + fn error(kind: ErrorKind) -> ErrorData { + Error::new(kind, "boom") + .with_provider(ProviderId::new("openai")) + .data() } #[test] @@ -275,7 +46,10 @@ mod tests { ); assert_eq!( failure_signature_hint( - &error(ErrorKind::RateLimit).with_retry(RetryClassification::Safe) + &Error::new(ErrorKind::RateLimit, "boom") + .with_provider(ProviderId::new("openai")) + .with_retry(RetryClassification::Safe) + .data() ), "api_transient|openai|rate_limit" ); @@ -284,42 +58,4 @@ mod tests { "api_canceled|openai|cancelled" ); } - - #[test] - fn failover_covers_provider_local_failures() { - assert!(failover_eligible(&error(ErrorKind::Authentication))); - assert!(failover_eligible(&error(ErrorKind::QuotaExceeded))); - assert!(!failover_eligible(&error(ErrorKind::InvalidRequest))); - assert!(!failover_eligible(&error(ErrorKind::ContextLength))); - assert!(!failover_eligible(&error(ErrorKind::ContentFilter))); - assert!(failover_eligible( - &error(ErrorKind::ContentFilter).with_provider_code("refusal") - )); - } - - #[test] - fn stored_errors_keep_the_facts_and_round_trip() { - let live = error(ErrorKind::RateLimit) - .with_status(429) - .with_provider_code("slow") - .with_retry(RetryClassification::after(Duration::from_secs(2))) - .with_source(std::io::Error::other("socket closed")); - let stored = LlmError::from(&live); - assert_eq!(stored.kind(), ErrorKind::RateLimit); - assert_eq!(stored.status(), Some(429)); - assert_eq!(stored.provider_code(), Some("slow")); - assert_eq!(stored.retry_after(), Some(Duration::from_secs(2))); - assert_eq!(stored.source_message(), Some("socket closed")); - assert_eq!(stored.to_string(), "boom"); - assert!(stored.is_retryable()); - assert_eq!( - stored.failure_signature_hint(), - failure_signature_hint(&live) - ); - - let json = serde_json::to_value(&stored).unwrap(); - assert_eq!(json["kind"], "rate_limit"); - let decoded: LlmError = serde_json::from_value(json).unwrap(); - assert_eq!(decoded, stored); - } } diff --git a/lib/components/fabro-llm/src/lib.rs b/lib/components/fabro-llm/src/lib.rs index 4e325b1c1..27199a2ed 100644 --- a/lib/components/fabro-llm/src/lib.rs +++ b/lib/components/fabro-llm/src/lib.rs @@ -8,28 +8,23 @@ //! - 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; -//! - constructing a client from a Fabro credential source ([`client`]); -//! - inlining local file attachments ([`attachments`]); -//! - normalizing readable reasoning into [`fabro_types::ReasoningOutput`] -//! ([`reasoning`]); -//! - one-shot structured output ([`structured`]); +//! - constructing a client from a Fabro credential store ([`client`]); //! - model and provider probes ([`probe`]), and the API views of the catalog //! ([`api`]); //! - the `fabro exec` gateway adapter that speaks to a Fabro server //! ([`gateway`]); -//! - error classification for retries, failover, and failure signatures -//! ([`error`]). +//! - the failure signature loop detection reads ([`error`]). +//! +//! Local-file inlining, structured output, readable-reasoning normalization, +//! and the retry, auth, and failover predicates are lithos-llm's own. pub mod api; -pub mod attachments; pub mod catalog; pub mod client; pub mod error; pub mod gateway; pub mod probe; -pub mod reasoning; pub mod selection; -pub mod structured; #[cfg(any(test, feature = "test-support"))] pub mod test_support; @@ -38,7 +33,7 @@ pub use client::{ ClientOptions, FabroClient, LlmSetupError, RetryListener, RetryNotice, build_client, build_offline_client, configured_providers, }; -pub use error::{ErrorFacts, LlmError}; +pub use error::failure_signature_hint; pub use lithos_llm::client::{Client, ClientBuild}; pub use lithos_llm::middleware::{CallContext, CancellationToken, RetryPolicy, RetryStage}; pub use lithos_llm::resolver::ModelSelectionError as RouteSelectionError; diff --git a/lib/components/fabro-llm/src/reasoning.rs b/lib/components/fabro-llm/src/reasoning.rs deleted file mode 100644 index ca34594d2..000000000 --- a/lib/components/fabro-llm/src/reasoning.rs +++ /dev/null @@ -1,292 +0,0 @@ -//! Normalization of provider reasoning material into [`ReasoningOutput`]. -//! -//! Every provider that returns readable reasoning does it differently, and -//! several return more than one channel at once. This module reduces a final -//! response's content parts to the two normalized fields without reaching -//! into opaque material (signatures, item ids, encrypted payloads) and -//! without failing a completion it cannot classify. -//! -//! Parsing is deliberately tolerant: provider payloads are read as -//! `serde_json::Value` with optional lookups, so unknown detail variants, -//! missing members, extra members, and unexpected member types are ignored -//! rather than surfaced as errors. - -use fabro_types::{ContentPart, ReasoningOutput}; - -/// OpenAI Responses reasoning items, as lithos stores them. -pub const OPENAI_REASONING_KIND: &str = "openai.reasoning"; -/// OpenAI Responses message items, as lithos stores them. -pub const OPENAI_MESSAGE_KIND: &str = "openai.message"; -/// OpenAI-compatible `reasoning_details` arrays, as lithos stores them. -pub const OPENAI_COMPAT_REASONING_DETAILS_KIND: &str = "openai_compatible.reasoning_details"; - -/// Separator between distinct complete reasoning blocks. -const BLOCK_SEPARATOR: &str = "\n\n"; - -#[derive(Default)] -struct Blocks<'a> { - explicit_summary: Vec<&'a str>, - explicit_trace: Vec<&'a str>, - fallback_trace: Vec<&'a str>, -} - -impl Blocks<'_> { - fn into_output(self) -> Option { - let summary = join_blocks(&self.explicit_summary); - let trace = join_blocks(&self.explicit_trace) - .or_else(|| join_blocks(&self.fallback_trace)) - .filter(|trace| summary.as_ref() != Some(trace)); - - match (summary, trace) { - (Some(summary), Some(trace)) => Some(ReasoningOutput::new(summary, trace)), - (Some(summary), None) => Some(ReasoningOutput::from_summary(summary)), - (None, Some(trace)) => Some(ReasoningOutput::from_trace(trace)), - (None, None) => None, - } - } -} - -fn join_blocks(blocks: &[&str]) -> Option { - (!blocks.is_empty()).then(|| blocks.join(BLOCK_SEPARATOR)) -} - -fn push_block<'a>(blocks: &mut Vec<&'a str>, block: &'a str) { - if !block.trim().is_empty() { - blocks.push(block); - } -} - -fn readable_member<'a>(entry: &'a serde_json::Value, member: &str) -> Option<&'a str> { - entry.get(member).and_then(serde_json::Value::as_str) -} - -fn collect_openai_reasoning_item<'a>(item: &'a serde_json::Value, blocks: &mut Blocks<'a>) { - if let Some(entries) = item.get("summary").and_then(serde_json::Value::as_array) { - for entry in entries { - if let Some(text) = entry.as_str() { - push_block(&mut blocks.explicit_summary, text); - } else if let Some(text) = entry.get("text").and_then(serde_json::Value::as_str) { - push_block(&mut blocks.explicit_summary, text); - } - } - } - if let Some(entries) = item.get("content").and_then(serde_json::Value::as_array) { - for entry in entries { - let Some(text) = entry.get("text").and_then(serde_json::Value::as_str) else { - continue; - }; - let entry_type = entry - .get("type") - .and_then(serde_json::Value::as_str) - .unwrap_or_default(); - if entry_type == "reasoning_text" { - push_block(&mut blocks.explicit_trace, text); - } - } - } -} - -fn collect_reasoning_details<'a>(details: &'a serde_json::Value, blocks: &mut Blocks<'a>) { - let Some(entries) = details.as_array() else { - return; - }; - for entry in entries { - let detail_type = entry - .get("type") - .and_then(serde_json::Value::as_str) - .unwrap_or_default(); - match detail_type { - "reasoning.text" => { - if let Some(text) = readable_member(entry, "text") { - push_block(&mut blocks.explicit_trace, text); - } - } - "reasoning.summary" => { - if let Some(text) = readable_member(entry, "summary") { - push_block(&mut blocks.explicit_summary, text); - } - } - _ => {} - } - } -} - -/// Normalizes the content parts of a final response into readable reasoning. -/// -/// Returns `None` when the response carries no readable reasoning. -#[must_use] -pub fn normalize(content: &[ContentPart]) -> Option { - let mut blocks = Blocks::default(); - for part in content { - match part { - ContentPart::Reasoning(reasoning) if !reasoning.redacted => { - push_block(&mut blocks.fallback_trace, &reasoning.text); - } - ContentPart::Opaque { kind, data } if kind == OPENAI_REASONING_KIND => { - collect_openai_reasoning_item(data, &mut blocks); - } - ContentPart::Opaque { kind, data } if kind == OPENAI_COMPAT_REASONING_DETAILS_KIND => { - collect_reasoning_details(data, &mut blocks); - } - _ => {} - } - } - blocks.into_output() -} - -/// Whether a part is provider-native replay material Fabro keeps in history -/// but never renders. -#[must_use] -pub fn is_provider_part(part: &ContentPart) -> bool { - matches!(part, ContentPart::Reasoning(_) | ContentPart::Opaque { .. }) -} - -/// Whether a part is an OpenAI Responses item tied to one specific API -/// response. Such items become invalid once compaction replaces their -/// surrounding context. -#[must_use] -pub fn is_opaque_openai(part: &ContentPart) -> bool { - matches!( - part, - ContentPart::Opaque { kind, .. } - if kind == OPENAI_REASONING_KIND || kind == OPENAI_MESSAGE_KIND - ) -} - -#[cfg(test)] -mod tests { - use fabro_types::ReasoningContent; - use serde_json::json; - - use super::*; - - fn thinking(text: &str) -> ContentPart { - ContentPart::Reasoning(ReasoningContent { - text: text.to_string(), - signature: None, - signature_origin: None, - redacted: false, - }) - } - - fn openai_reasoning(item: serde_json::Value) -> ContentPart { - ContentPart::opaque(OPENAI_REASONING_KIND, item) - } - - fn reasoning_details(details: serde_json::Value) -> ContentPart { - ContentPart::opaque(OPENAI_COMPAT_REASONING_DETAILS_KIND, details) - } - - #[test] - fn non_redacted_thinking_becomes_a_trace() { - let output = normalize(&[thinking("weighing the options")]).unwrap(); - assert!(output.summary().is_none()); - assert_eq!(output.trace(), Some("weighing the options")); - } - - #[test] - fn redacted_thinking_yields_no_readable_reasoning() { - let redacted = ContentPart::Reasoning(ReasoningContent { - text: "AAAAopaque".to_string(), - signature: Some("sig".to_string()), - signature_origin: Some("anthropic".to_string()), - redacted: true, - }); - assert!(normalize(&[redacted]).is_none()); - } - - #[test] - fn responses_item_with_summary_and_reasoning_text_produces_both_fields() { - let output = normalize(&[openai_reasoning(json!({ - "type": "reasoning", - "id": "rs_1", - "encrypted_content": "gAAAAA", - "summary": [{"type": "summary_text", "text": "inspect first"}], - "content": [{"type": "reasoning_text", "text": "step one"}], - }))]) - .unwrap(); - assert_eq!(output.summary(), Some("inspect first")); - assert_eq!(output.trace(), Some("step one")); - } - - #[test] - fn responses_blocks_join_in_provider_order() { - let output = normalize(&[openai_reasoning(json!({ - "summary": [ - {"type": "summary_text", "text": "first"}, - {"type": "summary_text", "text": "second"}, - ], - }))]) - .unwrap(); - assert_eq!(output.summary(), Some("first\n\nsecond")); - } - - #[test] - fn structured_details_produce_summary_and_trace() { - let output = normalize(&[reasoning_details(json!([ - {"type": "reasoning.summary", "summary": "checked the parser"}, - {"type": "reasoning.text", "text": "read convert.rs", "signature": "sig"}, - {"type": "reasoning.encrypted", "data": "gAAAAAsecret"}, - ]))]) - .unwrap(); - assert_eq!(output.summary(), Some("checked the parser")); - assert_eq!(output.trace(), Some("read convert.rs")); - } - - #[test] - fn malformed_details_are_ignored_without_failing() { - assert!(normalize(&[reasoning_details(json!("not-an-array"))]).is_none()); - assert!( - normalize(&[reasoning_details(json!([ - 42, - {"type": "reasoning.summary", "summary": 7}, - {"no_type": true}, - ]))]) - .is_none() - ); - } - - #[test] - fn structured_details_suppress_a_duplicate_flattened_value() { - let output = normalize(&[ - reasoning_details(json!([ - {"type": "reasoning.summary", "summary": "checked the parser"}, - ])), - thinking("checked the parser"), - ]) - .unwrap(); - assert_eq!(output.summary(), Some("checked the parser")); - assert!(output.trace().is_none()); - } - - #[test] - fn structured_trace_takes_precedence_over_flattened_trace() { - let output = normalize(&[ - reasoning_details(json!([{"type": "reasoning.text", "text": "verbatim"}])), - thinking("flattened"), - ]) - .unwrap(); - assert_eq!(output.trace(), Some("verbatim")); - } - - #[test] - fn whitespace_only_fragments_do_not_create_reasoning() { - assert!(normalize(&[thinking(" \n ")]).is_none()); - let output = normalize(&[thinking(" indented thought\n")]).unwrap(); - assert_eq!(output.trace(), Some(" indented thought\n")); - } - - #[test] - fn opaque_openai_items_are_recognized() { - assert!(is_opaque_openai(&openai_reasoning(json!({})))); - assert!(is_opaque_openai(&ContentPart::opaque( - OPENAI_MESSAGE_KIND, - json!({}) - ))); - assert!(!is_opaque_openai(&thinking("x"))); - assert!(is_provider_part(&thinking("x"))); - assert!(!is_provider_part(&ContentPart::Text { - text: "x".to_string(), - })); - } -} diff --git a/lib/components/fabro-llm/src/structured.rs b/lib/components/fabro-llm/src/structured.rs deleted file mode 100644 index 3bc5b730e..000000000 --- a/lib/components/fabro-llm/src/structured.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! One-shot structured output. - -use lithos_llm::client::Client; -use lithos_llm::middleware::CallContext; -use lithos_llm::types::{Error, ErrorKind, Request, Response, ResponseFormat}; - -/// A completion whose text parsed as the requested JSON object. -#[derive(Debug, Clone)] -pub struct StructuredCompletion { - pub response: Response, - pub object: serde_json::Value, -} - -/// Completes `request` under a JSON schema and parses the reply. -/// -/// The schema is attached as the request's response format, so providers -/// with native structured output enforce it. The reply text must still parse -/// as JSON; a reply that does not is a `ResponseDecode` error. -pub async fn complete_object( - client: &Client, - request: Request, - schema_name: &str, - schema: serde_json::Value, -) -> Result { - complete_object_with_context(client, request, schema_name, schema, CallContext::new()).await -} - -pub async fn complete_object_with_context( - client: &Client, - request: Request, - schema_name: &str, - schema: serde_json::Value, - context: CallContext, -) -> Result { - let request = request - .into_builder() - .response_format(ResponseFormat::JsonSchema { - name: schema_name.to_string(), - schema, - }) - .build() - .map_err(|source| { - Error::new( - ErrorKind::InvalidRequest, - "structured output request is invalid", - ) - .with_source(source) - })?; - let response = client.complete_with_context(request, context).await?; - let object = parse_object(&response)?; - Ok(StructuredCompletion { response, object }) -} - -/// Parses a response's JSON output: a `Json` part when the provider returned -/// one, else the concatenated text. -pub fn parse_object(response: &Response) -> Result { - if let Some(value) = response.content.iter().find_map(|part| match part { - fabro_types::ContentPart::Json { value } => Some(value.clone()), - _ => None, - }) { - return Ok(value); - } - let text = response.text(); - serde_json::from_str(text.trim()).map_err(|source| { - Error::new( - ErrorKind::ResponseDecode, - format!("the model did not return a JSON object: {source}"), - ) - .with_provider(response.model.provider().clone()) - .with_source(source) - }) -} - -#[cfg(test)] -mod tests { - use fabro_types::{ContentPart, ModelId, ProviderId}; - use serde_json::json; - - use super::*; - - fn response(parts: Vec) -> Response { - Response::new(ProviderId::new("openai"), ModelId::new("gpt-5.4"), parts) - } - - #[test] - fn parses_text_or_json_parts() { - let text = response(vec![ContentPart::Text { - text: " {\"title\": \"x\"} ".to_string(), - }]); - assert_eq!(parse_object(&text).unwrap(), json!({"title": "x"})); - let json = response(vec![ContentPart::Json { - value: json!({"a": 1}), - }]); - assert_eq!(parse_object(&json).unwrap(), json!({"a": 1})); - let prose = response(vec![ContentPart::Text { - text: "sorry".to_string(), - }]); - assert_eq!( - parse_object(&prose).unwrap_err().kind(), - ErrorKind::ResponseDecode - ); - } -} diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index d0b7eae10..552c5da33 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -2,7 +2,7 @@ use std::fmt; use std::sync::{Arc, LazyLock}; use fabro_graphviz::Error as GraphvizError; -use fabro_llm::{ErrorFacts, ErrorKind, LlmError, ModelSelectionError}; +use fabro_llm::{ErrorData, ErrorKind, ModelSelectionError, failure_signature_hint}; use fabro_template::TemplateError; pub use fabro_types::failure_signature::FailureSignature; pub use fabro_types::outcome::FailureCategory; @@ -18,7 +18,7 @@ use crate::outcome::{FailureDetail, Outcome, StageOutcome}; /// Classify an LLM error into a `FailureCategory` based on its structure. #[must_use] -pub fn classify_sdk_error(err: &E) -> FailureCategory { +pub fn classify_sdk_error(err: &ErrorData) -> FailureCategory { match err.kind() { ErrorKind::RateLimit | ErrorKind::Server @@ -309,7 +309,7 @@ pub enum Error { }, #[error("LLM error: {0}")] - Llm(LlmError), + Llm(Box), #[error("Checkpoint error: {0}")] Checkpoint(String), @@ -583,7 +583,7 @@ impl Error { #[must_use] pub fn failure_signature_hint(&self) -> Option { match self { - Self::Llm(sdk_err) => Some(FailureSignature(sdk_err.failure_signature_hint())), + Self::Llm(sdk_err) => Some(FailureSignature(failure_signature_hint(sdk_err))), _ => None, } } @@ -684,15 +684,15 @@ impl From for Error { } } -impl From for Error { - fn from(err: LlmError) -> Self { - Self::Llm(err) +impl From for Error { + fn from(err: ErrorData) -> Self { + Self::Llm(Box::new(err)) } } impl From for Error { fn from(err: fabro_llm::Error) -> Self { - Self::Llm(LlmError::from(err)) + Self::from(ErrorData::from(err)) } } @@ -749,15 +749,15 @@ mod tests { use super::*; /// A stored LLM error of `kind` from the `openai` provider. - fn sdk_error(kind: ErrorKind, message: &str) -> LlmError { - LlmError::from( + fn sdk_error(kind: ErrorKind, message: &str) -> ErrorData { + ErrorData::from( fabro_llm::Error::new(kind, message).with_provider(fabro_types::provider_ids::openai()), ) } /// A transient failure the provider may be asked to repeat. - fn transient_error(kind: ErrorKind, message: &str) -> LlmError { - LlmError::from( + fn transient_error(kind: ErrorKind, message: &str) -> ErrorData { + ErrorData::from( fabro_llm::Error::new(kind, message) .with_provider(fabro_types::provider_ids::openai()) .with_retry(RetryClassification::Safe), @@ -1157,16 +1157,16 @@ mod tests { #[test] fn llm_error_display() { let sdk_err = transient_error(ErrorKind::Network, "connection refused"); - let err = Error::Llm(sdk_err); + let err = Error::from(sdk_err); assert_eq!(err.to_string(), "LLM error: connection refused"); } #[test] fn llm_error_retryable_delegates_to_sdk() { - let retryable = Error::Llm(transient_error(ErrorKind::Network, "timeout")); + let retryable = Error::from(transient_error(ErrorKind::Network, "timeout")); assert!(retryable.is_retryable()); - let non_retryable = Error::Llm(sdk_error(ErrorKind::Configuration, "bad config")); + let non_retryable = Error::from(sdk_error(ErrorKind::Configuration, "bad config")); assert!(!non_retryable.is_retryable()); } @@ -1221,31 +1221,31 @@ mod tests { #[test] fn failure_class_llm_rate_limit() { - let err = Error::Llm(transient_error(ErrorKind::RateLimit, "too fast")); + let err = Error::from(transient_error(ErrorKind::RateLimit, "too fast")); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } #[test] fn failure_class_llm_context_length() { - let err = Error::Llm(sdk_error(ErrorKind::ContextLength, "too long")); + let err = Error::from(sdk_error(ErrorKind::ContextLength, "too long")); assert_eq!(err.failure_category(), FailureCategory::BudgetExhausted); } #[test] fn failure_class_llm_auth() { - let err = Error::Llm(sdk_error(ErrorKind::Authentication, "bad key")); + let err = Error::from(sdk_error(ErrorKind::Authentication, "bad key")); assert_eq!(err.failure_category(), FailureCategory::Deterministic); } #[test] fn failure_class_llm_abort() { - let err = Error::Llm(sdk_error(ErrorKind::Cancelled, "user cancelled")); + let err = Error::from(sdk_error(ErrorKind::Cancelled, "user cancelled")); assert_eq!(err.failure_category(), FailureCategory::Canceled); } #[test] fn failure_class_llm_timeout() { - let err = Error::Llm(transient_error(ErrorKind::Timeout, "timed out")); + let err = Error::from(transient_error(ErrorKind::Timeout, "timed out")); assert_eq!(err.failure_category(), FailureCategory::TransientInfra); } @@ -1941,7 +1941,7 @@ mod tests { #[test] fn failure_signature_hint_llm_returns_some() { - let err = Error::Llm(sdk_error(ErrorKind::Authentication, "bad key")); + let err = Error::from(sdk_error(ErrorKind::Authentication, "bad key")); assert_eq!( err.failure_signature_hint(), Some(FailureSignature( @@ -1966,7 +1966,7 @@ mod tests { #[test] fn to_fail_outcome_llm_has_class_and_signature() { - let err = Error::Llm(sdk_error(ErrorKind::Authentication, "bad key")); + let err = Error::from(sdk_error(ErrorKind::Authentication, "bad key")); let outcome = err.to_fail_outcome(); assert_eq!(outcome.status, crate::outcome::StageOutcome::Failed { retry_requested: false, @@ -1993,7 +1993,7 @@ mod tests { #[test] fn to_fail_outcome_includes_error_message_as_reason() { - let err = Error::Llm(transient_error(ErrorKind::Network, "connection refused")); + let err = Error::from(transient_error(ErrorKind::Network, "connection refused")); let outcome = err.to_fail_outcome(); assert!( outcome @@ -2005,7 +2005,7 @@ mod tests { #[test] fn to_fail_outcome_no_context_updates() { - let err = Error::Llm(transient_error(ErrorKind::Network, "refused")); + let err = Error::from(transient_error(ErrorKind::Network, "refused")); let outcome = err.to_fail_outcome(); assert!(outcome.context_updates.is_empty()); } @@ -2057,7 +2057,7 @@ mod tests { Error::engine("engine err"), Error::publish("publish err"), Error::handler("handler err"), - Error::Llm(transient_error(ErrorKind::Network, "refused")), + Error::from(transient_error(ErrorKind::Network, "refused")), Error::Checkpoint("cp err".into()), Error::Stylesheet("style err".into()), Error::Io("io err".into()), @@ -2165,7 +2165,7 @@ mod tests { // 1. Create SdkError → Error let sdk_err = transient_error(ErrorKind::RateLimit, "too fast"); - let arc_err = Error::Llm(sdk_err); + let arc_err = Error::from(sdk_err); assert_eq!(arc_err.failure_category(), FailureCategory::TransientInfra); // 2. Error → Outcome @@ -2236,7 +2236,7 @@ mod tests { fn e2e_serde_stability_agent_error() { use fabro_agent::Error as AgentError; - let err = AgentError::Llm(transient_error(ErrorKind::RateLimit, "too fast")); + let err = AgentError::from(transient_error(ErrorKind::RateLimit, "too fast")); let json = serde_json::to_string(&err).unwrap(); let v: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(v["type"], "llm"); diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index d254ca5f1..c2eeec956 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -12,10 +12,9 @@ use fabro_agent::{ }; use fabro_graphviz::graph::{AttrValue, Node}; use fabro_llm::credentials::CredentialProvider; -use fabro_llm::error::failover_eligible; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::types::ResponseFormat; -use fabro_llm::{Client, ClientOptions, FallbackTarget, LlmError, Request, Response}; +use fabro_llm::{Client, ClientOptions, ErrorData, FallbackTarget, Request, Response}; use fabro_mcp::config::McpServerSettings; use fabro_types::settings::run::RunModelControls; use fabro_types::{ @@ -110,7 +109,7 @@ enum AgentApiErrorDisposition { /// Session was interrupted via cancellation; surface as `Error::Cancelled`. Cancelled, /// Underlying LLM error eligible for provider failover. - FailoverEligible(LlmError), + FailoverEligible(ErrorData), /// Terminal error; abort the invocation with this workflow `Error`. Terminal(Error), } @@ -132,7 +131,7 @@ fn classify_agent_error(err: fabro_agent::Error, allow_failover: bool) -> AgentA )) } fabro_agent::Error::Llm(err) if allow_failover && err.failover_eligible() => { - AgentApiErrorDisposition::FailoverEligible(err) + AgentApiErrorDisposition::FailoverEligible(*err) } fabro_agent::Error::Llm(err) => AgentApiErrorDisposition::Terminal(Error::Llm(err)), other @ (fabro_agent::Error::SessionClosed @@ -755,7 +754,7 @@ impl LiveAgentInvocation { error: fabro_agent::Error, allow_failover: bool, emitter: &Arc, - ) -> Result { + ) -> Result { let disposition = classify_agent_error(error, allow_failover); self.abort_and_discard(emitter).await; match disposition { @@ -1197,7 +1196,7 @@ impl AgentApiBackend { async fn failover_agent_session( &self, fallback_plan: &mut FallbackPlan, - initial_error: LlmError, + initial_error: ErrorData, request: &CodergenRunRequest<'_>, input: &str, stage_scope: &StageScope, @@ -1205,7 +1204,7 @@ impl AgentApiBackend { live: &mut LiveAgentInvocation, ) -> Result<(), Error> { let emitter = request.emitter; - let mut last_error = Error::Llm(initial_error); + let mut last_error = Error::from(initial_error); while fallback_plan.advance() { Self::emit_failover( @@ -1269,7 +1268,7 @@ impl AgentApiBackend { begin_session_lifecycle(&live.session, emitter, None); if let Err(error) = live.session.initialize().await { let allow_failover = fallback_plan.has_next(); - last_error = Error::Llm( + last_error = Error::from( live.discard_for_error(error, allow_failover, emitter) .await?, ); @@ -1302,7 +1301,7 @@ impl AgentApiBackend { } Err(error) => { let allow_failover = fallback_plan.has_next(); - last_error = Error::Llm( + last_error = Error::from( live.discard_for_error(error, allow_failover, emitter) .await?, ); @@ -1431,7 +1430,7 @@ impl AgentApiBackend { .with_speed(route.controls.speed), }); } - Err(error) if failover_eligible(&error) && plan.has_next() => { + Err(error) if error.failover_eligible() && plan.has_next() => { let error_message = error.to_string(); plan.advance(); Self::emit_failover(node, emitter, stage_scope, plan, &error_message); @@ -1442,7 +1441,7 @@ impl AgentApiBackend { request.response_format().cloned(), )?; } - Err(error) => return Err(Error::Llm(LlmError::from(error))), + Err(error) => return Err(Error::from(error)), } } } @@ -4032,16 +4031,16 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = // --- Bridge guard tests --- - fn failover_eligible_llm_error() -> LlmError { - LlmError::from( + fn failover_eligible_llm_error() -> ErrorData { + ErrorData::from( fabro_llm::Error::new(ErrorKind::Network, "boom") .with_provider(provider_ids::openai()) .with_retry(RetryClassification::Safe), ) } - fn non_failover_llm_error() -> LlmError { - LlmError::from( + fn non_failover_llm_error() -> ErrorData { + ErrorData::from( fabro_llm::Error::new(ErrorKind::InvalidRequest, "bad key") .with_provider(provider_ids::openai()) .with_status(401), @@ -4270,7 +4269,7 @@ profile = "anthropic" #[test] fn classify_failover_eligible_llm_returns_failover_when_allowed() { - let err = fabro_agent::Error::Llm(failover_eligible_llm_error()); + let err = fabro_agent::Error::from(failover_eligible_llm_error()); assert!(matches!( classify_agent_error(err, true), AgentApiErrorDisposition::FailoverEligible(_) @@ -4279,7 +4278,7 @@ profile = "anthropic" #[test] fn classify_failover_eligible_llm_returns_terminal_when_not_allowed() { - let err = fabro_agent::Error::Llm(failover_eligible_llm_error()); + let err = fabro_agent::Error::from(failover_eligible_llm_error()); match classify_agent_error(err, false) { AgentApiErrorDisposition::Terminal(Error::Llm(_)) => {} _ => panic!("expected Terminal(Error::Llm) when failover disallowed"), @@ -4288,7 +4287,7 @@ profile = "anthropic" #[test] fn classify_non_failover_eligible_llm_is_terminal_llm() { - let err = fabro_agent::Error::Llm(non_failover_llm_error()); + let err = fabro_agent::Error::from(non_failover_llm_error()); match classify_agent_error(err, true) { AgentApiErrorDisposition::Terminal(Error::Llm(_)) => {} _ => panic!("expected Terminal(Error::Llm) for non-failover-eligible LLM error"), @@ -4297,7 +4296,7 @@ profile = "anthropic" #[test] fn classify_refusal_llm_returns_failover_when_allowed() { - let err = fabro_agent::Error::Llm(LlmError::from(refusal_llm_error())); + let err = fabro_agent::Error::from(refusal_llm_error()); assert!(matches!( classify_agent_error(err, true), AgentApiErrorDisposition::FailoverEligible(_) @@ -4306,7 +4305,7 @@ profile = "anthropic" #[test] fn classify_refusal_llm_returns_terminal_when_not_allowed() { - let err = fabro_agent::Error::Llm(LlmError::from(refusal_llm_error())); + let err = fabro_agent::Error::from(refusal_llm_error()); match classify_agent_error(err, false) { AgentApiErrorDisposition::Terminal(Error::Llm(llm_err)) => { assert!(llm_err.to_string().contains("claude-fable-5 refused")); diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index 1b60a3882..ecbec0492 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -6,7 +6,7 @@ use fabro_github::{self as github_app, ssh_url_to_https}; use fabro_graphviz::parser; use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; -use fabro_llm::{Client, ClientOptions, Request, selection, structured}; +use fabro_llm::{Client, ClientOptions, Request, selection}; use fabro_store::RunProjection; use fabro_types::settings::run::MergeStrategy; use fabro_types::{ProviderId, PullRequestLink, Role}; @@ -408,10 +408,10 @@ async fn build_pr_content_with_client( .message(fabro_types::Message::text(Role::User, prompt)) .build() .map_err(|e| format!("invalid PR content request: {e}"))?; - let completion = - structured::complete_object(&client, request, "pr_content", PR_CONTENT_SCHEMA.clone()) - .await - .map_err(|e| format!("LLM generation failed: {e}"))?; + let completion = client + .complete_object(request, "pr_content", PR_CONTENT_SCHEMA.clone()) + .await + .map_err(|e| format!("LLM generation failed: {e}"))?; let generated: PrContent = serde_json::from_value(completion.object) .map_err(|e| format!("Failed to deserialize PR content: {e}"))?; diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 24a51c0a0..58f99418c 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -30,7 +30,6 @@ pub mod parallel; pub mod principal; pub mod provider_ids; pub mod pull_request; -pub mod reasoning; pub mod repository; pub mod run; pub mod run_event; @@ -95,7 +94,8 @@ pub use interview::{ }; pub use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; pub use lithos_llm::types::{ - FinishReason, Request, RequestBuildError, RequestBuilder, Response, ResponseFormat, StreamEvent, + FinishReason, ReasoningOutput, Request, RequestBuildError, RequestBuilder, Response, + ResponseFormat, StreamEvent, }; pub use llm_backend::AgentBackend; pub use manifest_path::{ManifestPath, ManifestPathParseError}; @@ -125,7 +125,6 @@ pub use pull_request::{ PullRequestDetailsUnavailableReason, PullRequestGithubDetail, PullRequestLink, PullRequestMeta, PullRequestRef, PullRequestResponse, PullRequestTimestamps, PullRequestUser, }; -pub use reasoning::ReasoningOutput; pub use repository::{ GitHubRepositorySlug, GitHubRepositorySlugError, RepositoryProvider, RepositoryRef, is_valid_git_branch_name, is_valid_git_tag_name, normalize_git_commit_sha, diff --git a/lib/foundation/fabro-types/src/reasoning.rs b/lib/foundation/fabro-types/src/reasoning.rs deleted file mode 100644 index 89d521e9f..000000000 --- a/lib/foundation/fabro-types/src/reasoning.rs +++ /dev/null @@ -1,142 +0,0 @@ -use serde::{Deserialize, Serialize, de}; - -/// Readable model reasoning normalized into a provider-neutral shape. -/// -/// Providers expose reasoning through several unrelated channels: OpenAI -/// Responses reasoning items, OpenAI-compatible `reasoning_details`, and -/// flattened `reasoning`/`reasoning_content`/`thinking` strings. This type -/// reduces all of them to the two capabilities consumers actually care -/// about, so the durable event contract does not change shape when a -/// provider dialect does. -/// -/// Both fields may be populated for the same response. An emitted object -/// always carries at least one of them; opaque provider material -/// (signatures, IDs, encrypted or redacted payloads) never appears here. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub struct ReasoningOutput { - /// Model-authored summary of its reasoning, safe to show to users. - #[serde(default, skip_serializing_if = "Option::is_none")] - summary: Option, - /// Verbatim readable reasoning text, when the provider returns it in - /// addition to (or instead of) a summary. - #[serde(default, skip_serializing_if = "Option::is_none")] - trace: Option, -} - -impl ReasoningOutput { - /// Creates reasoning output with both a model-authored summary and a - /// verbatim trace. - #[must_use] - pub fn new(summary: impl Into, trace: impl Into) -> Self { - Self { - summary: Some(summary.into()), - trace: Some(trace.into()), - } - } - - /// Creates reasoning output containing only a model-authored summary. - #[must_use] - pub fn from_summary(summary: impl Into) -> Self { - Self { - summary: Some(summary.into()), - trace: None, - } - } - - /// Creates reasoning output containing only a verbatim trace. - #[must_use] - pub fn from_trace(trace: impl Into) -> Self { - Self { - summary: None, - trace: Some(trace.into()), - } - } - - /// Returns the model-authored summary, when present. - #[must_use] - pub fn summary(&self) -> Option<&str> { - self.summary.as_deref() - } - - /// Returns the verbatim readable reasoning trace, when present. - #[must_use] - pub fn trace(&self) -> Option<&str> { - self.trace.as_deref() - } -} - -impl<'de> Deserialize<'de> for ReasoningOutput { - fn deserialize>(deserializer: D) -> Result { - #[derive(Deserialize)] - struct Fields { - #[serde(default)] - summary: Option, - #[serde(default)] - trace: Option, - } - - let Fields { summary, trace } = Fields::deserialize(deserializer)?; - match (summary, trace) { - (Some(summary), Some(trace)) => Ok(Self::new(summary, trace)), - (Some(summary), None) => Ok(Self::from_summary(summary)), - (None, Some(trace)) => Ok(Self::from_trace(trace)), - (None, None) => Err(de::Error::custom( - "reasoning output requires a summary or trace", - )), - } - } -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn summary_only_round_trips_without_trace_member() { - let output = ReasoningOutput::from_summary("checked the parser first"); - let v = serde_json::to_value(&output).unwrap(); - assert_eq!(v, json!({"summary": "checked the parser first"})); - assert_eq!( - serde_json::from_value::(v).unwrap(), - output - ); - } - - #[test] - fn trace_only_round_trips_without_summary_member() { - let output = ReasoningOutput::from_trace("step one, step two"); - let v = serde_json::to_value(&output).unwrap(); - assert_eq!(v, json!({"trace": "step one, step two"})); - assert_eq!( - serde_json::from_value::(v).unwrap(), - output - ); - } - - #[test] - fn both_fields_round_trip() { - let output = ReasoningOutput::new("summary", "trace"); - let v = serde_json::to_value(&output).unwrap(); - assert_eq!(v, json!({"summary": "summary", "trace": "trace"})); - assert_eq!( - serde_json::from_value::(v).unwrap(), - output - ); - } - - #[test] - fn empty_object_is_rejected() { - let error = serde_json::from_value::(json!({})).unwrap_err(); - assert!(error.to_string().contains("requires a summary or trace")); - } - - #[test] - fn explicit_nulls_are_rejected() { - let error = - serde_json::from_value::(json!({"summary": null, "trace": null})) - .unwrap_err(); - assert!(error.to_string().contains("requires a summary or trace")); - } -} From f8df58966bc0aaded8355b9dc0dcee5d6b026c65 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:44:11 -0600 Subject: [PATCH 5/7] Use the lithos closest_supported_effort on ModelCapabilities The reasoning-effort substitution rule now lives on lithos's ModelCapabilities, so the workflow fallback planner calls it directly and fabro-types drops its controls module. ReasoningEffort is re-exported from lithos alongside the other request types. Co-Authored-By: Claude Fable 5.1 --- .../fabro-workflow/src/handler/llm/api.rs | 6 +- lib/foundation/fabro-types/src/controls.rs | 76 ------------------- lib/foundation/fabro-types/src/lib.rs | 6 +- 3 files changed, 4 insertions(+), 84 deletions(-) delete mode 100644 lib/foundation/fabro-types/src/controls.rs diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index c2eeec956..afc017684 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -20,7 +20,7 @@ use fabro_types::settings::run::RunModelControls; use fabro_types::{ AgentProfileKind, FailoverProps, Message, ModelHandle, ModelId, ModelRef, PermissionLevel, ProviderId, ReasoningEffort, Role, RunId, SessionCapability, Speed, StageId, StageTiming, - TokenCounts, ToolDefinition as LlmToolDefinition, UsdMicros, billing, controls, + TokenCounts, ToolDefinition as LlmToolDefinition, UsdMicros, billing, }; use serde::de::DeserializeOwned; use tokio::sync::mpsc; @@ -897,9 +897,7 @@ impl AgentApiBackend { return FallbackControls::Usable(requested); }; let capabilities = offering.model.capabilities(); - let effective_effort = controls::closest_supported_effort(requested_effort, |effort| { - capabilities.reasoning_effort(effort).is_supported() - }); + let effective_effort = capabilities.closest_supported_effort(requested_effort); match effective_effort { Some(effort) => FallbackControls::Usable(EffectiveRequestControls { reasoning_effort: Some(effort), diff --git a/lib/foundation/fabro-types/src/controls.rs b/lib/foundation/fabro-types/src/controls.rs deleted file mode 100644 index 0017f6bc7..000000000 --- a/lib/foundation/fabro-types/src/controls.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! Helpers over the lithos request-control enums. -//! -//! lithos owns [`ReasoningEffort`] and [`Speed`], their spellings, and their -//! parsing (`ALL`, `as_str`, `Display`, `FromStr`). What stays here is -//! Fabro's own rule for substituting a reasoning level a model lacks. - -pub use lithos_llm::types::{ReasoningEffort, Speed}; - -/// Position of an effort in the least-to-most ordering. -fn effort_rank(effort: ReasoningEffort) -> usize { - ReasoningEffort::ALL - .iter() - .position(|candidate| *candidate == effort) - .unwrap_or(ReasoningEffort::ALL.len()) -} - -/// Selects the supported effort nearest to `requested`. -/// -/// When two supported values are equally distant, the higher effort wins. -/// Returns `None` when nothing is supported. -#[must_use] -pub fn closest_supported_effort( - requested: ReasoningEffort, - supported: impl Fn(ReasoningEffort) -> bool, -) -> Option { - let target = effort_rank(requested); - ReasoningEffort::ALL - .into_iter() - .filter(|effort| supported(*effort)) - .min_by_key(|effort| { - let rank = effort_rank(*effort); - (rank.abs_diff(target), std::cmp::Reverse(rank)) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn lithos_spellings_match_serde() { - for effort in ReasoningEffort::ALL { - let json = serde_json::to_string(&effort).unwrap(); - assert_eq!(json, format!("\"{effort}\"")); - assert_eq!(effort.as_str().parse::().unwrap(), effort); - } - for speed in Speed::ALL { - let json = serde_json::to_string(&speed).unwrap(); - assert_eq!(json, format!("\"{speed}\"")); - assert_eq!(speed.as_str().parse::().unwrap(), speed); - } - assert!("standard".parse::().is_err()); - assert!("standard".parse::().is_err()); - } - - #[test] - fn closest_supported_prefers_the_higher_neighbor_on_ties() { - let supported = |effort| matches!(effort, ReasoningEffort::Low | ReasoningEffort::High); - assert_eq!( - closest_supported_effort(ReasoningEffort::Medium, supported), - Some(ReasoningEffort::High) - ); - assert_eq!( - closest_supported_effort(ReasoningEffort::Max, supported), - Some(ReasoningEffort::High) - ); - assert_eq!( - closest_supported_effort(ReasoningEffort::Minimal, supported), - Some(ReasoningEffort::Low) - ); - assert_eq!( - closest_supported_effort(ReasoningEffort::Medium, |_| false), - None - ); - } -} diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 58f99418c..ce95b8e18 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -11,7 +11,6 @@ pub mod catalog_api; pub mod checkpoint; pub mod command_output; pub mod conclusion; -pub mod controls; pub mod dense; pub mod diff; pub mod event_envelope; @@ -76,7 +75,6 @@ pub use catalog_api::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimi pub use checkpoint::Checkpoint; pub use command_output::{CommandOutputStream, CommandTermination}; pub use conclusion::{Conclusion, StageSummary}; -pub use controls::ReasoningEffort; pub use dense::{ServerSettings, UserSettings, WorkflowSettings}; pub use diff::{DiffStats, DiffSummary, RunDiff}; pub use event_envelope::EventEnvelope; @@ -94,8 +92,8 @@ pub use interview::{ }; pub use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; pub use lithos_llm::types::{ - FinishReason, ReasoningOutput, Request, RequestBuildError, RequestBuilder, Response, - ResponseFormat, StreamEvent, + FinishReason, ReasoningEffort, ReasoningOutput, Request, RequestBuildError, RequestBuilder, + Response, ResponseFormat, StreamEvent, }; pub use llm_backend::AgentBackend; pub use manifest_path::{ManifestPath, ManifestPathParseError}; From fa3e485c95a5a515d524032682dbb62b45e62358 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 09:48:37 -0600 Subject: [PATCH 6/7] Name built-in providers through lithos catalog::builtin lithos-llm now ships the built-in provider ids and constructors, so fabro-types drops its provider_ids module and every caller uses lithos_llm::catalog::builtin directly. The crates that name a provider now depend on lithos-llm themselves. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 6 +++ docs/public/reference/sdk.mdx | 5 +- lib/apps/fabro-cli/Cargo.toml | 1 + lib/apps/fabro-cli/src/commands/install.rs | 35 ++++++------- lib/apps/fabro-cli/src/commands/model.rs | 27 +++++----- .../src/commands/run/run_progress/mod.rs | 9 ++-- lib/apps/fabro-cli/src/main.rs | 8 +-- .../fabro-cli/src/shared/provider_auth.rs | 13 ++--- lib/apps/fabro-server/Cargo.toml | 1 + lib/apps/fabro-server/src/demo/mod.rs | 24 ++++----- lib/apps/fabro-server/src/install.rs | 6 +-- lib/apps/fabro-server/src/run_manifest.rs | 2 +- .../fabro-server/src/run_title_generation.rs | 5 +- .../src/server/handler/sessions.rs | 12 ++--- lib/apps/fabro-server/src/server/tests.rs | 42 ++++++++++------ lib/apps/fabro-server/tests/it/api/install.rs | 2 +- .../tests/it/scenario/run_completion.rs | 2 +- lib/components/fabro-agent/Cargo.toml | 1 + .../fabro-agent/src/agent_profile.rs | 5 +- lib/components/fabro-agent/src/cli.rs | 10 ++-- lib/components/fabro-agent/src/error.rs | 4 +- .../fabro-agent/src/profiles/anthropic.rs | 7 +-- .../fabro-agent/src/profiles/claude5.rs | 7 +-- .../fabro-agent/src/profiles/gemini.rs | 7 +-- .../fabro-agent/src/profiles/gpt56.rs | 18 +++---- .../fabro-agent/src/profiles/mod.rs | 24 +++------ .../fabro-agent/src/profiles/openai.rs | 7 +-- lib/components/fabro-agent/src/session.rs | 5 +- .../fabro-agent/src/test_support.rs | 9 ++-- lib/components/fabro-agent/src/tools.rs | 7 +-- lib/components/fabro-agent/src/types.rs | 5 +- .../fabro-agent/tests/it/parity_matrix.rs | 45 ++++++++--------- lib/components/fabro-hooks/Cargo.toml | 1 + lib/components/fabro-llm/src/api.rs | 12 ++--- lib/components/fabro-llm/src/selection.rs | 20 ++++---- lib/components/fabro-workflow/Cargo.toml | 1 + .../fabro-workflow/src/billing_rollup.rs | 5 +- lib/components/fabro-workflow/src/error.rs | 5 +- .../fabro-workflow/src/event/convert.rs | 13 ++--- .../fabro-workflow/src/event/redaction.rs | 5 +- .../fabro-workflow/src/event/sink.rs | 2 +- .../fabro-workflow/src/handler/llm/api.rs | 49 ++++++++++--------- .../src/handler/llm/preamble.rs | 5 +- .../fabro-workflow/src/lifecycle/git.rs | 2 +- .../fabro-workflow/src/operations/create.rs | 11 +++-- .../fabro-workflow/src/operations/start.rs | 5 +- lib/components/fabro-workflow/src/outcome.rs | 15 +++--- .../src/pipeline/execute/tests.rs | 8 +-- .../fabro-workflow/src/pipeline/finalize.rs | 4 +- .../fabro-workflow/src/pipeline/initialize.rs | 12 ++--- lib/components/fabro-workflow/src/services.rs | 2 +- .../fabro-workflow/src/test_support.rs | 7 +-- .../fabro-workflow/tests/materialize_run.rs | 7 +-- lib/foundation/fabro-api/Cargo.toml | 1 + .../fabro-api/tests/model_round_trip.rs | 7 ++- .../fabro-api/tests/provider_id_round_trip.rs | 7 +-- .../fabro-api/tests/provider_round_trip.rs | 5 +- .../tests/session_contract_round_trip.rs | 2 +- lib/foundation/fabro-auth/src/secrets.rs | 5 +- .../fabro-auth/src/strategies/codex_device.rs | 4 +- lib/foundation/fabro-auth/src/strategy.rs | 5 +- lib/foundation/fabro-auth/src/vault_source.rs | 5 +- lib/foundation/fabro-types/src/lib.rs | 1 - .../fabro-types/src/provider_ids.rs | 34 ------------- .../fabro-types/src/run_event/agent.rs | 4 +- .../fabro-types/src/run_event/mod.rs | 11 +++-- 66 files changed, 317 insertions(+), 326 deletions(-) delete mode 100644 lib/foundation/fabro-types/src/provider_ids.rs diff --git a/Cargo.lock b/Cargo.lock index 9b9bada2b..fbbd434d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2292,6 +2292,7 @@ dependencies = [ "insta", "jsonschema", "libc", + "lithos-llm", "paste", "serde", "serde_json", @@ -2318,6 +2319,7 @@ dependencies = [ "fabro-config", "fabro-environment", "fabro-types", + "lithos-llm", "openapiv3", "prettyplease", "progenitor", @@ -2463,6 +2465,7 @@ dependencies = [ "insta", "jsonwebtoken", "libc", + "lithos-llm", "miette", "nix 0.30.1", "object_store", @@ -2692,6 +2695,7 @@ dependencies = [ "fabro-types", "fabro-util", "httpmock", + "lithos-llm", "regex", "serde", "serde_json", @@ -3034,6 +3038,7 @@ dependencies = [ "http-body-util", "httpmock", "jsonwebtoken", + "lithos-llm", "mime_guess", "multer", "object_store", @@ -3372,6 +3377,7 @@ dependencies = [ "hex", "httpmock", "jsonschema", + "lithos-llm", "md5", "miette", "mime_guess", diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index 63be41cec..d45a75bba 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -32,7 +32,8 @@ use std::sync::Arc; use fabro_agent::{AgentProfile, AgentProfileBuilder, LocalSandbox, Session, SessionOptions}; use fabro_auth::VaultCredentialSource; use fabro_llm::ClientOptions; -use fabro_types::{AgentProfileKind, provider_ids}; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::builtin; #[tokio::main] async fn main() -> Result<(), Box> { @@ -48,7 +49,7 @@ async fn main() -> Result<(), Box> { let profile: Arc = Arc::from( AgentProfileBuilder::new( AgentProfileKind::Anthropic, - provider_ids::anthropic(), + builtin::anthropic(), "claude-sonnet-4.5", Arc::clone(&catalog), ) diff --git a/lib/apps/fabro-cli/Cargo.toml b/lib/apps/fabro-cli/Cargo.toml index 2b9c78d48..8b844e79d 100644 --- a/lib/apps/fabro-cli/Cargo.toml +++ b/lib/apps/fabro-cli/Cargo.toml @@ -45,6 +45,7 @@ fabro-telemetry = { path = "../../foundation/fabro-telemetry" } fabro-store = { path = "../../components/fabro-store" } fabro-vault = { path = "../../foundation/fabro-vault" } fabro-types = { path = "../../foundation/fabro-types", features = ["clap"] } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-redact.workspace = true fabro-util = { path = "../../foundation/fabro-util" } fabro-http.workspace = true diff --git a/lib/apps/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs index 11d88f15d..f888fc24b 100644 --- a/lib/apps/fabro-cli/src/commands/install.rs +++ b/lib/apps/fabro-cli/src/commands/install.rs @@ -39,13 +39,14 @@ use fabro_server::serve; use fabro_store::ArtifactStore; use fabro_types::settings::server::ServerAuthMethod; use fabro_types::settings::validate_public_url_with_label; -use fabro_types::{ProviderId, ServerSettings, provider_ids}; +use fabro_types::{ProviderId, ServerSettings}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; use fabro_util::{browser, dev_token, path, session_secret}; use fabro_vault::SecretType as VaultSecretType; use futures::future::BoxFuture; +use lithos_llm::catalog::builtin; use rand::Rng; use tokio::net::TcpListener; use tokio::process::Command as TokioCommand; @@ -429,14 +430,14 @@ impl InstallInputSource for InteractiveInstallInputSource { if use_device_auth { let credential = authenticate_provider_with_method( - provider_ids::openai(), + builtin::openai(), AuthMethod::CodexDevice(codex_oauth_config()), s, printer, ) .await?; credentials.push(credential); - configured_providers.push(provider_ids::openai()); + configured_providers.push(builtin::openai()); openai_configured = true; } } @@ -2698,7 +2699,7 @@ client_id = "client-id" description: None, }, credential_secret_request(&LoginResult::ApiKey { - provider: fabro_types::provider_ids::anthropic(), + provider: lithos_llm::catalog::builtin::anthropic(), key: "anthropic-key".to_string(), }) .unwrap(), @@ -3504,9 +3505,9 @@ root = "{}" fn install_llm_providers_come_from_catalog_api_key_providers() { let ids = install_llm_provider_ids(&INSTALL_CATALOG); - assert!(ids.contains(&fabro_types::provider_ids::anthropic())); - assert!(ids.contains(&fabro_types::provider_ids::openai())); - assert!(ids.contains(&fabro_types::provider_ids::gemini())); + assert!(ids.contains(&lithos_llm::catalog::builtin::anthropic())); + assert!(ids.contains(&lithos_llm::catalog::builtin::openai())); + assert!(ids.contains(&lithos_llm::catalog::builtin::gemini())); assert!(ids.contains(&ProviderId::new("moonshot"))); assert!(ids.contains(&ProviderId::new("zai"))); assert!(ids.contains(&ProviderId::new("minimax"))); @@ -3531,7 +3532,7 @@ root = "{}" #[test] fn non_interactive_source_rejects_hidden_args_without_switch() { let args = install_args(false, InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), ..InstallNonInteractiveArgs::default() }); let err = NonInteractiveInstallInputSource::new(&args).unwrap_err(); @@ -3544,7 +3545,7 @@ root = "{}" #[test] fn non_interactive_source_rejects_conflicting_api_key_inputs() { let args = install_args(true, InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_stdin: true, llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::Token), @@ -3640,7 +3641,7 @@ root = "{}" fn non_interactive_source_rejects_missing_github_strategy() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_username: Some("brynary".to_string()), ..InstallNonInteractiveArgs::default() @@ -3658,7 +3659,7 @@ root = "{}" fn non_interactive_source_rejects_missing_github_username_for_new_config() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::Token), ..InstallNonInteractiveArgs::default() @@ -3675,7 +3676,7 @@ root = "{}" fn non_interactive_source_allows_keep_existing_settings_without_username() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::Token), keep_existing_settings: true, @@ -3690,7 +3691,7 @@ root = "{}" fn non_interactive_source_rejects_missing_github_owner_for_app() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::App), ..InstallNonInteractiveArgs::default() @@ -3709,7 +3710,7 @@ root = "{}" fn non_interactive_source_rejects_github_owner_for_token() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::Token), github_owner: Some("personal".to_string()), @@ -3729,7 +3730,7 @@ root = "{}" fn non_interactive_source_rejects_github_username_for_app() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::App), github_owner: Some("personal".to_string()), @@ -3749,7 +3750,7 @@ root = "{}" fn non_interactive_source_allows_github_app_setup() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::App), github_owner: Some("personal".to_string()), @@ -3764,7 +3765,7 @@ root = "{}" async fn non_interactive_source_requires_config_choice_when_settings_exist() { let source = NonInteractiveInstallInputSource { args: InstallNonInteractiveArgs { - llm_provider: Some(fabro_types::provider_ids::anthropic()), + llm_provider: Some(lithos_llm::catalog::builtin::anthropic()), llm_api_key_env: Some("ANTHROPIC_API_KEY".to_string()), github_strategy: Some(InstallGitHubStrategyArg::Token), github_username: Some("brynary".to_string()), diff --git a/lib/apps/fabro-cli/src/commands/model.rs b/lib/apps/fabro-cli/src/commands/model.rs index 6a1eae9f5..caaf0f4e4 100644 --- a/lib/apps/fabro-cli/src/commands/model.rs +++ b/lib/apps/fabro-cli/src/commands/model.rs @@ -513,9 +513,8 @@ impl Default for ModelsCommand { #[cfg(test)] mod tests { - use fabro_types::{ - ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort, provider_ids, - }; + use fabro_types::{ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort}; + use lithos_llm::catalog::builtin; use super::*; @@ -902,7 +901,7 @@ mod tests { .header("Content-Type", "application/json") .body( serde_json::json!({ - "data": [test_model_json("test-model", provider_ids::anthropic())], + "data": [test_model_json("test-model", builtin::anthropic())], "meta": { "has_more": false } }) .to_string(), @@ -916,7 +915,7 @@ mod tests { mock.assert_async().await; assert_eq!(models.len(), 1); assert_eq!(models[0].id.as_str(), "test-model"); - assert_eq!(models[0].provider, provider_ids::anthropic()); + assert_eq!(models[0].provider, builtin::anthropic()); } #[tokio::test] @@ -933,7 +932,7 @@ mod tests { .header("Content-Type", "application/json") .body( serde_json::json!({ - "data": [test_model_json("model-a", provider_ids::anthropic())], + "data": [test_model_json("model-a", builtin::anthropic())], "meta": { "has_more": false } }) .to_string(), @@ -961,12 +960,12 @@ mod tests { then.status(200) .header("Content-Type", "application/json") .body( - serde_json::json!({ - "data": [test_model_json("claude-sonnet-4-5", provider_ids::anthropic())], - "meta": { "has_more": false } - }) - .to_string(), - ); + serde_json::json!({ + "data": [test_model_json("claude-sonnet-4-5", builtin::anthropic())], + "meta": { "has_more": false } + }) + .to_string(), + ); }) .await; @@ -991,7 +990,7 @@ mod tests { .header("Content-Type", "application/json") .body( serde_json::json!({ - "data": [test_model_json("model-a", provider_ids::anthropic())], + "data": [test_model_json("model-a", builtin::anthropic())], "meta": { "has_more": true } }) .to_string(), @@ -1008,7 +1007,7 @@ mod tests { .header("Content-Type", "application/json") .body( serde_json::json!({ - "data": [test_model_json("model-b", provider_ids::openai())], + "data": [test_model_json("model-b", builtin::openai())], "meta": { "has_more": false } }) .to_string(), diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs index a04f7b943..1f851de06 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs @@ -461,10 +461,11 @@ mod tests { use fabro_types::run_event::CliEnsureCompletedProps; use fabro_types::{ MetadataSnapshotFailureKind, MetadataSnapshotPhase, ModelId, ModelRef, ParallelBranchId, - SandboxProviderKind, StageId, TokenCounts, fixtures, provider_ids, + SandboxProviderKind, StageId, TokenCounts, fixtures, }; use fabro_workflow::event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}; use fabro_workflow::outcome::billed_model_usage_from_llm; + use lithos_llm::catalog::builtin; use super::*; use crate::commands::run::run_progress::stage_display::ToolCallStatus; @@ -570,7 +571,7 @@ mod tests { fn assistant_event(model: &str, text: &str) -> AgentEvent { AgentEvent::AssistantMessage { text: text.into(), - model: ModelRef::new(provider_ids::openai(), ModelId::new(model)), + model: ModelRef::new(builtin::openai(), ModelId::new(model)), usage: TokenCounts::default(), cost: None, tool_call_count: 0, @@ -589,7 +590,7 @@ mod tests { fn llm_request_started(stage: &str, model: &str) -> Event { agent_event(stage, AgentEvent::LlmRequestStarted { - requested_model: ModelRef::new(provider_ids::anthropic(), ModelId::new(model)), + requested_model: ModelRef::new(builtin::anthropic(), ModelId::new(model)), }) } @@ -605,7 +606,7 @@ mod tests { billing: Some( billed_model_usage_from_llm( &fabro_llm::test_support::test_catalog(), - &ModelRef::new(provider_ids::openai(), ModelId::new("gpt-5.4")), + &ModelRef::new(builtin::openai(), ModelId::new("gpt-5.4")), TokenCounts { input: 1200, output: 300, diff --git a/lib/apps/fabro-cli/src/main.rs b/lib/apps/fabro-cli/src/main.rs index 8374eaafb..8d4edd598 100644 --- a/lib/apps/fabro-cli/src/main.rs +++ b/lib/apps/fabro-cli/src/main.rs @@ -586,7 +586,7 @@ mod tests { ProviderCommand, ProviderNamespace, }; use clap::error::ErrorKind; - use fabro_types::provider_ids; + use lithos_llm::catalog::builtin; use temp_env::with_var; use tokio::runtime::Runtime; @@ -658,7 +658,7 @@ destination = "{destination}" Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { - assert_eq!(args.provider, provider_ids::openai()); + assert_eq!(args.provider, builtin::openai()); } _ => panic!("unexpected command variant"), } @@ -672,7 +672,7 @@ destination = "{destination}" Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { - assert_eq!(args.provider, provider_ids::anthropic()); + assert_eq!(args.provider, builtin::anthropic()); } _ => panic!("unexpected command variant"), } @@ -693,7 +693,7 @@ destination = "{destination}" Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { - assert_eq!(args.provider, provider_ids::anthropic()); + assert_eq!(args.provider, builtin::anthropic()); assert!(args.api_key_stdin); } _ => panic!("unexpected command variant"), diff --git a/lib/apps/fabro-cli/src/shared/provider_auth.rs b/lib/apps/fabro-cli/src/shared/provider_auth.rs index f9a2872b3..badd55c5d 100644 --- a/lib/apps/fabro-cli/src/shared/provider_auth.rs +++ b/lib/apps/fabro-cli/src/shared/provider_auth.rs @@ -20,9 +20,10 @@ use fabro_auth::{ }; use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; -use fabro_types::{ProviderId, provider_ids}; +use fabro_types::ProviderId; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; +use lithos_llm::catalog::builtin; use tokio::task::spawn_blocking; // --------------------------------------------------------------------------- @@ -184,7 +185,7 @@ async fn read_and_validate_api_key( } pub(crate) async fn pick_auth_method(provider: &ProviderId) -> Result { - if provider != &provider_ids::openai() { + if provider != &builtin::openai() { return Ok(AuthMethod::ApiKey); } @@ -374,9 +375,9 @@ mod tests { fn builtin_api_key_providers_have_key_urls() { let catalog = fabro_llm::default_catalog(); for provider in [ - provider_ids::anthropic(), - provider_ids::openai(), - provider_ids::gemini(), + builtin::anthropic(), + builtin::openai(), + builtin::gemini(), ProviderId::new("moonshot"), ProviderId::new("zai"), ProviderId::new("minimax"), @@ -408,7 +409,7 @@ mod tests { #[fabro_macros::e2e_test(live("ANTHROPIC_API_KEY"))] async fn validate_api_key_rejects_invalid_key() { let result = validate_api_key( - &provider_ids::anthropic(), + &builtin::anthropic(), "sk-invalid-key-12345", default_catalog_for_provider_auth(), ) diff --git a/lib/apps/fabro-server/Cargo.toml b/lib/apps/fabro-server/Cargo.toml index 97bf82e42..3efb0947f 100644 --- a/lib/apps/fabro-server/Cargo.toml +++ b/lib/apps/fabro-server/Cargo.toml @@ -44,6 +44,7 @@ fabro-proc = { path = "../../foundation/fabro-proc" } fabro-template = { path = "../../foundation/fabro-template" } fabro-tool = { path = "../../components/fabro-tool" } fabro-types = { path = "../../foundation/fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-util = { path = "../../foundation/fabro-util" } fabro-api = { path = "../../foundation/fabro-api" } fabro-client = { path = "../../foundation/fabro-client" } diff --git a/lib/apps/fabro-server/src/demo/mod.rs b/lib/apps/fabro-server/src/demo/mod.rs index f3001e045..7820a9431 100644 --- a/lib/apps/fabro-server/src/demo/mod.rs +++ b/lib/apps/fabro-server/src/demo/mod.rs @@ -1495,7 +1495,7 @@ mod runs { EventBody::AgentMessage(AgentMessageProps { text: "I'll start by loading the environment configurations for both production and staging to compare them.".into(), model: fabro_types::ModelRef::new( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), fabro_types::ModelId::new("claude-opus-4.6"), ), billing: BilledTokenCounts::default(), @@ -1571,7 +1571,7 @@ mod runs { EventBody::AgentMessage(AgentMessageProps { text: "I've detected drift in 3 resources between production and staging:\n\n1. **redis.max_connections** — production has 200, staging has 100\n2. **redis.tls** — enabled in production, disabled in staging\n3. **iam.session_duration** — production uses 3600s, staging uses 1800s".into(), model: fabro_types::ModelRef::new( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), fabro_types::ModelId::new("claude-opus-4.6"), ), billing: BilledTokenCounts::default(), @@ -1595,7 +1595,7 @@ mod runs { name: "Detect Drift".into(), }, model: Some(billing_model( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-opus-4-6", )), billing: BilledTokenCounts { @@ -1617,7 +1617,7 @@ mod runs { name: "Propose Changes".into(), }, model: Some(billing_model( - fabro_types::provider_ids::gemini(), + lithos_llm::catalog::builtin::gemini(), "gemini-3.1-pro-preview", )), billing: BilledTokenCounts { @@ -1639,7 +1639,7 @@ mod runs { name: "Review Changes".into(), }, model: Some(billing_model( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), "gpt-5.3-codex", )), billing: BilledTokenCounts { @@ -1661,7 +1661,7 @@ mod runs { name: "Apply Changes".into(), }, model: Some(billing_model( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-opus-4-6", )), billing: BilledTokenCounts { @@ -1700,7 +1700,7 @@ mod runs { total_usd_micros: Some(1_350_000), }, model: billing_model( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-opus-4-6", ), stages: 2, @@ -1716,7 +1716,7 @@ mod runs { total_usd_micros: Some(720_000), }, model: billing_model( - fabro_types::provider_ids::gemini(), + lithos_llm::catalog::builtin::gemini(), "gemini-3.1-pro-preview", ), stages: 1, @@ -1731,7 +1731,7 @@ mod runs { total_tokens: 11760, total_usd_micros: Some(190_000), }, - model: billing_model(fabro_types::provider_ids::openai(), "gpt-5.3-codex"), + model: billing_model(lithos_llm::catalog::builtin::openai(), "gpt-5.3-codex"), stages: 1, }, ], @@ -2108,7 +2108,7 @@ mod billing { total_usd_micros: Some(12_150_000), }, model: billing_model( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-opus-4-6", ), stages: 18, @@ -2124,7 +2124,7 @@ mod billing { total_usd_micros: Some(6_480_000), }, model: billing_model( - fabro_types::provider_ids::gemini(), + lithos_llm::catalog::builtin::gemini(), "gemini-3.1-pro-preview", ), stages: 9, @@ -2139,7 +2139,7 @@ mod billing { total_tokens: 105_840, total_usd_micros: Some(1_710_000), }, - model: billing_model(fabro_types::provider_ids::openai(), "gpt-5.3-codex"), + model: billing_model(lithos_llm::catalog::builtin::openai(), "gpt-5.3-codex"), stages: 9, }, ], diff --git a/lib/apps/fabro-server/src/install.rs b/lib/apps/fabro-server/src/install.rs index f91a28463..d04ebcb1e 100644 --- a/lib/apps/fabro-server/src/install.rs +++ b/lib/apps/fabro-server/src/install.rs @@ -2567,7 +2567,7 @@ mod tests { #[test] fn install_provider_base_url_falls_back_to_catalog_base_url() { let state = InstallAppState::for_test("expected"); - let provider = install_catalog_provider(&fabro_types::provider_ids::openai()).unwrap(); + let provider = install_catalog_provider(&lithos_llm::catalog::builtin::openai()).unwrap(); assert_eq!( provider_base_url_override(&state, provider), @@ -2578,10 +2578,10 @@ mod tests { #[test] fn install_provider_base_url_prefers_state_override() { let state = InstallAppState::for_test("expected").with_provider_base_url( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), "https://proxy.example.com/v1", ); - let provider = install_catalog_provider(&fabro_types::provider_ids::openai()).unwrap(); + let provider = install_catalog_provider(&lithos_llm::catalog::builtin::openai()).unwrap(); assert_eq!( provider_base_url_override(&state, provider), diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index fcc9c42cc..cf3984951 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -2017,7 +2017,7 @@ enabled = {clone_enabled} prepared.settings.clone(), validated.graph(), test_catalog().as_ref(), - &[fabro_types::provider_ids::anthropic()], + &[lithos_llm::catalog::builtin::anthropic()], ) .unwrap() .run; diff --git a/lib/apps/fabro-server/src/run_title_generation.rs b/lib/apps/fabro-server/src/run_title_generation.rs index f54be4e17..963fecc00 100644 --- a/lib/apps/fabro-server/src/run_title_generation.rs +++ b/lib/apps/fabro-server/src/run_title_generation.rs @@ -195,7 +195,8 @@ mod tests { use fabro_llm::adapter::{ProviderAdapter, ResolvedCall}; use fabro_llm::lithos_catalog::AdapterId; use fabro_llm::{Error as LlmError, Response, ResponseStream}; - use fabro_types::{RunId, provider_ids}; + use fabro_types::RunId; + use lithos_llm::catalog::builtin; use toml::Value as TomlValue; use super::*; @@ -358,7 +359,7 @@ mod tests { let title = generate_title_or_current(GenerateTitleInput { client, model_id: "gpt-5.4".to_string(), - provider_id: provider_ids::openai(), + provider_id: builtin::openai(), prompt: TitlePromptInput { run_id: &run_id, current_title: "Current", diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index bce2e83fe..ccd526c0f 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -1574,7 +1574,7 @@ enabled = true #[test] fn canonical_session_model_uses_readiness_priority_and_explicit_pins() { let catalog = portable_session_catalog(); - let openai = fabro_types::provider_ids::openai(); + let openai = lithos_llm::catalog::builtin::openai(); let openrouter = ProviderId::new("openrouter"); assert_eq!( @@ -1621,7 +1621,7 @@ enabled = true #[test] fn canonical_session_model_preserves_unknown_passthrough_on_selected_provider() { let catalog = portable_session_catalog(); - let openai = fabro_types::provider_ids::openai(); + let openai = lithos_llm::catalog::builtin::openai(); let openrouter = ProviderId::new("openrouter"); let both = std::collections::HashSet::from([openai.clone(), openrouter.clone()]); @@ -1641,7 +1641,7 @@ enabled = true #[test] fn canonical_session_model_passes_through_colon_bearing_model_ids() { let catalog = portable_session_catalog(); - let openai = fabro_types::provider_ids::openai(); + let openai = lithos_llm::catalog::builtin::openai(); let openrouter = ProviderId::new("openrouter"); let both = std::collections::HashSet::from([openai.clone(), openrouter.clone()]); @@ -1666,7 +1666,7 @@ enabled = true let catalog = portable_session_catalog(); let error = canonical_session_model( &catalog, - &std::collections::HashSet::from([fabro_types::provider_ids::openai()]), + &std::collections::HashSet::from([lithos_llm::catalog::builtin::openai()]), Some("gpt-56-sol"), Some(&ProviderId::new("openrouter")), ) @@ -1678,7 +1678,7 @@ enabled = true #[test] fn canonical_session_model_normalizes_legacy_builtin_selector_before_qualification() { let catalog = portable_session_catalog(); - let openai = fabro_types::provider_ids::openai(); + let openai = lithos_llm::catalog::builtin::openai(); let openrouter = ProviderId::new("openrouter"); let both = std::collections::HashSet::from([openai.clone(), openrouter.clone()]); @@ -1732,7 +1732,7 @@ enabled = true &catalog, &catalog.enabled_provider_ids().into_iter().collect(), Some("openrouter:gpt-56-sol"), - Some(&fabro_types::provider_ids::openai()), + Some(&lithos_llm::catalog::builtin::openai()), ) .unwrap_err(); diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 317451efa..0a00c7fc0 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -1787,7 +1787,7 @@ async fn resolve_llm_client_reads_openai_token_from_vault() { let llm_result = state.resolve_llm_client().await.unwrap(); assert_eq!(llm_result.provider_ids(), vec![ - fabro_types::provider_ids::openai() + lithos_llm::catalog::builtin::openai() ]); assert!(llm_result.auth_issues.is_empty()); } @@ -1861,7 +1861,7 @@ async fn llm_source_configured_providers_reads_openai_token_from_vault() { .unwrap(); assert_eq!(state.configured_llm_provider_ids().await, vec![ - fabro_types::provider_ids::openai() + lithos_llm::catalog::builtin::openai() ]); } @@ -6121,7 +6121,7 @@ fn context_window_event( event: fabro_agent::AgentEvent::AssistantMessage { text: "assistant response".to_string(), model: ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4"), ), usage: TokenCounts::default(), @@ -7201,7 +7201,10 @@ fn test_billed_usage( output_tokens: u64, ) -> fabro_types::BilledModelUsage { let mut usage = fabro_types::BilledModelUsage::new( - ModelRef::new(fabro_types::provider_ids::openai(), ModelId::new(model_id)), + ModelRef::new( + lithos_llm::catalog::builtin::openai(), + ModelId::new(model_id), + ), TokenCounts { input: input_tokens, output: output_tokens, @@ -16768,7 +16771,7 @@ async fn get_aggregate_billing_returns_provider_model_speed_identity() { agg.total_runs = 1; agg.by_model.insert( ModelRef::new( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), ModelId::new("claude-opus-4-6"), ), ModelBillingTotals { @@ -16786,7 +16789,7 @@ async fn get_aggregate_billing_returns_provider_model_speed_identity() { ); agg.by_model.insert( ModelRef::new( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), ModelId::new("claude-opus-4-6"), ) .with_speed(Some(Speed::Fast)), @@ -16846,7 +16849,10 @@ async fn get_aggregate_billing_saturates_total_cost_across_models() { .expect("aggregate billing lock"); for (model_id, total_usd_micros) in [("maximum", i64::MAX), ("one", 1)] { agg.by_model.insert( - ModelRef::new(fabro_types::provider_ids::openai(), ModelId::new(model_id)), + ModelRef::new( + lithos_llm::catalog::builtin::openai(), + ModelId::new(model_id), + ), ModelBillingTotals { stages: 1, billing: BilledTokenCounts { @@ -16891,7 +16897,7 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() { by_model: vec![ fabro_workflow::ProjectionBillingByModel { model: ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4"), ), stages: 1, @@ -16907,7 +16913,7 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() { }, fabro_workflow::ProjectionBillingByModel { model: ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4"), ) .with_speed(Some(Speed::Fast)), @@ -16933,21 +16939,25 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() { assert_eq!(accumulator.total_timing.wall_time_ms, 2000); assert_eq!(accumulator.by_model.len(), 2); assert_eq!( - accumulator.by_model - [&ModelRef::new(fabro_types::provider_ids::openai(), ModelId::new("gpt-5.4"))] + accumulator.by_model[&ModelRef::new( + lithos_llm::catalog::builtin::openai(), + ModelId::new("gpt-5.4") + )] .stages, 1 ); assert_eq!( - accumulator.by_model - [&ModelRef::new(fabro_types::provider_ids::openai(), ModelId::new("gpt-5.4"))] + accumulator.by_model[&ModelRef::new( + lithos_llm::catalog::builtin::openai(), + ModelId::new("gpt-5.4") + )] .billing .input_tokens, 100 ); assert_eq!( accumulator.by_model[&ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4") ) .with_speed(Some(Speed::Fast))] @@ -16956,7 +16966,7 @@ fn aggregate_billing_counts_projection_rollup_usage_visits() { ); assert_eq!( accumulator.by_model[&ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4") ) .with_speed(Some(Speed::Fast))] @@ -18300,7 +18310,7 @@ async fn attach_stream_replays_agent_message_reasoning() { event: fabro_agent::AgentEvent::AssistantMessage { text: String::new(), model: ModelRef::new( - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), ModelId::new("gpt-5.4"), ), usage: TokenCounts::default(), diff --git a/lib/apps/fabro-server/tests/it/api/install.rs b/lib/apps/fabro-server/tests/it/api/install.rs index 75fa128be..109e459a2 100644 --- a/lib/apps/fabro-server/tests/it/api/install.rs +++ b/lib/apps/fabro-server/tests/it/api/install.rs @@ -1416,7 +1416,7 @@ async fn install_validation_endpoints_validate_credentials_and_github_token() { let app = build_install_router( InstallAppState::for_test("test-install-token") .with_provider_base_url( - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), format!("{}/v1", llm_mock.url("")), ) .with_github_api_base_url(github_mock.url("")), diff --git a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs index f32702a30..6ceccb1ea 100644 --- a/lib/apps/fabro-server/tests/it/scenario/run_completion.rs +++ b/lib/apps/fabro-server/tests/it/scenario/run_completion.rs @@ -57,7 +57,7 @@ fn test_app_with_openai_agent_backend(openai_base_url: String, api_key: String) Some(Box::new( fabro_workflow::handler::llm::AgentApiBackend::new_with_catalog( OPENAI_AGENT_MODEL.to_string(), - fabro_types::provider_ids::openai(), + lithos_llm::catalog::builtin::openai(), fabro_workflow::model_fallback::ModelFallbackPolicy::default(), Arc::clone(&llm_source), Arc::clone(&steering_hub), diff --git a/lib/components/fabro-agent/Cargo.toml b/lib/components/fabro-agent/Cargo.toml index 6ecc8ca81..a61a700d5 100644 --- a/lib/components/fabro-agent/Cargo.toml +++ b/lib/components/fabro-agent/Cargo.toml @@ -27,6 +27,7 @@ anyhow.workspace = true fabro-auth = { path = "../../foundation/fabro-auth" } fabro-config = { path = "../../foundation/fabro-config", features = ["clap"] } fabro-types = { path = "../../foundation/fabro-types", features = ["clap"] } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-llm = { path = "../fabro-llm" } fabro-mcp = { path = "../fabro-mcp" } fabro-sandbox = { path = "../fabro-sandbox" } diff --git a/lib/components/fabro-agent/src/agent_profile.rs b/lib/components/fabro-agent/src/agent_profile.rs index 52a9ae2e2..47712159e 100644 --- a/lib/components/fabro-agent/src/agent_profile.rs +++ b/lib/components/fabro-agent/src/agent_profile.rs @@ -91,7 +91,8 @@ pub trait AgentProfile: Send + Sync { #[cfg(test)] mod tests { - use fabro_types::{AgentProfileKind, provider_ids}; + use fabro_types::AgentProfileKind; + use lithos_llm::catalog::builtin; use super::*; use crate::test_support::{MockSandbox, TestProfile}; @@ -100,7 +101,7 @@ mod tests { fn profile_provider_and_model() { let profile = TestProfile::new(); assert_eq!(profile.profile_kind(), AgentProfileKind::Anthropic); - assert_eq!(profile.provider_id(), provider_ids::anthropic()); + assert_eq!(profile.provider_id(), builtin::anthropic()); assert_eq!(profile.model(), "mock-model"); } diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index 51aa41db7..3a6afc7dc 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -822,7 +822,7 @@ mod tests { use fabro_llm::test_support::{ client_with_adapters, test_catalog as fabro_test_catalog, test_catalog_with_overlay, }; - use fabro_types::provider_ids; + use lithos_llm::catalog::builtin; use serde_json::json; use super::*; @@ -1027,7 +1027,7 @@ profile = "openai" #[test] fn ensure_provider_registered_reports_missing_credentials() { let client = client_with_adapters(Vec::new(), ClientOptions::default()); - let error = ensure_provider_registered(&client, &provider_ids::anthropic()).unwrap_err(); + let error = ensure_provider_registered(&client, &builtin::anthropic()).unwrap_err(); assert_eq!( error.to_string(), "LLM credentials not configured for provider 'anthropic'" @@ -1098,9 +1098,9 @@ profile = "openai" #[test] fn summarizer_model_id_prefers_the_provider_small_default() { let catalog = test_catalog(); - let model_id = summarizer_model_id(&provider_ids::openai(), &catalog, "gpt-5.4"); + let model_id = summarizer_model_id(&builtin::openai(), &catalog, "gpt-5.4"); - assert_eq!(model_id.provider(), &provider_ids::openai()); + assert_eq!(model_id.provider(), &builtin::openai()); assert_eq!(model_id.model().as_str(), "gpt-5.4-mini"); } @@ -1110,7 +1110,7 @@ profile = "openai" fn build_profile_can_register_subagent_tools() { let mut profile = AgentProfileBuilder::new( AgentProfileKind::Anthropic, - provider_ids::anthropic(), + builtin::anthropic(), "model", test_catalog(), ) diff --git a/lib/components/fabro-agent/src/error.rs b/lib/components/fabro-agent/src/error.rs index 21dca27f2..fc25ca015 100644 --- a/lib/components/fabro-agent/src/error.rs +++ b/lib/components/fabro-agent/src/error.rs @@ -86,8 +86,8 @@ mod tests { use std::time::Duration; use fabro_llm::{ErrorKind, RetryClassification}; - use fabro_types::provider_ids; use fabro_util::error; + use lithos_llm::catalog::builtin; use super::*; @@ -180,7 +180,7 @@ mod tests { fn serde_roundtrip_llm_provider() { let err = Error::from(ErrorData::from( fabro_llm::Error::new(ErrorKind::RateLimit, "too fast") - .with_provider(provider_ids::openai()) + .with_provider(builtin::openai()) .with_status(429) .with_retry(RetryClassification::after(Duration::from_secs(2))), )); diff --git a/lib/components/fabro-agent/src/profiles/anthropic.rs b/lib/components/fabro-agent/src/profiles/anthropic.rs index dfcd73340..70813c72a 100644 --- a/lib/components/fabro-agent/src/profiles/anthropic.rs +++ b/lib/components/fabro-agent/src/profiles/anthropic.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, provider_ids}; +use fabro_types::{AgentProfileKind, ProviderId}; +use lithos_llm::catalog::builtin; use super::EnvContext; use crate::agent_profile::AgentProfile; @@ -48,7 +49,7 @@ impl AnthropicProfile { Self { base: BaseProfile { profile_kind: AgentProfileKind::Anthropic, - provider_id: provider_ids::anthropic(), + provider_id: builtin::anthropic(), model: model.into(), catalog: None, registry, @@ -116,7 +117,7 @@ mod tests { fn anthropic_profile_identity() { let profile = AnthropicProfile::new("claude-sonnet-4-20250514"); assert_eq!(profile.profile_kind(), AgentProfileKind::Anthropic); - assert_eq!(profile.provider_id(), provider_ids::anthropic()); + assert_eq!(profile.provider_id(), builtin::anthropic()); assert_eq!(profile.model(), "claude-sonnet-4-20250514"); } diff --git a/lib/components/fabro-agent/src/profiles/claude5.rs b/lib/components/fabro-agent/src/profiles/claude5.rs index 66ad7a841..ec5ffd583 100644 --- a/lib/components/fabro-agent/src/profiles/claude5.rs +++ b/lib/components/fabro-agent/src/profiles/claude5.rs @@ -3,7 +3,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, provider_ids}; +use fabro_types::{AgentProfileKind, ProviderId}; +use lithos_llm::catalog::builtin; use super::EnvContext; use crate::agent_profile::AgentProfile; @@ -65,7 +66,7 @@ impl Claude5Profile { Self { base: BaseProfile { profile_kind: AgentProfileKind::Claude5, - provider_id: provider_ids::anthropic(), + provider_id: builtin::anthropic(), model: model.into(), catalog: None, registry, @@ -166,7 +167,7 @@ mod tests { fn profile_identity() { let profile = Claude5Profile::new("claude-fable-5"); assert_eq!(profile.profile_kind(), AgentProfileKind::Claude5); - assert_eq!(profile.provider_id(), provider_ids::anthropic()); + assert_eq!(profile.provider_id(), builtin::anthropic()); assert_eq!(profile.model(), "claude-fable-5"); } diff --git a/lib/components/fabro-agent/src/profiles/gemini.rs b/lib/components/fabro-agent/src/profiles/gemini.rs index d4e812b78..9af27f5ba 100644 --- a/lib/components/fabro-agent/src/profiles/gemini.rs +++ b/lib/components/fabro-agent/src/profiles/gemini.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, provider_ids}; +use fabro_types::{AgentProfileKind, ProviderId}; +use lithos_llm::catalog::builtin; use super::EnvContext; use crate::agent_profile::AgentProfile; @@ -42,7 +43,7 @@ impl GeminiProfile { Self { base: BaseProfile { profile_kind: AgentProfileKind::Gemini, - provider_id: provider_ids::gemini(), + provider_id: builtin::gemini(), model: model.into(), catalog: None, registry, @@ -108,7 +109,7 @@ mod tests { fn gemini_profile_identity() { let profile = GeminiProfile::new("gemini-2.0-flash"); assert_eq!(profile.profile_kind(), AgentProfileKind::Gemini); - assert_eq!(profile.provider_id(), provider_ids::gemini()); + assert_eq!(profile.provider_id(), builtin::gemini()); assert_eq!(profile.model(), "gemini-2.0-flash"); } diff --git a/lib/components/fabro-agent/src/profiles/gpt56.rs b/lib/components/fabro-agent/src/profiles/gpt56.rs index 2bb2dd1c4..c06fac909 100644 --- a/lib/components/fabro-agent/src/profiles/gpt56.rs +++ b/lib/components/fabro-agent/src/profiles/gpt56.rs @@ -17,7 +17,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, ToolDefinition, provider_ids}; +use fabro_types::{AgentProfileKind, ProviderId, ToolDefinition}; +use lithos_llm::catalog::builtin; use serde_json::Value; use super::EnvContext; @@ -69,7 +70,7 @@ impl Gpt56Profile { Self { base: BaseProfile { profile_kind: AgentProfileKind::Gpt56, - provider_id: provider_ids::openai(), + provider_id: builtin::openai(), model: model.into(), catalog: None, registry, @@ -252,7 +253,7 @@ enabled = true fn gpt56_profile_identity() { let profile = Gpt56Profile::new("gpt-5.6-sol"); assert_eq!(profile.profile_kind(), AgentProfileKind::Gpt56); - assert_eq!(profile.provider_id(), provider_ids::openai()); + assert_eq!(profile.provider_id(), builtin::openai()); assert_eq!(profile.model(), "gpt-5.6-sol"); } @@ -327,8 +328,7 @@ enabled = true /// it points 5.6 at a tool it was never given. #[test] fn shell_description_names_the_editor_actually_registered() { - let direct = - Gpt56Profile::new("gpt-5.6-sol").with_route(provider_ids::openai(), test_catalog()); + let direct = Gpt56Profile::new("gpt-5.6-sol").with_route(builtin::openai(), test_catalog()); let shell = direct.tool_registry().get("shell_command").unwrap(); assert!(shell.definition.description.contains("`apply_patch`")); assert!(!shell.definition.description.contains("`edit_file`")); @@ -349,8 +349,7 @@ enabled = true assert!(!rendered.contains("apply_patch")); assert!(!rendered.contains("*** Begin Patch")); - let direct = - Gpt56Profile::new("gpt-5.6-sol").with_route(provider_ids::openai(), test_catalog()); + let direct = Gpt56Profile::new("gpt-5.6-sol").with_route(builtin::openai(), test_catalog()); let rendered = prompt(&direct); assert!(rendered.contains("Use `apply_patch` for local file edits")); assert!(rendered.contains("*** Begin Patch")); @@ -427,8 +426,7 @@ enabled = true #[test] fn provider_prompt_uses_catalog_display_name() { - let direct = - Gpt56Profile::new("gpt-5.6-sol").with_route(provider_ids::openai(), test_catalog()); + let direct = Gpt56Profile::new("gpt-5.6-sol").with_route(builtin::openai(), test_catalog()); assert!(prompt(&direct).contains("powered by OpenAI")); let gateway = Gpt56Profile::new("gpt-5.6-sol") @@ -465,7 +463,7 @@ enabled = true #[test] fn catalog_reports_the_5_6_context_window() { let profile = - Gpt56Profile::new("gpt-5.6-sol").with_route(provider_ids::openai(), test_catalog()); + Gpt56Profile::new("gpt-5.6-sol").with_route(builtin::openai(), test_catalog()); assert_eq!(profile.context_window_size(), 1_050_000); } } diff --git a/lib/components/fabro-agent/src/profiles/mod.rs b/lib/components/fabro-agent/src/profiles/mod.rs index 27905fa86..374b265fb 100644 --- a/lib/components/fabro-agent/src/profiles/mod.rs +++ b/lib/components/fabro-agent/src/profiles/mod.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -#[cfg(test)] -use fabro_types::provider_ids; use fabro_types::{AgentProfileKind, ProviderId}; +#[cfg(test)] +use lithos_llm::catalog::builtin; pub mod anthropic; pub mod claude5; @@ -701,31 +701,23 @@ mod tests { let catalog = Arc::new(test_catalog()); let env = MockSandbox::linux(); let cases = [ - ( - AgentProfileKind::OpenAi, - provider_ids::openai(), - "gpt-5.4-mini", - ), + (AgentProfileKind::OpenAi, builtin::openai(), "gpt-5.4-mini"), ( AgentProfileKind::Anthropic, - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4-5", ), ( AgentProfileKind::Gemini, - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", ), ( AgentProfileKind::Claude5, - provider_ids::anthropic(), + builtin::anthropic(), "claude-sonnet-5", ), - ( - AgentProfileKind::Gpt56, - provider_ids::openai(), - "gpt-5.6-sol", - ), + (AgentProfileKind::Gpt56, builtin::openai(), "gpt-5.6-sol"), ]; for (profile_kind, provider_id, model) in cases { @@ -782,7 +774,7 @@ mod tests { ) { let builder = AgentProfileBuilder::new( profile_kind, - provider_ids::anthropic(), + builtin::anthropic(), model, Arc::new(test_catalog()), ); diff --git a/lib/components/fabro-agent/src/profiles/openai.rs b/lib/components/fabro-agent/src/profiles/openai.rs index 29018cfb5..92085a920 100644 --- a/lib/components/fabro-agent/src/profiles/openai.rs +++ b/lib/components/fabro-agent/src/profiles/openai.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, provider_ids}; +use fabro_types::{AgentProfileKind, ProviderId}; +use lithos_llm::catalog::builtin; use super::EnvContext; use crate::agent_profile::AgentProfile; @@ -43,7 +44,7 @@ impl OpenAiProfile { Self { base: BaseProfile { profile_kind: AgentProfileKind::OpenAi, - provider_id: provider_ids::openai(), + provider_id: builtin::openai(), model: model.into(), catalog: None, registry, @@ -117,7 +118,7 @@ mod tests { fn openai_profile_identity() { let profile = OpenAiProfile::new("o3-mini"); assert_eq!(profile.profile_kind(), AgentProfileKind::OpenAi); - assert_eq!(profile.provider_id(), provider_ids::openai()); + assert_eq!(profile.provider_id(), builtin::openai()); assert_eq!(profile.model(), "o3-mini"); } diff --git a/lib/components/fabro-agent/src/session.rs b/lib/components/fabro-agent/src/session.rs index 15500c1fd..484adbdd2 100644 --- a/lib/components/fabro-agent/src/session.rs +++ b/lib/components/fabro-agent/src/session.rs @@ -2250,9 +2250,10 @@ mod tests { use fabro_llm::{ErrorKind, ResponseStream, RetryPolicy}; use fabro_types::{ ContentPart, Cost, CostSource, ReasoningOutput, StageContextWindowCountMethod, - ToolDefinition, provider_ids, text_of, tool_result_to_json, + ToolDefinition, text_of, tool_result_to_json, }; use futures::stream; + use lithos_llm::catalog::builtin; use tokio::time::{sleep, timeout}; use super::*; @@ -2386,7 +2387,7 @@ mod tests { impl ScriptedError { fn build(&self) -> fabro_llm::Error { fabro_llm::Error::new(self.kind.clone(), self.message.clone()) - .with_provider(provider_ids::anthropic()) + .with_provider(builtin::anthropic()) .with_retry(self.retry) } } diff --git a/lib/components/fabro-agent/src/test_support.rs b/lib/components/fabro-agent/src/test_support.rs index bb3820aab..a25200467 100644 --- a/lib/components/fabro-agent/src/test_support.rs +++ b/lib/components/fabro-agent/src/test_support.rs @@ -10,9 +10,8 @@ use fabro_llm::{ Client, ClientOptions, Error as LlmError, FinishReason, Request, Response, ResponseStream, }; pub use fabro_sandbox::test_support::{MockSandbox, MutableMockSandbox}; -use fabro_types::{ - AgentProfileKind, ContentPart, ModelId, ProviderId, TokenCounts, ToolCall, provider_ids, -}; +use fabro_types::{AgentProfileKind, ContentPart, ModelId, ProviderId, TokenCounts, ToolCall}; +use lithos_llm::catalog::builtin; use crate::agent_profile::AgentProfile; use crate::config::SessionOptions; @@ -24,7 +23,7 @@ use crate::skills::{Skill, format_skills_prompt_section}; use crate::tool_registry::{RegisteredTool, ToolRegistry, ToolSource}; /// The provider every test profile routes to. -pub const TEST_PROVIDER: &str = provider_ids::ANTHROPIC; +pub const TEST_PROVIDER: &str = builtin::ids::ANTHROPIC; /// The model every test profile requests. It is not in the catalog, so the /// provider's passthrough route serves it. pub const TEST_MODEL: &str = "mock-model"; @@ -65,7 +64,7 @@ impl AgentProfile for TestProfile { } fn provider_id(&self) -> ProviderId { - provider_ids::anthropic() + builtin::anthropic() } fn model(&self) -> &'static str { diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index 7e284954a..fcc746de7 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -732,7 +732,8 @@ mod tests { use std::collections::HashMap; use fabro_llm::adapter::ProviderAdapter; - use fabro_types::{CommandTermination, ModelId, provider_ids}; + use fabro_types::{CommandTermination, ModelId}; + use lithos_llm::catalog::builtin; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; @@ -1978,7 +1979,7 @@ mod tests { let client = make_client(provider).await; let summarizer = WebFetchSummarizer { client, - model_id: ModelHandle::new(provider_ids::anthropic(), ModelId::new("mock-model")), + model_id: ModelHandle::new(builtin::anthropic(), ModelId::new("mock-model")), }; let tool = make_web_fetch_tool(Some(summarizer)); @@ -2075,7 +2076,7 @@ mod tests { let summarizer = WebFetchSummarizer { client, - model_id: ModelHandle::new(provider_ids::anthropic(), ModelId::new("target-model")), + model_id: ModelHandle::new(builtin::anthropic(), ModelId::new("target-model")), }; let tool = make_web_fetch_tool(Some(summarizer)); diff --git a/lib/components/fabro-agent/src/types.rs b/lib/components/fabro-agent/src/types.rs index b31458592..1fd5eb458 100644 --- a/lib/components/fabro-agent/src/types.rs +++ b/lib/components/fabro-agent/src/types.rs @@ -817,7 +817,8 @@ pub struct SessionEvent { #[cfg(test)] mod tests { use fabro_llm::{ErrorKind, RetryClassification}; - use fabro_types::{CostSource, ModelId, ProviderId, provider_ids}; + use fabro_types::{CostSource, ModelId, ProviderId}; + use lithos_llm::catalog::builtin; use super::*; @@ -1102,7 +1103,7 @@ mod tests { }; let event = AgentEvent::AssistantMessage { text: "Hello".into(), - model: ModelRef::new(provider_ids::openai(), ModelId::new("test-model")), + model: ModelRef::new(builtin::openai(), ModelId::new("test-model")), usage, cost: Some(Cost { usd_micros: 125_000, diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index f85b0b5f2..53888a64a 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -18,7 +18,8 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::client_from_env; use fabro_llm::{Client, ClientOptions, catalog}; use fabro_test::{EnvVars, TwinScenario, TwinScenarios, TwinToolCall, twin_openai}; -use fabro_types::{ModelHandle, ModelId, ProviderId, provider_ids}; +use fabro_types::{ModelHandle, ModelId, ProviderId}; +use lithos_llm::catalog::builtin; type Provider = ProviderId; @@ -30,11 +31,11 @@ struct OpenAiTwinOptions { fn summarizer_model_id(provider: &Provider) -> ModelHandle { let (provider, model) = match provider.as_str() { - provider_ids::OPENAI | "moonshot" | "zai" | "minimax" | "inception" => { - (provider_ids::openai(), "gpt-5.4-mini") + builtin::ids::OPENAI | "moonshot" | "zai" | "minimax" | "inception" => { + (builtin::openai(), "gpt-5.4-mini") } - provider_ids::GEMINI => (provider_ids::gemini(), "gemini-3-flash-preview"), - provider_ids::ANTHROPIC => (provider_ids::anthropic(), "claude-haiku-4.5"), + builtin::ids::GEMINI => (builtin::gemini(), "gemini-3-flash-preview"), + builtin::ids::ANTHROPIC => (builtin::anthropic(), "claude-haiku-4.5"), other => panic!("unexpected provider {other}"), }; ModelHandle::new(provider, ModelId::new(model)) @@ -142,7 +143,7 @@ fn twin_catalog(base_url: &str, overlay: &str) -> Catalog { } async fn make_client(provider: &Provider, twin: Option<&OpenAiTwinOptions>) -> Client { - if provider == &provider_ids::openai() && fabro_test::TestMode::from_env().is_twin() { + if provider == &builtin::openai() && fabro_test::TestMode::from_env().is_twin() { return make_twin_client(twin.expect("openai twin config should be provided")).await; } @@ -261,7 +262,7 @@ macro_rules! openai_twin_provider_test { .await; } let mut session = make_session( - provider_ids::openai(), + builtin::openai(), "gpt-5.4-mini", tmp.path(), ToolSecrets::default(), @@ -278,14 +279,14 @@ macro_rules! provider_tests { ($scenario:ident) => { provider_test!( $scenario, - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4.5", anthropic, keys = ["ANTHROPIC_API_KEY"] ); provider_test!( $scenario, - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini, keys = ["GEMINI_API_KEY"] @@ -406,21 +407,21 @@ provider_tests!(subagent_spawn); provider_test!( web_fetch, - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4-5", anthropic, keys = ["ANTHROPIC_API_KEY"] ); provider_test!( web_fetch, - provider_ids::openai(), + builtin::openai(), "gpt-5.4-mini", openai, keys = ["OPENAI_API_KEY"] ); provider_test!( web_fetch, - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini, keys = ["GEMINI_API_KEY"] @@ -457,19 +458,19 @@ provider_test!( ); web_search_provider_test!( - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4-5", anthropic, keys = ["ANTHROPIC_API_KEY", "BRAVE_SEARCH_API_KEY"] ); web_search_provider_test!( - provider_ids::openai(), + builtin::openai(), "gpt-5.4-mini", openai, keys = ["OPENAI_API_KEY", "BRAVE_SEARCH_API_KEY"] ); web_search_provider_test!( - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini, keys = ["GEMINI_API_KEY", "BRAVE_SEARCH_API_KEY"] @@ -518,14 +519,14 @@ macro_rules! non_openai_provider_tests { ($scenario:ident) => { provider_test!( $scenario, - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4.5", anthropic, keys = ["ANTHROPIC_API_KEY"] ); provider_test!( $scenario, - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini, keys = ["GEMINI_API_KEY"] @@ -800,7 +801,7 @@ macro_rules! reasoning_effort_tests { } reasoning_effort_tests!( - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4.5", anthropic_reasoning_effort, keys = ["ANTHROPIC_API_KEY"] @@ -808,7 +809,7 @@ reasoning_effort_tests!( // gpt-5-mini does not support the reasoning.effort parameter, so no OpenAI // test. reasoning_effort_tests!( - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini_reasoning_effort, keys = ["GEMINI_API_KEY"] @@ -878,19 +879,19 @@ macro_rules! loop_detection_tests { } loop_detection_tests!( - provider_ids::anthropic(), + builtin::anthropic(), "claude-haiku-4-5", anthropic_loop_detection, keys = ["ANTHROPIC_API_KEY"] ); loop_detection_tests!( - provider_ids::openai(), + builtin::openai(), "gpt-5.4-mini", openai_loop_detection, keys = ["OPENAI_API_KEY"] ); loop_detection_tests!( - provider_ids::gemini(), + builtin::gemini(), "gemini-3-flash-preview", gemini_loop_detection, keys = ["GEMINI_API_KEY"] diff --git a/lib/components/fabro-hooks/Cargo.toml b/lib/components/fabro-hooks/Cargo.toml index fe9659c76..183c5a970 100644 --- a/lib/components/fabro-hooks/Cargo.toml +++ b/lib/components/fabro-hooks/Cargo.toml @@ -18,6 +18,7 @@ fabro-auth = { path = "../../foundation/fabro-auth" } fabro-llm = { path = "../fabro-llm" } fabro-redact.workspace = true fabro-types = { path = "../../foundation/fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-util = { path = "../../foundation/fabro-util" } fabro-http.workspace = true serde.workspace = true diff --git a/lib/components/fabro-llm/src/api.rs b/lib/components/fabro-llm/src/api.rs index 965b3046f..d99e86907 100644 --- a/lib/components/fabro-llm/src/api.rs +++ b/lib/components/fabro-llm/src/api.rs @@ -117,7 +117,7 @@ fn saturating_i64(value: u64) -> i64 { #[cfg(test)] mod tests { - use fabro_types::provider_ids; + use lithos_llm::catalog::builtin; use super::*; use crate::test_support::test_catalog; @@ -125,17 +125,17 @@ mod tests { #[test] fn models_are_stamped_with_configured_providers() { let catalog = test_catalog(); - let configured = HashSet::from([provider_ids::openai()]); + let configured = HashSet::from([builtin::openai()]); let models = models(&catalog, &configured); let openai = models .iter() - .find(|model| model.provider == provider_ids::openai()) + .find(|model| model.provider == builtin::openai()) .expect("openai models listed"); assert!(openai.configured); assert!(openai.limits.context_window > 0); let anthropic = models .iter() - .find(|model| model.provider == provider_ids::anthropic()) + .find(|model| model.provider == builtin::anthropic()) .expect("anthropic models listed"); assert!(!anthropic.configured); assert!(models.iter().any(|model| model.default)); @@ -145,12 +145,12 @@ mod tests { fn providers_skip_stand_ins_and_disabled_entries() { let catalog = test_catalog(); let providers = providers(&catalog, &HashSet::new()); - assert!(providers.iter().any(|p| p.id == provider_ids::openai())); + assert!(providers.iter().any(|p| p.id == builtin::openai())); assert!(providers.iter().all(|p| p.id.as_str() != "openai-codex")); assert!(providers.iter().all(|p| p.id.as_str() != "ollama")); let openai = providers .iter() - .find(|p| p.id == provider_ids::openai()) + .find(|p| p.id == builtin::openai()) .unwrap(); assert_eq!( openai.expected_secret_name.as_deref(), diff --git a/lib/components/fabro-llm/src/selection.rs b/lib/components/fabro-llm/src/selection.rs index 65f26e9a0..85e1434c8 100644 --- a/lib/components/fabro-llm/src/selection.rs +++ b/lib/components/fabro-llm/src/selection.rs @@ -276,7 +276,7 @@ fn canonical_eligible(catalog: &Catalog, eligible: &HashSet) -> Hash #[cfg(test)] mod tests { - use fabro_types::provider_ids; + use lithos_llm::catalog::builtin; use super::*; use crate::test_support::{test_catalog, test_catalog_with_overlay}; @@ -291,7 +291,7 @@ mod tests { let selected = resolve_selection(&catalog, Some("sonnet"), None, &eligible(&["anthropic"])).unwrap(); assert_eq!(selected, SelectedModel { - provider: provider_ids::anthropic(), + provider: builtin::anthropic(), model: "claude-sonnet-5".to_string(), }); } @@ -306,7 +306,7 @@ mod tests { &eligible(&["openai", "anthropic"]), ) .unwrap(); - assert_eq!(selected.provider, provider_ids::anthropic()); + assert_eq!(selected.provider, builtin::anthropic()); assert_eq!(selected.model, "totally-new-model"); } @@ -321,7 +321,7 @@ mod tests { ) .unwrap(); assert_eq!(selected, SelectedModel { - provider: provider_ids::openai(), + provider: builtin::openai(), model: "gpt-5.6-sol".to_string(), }); @@ -333,7 +333,7 @@ mod tests { ) .unwrap(); assert_eq!(unknown, SelectedModel { - provider: provider_ids::openai(), + provider: builtin::openai(), model: "brand-new-model".to_string(), }); @@ -346,7 +346,7 @@ mod tests { assert_eq!( unavailable, Err(ModelSelectionError::ProviderUnavailable { - provider: provider_ids::openai(), + provider: builtin::openai(), }) ); } @@ -373,13 +373,13 @@ mod tests { let error = resolve_selection( &catalog, Some("gpt-5.4"), - Some(&provider_ids::openai()), + Some(&builtin::openai()), &eligible(&["anthropic"]), ) .unwrap_err(); assert!(matches!( error, - ModelSelectionError::ProviderUnavailable { provider } if provider == provider_ids::openai() + ModelSelectionError::ProviderUnavailable { provider } if provider == builtin::openai() )); } @@ -389,11 +389,11 @@ mod tests { let selected = resolve_selection_with_catalog_fallback( &catalog, Some("gpt-5.4"), - Some(&provider_ids::openai()), + Some(&builtin::openai()), &eligible(&["anthropic"]), ) .unwrap(); - assert_eq!(selected.provider, provider_ids::openai()); + assert_eq!(selected.provider, builtin::openai()); let error = resolve_selection_with_catalog_fallback( &catalog, None, diff --git a/lib/components/fabro-workflow/Cargo.toml b/lib/components/fabro-workflow/Cargo.toml index 388078b5b..7d31fa61b 100644 --- a/lib/components/fabro-workflow/Cargo.toml +++ b/lib/components/fabro-workflow/Cargo.toml @@ -43,6 +43,7 @@ fabro-core = { path = "../../foundation/fabro-core" } fabro-store = { path = "../fabro-store" } fabro-static.workspace = true fabro-types = { path = "../../foundation/fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-http.workspace = true thiserror.workspace = true strum.workspace = true diff --git a/lib/components/fabro-workflow/src/billing_rollup.rs b/lib/components/fabro-workflow/src/billing_rollup.rs index 9414a96a3..9d166ed08 100644 --- a/lib/components/fabro-workflow/src/billing_rollup.rs +++ b/lib/components/fabro-workflow/src/billing_rollup.rs @@ -7,8 +7,9 @@ pub use fabro_types::billing_rollup::{ mod tests { use fabro_types::{ AttrValue, BilledTokenCounts, Graph, ModelId, ModelRef, Node, RunProjection, RunSpec, - StageCompletion, StageOutcome, first_event_seq, provider_ids, test_support, + StageCompletion, StageOutcome, first_event_seq, test_support, }; + use lithos_llm::catalog::builtin; use super::billing_rollup_from_projection; use crate::test_support::test_usage; @@ -134,7 +135,7 @@ mod tests { #[test] fn rollup_keeps_in_flight_stage_usage_unpriced() { let mut projection = test_projection(); - let model = ModelRef::new(provider_ids::openai(), ModelId::new("gpt-5.4")); + let model = ModelRef::new(builtin::openai(), ModelId::new("gpt-5.4")); let stage = projection.stage_entry("agent", 1, first_event_seq(1)); stage.started_at = Some(chrono::Utc::now()); stage.usage = BilledTokenCounts { diff --git a/lib/components/fabro-workflow/src/error.rs b/lib/components/fabro-workflow/src/error.rs index 552c5da33..319987070 100644 --- a/lib/components/fabro-workflow/src/error.rs +++ b/lib/components/fabro-workflow/src/error.rs @@ -751,7 +751,8 @@ mod tests { /// A stored LLM error of `kind` from the `openai` provider. fn sdk_error(kind: ErrorKind, message: &str) -> ErrorData { ErrorData::from( - fabro_llm::Error::new(kind, message).with_provider(fabro_types::provider_ids::openai()), + fabro_llm::Error::new(kind, message) + .with_provider(lithos_llm::catalog::builtin::openai()), ) } @@ -759,7 +760,7 @@ mod tests { fn transient_error(kind: ErrorKind, message: &str) -> ErrorData { ErrorData::from( fabro_llm::Error::new(kind, message) - .with_provider(fabro_types::provider_ids::openai()) + .with_provider(lithos_llm::catalog::builtin::openai()) .with_retry(RetryClassification::Safe), ) } diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index da235b8ac..906d3fbb5 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -1463,14 +1463,14 @@ mod tests { use ::fabro_types::{ AutomationRef, EventBody, FailureReason, ModelId, ModelRef, ParallelBranchId, Principal, ProviderId, RunNoticeCode, RunNoticeLevel, RunProvenance, StageId, SystemActorKind, - TokenCounts as LlmTokenCounts, fixtures, provider_ids, run_event as fabro_types, - test_support, + TokenCounts as LlmTokenCounts, fixtures, run_event as fabro_types, test_support, }; use chrono::Utc; use fabro_agent::{ AgentEvent, McpToolSummary, MemoryFileSummary, SandboxEvent, SkillActivationSource, SkillSummary, }; + use lithos_llm::catalog::builtin; use super::*; use crate::error::Error; @@ -2522,10 +2522,7 @@ mod tests { visit: 1, event: AgentEvent::AssistantMessage { text: "ok".to_string(), - model: ModelRef::new( - provider_ids::anthropic(), - ModelId::new("claude-sonnet"), - ), + model: ModelRef::new(builtin::anthropic(), ModelId::new("claude-sonnet")), usage: LlmTokenCounts::default(), cost: None, tool_call_count: 0, @@ -2644,7 +2641,7 @@ mod tests { visit: 1, event: AgentEvent::AssistantMessage { text: "ok".to_string(), - model: ModelRef::new(provider_ids::openai(), ModelId::new("gpt-5.4")), + model: ModelRef::new(builtin::openai(), ModelId::new("gpt-5.4")), usage: LlmTokenCounts::default(), cost: None, tool_call_count: 0, @@ -2674,7 +2671,7 @@ mod tests { visit: 1, event: AgentEvent::AssistantMessage { text: String::new(), - model: ModelRef::new(provider_ids::openai(), ModelId::new("gpt-5.4")), + model: ModelRef::new(builtin::openai(), ModelId::new("gpt-5.4")), usage: LlmTokenCounts::default(), cost: None, tool_call_count: 1, diff --git a/lib/components/fabro-workflow/src/event/redaction.rs b/lib/components/fabro-workflow/src/event/redaction.rs index 9cfe29ee7..689f7868d 100644 --- a/lib/components/fabro-workflow/src/event/redaction.rs +++ b/lib/components/fabro-workflow/src/event/redaction.rs @@ -31,10 +31,11 @@ pub fn event_payload_from_redacted_json(line: &str, run_id: &RunId) -> Result ProviderId { - provider_ids::openai() + builtin::openai() } fn model(&self) -> &str { @@ -2250,20 +2251,20 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn agent_backend_stores_config() { let backend = AgentApiBackend::new( "claude-opus-4-6".to_string(), - provider_ids::openai(), + builtin::openai(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), ); assert_eq!(backend.model, "claude-opus-4-6"); - assert_eq!(backend.provider_id, provider_ids::openai()); + assert_eq!(backend.provider_id, builtin::openai()); } #[test] fn agent_backend_initializes_empty_sessions() { let backend = AgentApiBackend::new( "claude-opus-4-6".to_string(), - provider_ids::anthropic(), + builtin::anthropic(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -2952,7 +2953,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn build_profile_can_register_subagent_tools() { let mut profile = AgentProfileBuilder::new( AgentProfileKind::Anthropic, - provider_ids::anthropic(), + builtin::anthropic(), "claude-opus-4-6", Arc::new(test_catalog()), ) @@ -3130,7 +3131,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = .resolve_provider_context("gpt-5.4", Some("openai")) .unwrap(); - assert_eq!(provider.provider_id, provider_ids::openai()); + assert_eq!(provider.provider_id, builtin::openai()); } #[test] @@ -3175,7 +3176,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn api_backend_selects_claude5_profile_for_sonnet5() { let backend = AgentApiBackend::new_with_catalog( "claude-sonnet-5".to_string(), - provider_ids::anthropic(), + builtin::anthropic(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3186,7 +3187,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = .resolve_provider_context("claude-sonnet-5", None) .unwrap(); - assert_eq!(provider.provider_id, provider_ids::anthropic()); + assert_eq!(provider.provider_id, builtin::anthropic()); assert_eq!(provider.profile_kind, AgentProfileKind::Claude5); } @@ -3214,7 +3215,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn run_model_controls_apply_when_node_omits_controls() { let backend = AgentApiBackend::new( "gpt-5.4".to_string(), - provider_ids::openai(), + builtin::openai(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3235,7 +3236,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn node_controls_override_run_model_controls() { let backend = AgentApiBackend::new( "gpt-5.4".to_string(), - provider_ids::openai(), + builtin::openai(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3264,7 +3265,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn omitted_reasoning_effort_stays_unset() { let backend = AgentApiBackend::new( "gpt-5.4".to_string(), - provider_ids::openai(), + builtin::openai(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3332,7 +3333,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = ])); let backend = AgentApiBackend::new_with_catalog( "claude-fable-5".to_string(), - provider_ids::anthropic(), + builtin::anthropic(), policy, auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3340,7 +3341,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = ); let (mut plan, notices) = backend.fallback_plan( "claude-fable-5", - &provider_ids::anthropic(), + &builtin::anthropic(), EffectiveRequestControls::default(), ); @@ -3375,7 +3376,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = .unwrap(); let backend = AgentApiBackend::new( "claude-opus-4-6".to_string(), - provider_ids::anthropic(), + builtin::anthropic(), ModelFallbackPolicy::default(), Arc::new(VaultCredentialSource::with_env_lookup( Arc::new(AsyncRwLock::new(vault)), @@ -3390,7 +3391,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = assert_eq!( client.available_providers().iter().collect::>(), - vec![&provider_ids::anthropic()] + vec![&builtin::anthropic()] ); } @@ -3403,7 +3404,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = )])); let backend = AgentApiBackend::new( "claude-fable-5".to_string(), - provider_ids::anthropic(), + builtin::anthropic(), fallback_policy, auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3440,7 +3441,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = .unwrap(); let (mut fallback_plan, notices) = backend.fallback_plan( "claude-fable-5", - &provider_ids::anthropic(), + &builtin::anthropic(), EffectiveRequestControls::default(), ); assert!(notices.is_empty()); @@ -3458,7 +3459,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = .unwrap(); assert_eq!(completion.response.text(), "fallback ok"); - assert_eq!(completion.model.provider, provider_ids::openai()); + assert_eq!(completion.model.provider, builtin::openai()); assert_eq!(completion.model.model_id.as_str(), "gpt-5.5"); let failover = emitted_failover .lock() @@ -3907,7 +3908,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = async fn api_backend_shutdown_closes_cached_sessions_once() { let backend = AgentApiBackend::new( "gpt-5.4".to_string(), - provider_ids::openai(), + builtin::openai(), ModelFallbackPolicy::default(), auth_test_support::vault_only_credential_source(), SteeringHub::for_tests(), @@ -3940,7 +3941,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = ); let (fallback_plan, notices) = backend.fallback_plan( "gpt-5.4", - &provider_ids::openai(), + &builtin::openai(), EffectiveRequestControls::default(), ); assert!(notices.is_empty()); @@ -4032,7 +4033,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn failover_eligible_llm_error() -> ErrorData { ErrorData::from( fabro_llm::Error::new(ErrorKind::Network, "boom") - .with_provider(provider_ids::openai()) + .with_provider(builtin::openai()) .with_retry(RetryClassification::Safe), ) } @@ -4040,7 +4041,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = fn non_failover_llm_error() -> ErrorData { ErrorData::from( fabro_llm::Error::new(ErrorKind::InvalidRequest, "bad key") - .with_provider(provider_ids::openai()) + .with_provider(builtin::openai()) .with_status(401), ) } @@ -4050,7 +4051,7 @@ capabilities = {{ text = true, tools = true, response_format = {{ json_object = ErrorKind::ContentFilter, "claude-fable-5 refused the request", ) - .with_provider(provider_ids::anthropic()) + .with_provider(builtin::anthropic()) .with_provider_code("refusal") .with_raw_data(serde_json::json!({ "stop_reason": "refusal", diff --git a/lib/components/fabro-workflow/src/handler/llm/preamble.rs b/lib/components/fabro-workflow/src/handler/llm/preamble.rs index 5f812e533..9042f7a4a 100644 --- a/lib/components/fabro-workflow/src/handler/llm/preamble.rs +++ b/lib/components/fabro-workflow/src/handler/llm/preamble.rs @@ -589,7 +589,8 @@ fn build_summary_preamble( #[cfg(test)] mod tests { use fabro_graphviz::graph::AttrValue; - use fabro_types::{ModelId, ModelRef, TokenCounts, provider_ids}; + use fabro_types::{ModelId, ModelRef, TokenCounts}; + use lithos_llm::catalog::builtin; use super::*; use crate::outcome::{BilledModelUsage, billed_model_usage_from_llm}; @@ -597,7 +598,7 @@ mod tests { fn stage_usage(model: &str, input: u64, output: u64) -> BilledModelUsage { billed_model_usage_from_llm( &fabro_llm::test_support::test_catalog(), - &ModelRef::new(provider_ids::anthropic(), ModelId::new(model)), + &ModelRef::new(builtin::anthropic(), ModelId::new(model)), TokenCounts { input, output, diff --git a/lib/components/fabro-workflow/src/lifecycle/git.rs b/lib/components/fabro-workflow/src/lifecycle/git.rs index 509f1a397..6c05fb46f 100644 --- a/lib/components/fabro-workflow/src/lifecycle/git.rs +++ b/lib/components/fabro-workflow/src/lifecycle/git.rs @@ -1306,7 +1306,7 @@ mod tests { None, finalize_locations, tokio_util::sync::CancellationToken::new(), - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-sonnet-4-6".to_string(), auth_test_support::vault_only_credential_source(), Arc::new(fabro_llm::test_support::test_catalog()), diff --git a/lib/components/fabro-workflow/src/operations/create.rs b/lib/components/fabro-workflow/src/operations/create.rs index 196100033..8a7a6d383 100644 --- a/lib/components/fabro-workflow/src/operations/create.rs +++ b/lib/components/fabro-workflow/src/operations/create.rs @@ -688,9 +688,10 @@ mod tests { use fabro_store::Database; use fabro_types::settings::InterpString; use fabro_types::settings::run::RunMode; - use fabro_types::{EventBody, WorkflowSettings, fixtures, provider_ids, test_support}; + use fabro_types::{EventBody, WorkflowSettings, fixtures, test_support}; use fabro_util::error::collect_chain; use fabro_validate::Severity; + use lithos_llm::catalog::builtin; use object_store::local::LocalFileSystem; use object_store::memory::InMemory; @@ -2028,19 +2029,19 @@ mod tests { }"#; let catalog = portable_model_catalog(); let cases = [ - (vec![provider_ids::openai()], None, provider_ids::openai()), + (vec![builtin::openai()], None, builtin::openai()), ( vec![ProviderId::new("openrouter")], None, ProviderId::new("openrouter"), ), ( - vec![provider_ids::openai(), ProviderId::new("openrouter")], + vec![builtin::openai(), ProviderId::new("openrouter")], None, - provider_ids::openai(), + builtin::openai(), ), ( - vec![provider_ids::openai(), ProviderId::new("openrouter")], + vec![builtin::openai(), ProviderId::new("openrouter")], Some("openrouter"), ProviderId::new("openrouter"), ), diff --git a/lib/components/fabro-workflow/src/operations/start.rs b/lib/components/fabro-workflow/src/operations/start.rs index 78f7691b6..7f03e6ffe 100644 --- a/lib/components/fabro-workflow/src/operations/start.rs +++ b/lib/components/fabro-workflow/src/operations/start.rs @@ -1310,9 +1310,10 @@ mod tests { }; use fabro_types::{ BilledModelUsage, GitContext, ManifestPath, RunTarget, StageTiming, WorkflowSettings, - fixtures, provider_ids, test_support, + fixtures, test_support, }; use fabro_vault::SecretType; + use lithos_llm::catalog::builtin; use object_store::memory::InMemory; use super::*; @@ -1475,7 +1476,7 @@ mod tests { error, Error::ModelSelection(fabro_llm::ModelSelectionError::ProviderUnavailable { provider - }) if provider == provider_ids::openai() + }) if provider == builtin::openai() )); } diff --git a/lib/components/fabro-workflow/src/outcome.rs b/lib/components/fabro-workflow/src/outcome.rs index 2025bab23..c3f9135d5 100644 --- a/lib/components/fabro-workflow/src/outcome.rs +++ b/lib/components/fabro-workflow/src/outcome.rs @@ -125,7 +125,8 @@ pub fn format_cost(cost: f64) -> String { mod tests { use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::{test_catalog, test_catalog_with_overlay}; - use fabro_types::{ModelId, ModelRef, ProviderId, Speed, TokenCounts, UsdMicros, provider_ids}; + use fabro_types::{ModelId, ModelRef, ProviderId, Speed, TokenCounts, UsdMicros}; + use lithos_llm::catalog::builtin; use super::{OutcomeExt, billed_model_usage_from_llm}; @@ -149,7 +150,7 @@ mod tests { }; let billed = billed_model_usage_from_llm( &catalog(), - &model_ref(provider_ids::openai(), "gpt-5.4", None), + &model_ref(builtin::openai(), "gpt-5.4", None), usage, ) .unwrap(); @@ -169,7 +170,7 @@ mod tests { }; let billed = billed_model_usage_from_llm( &catalog(), - &model_ref(provider_ids::openai(), "gpt-5.4", None), + &model_ref(builtin::openai(), "gpt-5.4", None), usage, ) .unwrap() @@ -199,11 +200,7 @@ mod tests { }; let billed = billed_model_usage_from_llm( &catalog(), - &model_ref( - provider_ids::anthropic(), - "claude-opus-5", - Some(Speed::Fast), - ), + &model_ref(builtin::anthropic(), "claude-opus-5", Some(Speed::Fast)), usage, ) .unwrap(); @@ -253,7 +250,7 @@ pricing = { input_usd_micros_per_million = 1000000, output_usd_micros_per_millio fn passthrough_model_on_known_provider_has_no_cost() { let billed = billed_model_usage_from_llm( &catalog(), - &model_ref(provider_ids::openai(), "brand-new-model", None), + &model_ref(builtin::openai(), "brand-new-model", None), TokenCounts { input: 10, output: 5, diff --git a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs index ec254b3d2..426cf15a4 100644 --- a/lib/components/fabro-workflow/src/pipeline/execute/tests.rs +++ b/lib/components/fabro-workflow/src/pipeline/execute/tests.rs @@ -265,7 +265,7 @@ async fn execute_test_run_with_options( }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -325,7 +325,7 @@ async fn execute_runs_start_to_exit_and_returns_final_context() { }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -466,7 +466,7 @@ async fn resumed_in_flight_node_starts_a_new_stage_execution() { }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -580,7 +580,7 @@ async fn run_with_lifecycle( }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), diff --git a/lib/components/fabro-workflow/src/pipeline/finalize.rs b/lib/components/fabro-workflow/src/pipeline/finalize.rs index bc1ec1860..9d40f7520 100644 --- a/lib/components/fabro-workflow/src/pipeline/finalize.rs +++ b/lib/components/fabro-workflow/src/pipeline/finalize.rs @@ -996,7 +996,7 @@ mod tests { None, locations, tokio_util::sync::CancellationToken::new(), - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-sonnet-4-6".to_string(), auth_test_support::vault_only_credential_source(), Arc::new(fabro_llm::test_support::test_catalog()), @@ -1029,7 +1029,7 @@ mod tests { None, locations, tokio_util::sync::CancellationToken::new(), - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-sonnet-4-6".to_string(), auth_test_support::vault_only_credential_source(), Arc::new(fabro_llm::test_support::test_catalog()), diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index d4effcf2e..79bde9850 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -901,7 +901,7 @@ mod tests { sandbox: SandboxSpec::Local { working_directory }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -1091,7 +1091,7 @@ mod tests { assert_eq!(initialized.model, "test-model"); assert_eq!( initialized.engine.run.provider_id, - fabro_types::provider_ids::anthropic() + lithos_llm::catalog::builtin::anthropic() ); assert!( readiness( @@ -1217,7 +1217,7 @@ mod tests { let (_registry, effective_dry_run) = build_registry( &LlmSpec { model: "claude-opus-4-6".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -1344,7 +1344,7 @@ mod tests { }, llm: LlmSpec { model: "fake-acp".to_string(), - provider_id: fabro_types::provider_ids::openai(), + provider_id: lithos_llm::catalog::builtin::openai(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -1447,7 +1447,7 @@ mod tests { }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), @@ -1589,7 +1589,7 @@ mod tests { }, llm: LlmSpec { model: "test-model".to_string(), - provider_id: fabro_types::provider_ids::anthropic(), + provider_id: lithos_llm::catalog::builtin::anthropic(), fallbacks: ModelFallbackPolicy::default(), mcp_servers: Vec::new(), model_controls: RunModelControls::default(), diff --git a/lib/components/fabro-workflow/src/services.rs b/lib/components/fabro-workflow/src/services.rs index 2456b56fb..3bdf9c504 100644 --- a/lib/components/fabro-workflow/src/services.rs +++ b/lib/components/fabro-workflow/src/services.rs @@ -320,7 +320,7 @@ impl EngineServices { None, locations, CancellationToken::new(), - fabro_types::provider_ids::anthropic(), + lithos_llm::catalog::builtin::anthropic(), "claude-sonnet-4.6".to_string(), Arc::new(StubCredentialSource), Arc::new(fabro_llm::default_catalog()), diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 47410b8db..680ad5e50 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -14,7 +14,8 @@ use fabro_llm::test_support::test_catalog; use fabro_store::{ArtifactStore, RunProjection, test_support as store_test_support}; #[cfg(feature = "test-support")] use fabro_types::ProviderId; -use fabro_types::{ModelId, ModelRef, provider_ids}; +use fabro_types::{ModelId, ModelRef}; +use lithos_llm::catalog::builtin; use object_store::local::LocalFileSystem; use crate::artifact_upload::ArtifactSink; @@ -89,7 +90,7 @@ pub fn test_usage( output_tokens: u64, ) -> fabro_types::BilledModelUsage { let mut usage = fabro_types::BilledModelUsage::new( - ModelRef::new(provider_ids::openai(), ModelId::new(model_id)), + ModelRef::new(builtin::openai(), ModelId::new(model_id)), fabro_types::TokenCounts { input: input_tokens, output: output_tokens, @@ -269,7 +270,7 @@ async fn initialized( options.hook_runner, locations, run_options.cancel_token.clone(), - provider_ids::anthropic(), + builtin::anthropic(), "claude-sonnet-4-6".to_string(), options .llm_source diff --git a/lib/components/fabro-workflow/tests/materialize_run.rs b/lib/components/fabro-workflow/tests/materialize_run.rs index be8a28522..04939cb0a 100644 --- a/lib/components/fabro-workflow/tests/materialize_run.rs +++ b/lib/components/fabro-workflow/tests/materialize_run.rs @@ -1,10 +1,11 @@ use fabro_graphviz::graph::Graph; use fabro_graphviz::parser; use fabro_llm::test_support::test_catalog; +use fabro_types::WorkflowSettings; use fabro_types::settings::InterpString; use fabro_types::settings::run::{PullRequestSettings, RunGoal, RunModelSettings, RunNamespace}; -use fabro_types::{WorkflowSettings, provider_ids}; use fabro_workflow::run_materialization::materialize_run; +use lithos_llm::catalog::builtin; fn graph(source: &str) -> Graph { parser::parse(source).expect("graph should parse") @@ -35,7 +36,7 @@ fn materialize_run_applies_graph_and_catalog_defaults() { }; let materialized = materialize_run(settings, &graph(source), &test_catalog(), &[ - provider_ids::anthropic(), + builtin::anthropic(), ]) .unwrap(); let resolved = &materialized.run; @@ -62,7 +63,7 @@ fn materialize_run_uses_configured_provider_defaults() { WorkflowSettings::default(), &graph(source), &test_catalog(), - &[provider_ids::openai()], + &[builtin::openai()], ) .unwrap(); let resolved = &materialized.run; diff --git a/lib/foundation/fabro-api/Cargo.toml b/lib/foundation/fabro-api/Cargo.toml index 1930ef92f..02f85c3df 100644 --- a/lib/foundation/fabro-api/Cargo.toml +++ b/lib/foundation/fabro-api/Cargo.toml @@ -19,6 +19,7 @@ fabro-automation = { path = "../../components/fabro-automation" } fabro-config = { path = "../fabro-config" } fabro-environment.workspace = true fabro-types = { path = "../fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } progenitor-client = "0.13" regress = "0.10" reqwest.workspace = true diff --git a/lib/foundation/fabro-api/tests/model_round_trip.rs b/lib/foundation/fabro-api/tests/model_round_trip.rs index 0a18443f1..edde1b7db 100644 --- a/lib/foundation/fabro-api/tests/model_round_trip.rs +++ b/lib/foundation/fabro-api/tests/model_round_trip.rs @@ -1,9 +1,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{Model as ApiModel, ModelControls as ApiModelControls}; -use fabro_types::{ - Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort, provider_ids, -}; +use fabro_types::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort}; +use lithos_llm::catalog::builtin; #[test] fn model_reuses_canonical_type() { @@ -15,7 +14,7 @@ fn model_reuses_canonical_type() { fn model_json_matches_openapi_shape() { let model = Model { id: "claude-opus-4.7".into(), - provider: provider_ids::anthropic(), + provider: builtin::anthropic(), family: "claude-4".to_string(), display_name: "Claude Opus 4.7".to_string(), limits: ModelLimits { diff --git a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs index 877cc39ef..8b03868eb 100644 --- a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs +++ b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs @@ -1,7 +1,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ModelHandle as ApiModelHandle, ProviderId as ApiProviderId}; -use fabro_types::{ModelHandle, ModelId, ProviderId, provider_ids}; +use fabro_types::{ModelHandle, ModelId, ProviderId}; +use lithos_llm::catalog::builtin; use serde_json::json; #[test] @@ -13,7 +14,7 @@ fn provider_id_and_model_handle_reuse_lithos_types() { #[test] fn provider_id_json_is_a_bare_string() { assert_eq!( - serde_json::to_value(provider_ids::anthropic()).unwrap(), + serde_json::to_value(builtin::anthropic()).unwrap(), json!("anthropic") ); assert_eq!( @@ -24,7 +25,7 @@ fn provider_id_json_is_a_bare_string() { #[test] fn model_handle_json_matches_openapi_shape() { - let handle = ModelHandle::new(provider_ids::openai(), ModelId::new("gpt-5.4")); + let handle = ModelHandle::new(builtin::openai(), ModelId::new("gpt-5.4")); let json = serde_json::to_value(&handle).unwrap(); assert_eq!(json, json!({"provider": "openai", "model": "gpt-5.4"})); let round_trip: ApiModelHandle = serde_json::from_value(json).unwrap(); diff --git a/lib/foundation/fabro-api/tests/provider_round_trip.rs b/lib/foundation/fabro-api/tests/provider_round_trip.rs index e15492123..330b50278 100644 --- a/lib/foundation/fabro-api/tests/provider_round_trip.rs +++ b/lib/foundation/fabro-api/tests/provider_round_trip.rs @@ -1,7 +1,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::Provider as ApiProvider; -use fabro_types::{Provider, ProviderId, provider_ids}; +use fabro_types::{Provider, ProviderId}; +use lithos_llm::catalog::builtin; #[test] fn provider_reuses_canonical_type() { @@ -11,7 +12,7 @@ fn provider_reuses_canonical_type() { #[test] fn provider_json_matches_openapi_shape() { let provider = Provider { - id: provider_ids::anthropic(), + id: builtin::anthropic(), display_name: "Anthropic".to_string(), adapter: "anthropic".to_string(), base_url: "https://api.anthropic.test".to_string(), diff --git a/lib/foundation/fabro-api/tests/session_contract_round_trip.rs b/lib/foundation/fabro-api/tests/session_contract_round_trip.rs index 0afb3c683..044e8a0b6 100644 --- a/lib/foundation/fabro-api/tests/session_contract_round_trip.rs +++ b/lib/foundation/fabro-api/tests/session_contract_round_trip.rs @@ -33,7 +33,7 @@ fn session_detail_round_trips_messages_active_turn_and_last_seq() { title: Some("Ask Fabro".to_string()), status: SessionStatus::Running, model: Some("gpt-5.4".to_string()), - provider: Some(fabro_types::provider_ids::openai()), + provider: Some(lithos_llm::catalog::builtin::openai()), active_turn: Some(SessionTurn { id: turn_id, started_at: turn_started_at, diff --git a/lib/foundation/fabro-auth/src/secrets.rs b/lib/foundation/fabro-auth/src/secrets.rs index 3661f660f..09d0a7d17 100644 --- a/lib/foundation/fabro-auth/src/secrets.rs +++ b/lib/foundation/fabro-auth/src/secrets.rs @@ -6,8 +6,7 @@ //! vault under those same names, so the vault entry an operator creates and //! the environment variable a shell exports are spelled alike. -use fabro_types::provider_ids; -use lithos_llm::catalog::{AuthScheme, CatalogProvider, ProviderId}; +use lithos_llm::catalog::{AuthScheme, CatalogProvider, ProviderId, builtin}; use lithos_llm::credentials::ConventionalCredentials; use crate::OPENAI_CODEX_VAULT_SECRET_NAME; @@ -43,5 +42,5 @@ pub fn accepts_api_key(provider: &CatalogProvider) -> bool { /// The vault entry holding `provider`'s OAuth credential, for the providers /// Fabro can log into with a browser flow. pub(crate) fn oauth_secret_name(provider: &ProviderId) -> Option<&'static str> { - (provider.as_str() == provider_ids::OPENAI_CODEX).then_some(OPENAI_CODEX_VAULT_SECRET_NAME) + (provider.as_str() == builtin::ids::OPENAI_CODEX).then_some(OPENAI_CODEX_VAULT_SECRET_NAME) } diff --git a/lib/foundation/fabro-auth/src/strategies/codex_device.rs b/lib/foundation/fabro-auth/src/strategies/codex_device.rs index 47681b2e1..005a1b8a3 100644 --- a/lib/foundation/fabro-auth/src/strategies/codex_device.rs +++ b/lib/foundation/fabro-auth/src/strategies/codex_device.rs @@ -5,7 +5,7 @@ use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use chrono::{DateTime, Utc}; use fabro_http::HttpClient; -use fabro_types::provider_ids; +use lithos_llm::catalog::builtin; use serde::{Deserialize, Serialize}; use serde_json::json; use tokio::time::sleep; @@ -299,7 +299,7 @@ impl AuthStrategy for CodexDeviceStrategy { .map_err(anyhow::Error::msg)?; Ok(LoginResult::OAuth { - provider: provider_ids::openai(), + provider: builtin::openai(), credential: OAuthCredential { tokens: OAuthTokens { access_token: token_response.access_token, diff --git a/lib/foundation/fabro-auth/src/strategy.rs b/lib/foundation/fabro-auth/src/strategy.rs index 4c91b2486..8db2fbf22 100644 --- a/lib/foundation/fabro-auth/src/strategy.rs +++ b/lib/foundation/fabro-auth/src/strategy.rs @@ -1,6 +1,5 @@ use async_trait::async_trait; -use fabro_types::provider_ids; -use lithos_llm::catalog::{Catalog, ProviderId}; +use lithos_llm::catalog::{Catalog, ProviderId, builtin}; use crate::context::{AuthContextRequest, AuthContextResponse}; use crate::credential::{OAuthConfig, OAuthCredential}; @@ -74,7 +73,7 @@ pub fn strategy_for( // forgets the constraint. assert_eq!( provider_id.as_str(), - provider_ids::OPENAI, + builtin::ids::OPENAI, "CodexDevice auth is only constructed by CLI code for the \ OpenAI provider; all existing call sites enforce this pairing: \ got provider_id={provider_id}" diff --git a/lib/foundation/fabro-auth/src/vault_source.rs b/lib/foundation/fabro-auth/src/vault_source.rs index c1bef4db0..7f211d7d3 100644 --- a/lib/foundation/fabro-auth/src/vault_source.rs +++ b/lib/foundation/fabro-auth/src/vault_source.rs @@ -19,10 +19,9 @@ use std::sync::Arc; use async_trait::async_trait; use fabro_static::EnvVars; -use fabro_types::provider_ids; use fabro_types::settings::{InterpString, ResolveCtx}; use fabro_vault::{SecretType, Vault}; -use lithos_llm::catalog::{CatalogProvider, ProviderId}; +use lithos_llm::catalog::{CatalogProvider, ProviderId, builtin}; use lithos_llm::credentials::{ ConventionalCredentials, CredentialError, CredentialHeader, CredentialProvider, Credentials, HttpAuthentication, HttpCredentials, SecretValue, @@ -184,7 +183,7 @@ impl VaultCredentialSource { ) -> Credentials { if let Credentials::Http(http) = &mut credentials { http.extra_headers.extend(interpolated); - if provider.id().as_str() == provider_ids::OPENAI { + if provider.id().as_str() == builtin::ids::OPENAI { for (variable, header) in [ (EnvVars::OPENAI_ORG_ID, OPENAI_ORGANIZATION_HEADER), (EnvVars::OPENAI_PROJECT_ID, OPENAI_PROJECT_HEADER), diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index ce95b8e18..5f42b0798 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -27,7 +27,6 @@ pub mod outcome; pub mod pair; pub mod parallel; pub mod principal; -pub mod provider_ids; pub mod pull_request; pub mod repository; pub mod run; diff --git a/lib/foundation/fabro-types/src/provider_ids.rs b/lib/foundation/fabro-types/src/provider_ids.rs deleted file mode 100644 index e3ab92142..000000000 --- a/lib/foundation/fabro-types/src/provider_ids.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Well-known provider identifiers. -//! -//! Provider identity is open-ended catalog data, so [`ProviderId`] is a plain -//! string newtype. The first-party providers are named here because code -//! paths such as Codex login and the install flow refer to them directly. - -use lithos_llm::catalog::ProviderId; - -pub const ANTHROPIC: &str = "anthropic"; -pub const OPENAI: &str = "openai"; -/// The ChatGPT-subscription deployment that stands in for [`OPENAI`] when a -/// Codex OAuth credential is present. -pub const OPENAI_CODEX: &str = "openai-codex"; -pub const GEMINI: &str = "gemini"; - -#[must_use] -pub fn anthropic() -> ProviderId { - ProviderId::new(ANTHROPIC) -} - -#[must_use] -pub fn openai() -> ProviderId { - ProviderId::new(OPENAI) -} - -#[must_use] -pub fn openai_codex() -> ProviderId { - ProviderId::new(OPENAI_CODEX) -} - -#[must_use] -pub fn gemini() -> ProviderId { - ProviderId::new(GEMINI) -} diff --git a/lib/foundation/fabro-types/src/run_event/agent.rs b/lib/foundation/fabro-types/src/run_event/agent.rs index b06f21e08..639774573 100644 --- a/lib/foundation/fabro-types/src/run_event/agent.rs +++ b/lib/foundation/fabro-types/src/run_event/agent.rs @@ -523,16 +523,16 @@ pub struct AgentSkillActivatedProps { #[cfg(test)] mod tests { + use lithos_llm::catalog::builtin; use serde_json::json; use super::*; - use crate::provider_ids; use crate::transcript::{ ContentPart, MessageKind, MessageSource, TranscriptMessage, tool_result_from_json, }; fn sample_model_ref() -> ModelRef { - ModelRef::new(provider_ids::openai(), "gpt-5".into()) + ModelRef::new(builtin::openai(), "gpt-5".into()) } #[test] diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index fc2e6c9c5..bef0919f9 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -1036,12 +1036,13 @@ impl<'de> Deserialize<'de> for RunEvent { #[cfg(test)] mod tests { + use lithos_llm::catalog::builtin; use serde_json::json; use super::*; use crate::{ AuthMethod, BlobHash, CommandTermination, Edge, Graph, IdpIdentity, ModelRef, Node, - PendingReason, WorkflowSettings, fixtures, provider_ids, test_support, + PendingReason, WorkflowSettings, fixtures, test_support, }; fn user_principal(login: &str) -> Principal { @@ -2364,7 +2365,7 @@ mod tests { fn agent_message_omits_context_window_when_absent() { let body = EventBody::AgentMessage(AgentMessageProps { text: "ok".to_string(), - model: ModelRef::new(provider_ids::openai(), "gpt-5.4".into()), + model: ModelRef::new(builtin::openai(), "gpt-5.4".into()), billing: BilledTokenCounts::default(), cost_source: None, tool_call_count: 0, @@ -2391,7 +2392,7 @@ mod tests { fn agent_message_omits_reasoning_when_absent() { let body = EventBody::AgentMessage(AgentMessageProps { text: "ok".to_string(), - model: ModelRef::new(provider_ids::openai(), "gpt-5.4".into()), + model: ModelRef::new(builtin::openai(), "gpt-5.4".into()), billing: BilledTokenCounts::default(), cost_source: None, tool_call_count: 0, @@ -2415,7 +2416,7 @@ mod tests { fn agent_message_carries_reasoning_through_canonical_json() { let body = EventBody::AgentMessage(AgentMessageProps { text: String::new(), - model: ModelRef::new(provider_ids::openai(), "gpt-5.4".into()), + model: ModelRef::new(builtin::openai(), "gpt-5.4".into()), billing: BilledTokenCounts::default(), cost_source: None, tool_call_count: 1, @@ -2468,7 +2469,7 @@ mod tests { }; let body = EventBody::AgentMessage(AgentMessageProps { text: "ok".to_string(), - model: ModelRef::new(provider_ids::openai(), "gpt-5.4".into()), + model: ModelRef::new(builtin::openai(), "gpt-5.4".into()), billing: BilledTokenCounts::default(), cost_source: None, tool_call_count: 0, From 619cb44e3c4ad90807fe02227cac4bb8396fea2b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Thu, 10 Sep 2026 10:03:08 -0600 Subject: [PATCH 7/7] Import lithos-llm types directly instead of through fabro-types fabro-types no longer re-exports the lithos catalog and request types (ProviderId, ModelId, ModelHandle, Message, ContentPart, TokenCounts, Cost, Speed, ReasoningEffort, ReasoningOutput, and the rest). Every crate that uses them depends on lithos-llm and names them there, and the fabro-api progenitor replacements point at the lithos paths. Co-Authored-By: Claude Fable 5.1 --- Cargo.lock | 2 + docs/public/reference/sdk.mdx | 4 +- lib/apps/fabro-cli/src/args.rs | 7 +- lib/apps/fabro-cli/src/commands/exec.rs | 2 +- lib/apps/fabro-cli/src/commands/install.rs | 4 +- lib/apps/fabro-cli/src/commands/model.rs | 6 +- .../fabro-cli/src/commands/provider/login.rs | 2 +- .../src/commands/run/run_progress/mod.rs | 7 +- lib/apps/fabro-cli/src/main.rs | 4 +- .../fabro-cli/src/shared/provider_auth.rs | 3 +- lib/apps/fabro-server/src/demo/mod.rs | 10 +-- lib/apps/fabro-server/src/diagnostics.rs | 2 +- lib/apps/fabro-server/src/install.rs | 3 +- lib/apps/fabro-server/src/run_compiler.rs | 5 +- lib/apps/fabro-server/src/run_manifest.rs | 6 +- .../fabro-server/src/run_title_generation.rs | 3 +- lib/apps/fabro-server/src/server.rs | 6 +- .../src/server/handler/completions.rs | 2 +- .../fabro-server/src/server/handler/models.rs | 2 +- .../fabro-server/src/server/handler/pair.rs | 5 +- .../fabro-server/src/server/handler/runs.rs | 3 +- .../src/server/handler/sessions.rs | 8 ++- lib/apps/fabro-server/src/server/tests.rs | 20 +++--- lib/apps/fabro-server/src/test_support.rs | 3 +- lib/apps/fabro-server/tests/it/api/install.rs | 2 +- .../fabro-agent/src/agent_profile.rs | 4 +- lib/components/fabro-agent/src/apply_patch.rs | 5 +- lib/components/fabro-agent/src/cli.rs | 3 +- lib/components/fabro-agent/src/compaction.rs | 3 +- lib/components/fabro-agent/src/config.rs | 3 +- .../fabro-agent/src/context_window.rs | 8 +-- .../fabro-agent/src/file_tracker.rs | 3 +- lib/components/fabro-agent/src/history.rs | 8 +-- .../fabro-agent/src/loop_detection.rs | 2 +- .../fabro-agent/src/mcp_integration.rs | 2 +- .../fabro-agent/src/profiles/anthropic.rs | 4 +- .../fabro-agent/src/profiles/claude5.rs | 4 +- .../fabro-agent/src/profiles/claude5_tools.rs | 2 +- .../fabro-agent/src/profiles/gemini.rs | 4 +- .../fabro-agent/src/profiles/gpt56.rs | 5 +- .../fabro-agent/src/profiles/kimi.rs | 3 +- .../fabro-agent/src/profiles/kimi_tools.rs | 2 +- .../fabro-agent/src/profiles/mod.rs | 7 +- .../fabro-agent/src/profiles/openai.rs | 4 +- .../fabro-agent/src/question_tools.rs | 3 +- lib/components/fabro-agent/src/session.rs | 28 ++++---- lib/components/fabro-agent/src/skills.rs | 2 +- lib/components/fabro-agent/src/subagent.rs | 6 +- .../fabro-agent/src/task_reminder.rs | 2 +- .../fabro-agent/src/test_support.rs | 9 +-- lib/components/fabro-agent/src/todo_tools.rs | 3 +- .../fabro-agent/src/tool_execution.rs | 20 +++--- .../fabro-agent/src/tool_registry.rs | 7 +- lib/components/fabro-agent/src/tools.rs | 7 +- lib/components/fabro-agent/src/types.rs | 13 ++-- lib/components/fabro-agent/src/web_search.rs | 2 +- .../fabro-agent/tests/it/parity_matrix.rs | 6 +- lib/components/fabro-hooks/src/executor.rs | 5 +- lib/components/fabro-llm/src/api.rs | 8 +-- lib/components/fabro-llm/src/client.rs | 3 +- lib/components/fabro-llm/src/error.rs | 2 +- lib/components/fabro-llm/src/probe.rs | 5 +- lib/components/fabro-llm/src/selection.rs | 3 +- lib/components/fabro-llm/src/test_support.rs | 9 ++- lib/components/fabro-store/Cargo.toml | 1 + lib/components/fabro-store/src/run_state.rs | 27 ++++---- .../fabro-workflow/src/billing_rollup.rs | 4 +- .../fabro-workflow/src/event/convert.rs | 20 +++--- .../fabro-workflow/src/event/events.rs | 9 +-- .../fabro-workflow/src/event/redaction.rs | 8 +-- .../fabro-workflow/src/event/sink.rs | 8 ++- .../fabro-workflow/src/handler/agent.rs | 3 +- .../src/handler/llm/activation_lease.rs | 3 +- .../fabro-workflow/src/handler/llm/api.rs | 12 ++-- .../src/handler/llm/preamble.rs | 5 +- .../fabro-workflow/src/handler/llm/router.rs | 2 +- .../fabro-workflow/src/handler/llm/routing.rs | 3 +- .../fabro-workflow/src/handler/prompt.rs | 8 ++- .../fabro-workflow/src/model_fallback.rs | 6 +- .../fabro-workflow/src/operations/create.rs | 5 +- .../fabro-workflow/src/operations/start.rs | 3 +- .../fabro-workflow/src/operations/validate.rs | 3 +- lib/components/fabro-workflow/src/outcome.rs | 8 ++- .../src/pipeline/pull_request.rs | 13 ++-- .../fabro-workflow/src/pipeline/types.rs | 3 +- .../fabro-workflow/src/run_materialization.rs | 3 +- lib/components/fabro-workflow/src/services.rs | 3 +- .../fabro-workflow/src/test_support.rs | 11 ++-- .../src/transforms/model_resolution.rs | 2 +- .../fabro-workflow/tests/it/integration.rs | 5 +- lib/foundation/fabro-api/build.rs | 34 +++++----- lib/foundation/fabro-api/src/lib.rs | 64 ++++++++++--------- ...gent_session_activated_props_round_trip.rs | 3 +- .../tests/completion_message_round_trip.rs | 2 +- .../tests/completion_usage_round_trip.rs | 2 +- .../fabro-api/tests/cost_source_round_trip.rs | 2 +- .../create_completion_request_round_trip.rs | 2 +- .../fabro-api/tests/model_round_trip.rs | 3 +- .../fabro-api/tests/provider_id_round_trip.rs | 3 +- .../fabro-api/tests/provider_round_trip.rs | 4 +- .../tests/reasoning_output_round_trip.rs | 2 +- .../tests/run_billing_stage_round_trip.rs | 3 +- .../tests/stage_model_usage_round_trip.rs | 3 +- .../tests/stage_projection_round_trip.rs | 15 +++-- lib/foundation/fabro-client/Cargo.toml | 1 + lib/foundation/fabro-client/src/client.rs | 8 ++- lib/foundation/fabro-types/src/billing.rs | 3 +- lib/foundation/fabro-types/src/catalog_api.rs | 4 +- lib/foundation/fabro-types/src/lib.rs | 15 +---- .../fabro-types/src/run_event/agent.rs | 15 +++-- .../fabro-types/src/run_event/misc.rs | 5 +- .../fabro-types/src/run_event/mod.rs | 3 +- .../fabro-types/src/run_event/session.rs | 3 +- .../fabro-types/src/run_event/stage.rs | 4 +- .../fabro-types/src/run_projection.rs | 7 +- lib/foundation/fabro-types/src/transcript.rs | 6 +- 116 files changed, 401 insertions(+), 333 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbbd434d7..92168c80e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2517,6 +2517,7 @@ dependencies = [ "futures", "httpmock", "libc", + "lithos-llm", "progenitor-client", "rand 0.9.4", "serde", @@ -3120,6 +3121,7 @@ dependencies = [ "futures", "hex", "insta", + "lithos-llm", "object_store", "percent-encoding", "serde", diff --git a/docs/public/reference/sdk.mdx b/docs/public/reference/sdk.mdx index d45a75bba..2c6b3f91b 100644 --- a/docs/public/reference/sdk.mdx +++ b/docs/public/reference/sdk.mdx @@ -387,7 +387,7 @@ Credential sources live in `fabro-auth`: `VaultCredentialSource` reads a Fabro v ```rust use fabro_llm::Request; -use fabro_types::{Message, Role}; +use lithos_llm::types::{Message, Role}; let request = Request::builder() .model("openai/gpt-5.4") @@ -506,7 +506,7 @@ use std::sync::Arc; use fabro_llm::ClientOptions; use fabro_llm::gateway::GatewayAdapter; -use fabro_types::ProviderId; +use lithos_llm::catalog::ProviderId; let adapter = Arc::new(GatewayAdapter::new(Box::new(my_transport))); let built = fabro_llm::build_offline_client( diff --git a/lib/apps/fabro-cli/src/args.rs b/lib/apps/fabro-cli/src/args.rs index f44d554f5..cc04ef12a 100644 --- a/lib/apps/fabro-cli/src/args.rs +++ b/lib/apps/fabro-cli/src/args.rs @@ -7,10 +7,11 @@ use fabro_agent::cli::AgentArgs; use fabro_config::{CliLayer, CliLoggingLayer, CliOutputLayer, CliUpdatesLayer}; use fabro_server::serve::DEFAULT_TCP_PORT; use fabro_static::EnvVars; -use fabro_types::ReasoningEffort; use fabro_types::settings::cli::{OutputFormat, OutputVerbosity}; use fabro_types::settings::run::MergeStrategy; use fabro_util::printer::Printer; +use lithos_llm::catalog::ProviderId; +use lithos_llm::types::ReasoningEffort; pub(crate) const LONG_VERSION: &str = concat!( env!("CARGO_PKG_VERSION"), @@ -836,7 +837,7 @@ pub(crate) struct ProviderLoginArgs { /// LLM provider to authenticate with #[arg(long)] - pub(crate) provider: fabro_types::ProviderId, + pub(crate) provider: ProviderId, /// Read an API key from stdin instead of prompting #[arg(long)] @@ -1728,7 +1729,7 @@ pub(crate) struct InstallGithubArgs { #[derive(Args, Debug, Clone, Default)] pub(crate) struct InstallNonInteractiveArgs { #[arg(long, hide = true)] - pub(crate) llm_provider: Option, + pub(crate) llm_provider: Option, #[arg(long, hide = true)] pub(crate) llm_api_key_stdin: bool, diff --git a/lib/apps/fabro-cli/src/commands/exec.rs b/lib/apps/fabro-cli/src/commands/exec.rs index 46340bdde..5711729cd 100644 --- a/lib/apps/fabro-cli/src/commands/exec.rs +++ b/lib/apps/fabro-cli/src/commands/exec.rs @@ -11,10 +11,10 @@ use fabro_llm::ErrorKind; use fabro_llm::gateway::{GatewayAdapter, GatewayError, GatewayTransport}; use fabro_llm::lithos_catalog::Catalog; use fabro_mcp::config::McpServerSettings; -use fabro_types::ProviderId; use fabro_types::settings::cli::OutputFormat as SettingsOutputFormat; use fabro_types::settings::run::ResolvedMcpEntry; use fabro_util::exit::{self, ErrorExt, ExitClass}; +use lithos_llm::catalog::ProviderId; use crate::args::ExecArgs; use crate::command_context::CommandContext; diff --git a/lib/apps/fabro-cli/src/commands/install.rs b/lib/apps/fabro-cli/src/commands/install.rs index f888fc24b..1c61c032c 100644 --- a/lib/apps/fabro-cli/src/commands/install.rs +++ b/lib/apps/fabro-cli/src/commands/install.rs @@ -37,16 +37,16 @@ use fabro_install::{ use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_server::serve; use fabro_store::ArtifactStore; +use fabro_types::ServerSettings; use fabro_types::settings::server::ServerAuthMethod; use fabro_types::settings::validate_public_url_with_label; -use fabro_types::{ProviderId, ServerSettings}; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; use fabro_util::version::FABRO_VERSION; use fabro_util::{browser, dev_token, path, session_secret}; use fabro_vault::SecretType as VaultSecretType; use futures::future::BoxFuture; -use lithos_llm::catalog::builtin; +use lithos_llm::catalog::{ProviderId, builtin}; use rand::Rng; use tokio::net::TcpListener; use tokio::process::Command as TokioCommand; diff --git a/lib/apps/fabro-cli/src/commands/model.rs b/lib/apps/fabro-cli/src/commands/model.rs index caaf0f4e4..47385fb86 100644 --- a/lib/apps/fabro-cli/src/commands/model.rs +++ b/lib/apps/fabro-cli/src/commands/model.rs @@ -2,9 +2,10 @@ use anyhow::{Context, Result, bail}; use cli_table::format::{Border, Justify, Separator}; use cli_table::{Cell, CellStruct, Color, Style, Table}; use fabro_api::types as api_types; -use fabro_types::{Model, ModelTestMode, ProviderId}; +use fabro_types::{Model, ModelTestMode}; use fabro_util::terminal::Styles; use futures::{StreamExt, stream}; +use lithos_llm::catalog::ProviderId; use serde::Serialize; use crate::args::{ModelListArgs, ModelTestArgs, ModelsCommand}; @@ -513,8 +514,9 @@ impl Default for ModelsCommand { #[cfg(test)] mod tests { - use fabro_types::{ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort}; + use fabro_types::{ModelControls, ModelCosts, ModelFeatures, ModelLimits}; use lithos_llm::catalog::builtin; + use lithos_llm::types::ReasoningEffort; use super::*; diff --git a/lib/apps/fabro-cli/src/commands/provider/login.rs b/lib/apps/fabro-cli/src/commands/provider/login.rs index 13369584f..3cd0e3db2 100644 --- a/lib/apps/fabro-cli/src/commands/provider/login.rs +++ b/lib/apps/fabro-cli/src/commands/provider/login.rs @@ -1,9 +1,9 @@ use anyhow::{Context, Result}; use fabro_api::types; use fabro_auth::{AuthContextRequest, AuthMethod, LoginResult, OPENAI_CODEX_VAULT_SECRET_NAME}; -use fabro_types::ProviderId; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; +use lithos_llm::catalog::ProviderId; use tokio::task::spawn_blocking; use crate::args::ProviderLoginArgs; diff --git a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs index 1f851de06..5a7080be7 100644 --- a/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs +++ b/lib/apps/fabro-cli/src/commands/run/run_progress/mod.rs @@ -460,12 +460,13 @@ mod tests { use fabro_agent::{AgentEvent, SandboxEvent}; use fabro_types::run_event::CliEnsureCompletedProps; use fabro_types::{ - MetadataSnapshotFailureKind, MetadataSnapshotPhase, ModelId, ModelRef, ParallelBranchId, - SandboxProviderKind, StageId, TokenCounts, fixtures, + MetadataSnapshotFailureKind, MetadataSnapshotPhase, ModelRef, ParallelBranchId, + SandboxProviderKind, StageId, fixtures, }; use fabro_workflow::event::{Event, RunNoticeLevel, to_run_event, to_run_event_at}; use fabro_workflow::outcome::billed_model_usage_from_llm; - use lithos_llm::catalog::builtin; + use lithos_llm::catalog::{ModelId, builtin}; + use lithos_llm::types::TokenCounts; use super::*; use crate::commands::run::run_progress::stage_display::ToolCallStatus; diff --git a/lib/apps/fabro-cli/src/main.rs b/lib/apps/fabro-cli/src/main.rs index 8d4edd598..3ea411a3d 100644 --- a/lib/apps/fabro-cli/src/main.rs +++ b/lib/apps/fabro-cli/src/main.rs @@ -586,7 +586,7 @@ mod tests { ProviderCommand, ProviderNamespace, }; use clap::error::ErrorKind; - use lithos_llm::catalog::builtin; + use lithos_llm::catalog::{ProviderId, builtin}; use temp_env::with_var; use tokio::runtime::Runtime; @@ -1202,7 +1202,7 @@ destination = "{destination}" Commands::Provider(ProviderNamespace { command: ProviderCommand::Login(args), }) => { - assert_eq!(args.provider, fabro_types::ProviderId::new("bogus")); + assert_eq!(args.provider, ProviderId::new("bogus")); } _ => panic!("expected provider login command"), } diff --git a/lib/apps/fabro-cli/src/shared/provider_auth.rs b/lib/apps/fabro-cli/src/shared/provider_auth.rs index badd55c5d..0a337fbb2 100644 --- a/lib/apps/fabro-cli/src/shared/provider_auth.rs +++ b/lib/apps/fabro-cli/src/shared/provider_auth.rs @@ -20,10 +20,9 @@ use fabro_auth::{ }; use fabro_llm::lithos_catalog::{Catalog, CatalogProvider}; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; -use fabro_types::ProviderId; use fabro_util::printer::Printer; use fabro_util::terminal::Styles; -use lithos_llm::catalog::builtin; +use lithos_llm::catalog::{ProviderId, builtin}; use tokio::task::spawn_blocking; // --------------------------------------------------------------------------- diff --git a/lib/apps/fabro-server/src/demo/mod.rs b/lib/apps/fabro-server/src/demo/mod.rs index 7820a9431..da5c0b574 100644 --- a/lib/apps/fabro-server/src/demo/mod.rs +++ b/lib/apps/fabro-server/src/demo/mod.rs @@ -1097,6 +1097,7 @@ mod runs { RunLifecycle, RunLinks, RunOrigin, RunSize, RunTimestamps, StageId, WorkflowRef, WorkflowSettings, }; + use lithos_llm::catalog::{ModelId, ProviderId}; use super::ts; @@ -1115,7 +1116,7 @@ mod runs { .collect() } - fn billing_model(provider: fabro_types::ProviderId, model_id: &str) -> BillingModelRef { + fn billing_model(provider: ProviderId, model_id: &str) -> BillingModelRef { BillingModelRef { provider, model_id: model_id.into(), @@ -1496,7 +1497,7 @@ mod runs { text: "I'll start by loading the environment configurations for both production and staging to compare them.".into(), model: fabro_types::ModelRef::new( lithos_llm::catalog::builtin::anthropic(), - fabro_types::ModelId::new("claude-opus-4.6"), + ModelId::new("claude-opus-4.6"), ), billing: BilledTokenCounts::default(), cost_source: None, @@ -1572,7 +1573,7 @@ mod runs { text: "I've detected drift in 3 resources between production and staging:\n\n1. **redis.max_connections** — production has 200, staging has 100\n2. **redis.tls** — enabled in production, disabled in staging\n3. **iam.session_duration** — production uses 3600s, staging uses 1800s".into(), model: fabro_types::ModelRef::new( lithos_llm::catalog::builtin::anthropic(), - fabro_types::ModelId::new("claude-opus-4.6"), + ModelId::new("claude-opus-4.6"), ), billing: BilledTokenCounts::default(), cost_source: None, @@ -2074,8 +2075,9 @@ mod workflows { mod billing { use fabro_api::types::*; + use lithos_llm::catalog::ProviderId; - fn billing_model(provider: fabro_types::ProviderId, model_id: &str) -> BillingModelRef { + fn billing_model(provider: ProviderId, model_id: &str) -> BillingModelRef { BillingModelRef { provider, model_id: model_id.into(), diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index ff6edc66f..56e93e899 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -11,7 +11,6 @@ use fabro_llm::probe::{self, ModelTestStatus}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; use fabro_static::EnvVars; -use fabro_types::ProviderId; use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::server::GithubIntegrationStrategy; use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus}; @@ -19,6 +18,7 @@ use fabro_util::dev_token::validate_dev_token_format; use fabro_util::session_secret; use fabro_util::version::FABRO_VERSION; use futures_util::future::join_all; +use lithos_llm::catalog::ProviderId; use serde::Serialize; use tokio::time::error::Elapsed; use tokio::time::timeout; diff --git a/lib/apps/fabro-server/src/install.rs b/lib/apps/fabro-server/src/install.rs index d04ebcb1e..a0f5b38d4 100644 --- a/lib/apps/fabro-server/src/install.rs +++ b/lib/apps/fabro-server/src/install.rs @@ -29,13 +29,14 @@ use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; use fabro_sandbox::daytona; use fabro_static::EnvVars; use fabro_store::ArtifactStore; +use fabro_types::ServerSettings; use fabro_types::settings::run::EnvironmentProvider; use fabro_types::settings::server::ObjectStoreSettings; use fabro_types::settings::{is_wildcard_host, validate_public_url_with_label}; -use fabro_types::{ProviderId, ServerSettings}; use fabro_util::version::FABRO_VERSION; use fabro_util::{Home, session_secret}; use fabro_vault::SecretType as VaultSecretType; +use lithos_llm::catalog::ProviderId; use object_store::aws::resolve_bucket_region; use object_store::path::Path as ObjectStorePath; use object_store::{ClientOptions, RetryConfig}; diff --git a/lib/apps/fabro-server/src/run_compiler.rs b/lib/apps/fabro-server/src/run_compiler.rs index 5e85741f3..0d076f9d6 100644 --- a/lib/apps/fabro-server/src/run_compiler.rs +++ b/lib/apps/fabro-server/src/run_compiler.rs @@ -37,8 +37,8 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_types::settings::interp::{InterpString, ResolveError}; use fabro_types::settings::run::{McpServerSettings, RunGoal}; use fabro_types::{ - AutomationRef, GitContext, ManifestPath, ProviderId, RunId, RunProvenance, RunTarget, - WorkflowSettings, WorkflowVersionId, + AutomationRef, GitContext, ManifestPath, RunId, RunProvenance, RunTarget, WorkflowSettings, + WorkflowVersionId, }; use fabro_util::workspace_glob::{WorkspaceGlob, WorkspaceGlobError}; use fabro_workflow::Error as WorkflowError; @@ -47,6 +47,7 @@ use fabro_workflow::operations::{ CreateRunPersistenceMetadata, MaterializedRun, WorkflowInput, }; use fabro_workflow::workflow_bundle::{BundledWorkflow, WorkflowBundle}; +use lithos_llm::catalog::ProviderId; use tokio::task; /// One project settings source in the acquired source's path namespace. diff --git a/lib/apps/fabro-server/src/run_manifest.rs b/lib/apps/fabro-server/src/run_manifest.rs index cf3984951..37ad41bf9 100644 --- a/lib/apps/fabro-server/src/run_manifest.rs +++ b/lib/apps/fabro-server/src/run_manifest.rs @@ -30,8 +30,7 @@ use fabro_types::settings::cli::OutputVerbosity; use fabro_types::settings::interp::InterpString; use fabro_types::settings::run::{EnvironmentProvider, McpServerSettings, RunGoal, RunNamespace}; use fabro_types::{ - ManifestPath, ProviderId, RunId, RunNoticeLevel, SandboxProviderKind, ServerSettings, - WorkflowSettings, + ManifestPath, RunId, RunNoticeLevel, SandboxProviderKind, ServerSettings, WorkflowSettings, }; use fabro_util::check_report::{CheckDetail, CheckReport, CheckResult, CheckSection, CheckStatus}; use fabro_validate::Severity; @@ -44,6 +43,7 @@ use fabro_workflow::pipeline::Validated; use fabro_workflow::run_materialization::materialize_run_with_ready_providers; use fabro_workflow::workflow_bundle::{BundledWorkflow, ParsedWorkflowConfig, WorkflowBundle}; use futures_util::stream::{self, StreamExt}; +use lithos_llm::catalog::ProviderId; use tokio::process::Command; use tokio::time; @@ -1667,8 +1667,8 @@ fn report_to_api(report: &CheckReport) -> types::PreflightCheckReport { #[cfg(test)] mod tests { - use fabro_types::ProviderId; use fabro_workflow::run_materialization::materialize_run; + use lithos_llm::catalog::ProviderId; use super::*; diff --git a/lib/apps/fabro-server/src/run_title_generation.rs b/lib/apps/fabro-server/src/run_title_generation.rs index 963fecc00..dde818ea1 100644 --- a/lib/apps/fabro-server/src/run_title_generation.rs +++ b/lib/apps/fabro-server/src/run_title_generation.rs @@ -4,8 +4,9 @@ use std::time::Duration; use fabro_llm::{Client, Request}; use fabro_template::{TemplateContext, TemplateError}; -use fabro_types::{Graph, MAX_RUN_TITLE_CHARS, ProviderId, RunId}; +use fabro_types::{Graph, MAX_RUN_TITLE_CHARS, RunId}; use fabro_util::error; +use lithos_llm::catalog::ProviderId; use serde::Serialize; use toml::Value as TomlValue; diff --git a/lib/apps/fabro-server/src/server.rs b/lib/apps/fabro-server/src/server.rs index da28e8865..3de5fdb4d 100644 --- a/lib/apps/fabro-server/src/server.rs +++ b/lib/apps/fabro-server/src/server.rs @@ -93,9 +93,8 @@ use fabro_types::settings::server::{ use fabro_types::{ AgentBackend, AskFabro, AskFabroUnavailableReason, BilledTokenCounts, BlobHash, EventBody, InterviewQuestionRecord, ModelRef, ModelTestMode, PairId, PairMessageId, PairTarget, - PendingReason, Principal, ProviderId, PullRequestLink, QuestionType, RunControlAction, - RunEvent, RunId, RunRunnableSource, RunStatusKind, SandboxProviderKind, ServerSettings, - SessionCapability, + PendingReason, Principal, PullRequestLink, QuestionType, RunControlAction, RunEvent, RunId, + RunRunnableSource, RunStatusKind, SandboxProviderKind, ServerSettings, SessionCapability, }; use fabro_util::error::{ SharedError, collect_causes, render_compact_with_causes, render_with_causes, @@ -116,6 +115,7 @@ use fabro_workflow::run_lookup::{ use fabro_workflow::run_status::{FailureReason, RunStatus, SuccessReason}; use fabro_workflow::{Error as WorkflowError, operations, pull_request}; use futures_util::future::join_all; +use lithos_llm::catalog::ProviderId; use sha2::{Digest, Sha256}; use tempfile::NamedTempFile; use tokio::fs; diff --git a/lib/apps/fabro-server/src/server/handler/completions.rs b/lib/apps/fabro-server/src/server/handler/completions.rs index febd0c939..eb701d215 100644 --- a/lib/apps/fabro-server/src/server/handler/completions.rs +++ b/lib/apps/fabro-server/src/server/handler/completions.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{ModelSelectionError, Request, selection}; -use fabro_types::{Message, Role}; +use lithos_llm::types::{Message, Role}; use super::super::{ ApiError, AppState, CreateCompletionRequest, IntoResponse, Json, ProviderId, RequiredUser, diff --git a/lib/apps/fabro-server/src/server/handler/models.rs b/lib/apps/fabro-server/src/server/handler/models.rs index 10f3ae110..02c5f92b8 100644 --- a/lib/apps/fabro-server/src/server/handler/models.rs +++ b/lib/apps/fabro-server/src/server/handler/models.rs @@ -6,7 +6,7 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_llm::probe::{self, ApiKeyProbeError, ModelTestStatus}; use fabro_llm::{ModelSelectionError, api, selection}; use fabro_redact::redact_string; -use fabro_types::ReasoningEffort; +use lithos_llm::types::ReasoningEffort; use super::super::{ ApiError, AppState, FromStr, IntoResponse, Json, MAX_PAGE_OFFSET, ModelTestMode, Path, diff --git a/lib/apps/fabro-server/src/server/handler/pair.rs b/lib/apps/fabro-server/src/server/handler/pair.rs index 6047eadef..9d0d473e5 100644 --- a/lib/apps/fabro-server/src/server/handler/pair.rs +++ b/lib/apps/fabro-server/src/server/handler/pair.rs @@ -848,10 +848,11 @@ mod tests { use chrono::{TimeZone, Utc}; use fabro_types::run_event::AgentMessageProps; use fabro_types::{ - BilledTokenCounts, EventEnvelope, Graph, ModelId, ModelRef, PairMessageId, ProviderId, - RunEvent, StageId, WorkflowSettings, fixtures, test_support, + BilledTokenCounts, EventEnvelope, Graph, ModelRef, PairMessageId, RunEvent, StageId, + WorkflowSettings, fixtures, test_support, }; use fabro_workflow::event as workflow_event; + use lithos_llm::catalog::{ModelId, ProviderId}; use tower::ServiceExt; use super::*; diff --git a/lib/apps/fabro-server/src/server/handler/runs.rs b/lib/apps/fabro-server/src/server/handler/runs.rs index 1e52514ee..2c2171ae2 100644 --- a/lib/apps/fabro-server/src/server/handler/runs.rs +++ b/lib/apps/fabro-server/src/server/handler/runs.rs @@ -40,6 +40,7 @@ use fabro_workflow::command_log::{command_log_path, read_json_string_blob, read_ use fabro_workflow::run_status::RunStatus; use fabro_workflow::workflow_bundle::WorkflowBundle; use fabro_workflow::{Error as WorkflowError, operations}; +use lithos_llm::catalog::ProviderId; use strum::VariantArray as _; use tokio::fs; use tracing::info; @@ -1409,7 +1410,7 @@ struct GeneratedTitleTask { run_inputs: std::collections::HashMap, client: LlmClient, model_id: String, - provider_id: fabro_types::ProviderId, + provider_id: ProviderId, } fn spawn_generated_title_task(task: GeneratedTitleTask) { diff --git a/lib/apps/fabro-server/src/server/handler/sessions.rs b/lib/apps/fabro-server/src/server/handler/sessions.rs index ccd526c0f..5e6b7e676 100644 --- a/lib/apps/fabro-server/src/server/handler/sessions.rs +++ b/lib/apps/fabro-server/src/server/handler/sessions.rs @@ -36,11 +36,12 @@ use fabro_types::run_event::{ }; use fabro_types::settings::ModelRef as SettingsModelRef; use fabro_types::{ - AgentProfileKind, EventBody, EventEnvelope, ProviderId, RunEvent, RunId, SessionDetail, - SessionId, ToolDefinition, TurnId, + AgentProfileKind, EventBody, EventEnvelope, RunEvent, RunId, SessionDetail, SessionId, TurnId, }; use fabro_workflow::handler::llm::api::register_named_fabro_run_tools; use fabro_workflow::services::FabroRunToolServices; +use lithos_llm::catalog::ProviderId; +use lithos_llm::types::ToolDefinition; use serde_json::Value; use tokio::sync::broadcast::error::RecvError; use tokio::sync::mpsc; @@ -1514,7 +1515,8 @@ mod tests { use fabro_agent::config::ToolAccess; use fabro_agent::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource}; - use fabro_types::{ToolCall, ToolDefinition, test_support}; + use fabro_types::test_support; + use lithos_llm::types::{ToolCall, ToolDefinition}; use super::*; diff --git a/lib/apps/fabro-server/src/server/tests.rs b/lib/apps/fabro-server/src/server/tests.rs index 0a00c7fc0..d51d98414 100644 --- a/lib/apps/fabro-server/src/server/tests.rs +++ b/lib/apps/fabro-server/src/server/tests.rs @@ -25,17 +25,21 @@ use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::run::{ApprovalMode, EnvironmentProvider}; use fabro_types::{ AgentBackend, AttrValue, AuthMethod, BlobHash, CommandTermination, FailureCategory, - FailureDetail, GitRunTarget, Graph, InterviewQuestionRecord, ModelId, ModelRef, Node, Outcome, - ParallelBranchId, QuestionType, ReasoningEffort, RunId, RunSpec, RunTarget, - SandboxProviderKind, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory, - StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, - StageContextWindowWarning, StageModelUsage, StageTiming, SuccessReason, SystemActorKind, - TokenCounts, WorkflowSettings, fixtures, test_support, + FailureDetail, GitRunTarget, Graph, InterviewQuestionRecord, ModelRef, Node, Outcome, + ParallelBranchId, QuestionType, RunId, RunSpec, RunTarget, SandboxProviderKind, + StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, + StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, + StageModelUsage, StageTiming, SuccessReason, SystemActorKind, WorkflowSettings, fixtures, + test_support, }; use fabro_util::check_report::CheckStatus; use fabro_workflow::records::CheckpointExt; use httpmock::Method::{GET, POST}; use httpmock::MockServer; +use lithos_llm::catalog::ModelId; +use lithos_llm::types::{ + ReasoningEffort, ReasoningOutput, Request as LlmRequest, Speed, TokenCounts, +}; use serde_json::json; use tokio::sync::Notify; use tokio_stream::StreamExt as _; @@ -1903,7 +1907,7 @@ async fn resolve_llm_client_uses_vault_key_without_env_lookup_openai_settings() let response = llm_result .client .complete( - fabro_types::Request::builder() + LlmRequest::builder() .model("openai/gpt-5.4") .user("Hello") .build() @@ -18317,7 +18321,7 @@ async fn attach_stream_replays_agent_message_reasoning() { cost: None, tool_call_count: 1, context_window: None, - reasoning: Some(fabro_types::ReasoningOutput::new( + reasoning: Some(ReasoningOutput::new( "inspect the sink first", "read events.rs, then attach", )), diff --git a/lib/apps/fabro-server/src/test_support.rs b/lib/apps/fabro-server/src/test_support.rs index b3743aaeb..72339339c 100644 --- a/lib/apps/fabro-server/src/test_support.rs +++ b/lib/apps/fabro-server/src/test_support.rs @@ -24,9 +24,10 @@ use fabro_static::EnvVars; use fabro_store::{ArtifactStore, Database, test_support as store_test_support}; use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::run::EnvironmentProvider; -use fabro_types::{AuthMethod, IdpIdentity, ProviderId, ServerSettings}; +use fabro_types::{AuthMethod, IdpIdentity, ServerSettings}; use fabro_vault::{SecretType, Vault}; use fabro_workflow::handler::HandlerRegistry; +use lithos_llm::catalog::ProviderId; use object_store::memory::InMemory as MemoryObjectStore; use tokio::runtime::Builder as TokioRuntimeBuilder; use tokio_util::sync::CancellationToken; diff --git a/lib/apps/fabro-server/tests/it/api/install.rs b/lib/apps/fabro-server/tests/it/api/install.rs index 109e459a2..9efb4a9d0 100644 --- a/lib/apps/fabro-server/tests/it/api/install.rs +++ b/lib/apps/fabro-server/tests/it/api/install.rs @@ -17,11 +17,11 @@ use fabro_server::install::{ InstallAppState, InstallFinishHook, InstallFinishInfo, build_install_router, }; use fabro_server::test_support::test_environment_from_storage_dir; -use fabro_types::ProviderId; use fabro_util::Home; use fabro_vault::Vault; use httpmock::Method::GET; use httpmock::MockServer; +use lithos_llm::catalog::ProviderId; use tokio::time::sleep; use tower::ServiceExt; use tracing::field::{Field, Visit}; diff --git a/lib/components/fabro-agent/src/agent_profile.rs b/lib/components/fabro-agent/src/agent_profile.rs index 47712159e..faf560e9c 100644 --- a/lib/components/fabro-agent/src/agent_profile.rs +++ b/lib/components/fabro-agent/src/agent_profile.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use fabro_llm::catalog; use fabro_llm::lithos_catalog::{Catalog, Offering}; -use fabro_types::{AgentProfileKind, ProviderId, ToolDefinition}; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::ProviderId; +use lithos_llm::types::ToolDefinition; use crate::profiles::EnvContext; use crate::sandbox::Sandbox; diff --git a/lib/components/fabro-agent/src/apply_patch.rs b/lib/components/fabro-agent/src/apply_patch.rs index b237e4a06..cac09a884 100644 --- a/lib/components/fabro-agent/src/apply_patch.rs +++ b/lib/components/fabro-agent/src/apply_patch.rs @@ -5,7 +5,7 @@ use std::fmt::Write as _; use std::sync::Arc; -use fabro_types::ToolDefinition; +use lithos_llm::types::ToolDefinition; use crate::sandbox::Sandbox; use crate::tool_registry::{RegisteredTool, ToolSource}; @@ -502,7 +502,8 @@ pub fn make_apply_patch_tool() -> RegisteredTool { mod tests { use std::collections::HashMap; - use fabro_types::{ContentPart, ToolCall, tool_result_to_json}; + use fabro_types::tool_result_to_json; + use lithos_llm::types::{ContentPart, ToolCall}; use tokio::fs; use tokio_util::sync::CancellationToken; diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index 3a6afc7dc..2d9699090 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -18,9 +18,10 @@ use fabro_llm::middleware::{Call, Middleware, Next, Output}; use fabro_llm::{Client, ClientOptions, Error as LlmError, catalog}; use fabro_mcp::config::McpServerSettings; use fabro_static::EnvVars; -use fabro_types::{AgentProfileKind, ModelHandle, ModelId, ProviderId}; +use fabro_types::AgentProfileKind; use fabro_util::terminal::Styles; use fabro_vault::SecretStore; +use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; use tokio::io::{AsyncWriteExt, stdout}; use tokio::signal; diff --git a/lib/components/fabro-agent/src/compaction.rs b/lib/components/fabro-agent/src/compaction.rs index 5c36f2e2d..7112e4566 100644 --- a/lib/components/fabro-agent/src/compaction.rs +++ b/lib/components/fabro-agent/src/compaction.rs @@ -368,7 +368,8 @@ mod tests { use fabro_llm::catalog; use fabro_llm::lithos_catalog::{Catalog, Offering}; use fabro_llm::test_support::test_catalog; - use fabro_types::{TokenCounts, ToolCall, tool_result_from_json}; + use fabro_types::tool_result_from_json; + use lithos_llm::types::{TokenCounts, ToolCall}; use super::*; use crate::event::Emitter; diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs index b23c9b89c..f79362691 100644 --- a/lib/components/fabro-agent/src/config.rs +++ b/lib/components/fabro-agent/src/config.rs @@ -5,7 +5,8 @@ use std::time::Duration; use fabro_llm::RetryPolicy; use fabro_llm::client::default_retry_policy; use fabro_mcp::config::McpServerSettings; -use fabro_types::{AgentProfileKind, PermissionLevel, ReasoningEffort, Speed}; +use fabro_types::{AgentProfileKind, PermissionLevel}; +use lithos_llm::types::{ReasoningEffort, Speed}; /// Callback invoked before each tool execution. Return `Ok(())` to allow, /// `Err(message)` to deny with the given message. diff --git a/lib/components/fabro-agent/src/context_window.rs b/lib/components/fabro-agent/src/context_window.rs index 1d7964ee5..9900c3416 100644 --- a/lib/components/fabro-agent/src/context_window.rs +++ b/lib/components/fabro-agent/src/context_window.rs @@ -4,10 +4,10 @@ use chrono::Utc; use fabro_llm::Request; use fabro_llm::estimate::{self, EstimateWarning, TokenEstimate}; use fabro_types::{ - Role, StageContextWindowBreakdownItem, StageContextWindowCategory, - StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, - StageContextWindowWarning, TokenCounts, text_of, + StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, + StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, text_of, }; +use lithos_llm::types::{Role, TokenCounts}; use crate::memory::MemoryDocument; use crate::native_tool::ToolVocabulary; @@ -372,7 +372,7 @@ fn usage_percent(tokens: u64, denominator: u64) -> f64 { #[cfg(test)] mod tests { - use fabro_types::{Message as LlmMessage, ToolChoice, ToolDefinition}; + use lithos_llm::types::{Message as LlmMessage, ToolChoice, ToolDefinition}; use super::*; use crate::tool_registry::ToolDefinitionWithSource; diff --git a/lib/components/fabro-agent/src/file_tracker.rs b/lib/components/fabro-agent/src/file_tracker.rs index f9982a513..4b3a49559 100644 --- a/lib/components/fabro-agent/src/file_tracker.rs +++ b/lib/components/fabro-agent/src/file_tracker.rs @@ -1,7 +1,8 @@ use std::collections::BTreeMap; use std::fmt::Write; -use fabro_types::{ToolCall, ToolResult, tool_call_arguments, tool_result_to_json}; +use fabro_types::{tool_call_arguments, tool_result_to_json}; +use lithos_llm::types::{ToolCall, ToolResult}; use crate::native_tool::NativeTool; use crate::tool_permissions::canonical_tool_name; diff --git a/lib/components/fabro-agent/src/history.rs b/lib/components/fabro-agent/src/history.rs index addd9325c..21d5ec040 100644 --- a/lib/components/fabro-agent/src/history.rs +++ b/lib/components/fabro-agent/src/history.rs @@ -1,6 +1,7 @@ use std::collections::HashSet; -use fabro_types::{Message as LlmMessage, SessionMessage, TokenCounts}; +use fabro_types::SessionMessage; +use lithos_llm::types::{Message as LlmMessage, TokenCounts}; use crate::types::Message; @@ -164,9 +165,8 @@ mod tests { use std::time::SystemTime; use fabro_llm::types::OPENAI_REASONING_KIND; - use fabro_types::{ - ContentPart, ReasoningContent, Role, TokenCounts, ToolCall, text_of, tool_result_from_json, - }; + use fabro_types::{text_of, tool_result_from_json}; + use lithos_llm::types::{ContentPart, ReasoningContent, Role, TokenCounts, ToolCall}; use super::*; diff --git a/lib/components/fabro-agent/src/loop_detection.rs b/lib/components/fabro-agent/src/loop_detection.rs index 0b313e731..b2282df36 100644 --- a/lib/components/fabro-agent/src/loop_detection.rs +++ b/lib/components/fabro-agent/src/loop_detection.rs @@ -99,7 +99,7 @@ fn is_repeating_pattern(signatures: &[u64], pattern_len: usize) -> bool { mod tests { use std::time::SystemTime; - use fabro_types::{TokenCounts, ToolCall}; + use lithos_llm::types::{TokenCounts, ToolCall}; use super::*; diff --git a/lib/components/fabro-agent/src/mcp_integration.rs b/lib/components/fabro-agent/src/mcp_integration.rs index afd62ec2b..b4c247dc9 100644 --- a/lib/components/fabro-agent/src/mcp_integration.rs +++ b/lib/components/fabro-agent/src/mcp_integration.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use fabro_mcp::connection_manager::{McpConnectionManager, call_result_to_string}; -use fabro_types::ToolDefinition; +use lithos_llm::types::ToolDefinition; use crate::tool_registry::{RegisteredTool, ToolSource}; diff --git a/lib/components/fabro-agent/src/profiles/anthropic.rs b/lib/components/fabro-agent/src/profiles/anthropic.rs index 70813c72a..67bf1dfc5 100644 --- a/lib/components/fabro-agent/src/profiles/anthropic.rs +++ b/lib/components/fabro-agent/src/profiles/anthropic.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ProviderId, builtin}; use super::EnvContext; use crate::agent_profile::AgentProfile; diff --git a/lib/components/fabro-agent/src/profiles/claude5.rs b/lib/components/fabro-agent/src/profiles/claude5.rs index ec5ffd583..c48f0e752 100644 --- a/lib/components/fabro-agent/src/profiles/claude5.rs +++ b/lib/components/fabro-agent/src/profiles/claude5.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ProviderId, builtin}; use super::EnvContext; use crate::agent_profile::AgentProfile; diff --git a/lib/components/fabro-agent/src/profiles/claude5_tools.rs b/lib/components/fabro-agent/src/profiles/claude5_tools.rs index 069dad511..339d4c13d 100644 --- a/lib/components/fabro-agent/src/profiles/claude5_tools.rs +++ b/lib/components/fabro-agent/src/profiles/claude5_tools.rs @@ -7,8 +7,8 @@ use std::sync::Arc; use std::time::Duration; -use fabro_types::{ToolDefinition, ToolDefinitionKind}; use fabro_util::error as util_error; +use lithos_llm::types::{ToolDefinition, ToolDefinitionKind}; use serde_json::Value; use tokio::time; diff --git a/lib/components/fabro-agent/src/profiles/gemini.rs b/lib/components/fabro-agent/src/profiles/gemini.rs index 9af27f5ba..03755ffdc 100644 --- a/lib/components/fabro-agent/src/profiles/gemini.rs +++ b/lib/components/fabro-agent/src/profiles/gemini.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ProviderId, builtin}; use super::EnvContext; use crate::agent_profile::AgentProfile; diff --git a/lib/components/fabro-agent/src/profiles/gpt56.rs b/lib/components/fabro-agent/src/profiles/gpt56.rs index c06fac909..a7844b2bc 100644 --- a/lib/components/fabro-agent/src/profiles/gpt56.rs +++ b/lib/components/fabro-agent/src/profiles/gpt56.rs @@ -17,8 +17,9 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId, ToolDefinition}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ProviderId, builtin}; +use lithos_llm::types::ToolDefinition; use serde_json::Value; use super::EnvContext; diff --git a/lib/components/fabro-agent/src/profiles/kimi.rs b/lib/components/fabro-agent/src/profiles/kimi.rs index ee2dc4d87..a076e0cda 100644 --- a/lib/components/fabro-agent/src/profiles/kimi.rs +++ b/lib/components/fabro-agent/src/profiles/kimi.rs @@ -1,7 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::ProviderId; use super::EnvContext; use crate::agent_profile::AgentProfile; diff --git a/lib/components/fabro-agent/src/profiles/kimi_tools.rs b/lib/components/fabro-agent/src/profiles/kimi_tools.rs index 885943ef9..3bbb858ef 100644 --- a/lib/components/fabro-agent/src/profiles/kimi_tools.rs +++ b/lib/components/fabro-agent/src/profiles/kimi_tools.rs @@ -22,7 +22,7 @@ use std::fmt::Write as _; use std::str::FromStr; use std::sync::Arc; -use fabro_types::ToolDefinition; +use lithos_llm::types::ToolDefinition; use serde_json::Value; use strum::EnumString; diff --git a/lib/components/fabro-agent/src/profiles/mod.rs b/lib/components/fabro-agent/src/profiles/mod.rs index 374b265fb..23f241b43 100644 --- a/lib/components/fabro-agent/src/profiles/mod.rs +++ b/lib/components/fabro-agent/src/profiles/mod.rs @@ -2,7 +2,8 @@ use std::collections::HashMap; use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::ProviderId; #[cfg(test)] use lithos_llm::catalog::builtin; @@ -220,7 +221,7 @@ macro_rules! impl_base_profile_accessors { self.base.profile_kind } - fn provider_id(&self) -> ::fabro_types::ProviderId { + fn provider_id(&self) -> ::lithos_llm::catalog::ProviderId { self.base.provider_id.clone() } @@ -456,7 +457,7 @@ pub fn build_env_context_block_with(env: &dyn Sandbox, ctx: &EnvContext) -> Stri #[cfg(test)] mod tests { use fabro_llm::test_support::{test_catalog, test_catalog_with_overlay}; - use fabro_types::ToolDefinition; + use lithos_llm::types::ToolDefinition; use tokio_util::sync::CancellationToken; use super::*; diff --git a/lib/components/fabro-agent/src/profiles/openai.rs b/lib/components/fabro-agent/src/profiles/openai.rs index 92085a920..faeebde59 100644 --- a/lib/components/fabro-agent/src/profiles/openai.rs +++ b/lib/components/fabro-agent/src/profiles/openai.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{AgentProfileKind, ProviderId}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ProviderId, builtin}; use super::EnvContext; use crate::agent_profile::AgentProfile; diff --git a/lib/components/fabro-agent/src/question_tools.rs b/lib/components/fabro-agent/src/question_tools.rs index 4a7e0d8f8..a97bee11b 100644 --- a/lib/components/fabro-agent/src/question_tools.rs +++ b/lib/components/fabro-agent/src/question_tools.rs @@ -6,7 +6,8 @@ use std::ops::RangeInclusive; use std::sync::Arc; use async_trait::async_trait; -use fabro_types::{AgentProfileKind, InterviewOption, QuestionType, ToolDefinition}; +use fabro_types::{AgentProfileKind, InterviewOption, QuestionType}; +use lithos_llm::types::ToolDefinition; use serde::Deserialize; use serde_json::json; use tokio_util::sync::CancellationToken; diff --git a/lib/components/fabro-agent/src/session.rs b/lib/components/fabro-agent/src/session.rs index 484adbdd2..671091982 100644 --- a/lib/components/fabro-agent/src/session.rs +++ b/lib/components/fabro-agent/src/session.rs @@ -11,13 +11,17 @@ use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_mcp::connection_manager::McpConnectionManager; use fabro_mcp::http_transport; use fabro_types::{ - AgentProfileKind, AgentToolSummary, LlmOutputKind, LlmRetryPhase, Message as LlmMessage, - ModelId, ModelRef, PermissionLevel, Principal, ReasoningEffort, Role, SessionMessage, - SessionRecord, Speed, StageContextWindowProjection, SteeringMessage, TokenCounts, ToolCall, - ToolChoice, UsdMicros, billing, + AgentProfileKind, AgentToolSummary, LlmOutputKind, LlmRetryPhase, ModelRef, PermissionLevel, + Principal, SessionMessage, SessionRecord, StageContextWindowProjection, SteeringMessage, + UsdMicros, billing, }; use fabro_util::shell; use futures::StreamExt; +use lithos_llm::catalog::{ModelId, ProviderId}; +use lithos_llm::types::{ + ContentPart, Message as LlmMessage, ReasoningEffort, Role, Speed, TokenCounts, ToolCall, + ToolChoice, +}; use tokio::sync::{Notify, broadcast}; use tokio::time; use tokio_util::sync::CancellationToken; @@ -110,9 +114,9 @@ fn first_output_kind(event: &StreamEvent) -> Option { StreamEvent::TextDelta { .. } => Some(LlmOutputKind::Text), StreamEvent::ToolCallDelta { .. } => Some(LlmOutputKind::ToolCall), StreamEvent::ContentBlockEnd { part, .. } => match part { - fabro_types::ContentPart::Text { .. } => Some(LlmOutputKind::Text), - fabro_types::ContentPart::Reasoning(_) => Some(LlmOutputKind::Reasoning), - fabro_types::ContentPart::ToolCall(_) => Some(LlmOutputKind::ToolCall), + ContentPart::Text { .. } => Some(LlmOutputKind::Text), + ContentPart::Reasoning(_) => Some(LlmOutputKind::Reasoning), + ContentPart::ToolCall(_) => Some(LlmOutputKind::ToolCall), _ => None, }, _ => None, @@ -565,7 +569,7 @@ impl Session { } #[must_use] - pub fn provider_id(&self) -> fabro_types::ProviderId { + pub fn provider_id(&self) -> ProviderId { self.provider_profile.provider_id() } @@ -2248,12 +2252,10 @@ mod tests { ContentBlockId, ContentBlockKind, OPENAI_COMPAT_REASONING_DETAILS_KIND, ToolCallKind, }; use fabro_llm::{ErrorKind, ResponseStream, RetryPolicy}; - use fabro_types::{ - ContentPart, Cost, CostSource, ReasoningOutput, StageContextWindowCountMethod, - ToolDefinition, text_of, tool_result_to_json, - }; + use fabro_types::{StageContextWindowCountMethod, text_of, tool_result_to_json}; use futures::stream; use lithos_llm::catalog::builtin; + use lithos_llm::types::{ContentPart, Cost, CostSource, ReasoningOutput, ToolDefinition}; use tokio::time::{sleep, timeout}; use super::*; @@ -5440,7 +5442,7 @@ mod tests { #[tokio::test] async fn compaction_includes_structured_prompt_and_file_tracking() { - use fabro_types::ToolDefinition; + use lithos_llm::types::ToolDefinition; use crate::tool_registry::{RegisteredTool, ToolSource}; diff --git a/lib/components/fabro-agent/src/skills.rs b/lib/components/fabro-agent/src/skills.rs index 98e7ef296..d4c485471 100644 --- a/lib/components/fabro-agent/src/skills.rs +++ b/lib/components/fabro-agent/src/skills.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use fabro_types::ToolDefinition; +use lithos_llm::types::ToolDefinition; use tokio_util::sync::CancellationToken; use crate::error::{Error, InterruptReason}; diff --git a/lib/components/fabro-agent/src/subagent.rs b/lib/components/fabro-agent/src/subagent.rs index 8aa6de9b6..5d15b2170 100644 --- a/lib/components/fabro-agent/src/subagent.rs +++ b/lib/components/fabro-agent/src/subagent.rs @@ -3,9 +3,10 @@ use std::collections::{HashMap, VecDeque}; use std::sync::{Arc, Mutex, RwLock, Weak}; use std::time::Duration; -use fabro_types::{INITIAL_SUBAGENT_GENERATION, ToolDefinition}; +use fabro_types::INITIAL_SUBAGENT_GENERATION; use fabro_util::error as util_error; use futures::future; +use lithos_llm::types::ToolDefinition; use tokio::sync::{broadcast, mpsc, oneshot, watch}; use tokio::task::{AbortHandle, JoinHandle}; use tokio::time::{Instant, timeout_at}; @@ -1344,7 +1345,8 @@ pub fn make_close_agent_tool(supervisor: SubAgentSupervisor) -> RegisteredTool { #[cfg(test)] mod tests { use fabro_llm::adapter::ProviderAdapter; - use fabro_types::{Role, text_of}; + use fabro_types::text_of; + use lithos_llm::types::Role; use tokio::task::yield_now; use tokio::time; diff --git a/lib/components/fabro-agent/src/task_reminder.rs b/lib/components/fabro-agent/src/task_reminder.rs index 51d8145c4..0e956d3f9 100644 --- a/lib/components/fabro-agent/src/task_reminder.rs +++ b/lib/components/fabro-agent/src/task_reminder.rs @@ -74,7 +74,7 @@ fn is_task_reminder(content: &str) -> bool { mod tests { use std::time::SystemTime; - use fabro_types::{TokenCounts, ToolCall}; + use lithos_llm::types::{TokenCounts, ToolCall}; use super::*; fn assistant(tool_name: Option<&str>) -> Message { diff --git a/lib/components/fabro-agent/src/test_support.rs b/lib/components/fabro-agent/src/test_support.rs index a25200467..6076dbd27 100644 --- a/lib/components/fabro-agent/src/test_support.rs +++ b/lib/components/fabro-agent/src/test_support.rs @@ -10,8 +10,9 @@ use fabro_llm::{ Client, ClientOptions, Error as LlmError, FinishReason, Request, Response, ResponseStream, }; pub use fabro_sandbox::test_support::{MockSandbox, MutableMockSandbox}; -use fabro_types::{AgentProfileKind, ContentPart, ModelId, ProviderId, TokenCounts, ToolCall}; -use lithos_llm::catalog::builtin; +use fabro_types::AgentProfileKind; +use lithos_llm::catalog::{ModelId, ProviderId, builtin}; +use lithos_llm::types::{ContentPart, TokenCounts, ToolCall}; use crate::agent_profile::AgentProfile; use crate::config::SessionOptions; @@ -270,7 +271,7 @@ pub async fn make_session_with_tools_and_config( } pub fn make_echo_tool() -> RegisteredTool { - use fabro_types::ToolDefinition; + use lithos_llm::types::ToolDefinition; RegisteredTool { definition: ToolDefinition::function( "echo", @@ -291,7 +292,7 @@ pub fn make_echo_tool() -> RegisteredTool { } pub fn make_error_tool() -> RegisteredTool { - use fabro_types::ToolDefinition; + use lithos_llm::types::ToolDefinition; RegisteredTool { definition: ToolDefinition::function( "fail_tool", diff --git a/lib/components/fabro-agent/src/todo_tools.rs b/lib/components/fabro-agent/src/todo_tools.rs index f30d94aa8..29e114a91 100644 --- a/lib/components/fabro-agent/src/todo_tools.rs +++ b/lib/components/fabro-agent/src/todo_tools.rs @@ -12,7 +12,8 @@ use std::fmt::Write; use std::str::FromStr; use std::sync::Arc; -use fabro_types::{TodoListKind, TodoProjection, TodoStatus, TodoUpdatedProps, ToolDefinition}; +use fabro_types::{TodoListKind, TodoProjection, TodoStatus, TodoUpdatedProps}; +use lithos_llm::types::ToolDefinition; use serde_json::Value; use strum::{EnumString, IntoStaticStr}; diff --git a/lib/components/fabro-agent/src/tool_execution.rs b/lib/components/fabro-agent/src/tool_execution.rs index b444bde38..75d36bb0a 100644 --- a/lib/components/fabro-agent/src/tool_execution.rs +++ b/lib/components/fabro-agent/src/tool_execution.rs @@ -1,8 +1,9 @@ use std::borrow::Cow; use std::sync::Arc; -use fabro_types::{ToolCall, ToolInput, ToolResult, tool_call_arguments, tool_result_from_json}; +use fabro_types::{tool_call_arguments, tool_result_from_json}; use futures::future; +use lithos_llm::types::{ContentPart, ToolCall, ToolDefinitionKind, ToolInput, ToolResult}; use tokio_util::sync::CancellationToken; use tracing::debug; @@ -507,7 +508,7 @@ fn retain_tool_result( previous_stats: Option, ) -> RetainedToolResult { let output_stats = match result.content.as_mut_slice() { - [fabro_types::ContentPart::Text { text: output }] => { + [ContentPart::Text { text: output }] => { let previously_omitted = previous_stats.map_or(0, |stats| stats.omitted_bytes); let previewed = preview_tool_output(output, MAX_RETAINED_TOOL_OUTPUT_BYTES, previously_omitted); @@ -565,9 +566,7 @@ async fn execute_one_tool( _ => tool_call_arguments(tc), }; if matches!(tc.input, ToolInput::Function(_)) { - if let fabro_types::ToolDefinitionKind::Function { input_schema } = - &tool.definition.kind - { + if let ToolDefinitionKind::Function { input_schema } = &tool.definition.kind { if let Err(validation_error) = validate_tool_args(input_schema, &arguments) { return ExecutedToolResult { result: error_result(&tc.id, validation_error), @@ -622,9 +621,11 @@ fn truncate_tool_result( config: &SessionOptions, ) -> ToolResult { let content = match result.content.as_slice() { - [fabro_types::ContentPart::Text { text }] => vec![fabro_types::ContentPart::Text { - text: truncate_tool_output(text, tool_name, config), - }], + [ContentPart::Text { text }] => { + vec![ContentPart::Text { + text: truncate_tool_output(text, tool_name, config), + }] + } other => other.to_vec(), }; @@ -672,7 +673,8 @@ mod tests { use async_trait::async_trait; use fabro_types::run_event::{AgentToolCompletedProps, MAX_RUN_EVENT_BODY_BYTES}; - use fabro_types::{AgentProfileKind, ToolCall, ToolDefinition, tool_result_to_json}; + use fabro_types::{AgentProfileKind, tool_result_to_json}; + use lithos_llm::types::{ToolCall, ToolDefinition}; use tokio::sync::broadcast; use super::*; diff --git a/lib/components/fabro-agent/src/tool_registry.rs b/lib/components/fabro-agent/src/tool_registry.rs index c8b02e267..b963cfbcb 100644 --- a/lib/components/fabro-agent/src/tool_registry.rs +++ b/lib/components/fabro-agent/src/tool_registry.rs @@ -3,7 +3,8 @@ use std::future::Future; use std::pin::Pin; use std::sync::Arc; -use fabro_types::{AgentToolCategory, AgentToolSource, AgentToolSummary, ToolDefinition}; +use fabro_types::{AgentToolCategory, AgentToolSource, AgentToolSummary}; +use lithos_llm::types::{ToolDefinition, ToolDefinitionKind}; use tokio_util::sync::CancellationToken; use crate::config::{ToolAccessPolicy, ToolExposureMode}; @@ -83,14 +84,14 @@ pub trait ToolDefinitionExt { impl ToolDefinitionExt for ToolDefinition { fn parameters(&self) -> &serde_json::Value { match &self.kind { - fabro_types::ToolDefinitionKind::Function { input_schema } => input_schema, + ToolDefinitionKind::Function { input_schema } => input_schema, _ => panic!("custom tool '{}' has no parameter schema", self.name), } } fn custom_format(&self) -> Option<&serde_json::Value> { match &self.kind { - fabro_types::ToolDefinitionKind::Custom { format } => Some(format), + ToolDefinitionKind::Custom { format } => Some(format), _ => None, } } diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index fcc746de7..b7c288407 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -5,8 +5,9 @@ use std::sync::Arc; use fabro_llm::{Client, Request}; #[cfg(test)] use fabro_static::EnvVars; -use fabro_types::{ModelHandle, ToolDefinition}; use futures::{StreamExt, stream}; +use lithos_llm::catalog::ModelHandle; +use lithos_llm::types::ToolDefinition; use tokio::task; use crate::config::NativeToolOptions; @@ -732,8 +733,8 @@ mod tests { use std::collections::HashMap; use fabro_llm::adapter::ProviderAdapter; - use fabro_types::{CommandTermination, ModelId}; - use lithos_llm::catalog::builtin; + use fabro_types::CommandTermination; + use lithos_llm::catalog::{ModelId, builtin}; use tokio::sync::broadcast; use tokio_util::sync::CancellationToken; diff --git a/lib/components/fabro-agent/src/types.rs b/lib/components/fabro-agent/src/types.rs index 1fd5eb458..e10174589 100644 --- a/lib/components/fabro-agent/src/types.rs +++ b/lib/components/fabro-agent/src/types.rs @@ -3,9 +3,12 @@ use std::time::SystemTime; use chrono::{DateTime, Utc}; use fabro_llm::ErrorData; use fabro_types::{ - CommandTermination, ContentPart, Cost, ExecOutputTail, LlmOutputKind, LlmRetryPhase, - Message as LlmMessage, ModelRef, ReasoningOutput, Role, SessionMessage, Speed, - StageContextWindowProjection, TokenCounts, ToolCall, ToolResult, + CommandTermination, ExecOutputTail, LlmOutputKind, LlmRetryPhase, ModelRef, SessionMessage, + StageContextWindowProjection, +}; +use lithos_llm::types::{ + ContentPart, Cost, Message as LlmMessage, ReasoningOutput, Role, Speed, TokenCounts, ToolCall, + ToolResult, }; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; @@ -817,8 +820,8 @@ pub struct SessionEvent { #[cfg(test)] mod tests { use fabro_llm::{ErrorKind, RetryClassification}; - use fabro_types::{CostSource, ModelId, ProviderId}; - use lithos_llm::catalog::builtin; + use lithos_llm::catalog::{ModelId, ProviderId, builtin}; + use lithos_llm::types::CostSource; use super::*; diff --git a/lib/components/fabro-agent/src/web_search.rs b/lib/components/fabro-agent/src/web_search.rs index 6c9fdf151..47b74f90f 100644 --- a/lib/components/fabro-agent/src/web_search.rs +++ b/lib/components/fabro-agent/src/web_search.rs @@ -7,7 +7,7 @@ use std::fmt::Write; use std::sync::OnceLock; use std::time::Duration; -use fabro_types::ToolDefinition; +use lithos_llm::types::ToolDefinition; use crate::config::ToolSecrets; use crate::tool_registry::{RegisteredTool, ToolSource}; diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index 53888a64a..b542f537d 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -18,8 +18,8 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::client_from_env; use fabro_llm::{Client, ClientOptions, catalog}; use fabro_test::{EnvVars, TwinScenario, TwinScenarios, TwinToolCall, twin_openai}; -use fabro_types::{ModelHandle, ModelId, ProviderId}; -use lithos_llm::catalog::builtin; +use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId, builtin}; +use lithos_llm::types::ReasoningEffort; type Provider = ProviderId; @@ -786,7 +786,7 @@ macro_rules! reasoning_effort_tests { async fn $test_name() { let tmp = tempfile::tempdir().expect("failed to create tempdir"); let config = SessionOptions { - reasoning_effort: Some(fabro_types::ReasoningEffort::Low), + reasoning_effort: Some(ReasoningEffort::Low), ..SessionOptions::default() }; let mut session = diff --git a/lib/components/fabro-hooks/src/executor.rs b/lib/components/fabro-hooks/src/executor.rs index 28e86b08c..443573866 100644 --- a/lib/components/fabro-hooks/src/executor.rs +++ b/lib/components/fabro-hooks/src/executor.rs @@ -11,7 +11,8 @@ use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{Client, ClientOptions, Request}; use fabro_redact::redacted_url_for_log; use fabro_types::settings::{InterpString, ResolveCtx, ResolveError}; -use fabro_types::{Message, Role, ToolCall, tool_call_arguments, tool_result_from_json}; +use fabro_types::{tool_call_arguments, tool_result_from_json}; +use lithos_llm::types::{ContentPart, Message, Role, ToolCall}; use tokio::process::Command as TokioCommand; use tokio::time::timeout as tokio_timeout; use tokio_util::sync::CancellationToken; @@ -457,7 +458,7 @@ impl HookExecutorImpl { true, ), }; - results.push(fabro_types::ContentPart::ToolResult(result)); + results.push(ContentPart::ToolResult(result)); } messages.push(Message::new(Role::Tool, results)); } diff --git a/lib/components/fabro-llm/src/api.rs b/lib/components/fabro-llm/src/api.rs index d99e86907..d432951e6 100644 --- a/lib/components/fabro-llm/src/api.rs +++ b/lib/components/fabro-llm/src/api.rs @@ -5,11 +5,9 @@ use std::collections::HashSet; -use fabro_types::{ - Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider, ProviderId, - ReasoningEffort, -}; -use lithos_llm::catalog::{Catalog, CatalogProvider, Offering}; +use fabro_types::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, Provider}; +use lithos_llm::catalog::{Catalog, CatalogProvider, Offering, ProviderId}; +use lithos_llm::types::ReasoningEffort; const USD_MICROS_PER_USD: f64 = 1_000_000.0; diff --git a/lib/components/fabro-llm/src/client.rs b/lib/components/fabro-llm/src/client.rs index 9a7ff7674..64df12e17 100644 --- a/lib/components/fabro-llm/src/client.rs +++ b/lib/components/fabro-llm/src/client.rs @@ -3,9 +3,8 @@ use std::sync::Arc; use std::time::Duration; -use fabro_types::ProviderId; use lithos_llm::adapter::ProviderAdapter; -use lithos_llm::catalog::Catalog; +use lithos_llm::catalog::{Catalog, ProviderId}; use lithos_llm::client::{Client, ClientBuildError, ClientBuilder, ProviderBuildIssue}; use lithos_llm::credentials::{CredentialError, CredentialProvider}; use lithos_llm::middleware::{ diff --git a/lib/components/fabro-llm/src/error.rs b/lib/components/fabro-llm/src/error.rs index 6444b5597..3b0104da6 100644 --- a/lib/components/fabro-llm/src/error.rs +++ b/lib/components/fabro-llm/src/error.rs @@ -4,7 +4,7 @@ //! lithos `Error` and `ErrorData` themselves. What stays here is the loop and //! restart detector's signature format, which names Fabro's own categories. -use fabro_types::ProviderId; +use lithos_llm::catalog::ProviderId; use lithos_llm::types::{ErrorData, ErrorKind}; /// A stable `category|provider|detail` string for loop and restart detection. diff --git a/lib/components/fabro-llm/src/probe.rs b/lib/components/fabro-llm/src/probe.rs index 510733c22..9c44c5395 100644 --- a/lib/components/fabro-llm/src/probe.rs +++ b/lib/components/fabro-llm/src/probe.rs @@ -4,9 +4,10 @@ use std::sync::Arc; use std::time::Duration; use fabro_auth::ApiKeyCredentialSource; -use fabro_types::{ModelTestMode, ProviderId, ReasoningEffort}; -use lithos_llm::catalog::Catalog; +use fabro_types::ModelTestMode; +use lithos_llm::catalog::{Catalog, ProviderId}; use lithos_llm::client::{Client, ProbeOptions, ProbeOutcome}; +use lithos_llm::types::ReasoningEffort; use strum::IntoStaticStr; use crate::client::{ClientOptions, LlmSetupError, build_client}; diff --git a/lib/components/fabro-llm/src/selection.rs b/lib/components/fabro-llm/src/selection.rs index 85e1434c8..4372afddd 100644 --- a/lib/components/fabro-llm/src/selection.rs +++ b/lib/components/fabro-llm/src/selection.rs @@ -19,8 +19,7 @@ use std::collections::HashSet; use std::fmt; -use fabro_types::{ModelId, ProviderId}; -use lithos_llm::catalog::{Catalog, Offering}; +use lithos_llm::catalog::{Catalog, ModelId, Offering, ProviderId}; use thiserror::Error; /// A provider/model pair one of the selection functions chose. diff --git a/lib/components/fabro-llm/src/test_support.rs b/lib/components/fabro-llm/src/test_support.rs index ac008e96f..61acf8c9b 100644 --- a/lib/components/fabro-llm/src/test_support.rs +++ b/lib/components/fabro-llm/src/test_support.rs @@ -7,15 +7,14 @@ use std::time::Duration; use async_trait::async_trait; use fabro_auth::test_support::env_credential_source; use fabro_config::LlmLayer; -use fabro_types::{ContentPart, ModelId, ProviderId, TokenCounts}; use futures::stream; use lithos_llm::adapter::{ProviderAdapter, ResolvedCall}; -use lithos_llm::catalog::{AdapterId, Catalog}; +use lithos_llm::catalog::{AdapterId, Catalog, ModelId, ProviderId}; use lithos_llm::client::Client; use lithos_llm::middleware::RetryPolicy; use lithos_llm::types::{ - ContentBlockId, ContentBlockKind, Error, FinishReason, Response, ResponseStream, StreamEvent, - ToolCallKind, + ContentBlockId, ContentBlockKind, ContentPart, Error, FinishReason, Response, ResponseStream, + StreamEvent, TokenCounts, ToolCallKind, ToolInput, }; use crate::client::{ClientOptions, build_client, build_offline_client}; @@ -94,7 +93,7 @@ pub fn response_to_stream(response: Response) -> ResponseStream { id: call.id.clone(), name: Some(call.name.clone()), kind: match call.input { - fabro_types::ToolInput::Custom(_) => ToolCallKind::Custom, + ToolInput::Custom(_) => ToolCallKind::Custom, _ => ToolCallKind::Function, }, }, diff --git a/lib/components/fabro-store/Cargo.toml b/lib/components/fabro-store/Cargo.toml index 56d8e3acd..b97c58202 100644 --- a/lib/components/fabro-store/Cargo.toml +++ b/lib/components/fabro-store/Cargo.toml @@ -17,6 +17,7 @@ test-support = ["dep:fabro-db"] [dependencies] fabro-db = { path = "../../foundation/fabro-db", optional = true } fabro-types = { path = "../../foundation/fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-util = { path = "../../foundation/fabro-util" } hex.workspace = true slatedb.workspace = true diff --git a/lib/components/fabro-store/src/run_state.rs b/lib/components/fabro-store/src/run_state.rs index b2df4d2af..4f2c62395 100644 --- a/lib/components/fabro-store/src/run_state.rs +++ b/lib/components/fabro-store/src/run_state.rs @@ -1759,15 +1759,16 @@ mod tests { AgentBackend, AgentControlState, AttrValue, AutomationRef, BilledModelUsage, BilledTokenCounts, BlobHash, BlockedReason, Checkpoint, CheckpointRecord, CommandTermination, EventBody, FailureCategory, FailureDetail, FailureReason, Graph, - McpServerStatus, ModelId, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel, - ProviderId, PullRequestCreationStatus, PullRequestLink, QuestionType, ReasoningEffort, - RunApprovalState, RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, - RunStatus, Speed, StageContextWindowBreakdownItem, StageContextWindowCategory, - StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, - StageContextWindowWarning, StageHandler, StageModelUsage, StageOutcome, StageState, - StageTiming, SubAgentStatus, SuccessReason, WorkflowSettings, first_event_seq, fixtures, - test_support, + McpServerStatus, Node, Outcome, ParallelBranchId, PendingReason, PermissionLevel, + PullRequestCreationStatus, PullRequestLink, QuestionType, RunApprovalState, + RunBillingSummary, RunControlAction, RunDiff, RunEvent, RunSize, RunSpec, RunStatus, + StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, + StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowWarning, + StageHandler, StageModelUsage, StageOutcome, StageState, StageTiming, SubAgentStatus, + SuccessReason, WorkflowSettings, first_event_seq, fixtures, test_support, }; + use lithos_llm::catalog::{ModelId, ProviderId}; + use lithos_llm::types::{ReasoningEffort, Speed}; use serde_json::json; use super::{RunProjection, RunProjectionReducer, build_summary}; @@ -1782,9 +1783,8 @@ mod tests { AgentLlmFirstOutputProps, AgentLlmRetryProps, AgentLlmStartedProps, AgentToolCompletedProps, AgentToolStartedProps, }; - use fabro_types::{ - LlmOutputKind, LlmRetryPhase, ModelRef, Speed, StageOutcome, StageProjection, - }; + use fabro_types::{LlmOutputKind, LlmRetryPhase, ModelRef, StageOutcome, StageProjection}; + use lithos_llm::types::Speed; use super::*; @@ -7565,9 +7565,8 @@ mod tests { use fabro_types::run_event::{ AgentErrorProps, AgentLlmFirstOutputProps, AgentLlmRetryProps, AgentLlmStartedProps, }; - use fabro_types::{ - LlmOutputKind, LlmRetryPhase, ModelRef, Speed, StageInferenceProjection, - }; + use fabro_types::{LlmOutputKind, LlmRetryPhase, ModelRef, StageInferenceProjection}; + use lithos_llm::types::Speed; use super::*; diff --git a/lib/components/fabro-workflow/src/billing_rollup.rs b/lib/components/fabro-workflow/src/billing_rollup.rs index 9d166ed08..5dc6ca164 100644 --- a/lib/components/fabro-workflow/src/billing_rollup.rs +++ b/lib/components/fabro-workflow/src/billing_rollup.rs @@ -6,10 +6,10 @@ pub use fabro_types::billing_rollup::{ #[cfg(test)] mod tests { use fabro_types::{ - AttrValue, BilledTokenCounts, Graph, ModelId, ModelRef, Node, RunProjection, RunSpec, + AttrValue, BilledTokenCounts, Graph, ModelRef, Node, RunProjection, RunSpec, StageCompletion, StageOutcome, first_event_seq, test_support, }; - use lithos_llm::catalog::builtin; + use lithos_llm::catalog::{ModelId, builtin}; use super::billing_rollup_from_projection; use crate::test_support::test_usage; diff --git a/lib/components/fabro-workflow/src/event/convert.rs b/lib/components/fabro-workflow/src/event/convert.rs index 906d3fbb5..bd1fe1d34 100644 --- a/lib/components/fabro-workflow/src/event/convert.rs +++ b/lib/components/fabro-workflow/src/event/convert.rs @@ -1461,16 +1461,17 @@ mod tests { use std::collections::BTreeMap; use ::fabro_types::{ - AutomationRef, EventBody, FailureReason, ModelId, ModelRef, ParallelBranchId, Principal, - ProviderId, RunNoticeCode, RunNoticeLevel, RunProvenance, StageId, SystemActorKind, - TokenCounts as LlmTokenCounts, fixtures, run_event as fabro_types, test_support, + AutomationRef, EventBody, FailureReason, ModelRef, ParallelBranchId, Principal, + RunNoticeCode, RunNoticeLevel, RunProvenance, StageId, SystemActorKind, fixtures, + run_event as fabro_types, test_support, }; use chrono::Utc; use fabro_agent::{ AgentEvent, McpToolSummary, MemoryFileSummary, SandboxEvent, SkillActivationSource, SkillSummary, }; - use lithos_llm::catalog::builtin; + use lithos_llm::catalog::{ModelId, ProviderId, builtin}; + use lithos_llm::types::{Cost, CostSource, ReasoningOutput, TokenCounts as LlmTokenCounts}; use super::*; use crate::error::Error; @@ -2593,10 +2594,10 @@ mod tests { output: 34, ..LlmTokenCounts::default() }, - cost: Some(::fabro_types::Cost { + cost: Some(Cost { usd_micros: 125_000, - source: ::fabro_types::CostSource::Provider, + source: CostSource::Provider, }), tool_call_count: 0, context_window: None, @@ -2611,10 +2612,7 @@ mod tests { panic!("expected agent message body"); }; assert_eq!(message.billing.total_usd_micros, Some(125_000)); - assert_eq!( - message.cost_source, - Some(::fabro_types::CostSource::Provider) - ); + assert_eq!(message.cost_source, Some(CostSource::Provider)); } #[test] @@ -2676,7 +2674,7 @@ mod tests { cost: None, tool_call_count: 1, context_window: None, - reasoning: Some(::fabro_types::ReasoningOutput::new( + reasoning: Some(ReasoningOutput::new( "inspect the conversion first", "read convert.rs, then the sink", )), diff --git a/lib/components/fabro-workflow/src/event/events.rs b/lib/components/fabro-workflow/src/event/events.rs index f8adcb0ca..65bc43770 100644 --- a/lib/components/fabro-workflow/src/event/events.rs +++ b/lib/components/fabro-workflow/src/event/events.rs @@ -4,12 +4,13 @@ use ::fabro_types::{ AutomationRef, BilledTokenCounts, BlobHash, BlockedReason, CommandTermination, DiffSummary, FailureReason, ForkSourceRef, GitContext, PairId, PairMessageId, PairSystemMessageKind, PairTarget, ParallelBranchId, ParallelBranchResult, PendingReason, PermissionLevel, Principal, - PullRequestCreationId, PullRequestLink, ReasoningEffort, ReviewTarget, RunFailure, RunId, - RunNoticeLevel, RunPairEndedReason, RunPairFailedReason, RunProvenance, RunRunnableSource, - RunTarget, RunTiming, SandboxProviderKind, Speed, StageId, StageOutcome, StageTiming, - SuccessReason, WorkflowVersionId, run_event as fabro_types, + PullRequestCreationId, PullRequestLink, ReviewTarget, RunFailure, RunId, RunNoticeLevel, + RunPairEndedReason, RunPairFailedReason, RunProvenance, RunRunnableSource, RunTarget, + RunTiming, SandboxProviderKind, StageId, StageOutcome, StageTiming, SuccessReason, + WorkflowVersionId, run_event as fabro_types, }; use fabro_agent::{AgentEvent, SandboxEvent}; +use lithos_llm::types::{ReasoningEffort, Speed}; use serde::{Deserialize, Serialize}; use crate::error::{Error, run_failure_from_error}; diff --git a/lib/components/fabro-workflow/src/event/redaction.rs b/lib/components/fabro-workflow/src/event/redaction.rs index 689f7868d..510a9659f 100644 --- a/lib/components/fabro-workflow/src/event/redaction.rs +++ b/lib/components/fabro-workflow/src/event/redaction.rs @@ -30,12 +30,10 @@ pub fn event_payload_from_redacted_json(line: &str, run_id: &RunId) -> Result String { mod tests { use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::{test_catalog, test_catalog_with_overlay}; - use fabro_types::{ModelId, ModelRef, ProviderId, Speed, TokenCounts, UsdMicros}; - use lithos_llm::catalog::builtin; + use fabro_types::{ModelRef, UsdMicros}; + use lithos_llm::catalog::{ModelId, ProviderId, builtin}; + use lithos_llm::types::{Speed, TokenCounts}; use super::{OutcomeExt, billed_model_usage_from_llm}; diff --git a/lib/components/fabro-workflow/src/pipeline/pull_request.rs b/lib/components/fabro-workflow/src/pipeline/pull_request.rs index ecbec0492..879d91eaa 100644 --- a/lib/components/fabro-workflow/src/pipeline/pull_request.rs +++ b/lib/components/fabro-workflow/src/pipeline/pull_request.rs @@ -8,9 +8,11 @@ use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{Client, ClientOptions, Request, selection}; use fabro_store::RunProjection; +use fabro_types::PullRequestLink; use fabro_types::settings::run::MergeStrategy; -use fabro_types::{ProviderId, PullRequestLink, Role}; use fabro_util::text::strip_goal_decoration; +use lithos_llm::catalog::ProviderId; +use lithos_llm::types::{Message, Role}; use tokio::time::sleep; use tracing::{debug, info, warn}; @@ -405,7 +407,7 @@ async fn build_pr_content_with_client( let request = Request::builder() .model(model) .system(PR_BODY_SYSTEM_PROMPT) - .message(fabro_types::Message::text(Role::User, prompt)) + .message(Message::text(Role::User, prompt)) .build() .map_err(|e| format!("invalid PR content request: {e}"))?; let completion = client @@ -694,12 +696,13 @@ mod tests { use fabro_llm::{Response, ResponseStream}; use fabro_store::Database; use fabro_types::{ - BilledTokenCounts, ContentPart, RunProjection, RunSpec, SuccessReason, TokenCounts, - WorkflowSettings, first_event_seq, fixtures, test_support, + BilledTokenCounts, RunProjection, RunSpec, SuccessReason, WorkflowSettings, + first_event_seq, fixtures, test_support, }; use fabro_vault::{SecretType, Vault}; use httpmock::Method::{GET, POST}; use httpmock::MockServer; + use lithos_llm::types::{ContentPart, TokenCounts}; use object_store::memory::InMemory; use tokio::sync::RwLock as AsyncRwLock; @@ -802,7 +805,7 @@ capabilities = { text = true, tools = true, response_format = { json_object = tr let mut options = fabro_llm::ClientOptions::default(); options .adapters - .push((fabro_types::ProviderId::new(provider_name), adapter)); + .push((ProviderId::new(provider_name), adapter)); Arc::new( fabro_llm::build_offline_client(mock_catalog(), options) .expect("mock client should build") diff --git a/lib/components/fabro-workflow/src/pipeline/types.rs b/lib/components/fabro-workflow/src/pipeline/types.rs index 669db75e3..d2bba100d 100644 --- a/lib/components/fabro-workflow/src/pipeline/types.rs +++ b/lib/components/fabro-workflow/src/pipeline/types.rs @@ -11,9 +11,10 @@ use fabro_template::TemplateContext; use fabro_types::settings::run::{ PullRequestSettings, ResolvedGithubIntegration, RunModelControls, }; -use fabro_types::{ManifestPath, ProviderId, RunId, RunProjection}; +use fabro_types::{ManifestPath, RunId, RunProjection}; use fabro_validate::{Diagnostic, Severity}; use fabro_vault::Vault; +use lithos_llm::catalog::ProviderId; use tokio::sync::RwLock as AsyncRwLock; use crate::artifact_upload::ArtifactSink; diff --git a/lib/components/fabro-workflow/src/run_materialization.rs b/lib/components/fabro-workflow/src/run_materialization.rs index 714e184e1..35ce47e7d 100644 --- a/lib/components/fabro-workflow/src/run_materialization.rs +++ b/lib/components/fabro-workflow/src/run_materialization.rs @@ -3,9 +3,10 @@ use std::collections::HashSet; use fabro_graphviz::graph::Graph; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::{ModelSelectionError, selection}; +use fabro_types::WorkflowSettings; use fabro_types::settings::InterpString; use fabro_types::settings::run::RunGoal; -use fabro_types::{ProviderId, WorkflowSettings}; +use lithos_llm::catalog::ProviderId; use crate::error::Error; diff --git a/lib/components/fabro-workflow/src/services.rs b/lib/components/fabro-workflow/src/services.rs index 3bdf9c504..6054282e4 100644 --- a/lib/components/fabro-workflow/src/services.rs +++ b/lib/components/fabro-workflow/src/services.rs @@ -10,7 +10,8 @@ use fabro_hooks::{HookContext, HookDecision, HookExecutionContext, HookRunner}; use fabro_interview::Interviewer; use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; -use fabro_types::{ManifestPath, ProviderId, RunId}; +use fabro_types::{ManifestPath, RunId}; +use lithos_llm::catalog::ProviderId; use tokio_util::sync::CancellationToken; use crate::event::Emitter; diff --git a/lib/components/fabro-workflow/src/test_support.rs b/lib/components/fabro-workflow/src/test_support.rs index 680ad5e50..6fb04fd24 100644 --- a/lib/components/fabro-workflow/src/test_support.rs +++ b/lib/components/fabro-workflow/src/test_support.rs @@ -12,10 +12,11 @@ use fabro_llm::credentials::CredentialProvider; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::test_support::test_catalog; use fabro_store::{ArtifactStore, RunProjection, test_support as store_test_support}; +use fabro_types::ModelRef; #[cfg(feature = "test-support")] -use fabro_types::ProviderId; -use fabro_types::{ModelId, ModelRef}; -use lithos_llm::catalog::builtin; +use lithos_llm::catalog::ProviderId; +use lithos_llm::catalog::{ModelId, builtin}; +use lithos_llm::types::TokenCounts; use object_store::local::LocalFileSystem; use crate::artifact_upload::ArtifactSink; @@ -91,10 +92,10 @@ pub fn test_usage( ) -> fabro_types::BilledModelUsage { let mut usage = fabro_types::BilledModelUsage::new( ModelRef::new(builtin::openai(), ModelId::new(model_id)), - fabro_types::TokenCounts { + TokenCounts { input: input_tokens, output: output_tokens, - ..fabro_types::TokenCounts::default() + ..TokenCounts::default() }, None, ); diff --git a/lib/components/fabro-workflow/src/transforms/model_resolution.rs b/lib/components/fabro-workflow/src/transforms/model_resolution.rs index c60d38aa7..450fb65b6 100644 --- a/lib/components/fabro-workflow/src/transforms/model_resolution.rs +++ b/lib/components/fabro-workflow/src/transforms/model_resolution.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use fabro_graphviz::graph::{AttrValue, Graph}; use fabro_llm::lithos_catalog::Catalog; use fabro_llm::selection; -use fabro_types::ProviderId; +use lithos_llm::catalog::ProviderId; use super::Transform; use crate::error::Error; diff --git a/lib/components/fabro-workflow/tests/it/integration.rs b/lib/components/fabro-workflow/tests/it/integration.rs index 1be24c4cb..96c354a00 100644 --- a/lib/components/fabro-workflow/tests/it/integration.rs +++ b/lib/components/fabro-workflow/tests/it/integration.rs @@ -32,9 +32,7 @@ use fabro_interview::{ }; use fabro_llm::lithos_catalog::Catalog; use fabro_store::{ArtifactKey, ArtifactStore}; -use fabro_types::{ - EventBody, ProviderId, RunEvent, RunId, StageId, WorkflowSettings, parse_blob_ref, -}; +use fabro_types::{EventBody, RunEvent, RunId, StageId, WorkflowSettings, parse_blob_ref}; use fabro_validate::{Severity, validate, validate_or_raise}; use fabro_workflow::artifact; use fabro_workflow::context::Context; @@ -62,6 +60,7 @@ use fabro_workflow::test_support::{ }; use fabro_workflow::transforms::stylesheet::{apply_stylesheet, parse_stylesheet}; use fabro_workflow::transforms::{StylesheetApplicationTransform, TemplateTransform, Transform}; +use lithos_llm::catalog::ProviderId; use object_store::local::LocalFileSystem; use tokio_util::sync::CancellationToken; use ulid::Ulid; diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 6f4c965f1..9630aa75b 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -504,19 +504,19 @@ fn main() { "fabro_types::PendingInterviewRecord", &[], ), - ("CompletionUsage", "fabro_types::TokenCounts", &[]), + ("CompletionUsage", "lithos_llm::types::TokenCounts", &[]), ("BilledTokenCounts", "fabro_types::BilledTokenCounts", &[]), ("BillingModelRef", "fabro_types::ModelRef", &[]), - ("BillingSpeed", "fabro_types::Speed", &[]), + ("BillingSpeed", "lithos_llm::types::Speed", &[]), ("ExecOutputTail", "fabro_types::ExecOutputTail", &[]), ("StageTiming", "fabro_types::StageTiming", &[]), ("RunTiming", "fabro_types::RunTiming", &[]), - ("ProviderId", "fabro_types::ProviderId", &[]), - ("ModelHandle", "fabro_types::ModelHandle", &[]), + ("ProviderId", "lithos_llm::catalog::ProviderId", &[]), + ("ModelHandle", "lithos_llm::catalog::ModelHandle", &[]), ("Model", "fabro_types::Model", &[]), ("Provider", "fabro_types::Provider", &[]), ("ModelLimits", "fabro_types::ModelLimits", &[]), - ("ReasoningEffort", "fabro_types::ReasoningEffort", &[]), + ("ReasoningEffort", "lithos_llm::types::ReasoningEffort", &[]), ("ModelFeatures", "fabro_types::ModelFeatures", &[]), ("ModelControls", "fabro_types::ModelControls", &[]), ("ModelCosts", "fabro_types::ModelCosts", &[]), @@ -730,27 +730,31 @@ fn main() { ("SessionRecord", "fabro_types::SessionRecord", &[]), ("SessionSummary", "fabro_types::SessionSummary", &[]), ("SessionDetail", "fabro_types::SessionDetail", &[]), - ("ReasoningOutput", "fabro_types::ReasoningOutput", &[]), - ("CompletionMessage", "fabro_types::Message", &[]), - ("CompletionMessageRole", "fabro_types::Role", &[]), - ("CompletionContentPart", "fabro_types::ContentPart", &[]), + ("ReasoningOutput", "lithos_llm::types::ReasoningOutput", &[]), + ("CompletionMessage", "lithos_llm::types::Message", &[]), + ("CompletionMessageRole", "lithos_llm::types::Role", &[]), + ( + "CompletionContentPart", + "lithos_llm::types::ContentPart", + &[], + ), ( "CompletionToolDefinition", - "fabro_types::ToolDefinition", + "lithos_llm::types::ToolDefinition", &[], ), ( "CompletionToolDefinitionKind", - "fabro_types::ToolDefinitionKind", + "lithos_llm::types::ToolDefinitionKind", &[], ), - ("CompletionToolChoice", "fabro_types::ToolChoice", &[]), + ("CompletionToolChoice", "lithos_llm::types::ToolChoice", &[]), ( "CompletionResponseFormat", - "fabro_types::ResponseFormat", + "lithos_llm::types::ResponseFormat", &[], ), - ("CompletionCost", "fabro_types::Cost", &[]), + ("CompletionCost", "lithos_llm::types::Cost", &[]), ("WorkflowVersion", "fabro_types::WorkflowVersion", &[]), ("RunIntent", "fabro_types::RunIntent", &[]), ("RunIntentArgs", "fabro_types::RunIntentArgs", &[]), @@ -759,7 +763,7 @@ fn main() { ("WorkflowPath", "fabro_types::WorkflowPath", &[]), ("WorkflowVersionId", "fabro_types::WorkflowVersionId", &[]), ("BlobHash", "fabro_types::BlobHash", &[]), - ("CostSource", "fabro_types::CostSource", &[]), + ("CostSource", "lithos_llm::types::CostSource", &[]), ]; for (name, path, impls) in replacements { settings.with_replacement(*name, *path, impls.iter().copied()); diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 58a98e6b0..794a934db 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -41,45 +41,47 @@ pub mod types { ActivatedSkill, AgentControlState, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, AgentToolCategory, AgentToolSource, AgentToolSummary, AgentToolsAvailableProps, AskFabro, AuthMethod, AutomationRef, BilledTokenCounts, BlobHash, - CommandTermination, Conclusion, ContentPart, Cost as CompletionCost, CostSource, - CreateVariableRequest, DiffStats, DiffSummary, DirtyStatus, EventEnvelope, ExecOutputTail, - FailureCategory, FailureDetail, FailureSignature, GitContext, GitRunTarget, - GitRunTarget as AutomationGitWorkflowSource, IdpIdentity, IntegrationConnectionKind, - IntegrationConnectionState, IntegrationConnectionStatus, IntegrationProvider, - IntegrationStatus, InterviewOption, InterviewQuestionRecord, LlmOutputKind, - McpServerDraft as CreateMcpServerRequest, McpServerProjection, + CommandTermination, Conclusion, CreateVariableRequest, DiffStats, DiffSummary, DirtyStatus, + EventEnvelope, ExecOutputTail, FailureCategory, FailureDetail, FailureSignature, + GitContext, GitRunTarget, GitRunTarget as AutomationGitWorkflowSource, IdpIdentity, + IntegrationConnectionKind, IntegrationConnectionState, IntegrationConnectionStatus, + IntegrationProvider, IntegrationStatus, InterviewOption, InterviewQuestionRecord, + LlmOutputKind, McpServerDraft as CreateMcpServerRequest, McpServerProjection, McpServerReplace as ReplaceMcpServerRequest, McpServerStatus, McpServerView as McpServer, - McpTransportView, Message, Model, ModelControls, ModelCosts, ModelFeatures, ModelHandle, - ModelLimits, ModelRef as BillingModelRef, ModelTestMode, PairId, PairMessageId, - PairMessageRecord, PairMessageRequest, PairRecord, PairStartRequest, PairStatus, - PairTarget, PairTranscriptEntry, PairTranscriptResponse, ParallelBranchId, - ParallelBranchResult, PendingInterviewRecord, PermissionLevel, Principal, Provider, - ProviderId, PullRequest, PullRequestCreation, PullRequestCreationId, - PullRequestCreationStatus, PullRequestDetails, PullRequestDetailsStatus, - PullRequestDetailsUnavailableReason, PullRequestLink, PullRequestMeta, PullRequestResponse, - QuestionType, ReasoningEffort, ReasoningOutput, RepositoryRef, - ResponseFormat as CompletionResponseFormat, ReviewTarget, ReviewTargetKind, Role, Run, - RunApproval, RunApprovalState, RunClientProvenance, RunEvent, RunEventDetailContentKind, - RunEventDetailResponse, RunFailure, RunIntent, RunIntentArgs, RunPairStatusResponse, - RunProjection, RunProvenance, RunRunnableSource, RunSandbox, RunSandboxFailure, - RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, RunServerProvenance, - RunSize, RunTarget, SandboxDetails, SandboxInfo, SandboxListMeta, SandboxListResponse, - SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode, SandboxProviderKind, - SandboxProviderLookupError, SandboxResources, SandboxService, SandboxServiceListResponse, - SandboxState, SandboxTimestamps, SecretMetadata, SecretType, ServerSettings, SessionDetail, - SessionId, SessionMessage, SessionRecord, SessionStatus, SessionSummary, SessionTurn, - SkillsProjection, Speed as BillingSpeed, StageCompletion, StageContextWindow, + McpTransportView, Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, + ModelRef as BillingModelRef, ModelTestMode, PairId, PairMessageId, PairMessageRecord, + PairMessageRequest, PairRecord, PairStartRequest, PairStatus, PairTarget, + PairTranscriptEntry, PairTranscriptResponse, ParallelBranchId, ParallelBranchResult, + PendingInterviewRecord, PermissionLevel, Principal, Provider, PullRequest, + PullRequestCreation, PullRequestCreationId, PullRequestCreationStatus, PullRequestDetails, + PullRequestDetailsStatus, PullRequestDetailsUnavailableReason, PullRequestLink, + PullRequestMeta, PullRequestResponse, QuestionType, RepositoryRef, ReviewTarget, + ReviewTargetKind, Run, RunApproval, RunApprovalState, RunClientProvenance, RunEvent, + RunEventDetailContentKind, RunEventDetailResponse, RunFailure, RunIntent, RunIntentArgs, + RunPairStatusResponse, RunProjection, RunProvenance, RunRunnableSource, RunSandbox, + RunSandboxFailure, RunSandboxInstance, RunSandboxKind, RunSandboxPlan, RunSandboxRuntime, + RunServerProvenance, RunSize, RunTarget, SandboxDetails, SandboxInfo, SandboxListMeta, + SandboxListResponse, SandboxNetwork, SandboxNetworkPolicy, SandboxNetworkPolicyMode, + SandboxProviderKind, SandboxProviderLookupError, SandboxResources, SandboxService, + SandboxServiceListResponse, SandboxState, SandboxTimestamps, SecretMetadata, SecretType, + ServerSettings, SessionDetail, SessionId, SessionMessage, SessionRecord, SessionStatus, + SessionSummary, SessionTurn, SkillsProjection, StageCompletion, StageContextWindow, StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowUnavailableReason, StageContextWindowWarning, StageHandler, StageId, StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState, StageToolBatchProjection, SubAgentProjection, SubAgentStatus, SystemActorKind, - SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, + SystemIntegrationStatus, SystemIntegrationsResponse, TodoListProjection, TurnId, + UpdateVariableRequest, UserPrincipal, Variable, VariableListResponse, WorkflowPath, + WorkflowSettings, WorkflowVersion, WorkflowVersionId, + }; + pub use lithos_llm::catalog::{ModelHandle, ProviderId}; + pub use lithos_llm::types::{ + ContentPart, Cost as CompletionCost, CostSource, Message, ReasoningEffort, ReasoningOutput, + ResponseFormat as CompletionResponseFormat, Role, Speed as BillingSpeed, TokenCounts as CompletionUsage, ToolChoice as CompletionToolChoice, ToolDefinition as CompletionToolDefinition, - ToolDefinitionKind as CompletionToolDefinitionKind, TurnId, UpdateVariableRequest, - UserPrincipal, Variable, VariableListResponse, WorkflowPath, WorkflowSettings, - WorkflowVersion, WorkflowVersionId, + ToolDefinitionKind as CompletionToolDefinitionKind, }; pub use crate::generated::types::*; diff --git a/lib/foundation/fabro-api/tests/agent_session_activated_props_round_trip.rs b/lib/foundation/fabro-api/tests/agent_session_activated_props_round_trip.rs index 1cbf8dc04..4060a6fb7 100644 --- a/lib/foundation/fabro-api/tests/agent_session_activated_props_round_trip.rs +++ b/lib/foundation/fabro-api/tests/agent_session_activated_props_round_trip.rs @@ -2,7 +2,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::AgentSessionActivatedProps as ApiAgentSessionActivatedProps; use fabro_types::run_event::AgentSessionActivatedProps; -use fabro_types::{PermissionLevel, ReasoningEffort, SessionCapability, Speed}; +use fabro_types::{PermissionLevel, SessionCapability}; +use lithos_llm::types::{ReasoningEffort, Speed}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/completion_message_round_trip.rs b/lib/foundation/fabro-api/tests/completion_message_round_trip.rs index 3b4fc1419..f6b914e36 100644 --- a/lib/foundation/fabro-api/tests/completion_message_round_trip.rs +++ b/lib/foundation/fabro-api/tests/completion_message_round_trip.rs @@ -7,7 +7,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ContentPart as ApiContentPart, Message as ApiMessage, Role as ApiRole}; -use fabro_types::{ContentPart, Message, Role, ToolCall, ToolResult}; +use lithos_llm::types::{ContentPart, Message, Role, ToolCall, ToolResult}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/completion_usage_round_trip.rs b/lib/foundation/fabro-api/tests/completion_usage_round_trip.rs index 5c38c4e74..03eef28ec 100644 --- a/lib/foundation/fabro-api/tests/completion_usage_round_trip.rs +++ b/lib/foundation/fabro-api/tests/completion_usage_round_trip.rs @@ -1,7 +1,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::CompletionUsage as ApiCompletionUsage; -use fabro_types::TokenCounts; +use lithos_llm::types::TokenCounts; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/cost_source_round_trip.rs b/lib/foundation/fabro-api/tests/cost_source_round_trip.rs index 4b69183d6..484e5847a 100644 --- a/lib/foundation/fabro-api/tests/cost_source_round_trip.rs +++ b/lib/foundation/fabro-api/tests/cost_source_round_trip.rs @@ -1,7 +1,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{CompletionCost as ApiCost, CostSource as ApiCostSource}; -use fabro_types::{Cost, CostSource}; +use lithos_llm::types::{Cost, CostSource}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/create_completion_request_round_trip.rs b/lib/foundation/fabro-api/tests/create_completion_request_round_trip.rs index 9abcfa7b2..345e6fd55 100644 --- a/lib/foundation/fabro-api/tests/create_completion_request_round_trip.rs +++ b/lib/foundation/fabro-api/tests/create_completion_request_round_trip.rs @@ -1,5 +1,5 @@ use fabro_api::types::CreateCompletionRequest; -use fabro_types::{ReasoningEffort, ResponseFormat, Speed, ToolChoice, ToolDefinitionKind}; +use lithos_llm::types::{ReasoningEffort, ResponseFormat, Speed, ToolChoice, ToolDefinitionKind}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/model_round_trip.rs b/lib/foundation/fabro-api/tests/model_round_trip.rs index edde1b7db..092afcf16 100644 --- a/lib/foundation/fabro-api/tests/model_round_trip.rs +++ b/lib/foundation/fabro-api/tests/model_round_trip.rs @@ -1,8 +1,9 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{Model as ApiModel, ModelControls as ApiModelControls}; -use fabro_types::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits, ReasoningEffort}; +use fabro_types::{Model, ModelControls, ModelCosts, ModelFeatures, ModelLimits}; use lithos_llm::catalog::builtin; +use lithos_llm::types::ReasoningEffort; #[test] fn model_reuses_canonical_type() { diff --git a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs index 8b03868eb..1ac9faae0 100644 --- a/lib/foundation/fabro-api/tests/provider_id_round_trip.rs +++ b/lib/foundation/fabro-api/tests/provider_id_round_trip.rs @@ -1,8 +1,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ModelHandle as ApiModelHandle, ProviderId as ApiProviderId}; -use fabro_types::{ModelHandle, ModelId, ProviderId}; -use lithos_llm::catalog::builtin; +use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId, builtin}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/provider_round_trip.rs b/lib/foundation/fabro-api/tests/provider_round_trip.rs index 330b50278..00d3fa3a4 100644 --- a/lib/foundation/fabro-api/tests/provider_round_trip.rs +++ b/lib/foundation/fabro-api/tests/provider_round_trip.rs @@ -1,8 +1,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::Provider as ApiProvider; -use fabro_types::{Provider, ProviderId}; -use lithos_llm::catalog::builtin; +use fabro_types::Provider; +use lithos_llm::catalog::{ProviderId, builtin}; #[test] fn provider_reuses_canonical_type() { diff --git a/lib/foundation/fabro-api/tests/reasoning_output_round_trip.rs b/lib/foundation/fabro-api/tests/reasoning_output_round_trip.rs index 035e66c23..4b8566ce9 100644 --- a/lib/foundation/fabro-api/tests/reasoning_output_round_trip.rs +++ b/lib/foundation/fabro-api/tests/reasoning_output_round_trip.rs @@ -3,7 +3,7 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ AgentMessageProps as ApiAgentMessageProps, ReasoningOutput as ApiReasoningOutput, }; -use fabro_types::ReasoningOutput; +use lithos_llm::types::ReasoningOutput; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/run_billing_stage_round_trip.rs b/lib/foundation/fabro-api/tests/run_billing_stage_round_trip.rs index 4dec969dd..c73963932 100644 --- a/lib/foundation/fabro-api/tests/run_billing_stage_round_trip.rs +++ b/lib/foundation/fabro-api/tests/run_billing_stage_round_trip.rs @@ -1,7 +1,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{BillingByModel, BillingModelRef, BillingSpeed, RunBillingStage}; -use fabro_types::{ModelRef, Speed, StageState}; +use fabro_types::{ModelRef, StageState}; +use lithos_llm::types::Speed; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/stage_model_usage_round_trip.rs b/lib/foundation/fabro-api/tests/stage_model_usage_round_trip.rs index 0b57cf53a..a6b78d6ba 100644 --- a/lib/foundation/fabro-api/tests/stage_model_usage_round_trip.rs +++ b/lib/foundation/fabro-api/tests/stage_model_usage_round_trip.rs @@ -3,7 +3,8 @@ use std::any::{TypeId, type_name}; use fabro_api::types::{ ReasoningEffort as ApiReasoningEffort, StageModelUsage as ApiStageModelUsage, }; -use fabro_types::{ReasoningEffort, Speed, StageModelUsage}; +use fabro_types::StageModelUsage; +use lithos_llm::types::{ReasoningEffort, Speed}; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs index baa71e871..9e3bde8c4 100644 --- a/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs +++ b/lib/foundation/fabro-api/tests/stage_projection_round_trip.rs @@ -25,14 +25,15 @@ use fabro_api::types::{ use fabro_types::{ ActivatedSkill, AgentControlState, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, AgentToolCategory, AgentToolSource, AgentToolSummary, - AgentToolsAvailableProps, LlmOutputKind, McpServerProjection, McpServerStatus, ModelId, - ModelRef, ParallelBranchId, ParallelBranchResult, PermissionLevel, ProviderId, - SkillsProjection, Speed, StageContextWindow, StageContextWindowBreakdownItem, - StageContextWindowCategory, StageContextWindowCountMethod, StageContextWindowProjection, - StageContextWindowStaleness, StageContextWindowUnavailableReason, StageContextWindowWarning, - StageId, StageInferenceProjection, StageProjection, StageToolBatchProjection, - SubAgentProjection, SubAgentStatus, TodoListKind, TodoListProjection, + AgentToolsAvailableProps, LlmOutputKind, McpServerProjection, McpServerStatus, ModelRef, + ParallelBranchId, ParallelBranchResult, PermissionLevel, SkillsProjection, StageContextWindow, + StageContextWindowBreakdownItem, StageContextWindowCategory, StageContextWindowCountMethod, + StageContextWindowProjection, StageContextWindowStaleness, StageContextWindowUnavailableReason, + StageContextWindowWarning, StageId, StageInferenceProjection, StageProjection, + StageToolBatchProjection, SubAgentProjection, SubAgentStatus, TodoListKind, TodoListProjection, }; +use lithos_llm::catalog::{ModelId, ProviderId}; +use lithos_llm::types::Speed; use serde_json::json; #[test] diff --git a/lib/foundation/fabro-client/Cargo.toml b/lib/foundation/fabro-client/Cargo.toml index c12175924..64ba40768 100644 --- a/lib/foundation/fabro-client/Cargo.toml +++ b/lib/foundation/fabro-client/Cargo.toml @@ -20,6 +20,7 @@ fabro-api = { path = "../fabro-api" } fabro-http.workspace = true fabro-static.workspace = true fabro-types = { path = "../fabro-types" } +lithos-llm = { workspace = true, features = ["runtime"] } fabro-util = { path = "../fabro-util" } fs2.workspace = true futures.workspace = true diff --git a/lib/foundation/fabro-client/src/client.rs b/lib/foundation/fabro-client/src/client.rs index f8536bbcf..4f91a7a38 100644 --- a/lib/foundation/fabro-client/src/client.rs +++ b/lib/foundation/fabro-client/src/client.rs @@ -13,13 +13,15 @@ use fabro_http::multipart::{Form, Part}; use fabro_types::settings::run::MergeStrategy; use fabro_types::{ ArtifactUpload, BlobHash, EventEnvelope, Model, ModelTestMode, PairId, PairMessageRecord, - PairMessageRequest, PairRecord, PairStartRequest, PairTranscriptResponse, ProviderId, - ReasoningEffort, Run, RunEvent, RunEventDetailResponse, RunId, RunPairStatusResponse, - RunProjection, SessionId, SessionRecord, StageId, WorkflowVersion, WorkflowVersionId, + PairMessageRequest, PairRecord, PairStartRequest, PairTranscriptResponse, Run, RunEvent, + RunEventDetailResponse, RunId, RunPairStatusResponse, RunProjection, SessionId, SessionRecord, + StageId, WorkflowVersion, WorkflowVersionId, }; use fabro_util::exit::{ErrorExt, ExitClass}; use futures::future::BoxFuture; use futures::{Stream, StreamExt}; +use lithos_llm::catalog::ProviderId; +use lithos_llm::types::ReasoningEffort; use serde::{Deserialize, Serialize}; use tokio::fs::File; use tokio::sync::Mutex; diff --git a/lib/foundation/fabro-types/src/billing.rs b/lib/foundation/fabro-types/src/billing.rs index 1fe4c77ae..2f483f73f 100644 --- a/lib/foundation/fabro-types/src/billing.rs +++ b/lib/foundation/fabro-types/src/billing.rs @@ -7,7 +7,7 @@ //! grouped under. use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; -pub use lithos_llm::types::{Cost, CostSource, Speed, TokenCounts}; +use lithos_llm::types::{Cost, Speed, TokenCounts}; use serde::{Deserialize, Serialize}; const USD_MICROS_PER_USD_F64: f64 = 1_000_000.0; @@ -330,6 +330,7 @@ impl BilledTokenCounts { #[cfg(test)] mod tests { + use lithos_llm::types::CostSource; use serde_json::json; use super::*; diff --git a/lib/foundation/fabro-types/src/catalog_api.rs b/lib/foundation/fabro-types/src/catalog_api.rs index f304dc0e7..35076048c 100644 --- a/lib/foundation/fabro-types/src/catalog_api.rs +++ b/lib/foundation/fabro-types/src/catalog_api.rs @@ -4,10 +4,10 @@ //! lithos catalog plus Fabro policy, stamped per request with whether the //! server holds credentials for each provider. +use lithos_llm::catalog::{ModelId, ProviderId}; +use lithos_llm::types::ReasoningEffort; use serde::{Deserialize, Serialize}; -use crate::{ModelId, ProviderId, ReasoningEffort}; - /// Token limits for a model. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ModelLimits { diff --git a/lib/foundation/fabro-types/src/lib.rs b/lib/foundation/fabro-types/src/lib.rs index 5f42b0798..97d16671c 100644 --- a/lib/foundation/fabro-types/src/lib.rs +++ b/lib/foundation/fabro-types/src/lib.rs @@ -65,9 +65,7 @@ pub mod workflow_version_id; pub use agent_profile::AgentProfileKind; pub use artifact::ArtifactUpload; pub use auth::{IdpIdentity, IdpIdentityError}; -pub use billing::{ - BilledModelUsage, BilledTokenCounts, Cost, CostSource, ModelRef, Speed, TokenCounts, UsdMicros, -}; +pub use billing::{BilledModelUsage, BilledTokenCounts, ModelRef, UsdMicros}; 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}; @@ -89,11 +87,6 @@ pub use input_scalar::{ pub use interview::{ InterviewQuestionRecord, QuestionType, ReviewTarget, ReviewTargetError, ReviewTargetKind, }; -pub use lithos_llm::catalog::{ModelHandle, ModelId, ProviderId}; -pub use lithos_llm::types::{ - FinishReason, ReasoningEffort, ReasoningOutput, Request, RequestBuildError, RequestBuilder, - Response, ResponseFormat, StreamEvent, -}; pub use llm_backend::AgentBackend; pub use manifest_path::{ManifestPath, ManifestPathParseError}; pub use mcp_store::{ @@ -197,10 +190,8 @@ pub use system_integrations::{ pub use timing::{RunTiming, StageTiming}; pub use todo::{TodoListKind, TodoListProjection, TodoPatch, TodoProjection, TodoStatus}; pub use transcript::{ - AudioContent, ContentPart, DocumentContent, ImageContent, MediaSource, Message, MessageId, - MessageKind, MessageSource, PairMessageRef, ReasoningContent, Role, ToolCall, ToolCallKind, - ToolChoice, ToolDefinition, ToolDefinitionKind, ToolInput, ToolResult, TranscriptMessage, - text_of, tool_call_arguments, tool_result_from_json, tool_result_to_json, + MessageId, MessageKind, MessageSource, PairMessageRef, TranscriptMessage, text_of, + tool_call_arguments, tool_result_from_json, tool_result_to_json, }; pub use variable::{ CreateVariableRequest, UpdateVariableRequest, Variable, VariableListResponse, is_env_style_name, diff --git a/lib/foundation/fabro-types/src/run_event/agent.rs b/lib/foundation/fabro-types/src/run_event/agent.rs index 639774573..b42f1d730 100644 --- a/lib/foundation/fabro-types/src/run_event/agent.rs +++ b/lib/foundation/fabro-types/src/run_event/agent.rs @@ -1,13 +1,15 @@ +use lithos_llm::types::{ + CostSource, ReasoningEffort, ReasoningOutput, Speed, ToolCall, ToolResult, +}; use serde::{Deserialize, Serialize}; use serde_json::Value; use strum::{Display, EnumString, IntoStaticStr}; use super::{BilledTokenCounts, ExecOutputTail}; -use crate::transcript::{ToolCall, ToolResult, TranscriptMessage}; +use crate::transcript::TranscriptMessage; use crate::{ - CommandTermination, CostSource, MessageId, ModelRef, PairId, PairMessageId, - PairSystemMessageKind, PermissionLevel, ReasoningEffort, ReasoningOutput, Speed, - StageContextWindowProjection, TurnId, + CommandTermination, MessageId, ModelRef, PairId, PairMessageId, PairSystemMessageKind, + PermissionLevel, StageContextWindowProjection, TurnId, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -524,12 +526,11 @@ pub struct AgentSkillActivatedProps { #[cfg(test)] mod tests { use lithos_llm::catalog::builtin; + use lithos_llm::types::ContentPart; use serde_json::json; use super::*; - use crate::transcript::{ - ContentPart, MessageKind, MessageSource, TranscriptMessage, tool_result_from_json, - }; + use crate::transcript::{MessageKind, MessageSource, TranscriptMessage, tool_result_from_json}; fn sample_model_ref() -> ModelRef { ModelRef::new(builtin::openai(), "gpt-5".into()) diff --git a/lib/foundation/fabro-types/src/run_event/misc.rs b/lib/foundation/fabro-types/src/run_event/misc.rs index b9206ac33..bb3116992 100644 --- a/lib/foundation/fabro-types/src/run_event/misc.rs +++ b/lib/foundation/fabro-types/src/run_event/misc.rs @@ -1,9 +1,10 @@ +use lithos_llm::types::ReasoningEffort; use serde::{Deserialize, Serialize}; use super::ExecOutputTail; use crate::{ - CommandTermination, ParallelBranchResult, PullRequestCreationId, PullRequestLink, - ReasoningEffort, ReviewTarget, StageId, StageOutcome, + CommandTermination, ParallelBranchResult, PullRequestCreationId, PullRequestLink, ReviewTarget, + StageId, StageOutcome, }; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] diff --git a/lib/foundation/fabro-types/src/run_event/mod.rs b/lib/foundation/fabro-types/src/run_event/mod.rs index bef0919f9..edf64822c 100644 --- a/lib/foundation/fabro-types/src/run_event/mod.rs +++ b/lib/foundation/fabro-types/src/run_event/mod.rs @@ -1037,6 +1037,7 @@ impl<'de> Deserialize<'de> for RunEvent { #[cfg(test)] mod tests { use lithos_llm::catalog::builtin; + use lithos_llm::types::ReasoningOutput; use serde_json::json; use super::*; @@ -2423,7 +2424,7 @@ mod tests { visit: 1, message: None, context_window: None, - reasoning: Some(crate::ReasoningOutput::new( + reasoning: Some(ReasoningOutput::new( "inspect the implementation first", "read convert.rs, then the sink", )), diff --git a/lib/foundation/fabro-types/src/run_event/session.rs b/lib/foundation/fabro-types/src/run_event/session.rs index 6f50a88f2..6cb2766c9 100644 --- a/lib/foundation/fabro-types/src/run_event/session.rs +++ b/lib/foundation/fabro-types/src/run_event/session.rs @@ -1,7 +1,8 @@ +use lithos_llm::catalog::ProviderId; use serde::{Deserialize, Serialize}; use serde_json::Value; -use crate::{ProviderId, TurnId}; +use crate::TurnId; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct RunSessionCreatedProps { diff --git a/lib/foundation/fabro-types/src/run_event/stage.rs b/lib/foundation/fabro-types/src/run_event/stage.rs index cc6aa69ab..422780c4d 100644 --- a/lib/foundation/fabro-types/src/run_event/stage.rs +++ b/lib/foundation/fabro-types/src/run_event/stage.rs @@ -1,12 +1,12 @@ use std::collections::BTreeMap; +use lithos_llm::types::{ReasoningEffort, Speed}; use serde::{Deserialize, Serialize}; use serde_json::Value; use super::ExecOutputTail; use crate::{ - BilledModelUsage, DiffSummary, FailureDetail, Outcome, ReasoningEffort, Speed, StageId, - StageOutcome, StageTiming, + BilledModelUsage, DiffSummary, FailureDetail, Outcome, StageId, StageOutcome, StageTiming, }; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/lib/foundation/fabro-types/src/run_projection.rs b/lib/foundation/fabro-types/src/run_projection.rs index b05d249e3..fb727d9b5 100644 --- a/lib/foundation/fabro-types/src/run_projection.rs +++ b/lib/foundation/fabro-types/src/run_projection.rs @@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::num::NonZeroU32; use chrono::{DateTime, Utc}; +use lithos_llm::types::{ReasoningEffort, Speed}; use strum::{Display, EnumString, IntoStaticStr}; use crate::run_event::{AgentSessionActivatedProps, StagePromptProps}; @@ -10,9 +11,9 @@ use crate::{ AgentBackend, AgentMcpToolSummary, AgentSkillActivationSource, AgentSkillSummary, AgentToolSummary, BilledTokenCounts, Checkpoint, Conclusion, InterviewQuestionRecord, InvalidTransition, LlmOutputKind, ModelRef, ParallelBranchId, PermissionLevel, - PullRequestCreation, PullRequestLink, ReasoningEffort, RunApproval, RunControlAction, RunDiff, - RunId, RunSandbox, RunSpec, RunStatus, RunTiming, Speed, StageCompletion, StageHandler, - StageId, StageState, StageTiming, StartRecord, TodoListProjection, timing, + PullRequestCreation, PullRequestLink, RunApproval, RunControlAction, RunDiff, RunId, + RunSandbox, RunSpec, RunStatus, RunTiming, StageCompletion, StageHandler, StageId, StageState, + StageTiming, StartRecord, TodoListProjection, timing, }; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/lib/foundation/fabro-types/src/transcript.rs b/lib/foundation/fabro-types/src/transcript.rs index 32efda94f..4f1a44857 100644 --- a/lib/foundation/fabro-types/src/transcript.rs +++ b/lib/foundation/fabro-types/src/transcript.rs @@ -7,11 +7,7 @@ //! around lithos content parts. use chrono::{DateTime, Utc}; -pub use lithos_llm::types::{ - AudioContent, ContentPart, DocumentContent, ImageContent, MediaSource, Message, - ReasoningContent, Role, TokenCounts, ToolArgumentError, ToolArguments, ToolCall, ToolCallKind, - ToolChoice, ToolDefinition, ToolDefinitionKind, ToolInput, ToolResult, UnknownContent, -}; +use lithos_llm::types::{ContentPart, TokenCounts, ToolCall, ToolResult}; use serde::{Deserialize, Serialize}; use strum::{Display, EnumString, IntoStaticStr};