mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
feat(server): inject settings-backed model catalog (#247)
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
## Summary This PR moves the server-facing model catalog paths onto a resolved catalog stored in `AppState`, using configured `[llm]` provider/model overrides layered on top of the built-in catalog. The server now uses the injected catalog for: - `/models` listing and model test lookup - `/completions` default model and provider inference - manifest preflight materialization and LLM model alias resolution - diagnostics LLM probes - pull request default model selection - runtime settings refresh via `replace_settings` It also adds catalog overlay helpers in `fabro-model` and converts resolved server runtime `[llm]` settings into the catalog shape in `fabro-config`. This branch also includes the earlier `chore: update dockerfile` commit, which updates the Daytona snapshot to `fabro-v10` and installs Chromium through the xtradeb PPA with an XFCE/browser wrapper. Related: #210 ## Tests - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo nextest run -p fabro-config -p fabro-model -p fabro-server` (844 tests passed) - `cargo +nightly-2026-04-14 clippy -p fabro-config -p fabro-model -p fabro-server --all-targets -- -D warnings`
This commit is contained in:
parent
34d83db801
commit
264cac3c64
12 changed files with 706 additions and 40 deletions
|
|
@ -11,7 +11,7 @@ auto_stop_interval = 30
|
|||
repo = "fabro-sh/fabro"
|
||||
|
||||
[run.sandbox.daytona.snapshot]
|
||||
name = "fabro-v9"
|
||||
name = "fabro-v10"
|
||||
cpu = 8
|
||||
memory = "16GB"
|
||||
disk = "20GB"
|
||||
|
|
@ -24,6 +24,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||
libx11-6 libxrandr2 libxext6 libxrender1 libxfixes3 libxss1 libxtst6 libxi6 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install real Chromium (not the snap stub) via xtradeb PPA
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
software-properties-common curl gnupg \
|
||||
&& add-apt-repository -y ppa:xtradeb/apps \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends chromium \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Wrapper: Chromium needs --no-sandbox when running as root in a container,
|
||||
# and --disable-dev-shm-usage avoids crashes from small /dev/shm
|
||||
RUN printf '#!/bin/bash\nexec /usr/bin/chromium --no-sandbox --disable-dev-shm-usage "$@"\n' \
|
||||
> /usr/local/bin/chromium-wrapper \
|
||||
&& chmod +x /usr/local/bin/chromium-wrapper
|
||||
|
||||
# Make the wrapper the default in the system .desktop file and via alternatives
|
||||
RUN sed -i 's|^Exec=.*|Exec=/usr/local/bin/chromium-wrapper %U|' \
|
||||
/usr/share/applications/chromium.desktop \
|
||||
&& update-alternatives --install /usr/bin/x-www-browser x-www-browser \
|
||||
/usr/local/bin/chromium-wrapper 100
|
||||
|
||||
# Tell XFCE's exo-open that Chromium is the WebBrowser helper (system-wide)
|
||||
RUN mkdir -p /etc/xdg/xfce4 /usr/share/xfce4/helpers \
|
||||
&& printf 'WebBrowser=custom-WebBrowser\n' > /etc/xdg/xfce4/helpers.rc \
|
||||
&& printf '[Desktop Entry]\n\
|
||||
Version=1.0\n\
|
||||
Type=X-XFCE-Helper\n\
|
||||
Name=Chromium\n\
|
||||
Icon=chromium\n\
|
||||
X-XFCE-Category=WebBrowser\n\
|
||||
X-XFCE-CommandsWithParameter=/usr/local/bin/chromium-wrapper "%%s"\n\
|
||||
X-XFCE-Commands=/usr/local/bin/chromium-wrapper\n' \
|
||||
> /usr/share/xfce4/helpers/custom-WebBrowser.desktop
|
||||
|
||||
# GitHub CLI
|
||||
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
||||
| dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use fabro_model::catalog as model_catalog;
|
||||
use fabro_types::settings::{RunNamespace, WorkflowNamespace};
|
||||
use fabro_types::{ServerSettings, UserSettings, WorkflowSettings};
|
||||
use fabro_util::error::SharedError;
|
||||
|
|
@ -12,7 +14,11 @@ use crate::resolve::{
|
|||
resolve_workflow,
|
||||
};
|
||||
use crate::user::load_settings_config;
|
||||
use crate::{CliLayer, Combine, Error, Result, RunLayer, ServerLayer, SettingsLayer, run};
|
||||
use crate::{
|
||||
CliLayer, Combine, CostRates, Error, LlmLayer, LlmModelFeatures, LlmModelLimits, ModelControls,
|
||||
ModelCostTable, ModelSettings, ProviderSettings, Result, RunLayer, ServerLayer, SettingsLayer,
|
||||
run,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ResolveErrors(pub Vec<ResolveError>);
|
||||
|
|
@ -204,6 +210,7 @@ pub struct ServerRuntimeSettings {
|
|||
pub server_settings: ServerSettings,
|
||||
pub manifest_run_defaults: RunLayer,
|
||||
pub manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub llm_catalog_settings: model_catalog::LlmCatalogSettings,
|
||||
}
|
||||
|
||||
pub fn load_server_runtime_settings(
|
||||
|
|
@ -251,14 +258,134 @@ fn resolve_server_runtime_settings(
|
|||
}
|
||||
|
||||
let manifest_run_defaults = layer.run.clone().unwrap_or_default();
|
||||
let llm_catalog_settings = llm_catalog_settings_from_layer(&layer);
|
||||
Ok(ServerRuntimeSettings {
|
||||
server_settings: ServerSettingsBuilder::from_layer(&layer)?,
|
||||
manifest_run_settings: RunSettingsBuilder::from_run_layer(&manifest_run_defaults)
|
||||
.map_err(|err| SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
llm_catalog_settings,
|
||||
})
|
||||
}
|
||||
|
||||
fn llm_catalog_settings_from_layer(layer: &SettingsLayer) -> model_catalog::LlmCatalogSettings {
|
||||
let layer = layer.clone().combine(DEFAULTS_LAYER.clone());
|
||||
layer
|
||||
.llm
|
||||
.map(llm_layer_to_catalog_settings)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn llm_layer_to_catalog_settings(llm: LlmLayer) -> model_catalog::LlmCatalogSettings {
|
||||
model_catalog::LlmCatalogSettings {
|
||||
providers: llm
|
||||
.providers
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|(id, settings)| (id, provider_settings_to_catalog(settings)))
|
||||
.collect(),
|
||||
models: llm
|
||||
.models
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|(id, settings)| (id, model_settings_to_catalog(settings)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_settings_to_catalog(
|
||||
settings: ProviderSettings,
|
||||
) -> model_catalog::ProviderCatalogSettings {
|
||||
model_catalog::ProviderCatalogSettings {
|
||||
display_name: settings.display_name,
|
||||
adapter: settings.adapter,
|
||||
base_url: settings.base_url,
|
||||
credentials: settings.credentials,
|
||||
extra_headers: settings.extra_headers,
|
||||
priority: settings.priority,
|
||||
enabled: settings.enabled,
|
||||
aliases: settings.aliases,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_settings_to_catalog(settings: ModelSettings) -> model_catalog::ModelCatalogSettings {
|
||||
let ModelSettings {
|
||||
provider,
|
||||
api_id,
|
||||
display_name,
|
||||
family,
|
||||
training,
|
||||
knowledge_cutoff,
|
||||
default,
|
||||
enabled,
|
||||
aliases,
|
||||
estimated_output_tps,
|
||||
limits,
|
||||
features,
|
||||
controls,
|
||||
costs,
|
||||
} = settings;
|
||||
model_catalog::ModelCatalogSettings {
|
||||
provider,
|
||||
api_id,
|
||||
display_name,
|
||||
family,
|
||||
training,
|
||||
knowledge_cutoff,
|
||||
default,
|
||||
enabled,
|
||||
aliases,
|
||||
estimated_output_tps,
|
||||
limits: limits.as_ref().map(model_limits_to_catalog),
|
||||
features: features.as_ref().map(model_features_to_catalog),
|
||||
controls: controls.map(model_controls_to_catalog),
|
||||
costs: costs.as_ref().map(model_cost_table_to_catalog),
|
||||
}
|
||||
}
|
||||
|
||||
fn model_limits_to_catalog(limits: &LlmModelLimits) -> model_catalog::SettingsModelLimits {
|
||||
model_catalog::SettingsModelLimits {
|
||||
context_window: limits.context_window,
|
||||
max_output: limits.max_output,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_features_to_catalog(features: &LlmModelFeatures) -> model_catalog::SettingsModelFeatures {
|
||||
model_catalog::SettingsModelFeatures {
|
||||
tools: features.tools,
|
||||
vision: features.vision,
|
||||
reasoning: features.reasoning,
|
||||
effort: features.effort,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_controls_to_catalog(controls: ModelControls) -> model_catalog::SettingsModelControls {
|
||||
model_catalog::SettingsModelControls {
|
||||
reasoning_effort: controls.reasoning_effort,
|
||||
speed: controls.speed,
|
||||
}
|
||||
}
|
||||
|
||||
fn model_cost_table_to_catalog(costs: &ModelCostTable) -> model_catalog::SettingsModelCostTable {
|
||||
model_catalog::SettingsModelCostTable {
|
||||
base: cost_rates_to_catalog(&costs.base),
|
||||
speed: costs.speed.as_ref().map(|speed| {
|
||||
speed
|
||||
.iter()
|
||||
.map(|(key, rates)| (key.clone(), cost_rates_to_catalog(rates)))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_rates_to_catalog(rates: &CostRates) -> model_catalog::CostRates {
|
||||
model_catalog::CostRates {
|
||||
input_cost_per_mtok: rates.input_cost_per_mtok,
|
||||
output_cost_per_mtok: rates.output_cost_per_mtok,
|
||||
cache_input_cost_per_mtok: rates.cache_input_cost_per_mtok,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct WorkflowSettingsBuilder {
|
||||
args: SettingsLayer,
|
||||
|
|
@ -456,7 +583,7 @@ mod tests {
|
|||
use fabro_types::settings::cli::OutputVerbosity;
|
||||
use fabro_types::settings::run::{ApprovalMode, RunMode};
|
||||
|
||||
use super::{RunSettingsBuilder, WorkflowSettingsBuilder};
|
||||
use super::{RunSettingsBuilder, WorkflowSettingsBuilder, server_runtime_settings_from_toml};
|
||||
use crate::{CliLayer, CliOutputLayer, ReplaceMap, RunExecutionLayer, RunLayer, RunModelLayer};
|
||||
|
||||
#[test]
|
||||
|
|
@ -531,4 +658,51 @@ command = ["demo-mcp"]
|
|||
assert_eq!(settings.run.execution.mode, RunMode::DryRun);
|
||||
assert_eq!(settings.run.execution.approval, ApprovalMode::Auto);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_runtime_settings_preserves_llm_catalog_overrides() {
|
||||
let settings = server_runtime_settings_from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[llm.providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
credentials = ["env:VENICE_API_KEY"]
|
||||
|
||||
[llm.models."venice-large"]
|
||||
provider = "venice"
|
||||
display_name = "Venice Large"
|
||||
family = "venice"
|
||||
default = true
|
||||
|
||||
[llm.models."venice-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[llm.models."venice-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
effort = false
|
||||
"#,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("server runtime settings should resolve");
|
||||
|
||||
let catalog =
|
||||
fabro_model::Catalog::from_builtin_with_overrides(&settings.llm_catalog_settings)
|
||||
.expect("catalog overrides should build");
|
||||
|
||||
assert_eq!(
|
||||
catalog
|
||||
.get("venice-large")
|
||||
.map(|model| model.provider.clone()),
|
||||
Some(fabro_model::ProviderId::new("venice"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -610,6 +610,14 @@ impl Catalog {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn from_builtin_with_overrides(
|
||||
overrides: &LlmCatalogSettings,
|
||||
) -> Result<Self, CatalogBuildError> {
|
||||
let builtins = Self::builtin_settings()?;
|
||||
let settings = merge_catalog_settings(overrides.clone(), builtins);
|
||||
Self::from_settings(&settings)
|
||||
}
|
||||
|
||||
/// Create a catalog from a custom set of models (useful for testing).
|
||||
#[must_use]
|
||||
pub fn from_models(models: Vec<Model>) -> Self {
|
||||
|
|
@ -658,7 +666,7 @@ impl Catalog {
|
|||
}
|
||||
}
|
||||
|
||||
fn from_builtin_toml() -> Result<Self, CatalogBuildError> {
|
||||
fn builtin_settings() -> Result<LlmCatalogSettings, CatalogBuildError> {
|
||||
let mut layer = LlmCatalogSettings::default();
|
||||
let mut paths = BuiltinCatalogToml::iter()
|
||||
.filter(|path| path.ends_with(".toml"))
|
||||
|
|
@ -688,7 +696,11 @@ impl Catalog {
|
|||
layer.models.extend(fragment.models);
|
||||
}
|
||||
|
||||
Self::from_settings(&layer)
|
||||
Ok(layer)
|
||||
}
|
||||
|
||||
fn from_builtin_toml() -> Result<Self, CatalogBuildError> {
|
||||
Self::from_settings(&Self::builtin_settings()?)
|
||||
}
|
||||
|
||||
/// Look up a model by ID or alias.
|
||||
|
|
@ -899,6 +911,146 @@ fn build_model_index(models: &[Model]) -> HashMap<String, usize> {
|
|||
index
|
||||
}
|
||||
|
||||
fn merge_catalog_settings(
|
||||
higher: LlmCatalogSettings,
|
||||
mut fallback: LlmCatalogSettings,
|
||||
) -> LlmCatalogSettings {
|
||||
for (id, provider) in higher.providers {
|
||||
let provider = match fallback.providers.remove(&id) {
|
||||
Some(fallback_provider) => merge_provider_settings(provider, fallback_provider),
|
||||
None => provider,
|
||||
};
|
||||
fallback.providers.insert(id, provider);
|
||||
}
|
||||
|
||||
for (id, model) in higher.models {
|
||||
let model = match fallback.models.remove(&id) {
|
||||
Some(fallback_model) => merge_model_settings(model, fallback_model),
|
||||
None => model,
|
||||
};
|
||||
fallback.models.insert(id, model);
|
||||
}
|
||||
|
||||
fallback
|
||||
}
|
||||
|
||||
fn merge_provider_settings(
|
||||
higher: ProviderCatalogSettings,
|
||||
fallback: ProviderCatalogSettings,
|
||||
) -> ProviderCatalogSettings {
|
||||
ProviderCatalogSettings {
|
||||
display_name: higher.display_name.or(fallback.display_name),
|
||||
adapter: higher.adapter.or(fallback.adapter),
|
||||
base_url: higher.base_url.or(fallback.base_url),
|
||||
credentials: higher.credentials.or(fallback.credentials),
|
||||
extra_headers: higher.extra_headers.or(fallback.extra_headers),
|
||||
priority: higher.priority.or(fallback.priority),
|
||||
enabled: higher.enabled.or(fallback.enabled),
|
||||
aliases: higher.aliases.or(fallback.aliases),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_model_settings(
|
||||
higher: ModelCatalogSettings,
|
||||
fallback: ModelCatalogSettings,
|
||||
) -> ModelCatalogSettings {
|
||||
ModelCatalogSettings {
|
||||
provider: higher.provider.or(fallback.provider),
|
||||
api_id: higher.api_id.or(fallback.api_id),
|
||||
display_name: higher.display_name.or(fallback.display_name),
|
||||
family: higher.family.or(fallback.family),
|
||||
training: higher.training.or(fallback.training),
|
||||
knowledge_cutoff: higher.knowledge_cutoff.or(fallback.knowledge_cutoff),
|
||||
default: higher.default.or(fallback.default),
|
||||
enabled: higher.enabled.or(fallback.enabled),
|
||||
aliases: higher.aliases.or(fallback.aliases),
|
||||
estimated_output_tps: higher
|
||||
.estimated_output_tps
|
||||
.or(fallback.estimated_output_tps),
|
||||
limits: merge_optional(
|
||||
higher.limits,
|
||||
fallback.limits,
|
||||
merge_model_limits_settings,
|
||||
),
|
||||
features: merge_optional(
|
||||
higher.features,
|
||||
fallback.features,
|
||||
merge_model_features_settings,
|
||||
),
|
||||
controls: merge_optional(
|
||||
higher.controls,
|
||||
fallback.controls,
|
||||
merge_model_controls_settings,
|
||||
),
|
||||
costs: merge_optional(higher.costs, fallback.costs, merge_model_cost_table),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_optional<T>(higher: Option<T>, fallback: Option<T>, merge: fn(&T, &T) -> T) -> Option<T> {
|
||||
match (higher, fallback) {
|
||||
(Some(higher), Some(fallback)) => Some(merge(&higher, &fallback)),
|
||||
(Some(higher), None) => Some(higher),
|
||||
(None, fallback) => fallback,
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_model_limits_settings(
|
||||
higher: &SettingsModelLimits,
|
||||
fallback: &SettingsModelLimits,
|
||||
) -> SettingsModelLimits {
|
||||
SettingsModelLimits {
|
||||
context_window: higher.context_window.or(fallback.context_window),
|
||||
max_output: higher.max_output.or(fallback.max_output),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_model_features_settings(
|
||||
higher: &SettingsModelFeatures,
|
||||
fallback: &SettingsModelFeatures,
|
||||
) -> SettingsModelFeatures {
|
||||
SettingsModelFeatures {
|
||||
tools: higher.tools.or(fallback.tools),
|
||||
vision: higher.vision.or(fallback.vision),
|
||||
reasoning: higher.reasoning.or(fallback.reasoning),
|
||||
effort: higher.effort.or(fallback.effort),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_model_controls_settings(
|
||||
higher: &SettingsModelControls,
|
||||
fallback: &SettingsModelControls,
|
||||
) -> SettingsModelControls {
|
||||
SettingsModelControls {
|
||||
reasoning_effort: higher
|
||||
.reasoning_effort
|
||||
.clone()
|
||||
.or_else(|| fallback.reasoning_effort.clone()),
|
||||
speed: higher.speed.clone().or_else(|| fallback.speed.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_model_cost_table(
|
||||
higher: &SettingsModelCostTable,
|
||||
fallback: &SettingsModelCostTable,
|
||||
) -> SettingsModelCostTable {
|
||||
SettingsModelCostTable {
|
||||
base: merge_cost_rates(&higher.base, &fallback.base),
|
||||
speed: higher.speed.clone().or_else(|| fallback.speed.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_cost_rates(higher: &CostRates, fallback: &CostRates) -> CostRates {
|
||||
CostRates {
|
||||
input_cost_per_mtok: higher.input_cost_per_mtok.or(fallback.input_cost_per_mtok),
|
||||
output_cost_per_mtok: higher
|
||||
.output_cost_per_mtok
|
||||
.or(fallback.output_cost_per_mtok),
|
||||
cache_input_cost_per_mtok: higher
|
||||
.cache_input_cost_per_mtok
|
||||
.or(fallback.cache_input_cost_per_mtok),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_providers(
|
||||
settings: &LlmCatalogSettings,
|
||||
) -> Result<Vec<CatalogProvider>, CatalogBuildError> {
|
||||
|
|
@ -1354,6 +1506,85 @@ mod tests {
|
|||
|
||||
// ---- Catalog struct tests ----
|
||||
|
||||
#[test]
|
||||
fn builtin_with_empty_overrides_matches_builtin_catalog() {
|
||||
let catalog = Catalog::from_builtin_with_overrides(&LlmCatalogSettings::default())
|
||||
.expect("empty overrides should build");
|
||||
|
||||
assert_eq!(
|
||||
catalog.get("sonnet").map(|model| model.id.as_str()),
|
||||
Catalog::builtin()
|
||||
.get("sonnet")
|
||||
.map(|model| model.id.as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
catalog.default_model().id,
|
||||
Catalog::builtin().default_model().id
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_overrides_sparse_provider_fields() {
|
||||
let catalog = Catalog::from_builtin_with_overrides(&minimal_settings(
|
||||
r"
|
||||
[providers.anthropic]
|
||||
enabled = false
|
||||
",
|
||||
))
|
||||
.expect("sparse built-in provider override should build");
|
||||
|
||||
assert!(catalog.provider(&ProviderId::anthropic()).is_none());
|
||||
assert!(catalog.get("claude-sonnet-4-5").is_none());
|
||||
assert!(
|
||||
catalog
|
||||
.providers()
|
||||
.iter()
|
||||
.any(|provider| provider.id == ProviderId::openai())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_overrides_add_custom_openai_compatible_provider_and_model() {
|
||||
let catalog = Catalog::from_builtin_with_overrides(&minimal_settings(
|
||||
r#"
|
||||
[providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
credentials = ["env:VENICE_API_KEY"]
|
||||
priority = 120
|
||||
aliases = ["venice-ai"]
|
||||
|
||||
[models."venice-large"]
|
||||
provider = "venice"
|
||||
display_name = "Venice Large"
|
||||
family = "venice"
|
||||
default = true
|
||||
aliases = ["vl"]
|
||||
|
||||
[models."venice-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[models."venice-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
effort = false
|
||||
"#,
|
||||
))
|
||||
.expect("custom provider overlay should build");
|
||||
|
||||
let provider = catalog
|
||||
.provider(&ProviderId::new("venice-ai"))
|
||||
.expect("provider alias should resolve");
|
||||
assert_eq!(provider.id, ProviderId::new("venice"));
|
||||
assert_eq!(provider.adapter, "openai_compatible");
|
||||
|
||||
let model = catalog.get("vl").expect("model alias should resolve");
|
||||
assert_eq!(model.id, "venice-large");
|
||||
assert_eq!(model.provider, ProviderId::new("venice"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_get_by_id() {
|
||||
let m = Catalog::builtin().get("claude-opus-4-6").unwrap();
|
||||
|
|
|
|||
|
|
@ -119,13 +119,17 @@ async fn check_llm_providers(state: &AppState) -> CheckResult {
|
|||
.filter_map(|name| name.parse::<Provider>().ok())
|
||||
.collect();
|
||||
let client = &result.client;
|
||||
let probe_outcomes = join_all(providers.iter().map(|&provider| async move {
|
||||
let outcome = timeout(
|
||||
Duration::from_secs(30),
|
||||
probe_llm_provider(client, provider),
|
||||
)
|
||||
.await;
|
||||
(provider, outcome)
|
||||
let catalog = state.catalog();
|
||||
let probe_outcomes = join_all(providers.iter().map(|&provider| {
|
||||
let catalog = catalog.clone();
|
||||
async move {
|
||||
let outcome = timeout(
|
||||
Duration::from_secs(30),
|
||||
probe_llm_provider(client, provider, catalog.as_ref()),
|
||||
)
|
||||
.await;
|
||||
(provider, outcome)
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
for (provider, probe_result) in probe_outcomes {
|
||||
|
|
@ -200,15 +204,19 @@ fn short_error_line(rendered: &str) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
fn probe_model(provider: Provider) -> String {
|
||||
Catalog::builtin()
|
||||
fn probe_model(provider: Provider, catalog: &Catalog) -> String {
|
||||
catalog
|
||||
.probe_for_provider(provider)
|
||||
.map_or_else(|| format!("unknown-{provider}"), |m| m.id.clone())
|
||||
}
|
||||
|
||||
async fn probe_llm_provider(client: &LlmClient, provider: Provider) -> fabro_llm::Result<()> {
|
||||
async fn probe_llm_provider(
|
||||
client: &LlmClient,
|
||||
provider: Provider,
|
||||
catalog: &Catalog,
|
||||
) -> fabro_llm::Result<()> {
|
||||
let request = Request {
|
||||
model: probe_model(provider),
|
||||
model: probe_model(provider, catalog),
|
||||
messages: vec![Message::user("hi")],
|
||||
provider: Some(provider.to_string()),
|
||||
tools: None,
|
||||
|
|
|
|||
|
|
@ -436,10 +436,11 @@ async fn build_preflight_report(
|
|||
}
|
||||
|
||||
let configured_providers = state.llm_source.configured_providers().await;
|
||||
let catalog = state.catalog();
|
||||
let materialized = materialize_run(
|
||||
prepared.settings.clone(),
|
||||
graph,
|
||||
Catalog::builtin(),
|
||||
catalog.as_ref(),
|
||||
&configured_providers,
|
||||
);
|
||||
let resolved_run = materialized.run;
|
||||
|
|
@ -486,6 +487,7 @@ async fn build_preflight_report(
|
|||
graph,
|
||||
&resolved_run,
|
||||
&configured_providers,
|
||||
catalog.as_ref(),
|
||||
)
|
||||
.await;
|
||||
run_github_token_check(&mut checks, prepared, &resolved_run, github_app).await;
|
||||
|
|
@ -887,8 +889,9 @@ async fn run_llm_check(
|
|||
graph: &Graph,
|
||||
settings: &RunNamespace,
|
||||
configured_providers: &[ProviderId],
|
||||
catalog: &Catalog,
|
||||
) -> bool {
|
||||
let (model, provider) = resolve_model_provider(settings, graph, configured_providers);
|
||||
let (model, provider) = resolve_model_provider(settings, graph, configured_providers, catalog);
|
||||
let default_provider = provider.as_deref().unwrap_or("anthropic");
|
||||
|
||||
match state.resolve_llm_client().await {
|
||||
|
|
@ -912,7 +915,7 @@ async fn run_llm_check(
|
|||
let node_model = node.model().unwrap_or(&model);
|
||||
let node_provider = node.provider().unwrap_or(default_provider);
|
||||
let (resolved_model, resolved_provider) =
|
||||
if let Some(info) = Catalog::builtin().get(node_model) {
|
||||
if let Some(info) = catalog.get(node_model) {
|
||||
(info.id.clone(), info.provider.to_string())
|
||||
} else {
|
||||
(node_model.to_string(), node_provider.to_string())
|
||||
|
|
@ -930,12 +933,11 @@ async fn run_llm_check(
|
|||
}
|
||||
|
||||
if model_providers.is_empty() {
|
||||
let (resolved_model, resolved_provider) =
|
||||
if let Some(info) = Catalog::builtin().get(&model) {
|
||||
(info.id.clone(), info.provider.to_string())
|
||||
} else {
|
||||
(model.clone(), default_provider.to_string())
|
||||
};
|
||||
let (resolved_model, resolved_provider) = if let Some(info) = catalog.get(&model) {
|
||||
(info.id.clone(), info.provider.to_string())
|
||||
} else {
|
||||
(model.clone(), default_provider.to_string())
|
||||
};
|
||||
model_providers.insert((resolved_model, resolved_provider));
|
||||
}
|
||||
|
||||
|
|
@ -1040,6 +1042,7 @@ fn resolve_model_provider(
|
|||
settings: &RunNamespace,
|
||||
_graph: &Graph,
|
||||
configured_providers: &[ProviderId],
|
||||
catalog: &Catalog,
|
||||
) -> (String, Option<String>) {
|
||||
let provider = settings
|
||||
.model
|
||||
|
|
@ -1048,7 +1051,7 @@ fn resolve_model_provider(
|
|||
.map(InterpString::as_source);
|
||||
let model = settings.model.name.as_ref().map_or_else(
|
||||
|| {
|
||||
Catalog::builtin()
|
||||
catalog
|
||||
.default_for_configured_ids(configured_providers)
|
||||
.id
|
||||
.clone()
|
||||
|
|
@ -1056,7 +1059,7 @@ fn resolve_model_provider(
|
|||
InterpString::as_source,
|
||||
);
|
||||
|
||||
match Catalog::builtin().get(&model) {
|
||||
match catalog.get(&model) {
|
||||
Some(info) => (
|
||||
info.id.clone(),
|
||||
provider.or(Some(info.provider.to_string())),
|
||||
|
|
@ -2039,6 +2042,77 @@ digraph Demo {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preflight_resolves_model_aliases_from_app_state_catalog() {
|
||||
let llm_catalog_settings: fabro_model::catalog::LlmCatalogSettings = toml::from_str(
|
||||
r#"
|
||||
[providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
credentials = ["env:VENICE_API_KEY"]
|
||||
|
||||
[models."venice-large"]
|
||||
provider = "venice"
|
||||
display_name = "Venice Large"
|
||||
family = "venice"
|
||||
default = true
|
||||
aliases = ["vl"]
|
||||
|
||||
[models."venice-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[models."venice-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
effort = false
|
||||
"#,
|
||||
)
|
||||
.expect("catalog fixture should parse");
|
||||
let state = crate::test_support::TestAppStateBuilder::new()
|
||||
.llm_catalog_settings(llm_catalog_settings)
|
||||
.build();
|
||||
let mut manifest = minimal_manifest();
|
||||
manifest.workflows.get_mut("workflow.fabro").unwrap().source = r#"
|
||||
digraph Demo {
|
||||
start [shape=Mdiamond]
|
||||
exit [shape=Msquare]
|
||||
work [prompt="Do work", model="vl"]
|
||||
start -> work -> exit
|
||||
}
|
||||
"#
|
||||
.to_string();
|
||||
let prepared = prepare_manifest(
|
||||
&manifest_run_defaults(Some(&default_settings_fixture())),
|
||||
&manifest,
|
||||
)
|
||||
.unwrap();
|
||||
let validated = validate_prepared_manifest(&prepared, RenderMode::Strict).unwrap();
|
||||
|
||||
let (response, ok) = run_preflight(state.as_ref(), &prepared, &validated)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!ok);
|
||||
let llm_check = response.checks.sections[0]
|
||||
.checks
|
||||
.iter()
|
||||
.find(|check| check.name == "LLM" && check.summary == "venice-large")
|
||||
.expect("preflight should resolve the catalog alias");
|
||||
assert_eq!(llm_check.status, types::PreflightCheckResultStatus::Warning);
|
||||
assert_eq!(
|
||||
llm_check.remediation.as_deref(),
|
||||
Some("Provider \"venice\" is not configured")
|
||||
);
|
||||
assert!(
|
||||
llm_check
|
||||
.details
|
||||
.iter()
|
||||
.any(|detail| detail.text == "Provider: venice")
|
||||
);
|
||||
}
|
||||
|
||||
mod root_workflow_run_layer_tests {
|
||||
//! `root_workflow_run_layer` parses bundled workflow.toml through
|
||||
//! the strict `SettingsLayer` schema, so unknown fields anywhere in
|
||||
|
|
|
|||
|
|
@ -713,6 +713,7 @@ where
|
|||
server_settings: runtime_settings.server_settings,
|
||||
manifest_run_defaults: runtime_settings.manifest_run_defaults,
|
||||
manifest_run_settings: runtime_settings.manifest_run_settings,
|
||||
llm_catalog_settings: runtime_settings.llm_catalog_settings,
|
||||
};
|
||||
let resolved_server_settings = resolved_app_settings.server_settings.server.clone();
|
||||
let (auth_mode, server_secrets) = resolve_startup(
|
||||
|
|
@ -882,6 +883,7 @@ where
|
|||
server_settings: resolved.server_settings,
|
||||
manifest_run_defaults: resolved.manifest_run_defaults,
|
||||
manifest_run_settings: resolved.manifest_run_settings,
|
||||
llm_catalog_settings: resolved.llm_catalog_settings,
|
||||
}
|
||||
});
|
||||
match resolved {
|
||||
|
|
@ -1262,6 +1264,7 @@ mod tests {
|
|||
.map_err(|err| fabro_util::error::SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
server_settings: server_settings(source),
|
||||
llm_catalog_settings: fabro_model::catalog::LlmCatalogSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ use fabro_llm::types::{
|
|||
ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest, Role, ToolChoice,
|
||||
ToolDefinition,
|
||||
};
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::{BilledTokenCounts, Catalog, ModelTestMode, ProviderId};
|
||||
use fabro_redact::redact_jsonl_line;
|
||||
use fabro_sandbox::daytona::{self, DaytonaSandbox};
|
||||
|
|
@ -527,6 +528,7 @@ pub struct AppState {
|
|||
manifest_run_defaults: RwLock<Arc<RunLayer>>,
|
||||
manifest_run_settings: RwLock<std::result::Result<RunNamespace, SharedError>>,
|
||||
pub(crate) server_settings: RwLock<Arc<ServerSettings>>,
|
||||
catalog: RwLock<Arc<Catalog>>,
|
||||
pub(crate) env_lookup: EnvLookup,
|
||||
pub(crate) github_api_base_url: String,
|
||||
http_client: Option<fabro_http::HttpClient>,
|
||||
|
|
@ -602,6 +604,7 @@ pub(crate) struct ResolvedAppStateSettings {
|
|||
pub(crate) server_settings: ServerSettings,
|
||||
pub(crate) manifest_run_defaults: RunLayer,
|
||||
pub(crate) manifest_run_settings: std::result::Result<RunNamespace, SharedError>,
|
||||
pub(crate) llm_catalog_settings: LlmCatalogSettings,
|
||||
}
|
||||
|
||||
fn accumulate_billing_rollup(
|
||||
|
|
@ -660,6 +663,10 @@ impl AppState {
|
|||
)
|
||||
}
|
||||
|
||||
pub(crate) fn catalog(&self) -> Arc<Catalog> {
|
||||
Arc::clone(&self.catalog.read().expect("catalog lock poisoned"))
|
||||
}
|
||||
|
||||
pub(crate) fn manifest_run_settings(&self) -> std::result::Result<RunNamespace, SharedError> {
|
||||
self.manifest_run_settings
|
||||
.read()
|
||||
|
|
@ -826,9 +833,14 @@ impl AppState {
|
|||
server_settings,
|
||||
manifest_run_defaults,
|
||||
manifest_run_settings,
|
||||
llm_catalog_settings,
|
||||
} = resolved_settings;
|
||||
let server_settings = Arc::new(server_settings);
|
||||
let manifest_run_defaults = Arc::new(manifest_run_defaults);
|
||||
let catalog = Arc::new(
|
||||
Catalog::from_builtin_with_overrides(&llm_catalog_settings)
|
||||
.context("building LLM model catalog")?,
|
||||
);
|
||||
resolve_canonical_origin(&server_settings.server, &self.env_lookup)
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
|
||||
|
|
@ -844,6 +856,7 @@ impl AppState {
|
|||
.server_settings
|
||||
.write()
|
||||
.expect("server settings lock poisoned") = server_settings;
|
||||
*self.catalog.write().expect("catalog lock poisoned") = catalog;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1519,6 +1532,10 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
let current_server_settings = Arc::new(resolved_settings.server_settings);
|
||||
let current_manifest_run_defaults = Arc::new(resolved_settings.manifest_run_defaults);
|
||||
let current_manifest_run_settings = resolved_settings.manifest_run_settings;
|
||||
let current_catalog = Arc::new(
|
||||
Catalog::from_builtin_with_overrides(&resolved_settings.llm_catalog_settings)
|
||||
.context("building LLM model catalog")?,
|
||||
);
|
||||
let slack_service = {
|
||||
current_server_settings
|
||||
.server
|
||||
|
|
@ -1563,6 +1580,7 @@ pub(crate) fn build_app_state(config: AppStateConfig) -> anyhow::Result<Arc<AppS
|
|||
manifest_run_defaults: RwLock::new(current_manifest_run_defaults),
|
||||
manifest_run_settings: RwLock::new(current_manifest_run_settings),
|
||||
server_settings: RwLock::new(current_server_settings),
|
||||
catalog: RwLock::new(current_catalog),
|
||||
env_lookup: Arc::clone(&env_lookup),
|
||||
github_api_base_url,
|
||||
http_client,
|
||||
|
|
|
|||
|
|
@ -77,14 +77,12 @@ async fn create_completion(
|
|||
Json(req): Json<CreateCompletionRequest>,
|
||||
) -> Response {
|
||||
// Resolve model
|
||||
let model_id = req.model.unwrap_or_else(|| {
|
||||
fabro_model::Catalog::builtin()
|
||||
.list(None)
|
||||
.first()
|
||||
.map_or_else(|| "claude-sonnet-4-5".to_string(), |m| m.id.clone())
|
||||
});
|
||||
let catalog = state.catalog();
|
||||
let model_id = req
|
||||
.model
|
||||
.unwrap_or_else(|| catalog.default_model().id.clone());
|
||||
|
||||
let catalog_info = fabro_model::Catalog::builtin().get(&model_id);
|
||||
let catalog_info = catalog.get(&model_id);
|
||||
|
||||
// Resolve provider: explicit request > catalog > None
|
||||
let explicit_provider = req.provider;
|
||||
|
|
@ -131,7 +129,7 @@ async fn create_completion(
|
|||
let request = LlmRequest {
|
||||
model: model_id.clone(),
|
||||
messages,
|
||||
provider: provider_name,
|
||||
provider: provider_name.clone(),
|
||||
tools,
|
||||
tool_choice,
|
||||
response_format: None,
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ async fn list_models(
|
|||
.into_iter()
|
||||
.collect();
|
||||
|
||||
let mut models = fabro_model::Catalog::builtin()
|
||||
let catalog = state.catalog();
|
||||
let mut models = catalog
|
||||
.list(provider_id.as_ref())
|
||||
.into_iter()
|
||||
.filter(|model| match &query {
|
||||
|
|
@ -100,7 +101,8 @@ async fn test_model(
|
|||
},
|
||||
None => ModelTestMode::Basic,
|
||||
};
|
||||
let Some(info) = fabro_model::Catalog::builtin().get(&id) else {
|
||||
let catalog = state.catalog();
|
||||
let Some(info) = catalog.get(&id) else {
|
||||
return ApiError::not_found(format!("Model not found: {id}")).into_response();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use super::super::{
|
||||
ApiError, AppState, Catalog, CloseRunPullRequestResponse, CreateRunPullRequestRequest,
|
||||
IntoResponse, Json, MergeRunPullRequestRequest, MergeRunPullRequestResponse, PullRequestRecord,
|
||||
ApiError, AppState, CloseRunPullRequestResponse, CreateRunPullRequestRequest, IntoResponse,
|
||||
Json, MergeRunPullRequestRequest, MergeRunPullRequestResponse, PullRequestRecord,
|
||||
RequireRunScoped, Response, Router, RunId, State, StatusCode, get, lock_pull_request_create,
|
||||
post, pull_request, warn, workflow_event,
|
||||
};
|
||||
|
|
@ -248,7 +248,8 @@ async fn create_run_pull_request(
|
|||
model
|
||||
} else {
|
||||
let configured = state.llm_source.configured_providers().await;
|
||||
Catalog::builtin()
|
||||
state
|
||||
.catalog()
|
||||
.default_for_configured_ids(&configured)
|
||||
.id
|
||||
.clone()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use fabro_interview::{
|
|||
AnswerValue, ControlInterviewer, Interviewer, Question, WorkerControlMessage,
|
||||
};
|
||||
use fabro_llm::types::{Message as LlmMessage, Request as LlmRequest};
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_model::{ModelRef, Provider};
|
||||
use fabro_types::settings::ServerAuthMethod;
|
||||
use fabro_types::{
|
||||
|
|
@ -72,6 +73,7 @@ fn resolved_runtime_settings_from_toml(source: &str) -> ResolvedAppStateSettings
|
|||
resolved_runtime_settings_for_tests(
|
||||
server_settings_from_toml(source),
|
||||
manifest_run_defaults_from_toml(source),
|
||||
LlmCatalogSettings::default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1756,6 +1758,7 @@ methods = ["dev-token"]
|
|||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
server_settings,
|
||||
RunLayer::default(),
|
||||
LlmCatalogSettings::default(),
|
||||
),
|
||||
registry_factory_override: None,
|
||||
max_concurrent_runs: 5,
|
||||
|
|
@ -3455,6 +3458,7 @@ fn create_github_token_app_state_with_env_lookup(
|
|||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
github_token_settings(),
|
||||
RunLayer::default(),
|
||||
LlmCatalogSettings::default(),
|
||||
),
|
||||
registry_factory_override: None,
|
||||
max_concurrent_runs: 5,
|
||||
|
|
@ -3837,6 +3841,53 @@ async fn list_models_unknown_provider_returns_empty_page() {
|
|||
assert_eq!(body["meta"]["has_more"].as_bool(), Some(false));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_models_uses_app_state_catalog_overrides() {
|
||||
let llm_catalog_settings: LlmCatalogSettings = toml::from_str(
|
||||
r#"
|
||||
[providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
credentials = ["env:VENICE_API_KEY"]
|
||||
priority = 120
|
||||
|
||||
[models."venice-large"]
|
||||
provider = "venice"
|
||||
display_name = "Venice Large"
|
||||
family = "venice"
|
||||
default = true
|
||||
|
||||
[models."venice-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[models."venice-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
effort = false
|
||||
"#,
|
||||
)
|
||||
.expect("catalog fixture should parse");
|
||||
let state = TestAppStateBuilder::new()
|
||||
.llm_catalog_settings(llm_catalog_settings)
|
||||
.build();
|
||||
let app = crate::test_support::build_test_router(state);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/models?provider=venice"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
let body = response_json!(response, StatusCode::OK).await;
|
||||
let models = body["data"].as_array().unwrap();
|
||||
assert_eq!(models.len(), 1);
|
||||
assert_eq!(models[0]["id"], "venice-large");
|
||||
assert_eq!(models[0]["provider"], "venice");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_login_github_redirects_to_github() {
|
||||
let source = r#"
|
||||
|
|
@ -9125,6 +9176,68 @@ async fn create_completion_unknown_provider_returns_clear_error() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_completion_default_model_uses_app_state_catalog() {
|
||||
let llm_catalog_settings: LlmCatalogSettings = toml::from_str(
|
||||
r#"
|
||||
[providers.venice]
|
||||
display_name = "Venice"
|
||||
adapter = "openai_compatible"
|
||||
base_url = "https://api.venice.ai/api/v1"
|
||||
credentials = ["env:VENICE_API_KEY"]
|
||||
priority = 120
|
||||
|
||||
[models."venice-large"]
|
||||
provider = "venice"
|
||||
display_name = "Venice Large"
|
||||
family = "venice"
|
||||
default = true
|
||||
|
||||
[models."venice-large".limits]
|
||||
context_window = 128000
|
||||
|
||||
[models."venice-large".features]
|
||||
tools = true
|
||||
vision = false
|
||||
reasoning = false
|
||||
effort = false
|
||||
"#,
|
||||
)
|
||||
.expect("catalog fixture should parse");
|
||||
let state = TestAppStateBuilder::new()
|
||||
.llm_catalog_settings(llm_catalog_settings)
|
||||
.build();
|
||||
let app = crate::test_support::build_test_router(state);
|
||||
|
||||
let req = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/completions"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"stream": false,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [{"kind": "text", "data": "hi"}]
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(req).await.unwrap();
|
||||
let body = response_json!(response, StatusCode::BAD_GATEWAY).await;
|
||||
assert!(
|
||||
body["errors"][0]["detail"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("Provider 'venice' not registered"),
|
||||
"unexpected error body: {body:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn demo_boards_runs_returns_run_list_items() {
|
||||
let state = test_app_state();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ use axum::{Router, middleware};
|
|||
use chrono::Duration as ChronoDuration;
|
||||
use fabro_config::{RunLayer, RunSettingsBuilder, ServerSettingsBuilder, envfile};
|
||||
use fabro_interview::Interviewer;
|
||||
use fabro_model::catalog::LlmCatalogSettings;
|
||||
use fabro_static::EnvVars;
|
||||
use fabro_store::{ArtifactStore, Database};
|
||||
use fabro_types::settings::ServerAuthMethod;
|
||||
|
|
@ -64,6 +65,7 @@ pub struct TestAppStateBuilder {
|
|||
server_env_path: Option<PathBuf>,
|
||||
server_secret_env: HashMap<String, String>,
|
||||
env_lookup: EnvLookup,
|
||||
llm_catalog_settings: LlmCatalogSettings,
|
||||
}
|
||||
|
||||
impl Default for TestAppStateBuilder {
|
||||
|
|
@ -78,6 +80,7 @@ impl Default for TestAppStateBuilder {
|
|||
server_env_path: None,
|
||||
server_secret_env: HashMap::new(),
|
||||
env_lookup: default_env_lookup(),
|
||||
llm_catalog_settings: LlmCatalogSettings::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +124,11 @@ impl TestAppStateBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn llm_catalog_settings(mut self, settings: LlmCatalogSettings) -> Self {
|
||||
self.llm_catalog_settings = settings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn server_secret_env(mut self, server_secret_env: HashMap<String, String>) -> Self {
|
||||
self.server_secret_env = server_secret_env;
|
||||
self
|
||||
|
|
@ -151,6 +159,7 @@ impl TestAppStateBuilder {
|
|||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
self.server_settings,
|
||||
self.manifest_run_defaults,
|
||||
self.llm_catalog_settings,
|
||||
),
|
||||
registry_factory_override: self.registry_factory_override,
|
||||
max_concurrent_runs: self.max_concurrent_runs,
|
||||
|
|
@ -219,12 +228,14 @@ pub fn test_app_state_with_options(
|
|||
pub(crate) fn resolved_runtime_settings_for_tests(
|
||||
server_settings: ServerSettings,
|
||||
manifest_run_defaults: RunLayer,
|
||||
llm_catalog_settings: LlmCatalogSettings,
|
||||
) -> ResolvedAppStateSettings {
|
||||
ResolvedAppStateSettings {
|
||||
manifest_run_settings: RunSettingsBuilder::from_run_layer(&manifest_run_defaults)
|
||||
.map_err(|err| SharedError::new(anyhow::Error::new(err))),
|
||||
manifest_run_defaults,
|
||||
server_settings,
|
||||
llm_catalog_settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue